Source: renderer.js

import * as THREE from 'three/webgpu';
import { ResizeSensor } from 'css-element-queries';
import { Scene } from './scene';
import { onRenderRequest, offRenderRequest } from './renderRequests';
import { setTextPixelsPerUnit } from './textSprite';
import { setGlyphComputeSupported } from './tsl/glyphTransform';
/**
 * Create a Zinc 3D renderer in the container provided.
 * The primary function of a Zinc 3D renderer is to display the current
 * scene (@link Scene} set to the renderer and each scene may contain as
 * many geometries, glyphset and other primitives as the system can support.
 * Zinc.Renderer also allows additional scenes to be displayed.
 *
 * @param {Object} containerIn - Container to create the renderer on.
 * @class
 * @author Alan Wu
 * @return {Renderer}
 */
const Renderer = function (containerIn) {
  let container = containerIn;

  const stats = 0;

  let renderer = undefined;
  let currentScene = undefined;

  //myGezincGeometriestains a tuple of the threejs mesh, timeEnabled, morphColour flag, unique id and morph
  const clock = new THREE.Timer();
  this.playAnimation = true;
  /* default animation update rate, rate is 1000 and duration
    is default to 6000, 6s to finish a full animation */
  let playRate = 1000;
  let preRenderCallbackFunctions = {};
  let preRenderCallbackFunctions_id = 0;
  let postRenderCallbackFunctions = {};
  let postRenderCallbackFunctions_id = 0;
  let contextLostCallbackFunctions = {};
  let contextLostCallbackFunctions_id = 0;
  let contextRestoredCallbackFunctions = {};
  let contextRestoredCallbackFunctions_id = 0;
  let cameraOrtho = undefined,
    sceneOrtho = undefined,
    logoSprite = undefined;
  let sceneMap = [];
  let additionalActiveScenes = [];
  let scenesGroup = new THREE.Group();
  let canvas = undefined;
  let sensor = undefined;
  let isRendering = false;
  let isInitialised = false;
  const _this = this;
  const currentSize = [0, 0];
  const currentOffset = [0, 0];
  //Render on demand is opt-in, when enabled the scene is only drawn when
  //something has changed.
  let renderOnDemand = false;
  let needsRender = true;
  const requestRender = () => {
    needsRender = true;
  };
  //Camera of the current scene shared with additional active scenes
  const sharedCamera = { controls: undefined, updated: false };

  this.getDrawingWidth = () => {
    if (container) {
      return container.clientWidth;
    } else if (canvas)
      if (typeof canvas.clientWidth !== 'undefined') return Math.round(canvas.clientWidth);
      else return Math.round(canvas.width);
    return 0;
  };

  this.getDrawingHeight = () => {
    if (container) {
      return container.clientHeight;
    } else if (canvas)
      if (typeof canvas.clientHeight !== 'undefined') return Math.round(canvas.clientHeight);
      else return Math.round(canvas.height);
    return 0;
  };

  /**
   * Call this to resize the renderer, this is normally call automatically.
   */
  this.onWindowResize = () => {
    needsRender = true;
    currentScene.onWindowResize();
    //Give it a miniumum size of 1 x 1
    const width = this.getDrawingWidth() || 1;
    const height = this.getDrawingHeight() || 1;
    if (renderer != undefined) {
      let localRect = undefined;
      if (container) {
        localRect = container.getBoundingClientRect();
        renderer.setSize(width, height);
      } else if (canvas) {
        if (typeof canvas.getBoundingClientRect !== 'undefined') {
          localRect = canvas.getBoundingClientRect();
          canvas.width = width;
          canvas.height = height;
          renderer.setSize(width, height, false);
        } else {
          renderer.setSize(width, height, false);
        }
      }
      if (localRect) {
        currentOffset[0] = localRect.left;
        currentOffset[1] = localRect.top;
      }
      const target = new THREE.Vector2();
      renderer.getSize(target);
      currentSize[0] = target.x;
      currentSize[1] = target.y;
      //Text sprites are drawn at the resolution they occupy on screen
      const fov = currentScene?.camera?.fov;
      if (fov) {
        setTextPixelsPerUnit(
          (currentSize[1] * renderer.getPixelRatio()) / (2 * Math.tan((fov * Math.PI) / 360)),
        );
      }
    }
  };

  /**
   * Initialise the renderer and its visualisations.
   */
  this.initialiseVisualisation = async (parameters) => {
    if (!isInitialised) {
      parameters = parameters || {};
      if (parameters['antialias'] === undefined) {
        let onMobile = false;
        try {
          if (
            /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
              navigator.userAgent,
            )
          ) {
            onMobile = true;
          }
        } catch (err) {
          onMobile = false;
        }
        if (onMobile) parameters['antialias'] = false;
        else parameters['antialias'] = true;
      }
      if (parameters['canvas']) {
        container = undefined;
        canvas = parameters['canvas'];
      }
      //WebGPURenderer otherwise requests a device with the default
      //maxTextureArrayLayers (256 on many adapters), which is too low for
      //some currnetly sypported scaffold, and the default
      //maxStorageBuffersPerShaderStage (8 on many adapters). The glyph
      //transform compute pass only needs two storage buffers, the higher
      //limit is kept as headroom. We will request the highest possible on
      //the device for both.
      if (
        parameters.device === undefined &&
        parameters.requiredLimits === undefined &&
        typeof navigator !== 'undefined' &&
        navigator.gpu
      ) {
        try {
          const adapter = await navigator.gpu.requestAdapter({
            powerPreference: parameters.powerPreference,
          });
          if (adapter) {
            parameters.requiredLimits = {
              maxTextureArrayLayers: adapter.limits.maxTextureArrayLayers,
              maxTextureDimension2D: adapter.limits.maxTextureDimension2D,
              maxStorageBuffersPerShaderStage: adapter.limits.maxStorageBuffersPerShaderStage,
            };
          }
        } catch (err) {
          //Fall through and let WebGPURenderer's own adapter/device request
          //use its defaults.
        }
      }
      renderer = new THREE.WebGPURenderer(parameters);
      await renderer.init();
      //animate() may have been called before the renderer was ready
      if (isRendering) renderer.setAnimationLoop(this.render);
      //GPU glyph animation requires the WebGPU backend, the WebGL fallback
      //cannot index storage buffers freely.
      setGlyphComputeSupported(renderer.backend?.isWebGPUBackend === true);

      //renderer = new THREE.WebGLRenderer(parameters);
      if (container !== undefined) {
        container.appendChild(renderer.domElement);
      }
      renderer.setClearColor(0xffffff, 1);
      if (canvas && canvas.style) {
        canvas.style.height = '100%';
        canvas.style.width = '100%';
      }
      const domCanvas = renderer.domElement;
      domCanvas.addEventListener('webglcontextlost', onContextLost, false);
      domCanvas.addEventListener('webglcontextrestored', onContextRestored, false);

      renderer.autoClear = false;
      const scene = this.createScene('default');
      this.setCurrentScene(scene);
      onRenderRequest(requestRender);
      isInitialised = true;
    }
  };

  const onContextLost = (event) => {
    event.preventDefault();
    for (let key of Object.keys(preRenderCallbackFunctions)) {
      if (contextLostCallbackFunctions.hasOwnProperty(key)) {
        contextLostCallbackFunctions[key].call();
      }
    }
  };

  const onContextRestored = (event) => {
    event.preventDefault();
    for (let key of Object.keys(preRenderCallbackFunctions)) {
      if (contextRestoredCallbackFunctions.hasOwnProperty(key)) {
        contextRestoredCallbackFunctions[key].call();
      }
    }
  };

  /**
   * Get the current scene on display.
   * @return {Zinc.Scene};
   */
  this.getCurrentScene = () => {
    return currentScene;
  };

  /**
   * Set the current scene on display.
   *
   * @param {Zinc.Scene} sceneIn - The scene to be set, only scene created by this instance
   * of ZincRenderer is supported currently.
   */
  this.setCurrentScene = (sceneIn) => {
    if (sceneIn) {
      this.removeActiveScene(sceneIn);
      const oldScene = currentScene;
      currentScene = sceneIn;
      if (oldScene) {
        oldScene.setInteractiveControlEnable(false);
      }
      currentScene.setInteractiveControlEnable(true);
      currentScene.setAdditionalScenesGroup(scenesGroup);
      needsRender = true;
      this.onWindowResize();
    }
  };

  /**
   * Return scene with the matching name if scene with that name has been created.
   *
   * @param {String} name - Name to match
   * @return {Zinc.Scene}
   */
  this.getSceneByName = (name) => {
    return sceneMap[name];
  };

  /*
   * Create a new scene with the provided name if scene with the same name exists,
   * return undefined.
   *
   * @param {String} name - Name of the scene to be created.
   * @return {Zinc.Scene}
   */
  this.createScene = (name) => {
    if (sceneMap[name] != undefined) {
      return undefined;
    } else {
      let new_scene = undefined;
      if (canvas) new_scene = new Scene(canvas, renderer);
      else new_scene = new Scene(container, renderer);
      sceneMap[name] = new_scene;
      new_scene.sceneName = name;
      return new_scene;
    }
  };

  const updateOrthoScene = () => {
    if (logoSprite != undefined) {
      const material = logoSprite.material;
      if (material.map) {
        const width = this.getDrawingWidth();
        const height = this.getDrawingHeight();
        const calculatedWidth = (width - material.map.image.width) / 2;
        const calculatedHeight = (-height + material.map.image.height) / 2;
        logoSprite.position.set(calculatedWidth, calculatedHeight, 1);
      }
    }
    return sceneMap[name];
  };

  /**
	};

	const updateOrthoCamera = () => {
		if (cameraOrtho != undefined) {
			const width = this.getDrawingWidth();
			const height = this.getDrawingHeight();
			cameraOrtho.left = -width / 2;
			cameraOrtho.right = width / 2;
			cameraOrtho.top =  height / 2;
			cameraOrtho.bottom = -height / 2;
			cameraOrtho.updateProjectionMatrix();
		}
	};

	/**
	 * Reset the viewport of the current scene to its original state.
	 */
  this.resetView = () => {
    currentScene.resetView();
  };

  /**
   * Adjust zoom distance to include all primitives in scene and also the additional scenes
   * but the lookat direction and up vectors will remain constant.
   */
  this.viewAll = () => {
    if (currentScene) {
      const boundingBox = currentScene.getBoundingBox();
      if (boundingBox) {
        for (let i = 0; i < additionalActiveScenes.length; i++) {
          const boundingBox2 = additionalActiveScenes[i].getBoundingBox();
          if (boundingBox2) {
            boundingBox.union(boundingBox2);
          }
        }
        currentScene.viewAllWithBoundingBox(boundingBox);
      }
    }
  };

  /**
   * Load a legacy model(s) format with the provided URLs and parameters. This only loads the geometry
   * without any of the metadata. Therefore, extra parameters should be provided. This should be
   * called from {@link Zinc.Scene}.
   *
   * @deprecated
   */
  this.loadModelsURL = (urls, colours, opacities, timeEnabled, morphColour, finishCallback) => {
    currentScene.loadModelsURL(urls, colours, opacities, timeEnabled, morphColour, finishCallback);
  };

  const loadView = (viewData) => {
    currentScene.loadView(viewData);
  };

  /**
   * Load the viewport from an external location provided by the url. This should be
   * called from {@link Zinc.Scene};
   * @param {String} URL - address to the file containing viewport information.
   * @deprecated
   */
  this.loadViewURL = (url) => {
    currentScene.loadViewURL(url);
  };

  /**
   * Load a legacy file format containing the viewport and its model file from an external
   * location provided by the url. Use the new metadata format with
   * {@link Zinc.Scene#loadMetadataURL} instead. This should be
   * called from {@link Zinc.Scene};
   *
   * @param {String} URL - address to the file containing viewport and model information.
   * @deprecated
   */
  this.loadFromViewURL = (jsonFilePrefix, finishCallback) => {
    currentScene.loadFromViewURL(jsonFilePrefix, finishCallback);
  };

  this.updateDirectionalLight = () => {
    currentScene.updateDirectionalLight();
  };

  /**
   * Stop the animation and renderer to get into the render loop.
   */
  this.stopAnimate = () => {
    if (isRendering) {
      isRendering = false;
      renderer?.setAnimationLoop(null);
    }
  };

  /**
   * Start the animation and begin the rendering loop.
   */
  this.animate = () => {
    if (!isRendering) {
      clock.reset();
      isRendering = true;
      needsRender = true;
      //WebGPURenderer always runs its own requestAnimationFrame loop which
      //updates the node frame and the per frame info, render from that loop
      //instead of running a second one.
      renderer?.setAnimationLoop(this.render);
    }
  };

  const prevTime = Date.now();

  /**
   * Add a callback function which will be called when the gl context is lost.
   * @param {Function} callbackFunction - callbackFunction to be added.
   *
   * @return {Number}
   */
  this.addContextLostCallbackFunction = (callbackFunction) => {
    contextLostCallbackFunctions_id = contextLostCallbackFunctions_id + 1;
    contextLostCallbackFunctions[contextLostCallbackFunctions_id] = callbackFunction;
    return contextLostCallbackFunctions_id;
  };

  /**
   * Remove a callback function that is previously added to the renderer.
   * @param {Number} id - identifier of the previously added callback function.
   */
  this.removeContextLostCallbackFunction = (id) => {
    if (id in contextLostCallbackFunctions) {
      delete contextLostCallbackFunctions[id];
    }
  };

  /**
   * Add a callback function which will be called when the gl context is restored.
   * @param {Function} callbackFunction - callbackFunction to be added.
   *
   * @return {Number}
   */
  this.addContextRestoredCallbackFunction = (callbackFunction) => {
    contextRestoredCallbackFunctions_id = contextRestoredCallbackFunctions_id + 1;
    contextRestoredCallbackFunctions[contextRestoredCallbackFunctions_id] = callbackFunction;
    return contextRestoredCallbackFunctions_id;
  };

  /**
   * Remove a callback function that is previously added to the renderer.
   * @param {Number} id - identifier of the previously added callback function.
   */
  this.removeContextRestoredCallbackFunction = (id) => {
    if (id in contextRestoredCallbackFunctions_id) {
      delete contextRestoredCallbackFunctions_id[id];
    }
  };

  /**
   * Add a callback function which will be called everytime before the renderer renders its scene.
   * @param {Function} callbackFunction - callbackFunction to be added.
   *
   * @return {Number}
   */
  this.addPreRenderCallbackFunction = (callbackFunction) => {
    preRenderCallbackFunctions_id = preRenderCallbackFunctions_id + 1;
    preRenderCallbackFunctions[preRenderCallbackFunctions_id] = callbackFunction;
    return preRenderCallbackFunctions_id;
  };

  /**
   * Remove a callback function that is previously added to the scene.
   * @param {Number} id - identifier of the previously added callback function.
   */
  this.removePreRenderCallbackFunction = (id) => {
    if (id in preRenderCallbackFunctions) {
      delete preRenderCallbackFunctions[id];
    }
  };

  /**
   * Add a callback function which will be called everytime after the renderer renders its scene.
   * @param {Function} callbackFunction - callbackFunction to be added.
   *
   * @return {Number}
   */
  this.addPostRenderCallbackFunction = (callbackFunction) => {
    postRenderCallbackFunctions_id = postRenderCallbackFunctions_id + 1;
    postRenderCallbackFunctions[postRenderCallbackFunctions_id] = callbackFunction;
    return postRenderCallbackFunctions_id;
  };

  /**
   * Remove a callback function that is previously added to the scene.
   * @param {Number} id - identifier of the previously added callback function.
   */
  this.removePostRenderCallbackFunction = (id) => {
    if (id in postRenderCallbackFunctions) {
      delete postRenderCallbackFunctions[id];
    }
  };

  /**
   * Get the current play rate, playrate affects how fast an animated object animates.
   * Also see {@link Zinc.Scene#duration}.
   */
  this.getPlayRate = () => {
    return playRate;
  };

  /**
   * Set the current play rate, playrate affects how fast an animated object animates.
   * @param {Number} PlayRateIn - value to set the playrate to.
   * Also see {@link Zinc.Scene#duration}.
   */
  this.setPlayRate = (playRateIn) => {
    playRate = playRateIn;
    needsRender = true;
  };

  /**
   * Enable or disable render on demand, it is disabled by default.
   * When enabled, the scenes are only drawn when something has changed,
   * e.g. camera movement, animation, loading and changes made through
   * the Zinc APIs. Call {@link Renderer#invalidate} after modifying
   * THREE.js objects directly or after changing the THREE.js renderer,
   * e.g. setClearColor.
   * Pre-render callbacks are called on every frame, post-render callbacks
   * are only called when the scenes are drawn.
   *
   * @param {Boolean} flag - Enable or disable render on demand.
   */
  this.setRenderOnDemand = (flag) => {
    renderOnDemand = flag ? true : false;
    needsRender = true;
  };

  /**
   * Check if render on demand is enabled.
   *
   * @return {Boolean}
   */
  this.isRenderOnDemand = () => {
    return renderOnDemand;
  };

  /**
   * Request the scenes to be drawn on the next frame, this is only
   * required when render on demand is enabled.
   */
  this.invalidate = () => {
    needsRender = true;
  };

  this.getCurrentTime = () => {
    return currentScene.getCurrentTime();
  };

  /**
   * Get the current play rate, playrate affects how fast an animated object animates.
   * Also see {@link Zinc.Scene#duration}.
   */
  this.setMorphsTime = (time) => {
    currentScene.setMorphsTime(time);
  };

  /**
   * Get {Zinc.Geoemtry} by its id. This should be called from {@link Zinc.Scene};
   *
   * @depreacted
   * @return {Zinc.Geometry}
   */
  this.getZincGeometryByID = (id) => {
    return currentScene.getZincGeometryByID(id);
  };

  /**
   * Add {Three.Object} to the current scene.
   */
  this.addToScene = (object) => {
    currentScene.addObject(object);
  };

  /**
   * Add {Three.Object} to the ortho scene, objects added to the ortho scene are rendered in
   * normalised coordinates and overlay on top of current scene.
   *
   */
  this.addToOrthoScene = (object) => {
    if (sceneOrtho == undefined) sceneOrtho = new THREE.Scene();
    if (cameraOrtho == undefined) {
      const width = this.getDrawingWidth();
      const height = this.getDrawingHeight();
      cameraOrtho = new THREE.OrthographicCamera(
        -width / 2,
        width / 2,
        height / 2,
        -height / 2,
        1,
        10,
      );
      cameraOrtho.position.z = 10;
    }
    sceneOrtho.add(object);
    needsRender = true;
  };

  const createHUDSprites = (logoSprite) => {
    return (texture) => {
      texture.needsUpdate = true;
      const material = new THREE.SpriteMaterial({ map: texture });
      const imagewidth = material.map.image.width;
      const imageheight = material.map.image.height;
      logoSprite.material = material;
      logoSprite.scale.set(imagewidth, imageheight, 1);
      const width = this.getDrawingWidth();
      const height = this.getDrawingHeight();
      logoSprite.position.set((width - imagewidth) / 2, (-height + imageheight) / 2, 1);
      this.addToOrthoScene(logoSprite);
    };
  };

  this.addLogo = () => {
    logoSprite = new THREE.Sprite();
    const logo = THREE.ImageUtils.loadTexture(
      'images/abi_big_logo_transparent_small.png',
      undefined,
      createHUDSprites(logoSprite),
    );
  };

  /**
   * Render the current and all additional scenes. It will first update all geometries and glyphsets
   * in scenes, clear depth buffer and render the ortho scene, call the preRenderCallbackFunctions stack
   * , render the scenes then postRenderCallback.
   */
  this.render = () => {
    if (!sensor) {
      if (container) {
        if (container.clientWidth > 0 && container.clientHeight > 0) {
          sensor = new ResizeSensor(container, this.onWindowResize);
        }
      } else if (canvas) {
        if (canvas.width > 0 && canvas.height > 0)
          sensor = new ResizeSensor(canvas, this.onWindowResize);
      }
    }
    clock.update();
    const delta = clock.getDelta();
    //Objects are only updated on every frame when render on demand is off
    //or the renderer itself has requested a frame, e.g. on resize
    const forceUpdate = !renderOnDemand || needsRender;
    let changed = currentScene.renderGeometries(
      playRate,
      delta,
      this.playAnimation,
      undefined,
      forceUpdate,
    );
    if (additionalActiveScenes.length > 0) {
      //Additional scenes are displayed with the current scene's camera
      sharedCamera.controls = currentScene.getZincCameraControls();
      sharedCamera.updated = currentScene.isCameraUpdated();
    }
    for (let i = 0; i < additionalActiveScenes.length; i++) {
      const sceneItem = additionalActiveScenes[i];
      if (
        sceneItem.renderGeometries(playRate, delta, this.playAnimation, sharedCamera, forceUpdate)
      )
        changed = true;
    }
    for (let key in preRenderCallbackFunctions) {
      if (preRenderCallbackFunctions.hasOwnProperty(key)) {
        preRenderCallbackFunctions[key].call();
      }
    }
    //Pre-render callbacks may have requested a frame
    if (renderOnDemand && !changed && !needsRender) {
      return;
    }
    needsRender = false;
    currentScene.render(renderer);
    if (cameraOrtho != undefined && sceneOrtho != undefined) {
      renderer.clearDepth();
      renderer.render(sceneOrtho, cameraOrtho);
    }
    for (let key in postRenderCallbackFunctions) {
      if (postRenderCallbackFunctions.hasOwnProperty(key)) {
        postRenderCallbackFunctions[key].call();
      }
    }
  };

  this.forceContextLoss = () => {
    //	renderer.forceContextLoss();
  };

  this.forceContextRestore = () => {
    //	renderer.forceContextRestore();
  };

  /**
   * Get the internal {@link Three.Renderer}, to gain access to ThreeJS APIs.
   */
  this.getThreeJSRenderer = () => {
    return renderer;
  };

  /**
   * Check if a scene is currently active.
   * @param {Zinc.Scene} sceneIn - Scene to check if it is currently
   * rendered.
   */
  this.isSceneActive = (sceneIn) => {
    if (currentScene === sceneIn) {
      return true;
    } else {
      for (let i = 0; i < additionalActiveScenes.length; i++) {
        const sceneItem = additionalActiveScenes[i];
        if (sceneItem === sceneIn) return true;
      }
    }
    return false;
  };

  /**
   * Add additional active scene for rendering, this scene will also be rendered but
   * viewport of the currentScene will be used.
   * @param {Zinc.Scene} additionalScene - Scene to be added to the rendering.
   */
  this.addActiveScene = (additionalScene) => {
    if (!this.isSceneActive(additionalScene)) {
      additionalActiveScenes.push(additionalScene);
      scenesGroup.add(additionalScene.getThreeJSScene());
      needsRender = true;
    }
  };

  /**
   * Remove a currenrtly active scene from the renderer, this scene will also be rendered but
   * viewport of the currentScene will be used.
   * @param {Zinc.Scene} additionalScene - Scene to be removed from rendering.
   */
  this.removeActiveScene = (additionalScene) => {
    for (let i = 0; i < additionalActiveScenes.length; i++) {
      const sceneItem = additionalActiveScenes[i];
      if (sceneItem === additionalScene) {
        additionalActiveScenes.splice(i, 1);
        scenesGroup.remove(additionalScene.getThreeJSScene());
        needsRender = true;
        return;
      }
    }
  };

  /**
   * Clear all additional scenes from rendering except for curentScene.
   */
  this.clearAllActiveScene = () => {
    for (let i = 0; i < additionalActiveScenes.length; i++) {
      scenesGroup.remove(additionalActiveScenes[i].getThreeJSScene());
    }
    additionalActiveScenes.splice(0, additionalActiveScenes.length);
    needsRender = true;
  };

  /**
   * Dispose all memory allocated, this will effetively destroy all scenes.
   */
  this.dispose = () => {
    for (const key in sceneMap) {
      if (sceneMap.hasOwnProperty(key)) {
        sceneMap[key].clearAll();
      }
    }
    sceneMap = [];
    additionalActiveScenes = [];
    scenesGroup = new THREE.Group();
    this.stopAnimate();
    preRenderCallbackFunctions = {};
    preRenderCallbackFunctions_id = 0;
    postRenderCallbackFunctions = {};
    postRenderCallbackFunctions_id = 0;
    contextLostCallbackFunctions = {};
    contextLostCallbackFunctions_id = 0;
    contextRestoredCallbackFunctions = {};
    contextRestoredCallbackFunctions_id = 0;
    cameraOrtho = undefined;
    sceneOrtho = undefined;
    logoSprite = undefined;
    const scene = this.createScene('default');
    this.setCurrentScene(scene);
    sensor = undefined;
    offRenderRequest(requestRender);
    renderer?.dispose();
  };

  /**
   * Transition from the current viewport to the endingScene's viewport in the specified duration.
   *
   * @param {Zinc.Scene} endingScene - Viewport of this scene will be used as the destination.
   * @param {Number} duration - Amount of time to transition from current viewport to the
   * endingScene's viewport.
   */
  this.transitionScene = (endingScene, duration) => {
    if (currentScene) {
      const currentCamera = currentScene.getZincCameraControls();
      const boundingBox = endingScene.getBoundingBox();
      if (boundingBox) {
        const radius = boundingBox.min.distanceTo(boundingBox.max) / 2.0;
        const centreX = (boundingBox.min.x + boundingBox.max.x) / 2.0;
        const centreY = (boundingBox.min.y + boundingBox.max.y) / 2.0;
        const centreZ = (boundingBox.min.z + boundingBox.max.z) / 2.0;
        const clip_factor = 4.0;
        const endingViewport = currentCamera.getViewportFromCentreAndRadius(
          centreX,
          centreY,
          centreZ,
          radius,
          40,
          radius * clip_factor,
        );
        const startingViewport = currentCamera.getCurrentViewport();
        currentCamera.cameraTransition(startingViewport, endingViewport, duration);
        currentCamera.enableCameraTransition();
      }
    }
  };

  /**
   * Check if the renderer is running on the WebGL 2 fallback backend.
   * WebGPURenderer never uses WebGL 1, so this is false when WebGPU is used.
   *
   * @return {Boolean}
   */
  this.isWebGL2 = () => {
    return renderer?.backend?.isWebGLBackend === true;
  };

  /**
   * Check if the renderer is running on the WebGPU backend.
   *
   * @return {Boolean}
   */
  this.isWebGPU = () => {
    return renderer?.backend?.isWebGPUBackend === true;
  };
};

export { Renderer };