在 A-Frame 中同时从两个摄像头进行渲染

Rendering from two cameras at the same time in A-Frame

最近的 v0.3.0 博客 post 提到 WebVR 1.0 支持允许 "us to have different content on the desktop display than the headset, opening the door for asynchronous gameplay and spectator modes." 这正是我正在尝试的工作。我希望场景中的一个摄像头代表 HMD 的视点,另一个摄像头代表同一场景的观众,并将该视图渲染到同一网页上的 canvas。 0.3.0 移除了将场景渲染到特定 canvas 的能力,以支持嵌入式组件。关于如何完成两个相机同时渲染单个场景的任何想法?

我的意图是让桌面显示从不同的角度显示用户正在做什么。我的最终目标是能够构建混合现实绿屏组件。

虽然将来可能会有更好或更简洁的方法来执行此操作,但我能够通过查看 THREE.js 世界中如何完成此操作的示例来获得第二个相机渲染。

我将一个组件添加到名为旁观者的非活动相机中。在 init 函数中,我设置了一个新的渲染器并附加到场景外的 div 以创建一个新的 canvas。然后我在生命周期的 tick() 部分调用 render 方法。

我还没有想出如何隔离这个相机的运动。 0.3.0 aframe 场景的默认外观控件仍然控制两个相机

源代码: https://gist.github.com/derickson/334a48eb1f53f6891c59a2c137c180fa

我已经创建了一组组件来帮助解决这个问题。 https://github.com/diarmidmackenzie/aframe-multi-camera

这里的示例显示了 A-Frame 1.2.0 的用法,在屏幕的左半部分显示主摄像头,在右半部分显示辅助摄像头。

<!DOCTYPE html>
<html>
  <head>
    <script src="https://aframe.io/releases/1.2.0/aframe.min.js"></script>
    <script src="https://cdn.jsdelivr.net/gh/diarmidmackenzie/aframe-multi-camera@latest/src/multi-camera.min.js"></script>    
  </head>
  <body>
    <div>      
      <a-scene>
        <a-entity camera look-controls wasd-controls position="0 1.6 0">
          <!-- first secondary camera is a child of the main camera, so that it always has the same position / rotation -->
          <!-- replace main camera (since main camera is rendered across  the whole screen, which we don't want) -->
          <a-entity
            id="camera1"
            secondary-camera="outputElement:#viewport1;sequence: replace"            
          >
          </a-entity>
        </a-entity>

        <!-- PUT YOUR SCENE CONTENT HERE-->
        

        <!-- position of 2nd secondary camera-->
        <a-entity
          id="camera2"
          secondary-camera="outputElement:#viewport2"
          position="8 1.6 -6"
          rotation="0 90 0"
        >
        </a-entity>
      </a-scene>
    </div>

    <!-- standard HTML to contrl layout of the two viewports-->
    <div style="width: 100%; height:100%; display: flex">
      <div id="viewport1" style="width: 50%; height:100%"></div>
      <div id="viewport2" style="width: 50%; height:100%"></div>
    </div>
  </body>
</html>

这里也是一个小故障:https://glitch.com/edit/#!/recondite-polar-hyssop

还有人建议我 post 此处 multi-camera 组件的完整源代码。

这里是...

/* System that supports capture of the the main A-Frame render() call
   by add-render-call */
AFRAME.registerSystem('add-render-call', {

  init() {

    this.render = this.render.bind(this);
    this.originalRender = this.el.sceneEl.renderer.render;
    this.el.sceneEl.renderer.render = this.render;
    this.el.sceneEl.renderer.autoClear = false;

    this.preRenderCalls = [];
    this.postRenderCalls = [];
    this.suppresssDefaultRenderCount = 0;
  },

  addPreRenderCall(render) {
    this.preRenderCalls.push(render)
  },

  removePreRenderCall(render) {
    const index = this.preRenderCalls.indexOf(render);
    if (index > -1) {
      this.preRenderCalls.splice(index, 1);
    }
  },

  addPostRenderCall(render) {
    this.postRenderCalls.push(render)
  },

  removePostRenderCall(render) {
    const index = this.postRenderCalls.indexOf(render);
    if (index > -1) {
      this.postRenderCalls.splice(index, 1);
    }
    else {
      console.warn("Unexpected failure to remove render call")
    }
  },

  suppressOriginalRender() {
    this.suppresssDefaultRenderCount++;
  },

  unsuppressOriginalRender() {
    this.suppresssDefaultRenderCount--;

    if (this.suppresssDefaultRenderCount < 0) {
      console.warn("Unexpected unsuppression of original render")
      this.suppresssDefaultRenderCount = 0;
    }
  },

  render(scene, camera) {

    renderer = this.el.sceneEl.renderer

    // set up THREE.js stats to correctly count across all render calls.
    renderer.info.autoReset = false;
    renderer.info.reset();

    this.preRenderCalls.forEach((f) => f());

    if (this.suppresssDefaultRenderCount <= 0) {
      this.originalRender.call(renderer, scene, camera)
    }

    this.postRenderCalls.forEach((f) => f());
  }
});

/* Component that captures the main A-Frame render() call
   and adds an additional render call.
   Must specify an entity and component that expose a function call render(). */
AFRAME.registerComponent('add-render-call', {

  multiple: true,

  schema: {
    entity: {type: 'selector'},
    componentName: {type: 'string'},
    sequence: {type: 'string', oneOf: ['before', 'after', 'replace'], default: 'after'}
  },

  init() {

    this.invokeRender = this.invokeRender.bind(this);

  },

  update(oldData) {

    // first clean up any old settings.
    this.removeSettings(oldData)

    // now add new settings.
    if (this.data.sequence === "before") {
        this.system.addPreRenderCall(this.invokeRender)
    }

    if (this.data.sequence === "replace") {
        this.system.suppressOriginalRender()
    }

    if (this.data.sequence === "after" ||
        this.data.sequence === "replace")
     {
      this.system.addPostRenderCall(this.invokeRender)
    }
  },

  remove() {
    this.removeSettings(this.data)
  },

  removeSettings(data) {
    if (data.sequence === "before") {
        this.system.removePreRenderCall(this.invokeRender)
    }

    if (data.sequence === "replace") {
        this.system.unsuppressOriginalRender()
    }

    if (data.sequence === "after" ||
        data.sequence === "replace")
     {
      this.system.removePostRenderCall(this.invokeRender)
    }
  },

  invokeRender()
  {
    const componentName = this.data.componentName;
    if ((this.data.entity) &&
        (this.data.entity.components[componentName])) {
        this.data.entity.components[componentName].render(this.el.sceneEl.renderer, this.system.originalRender);
    }
  }
});

/* Component to set layers via HTML attribute. */
AFRAME.registerComponent('layers', {
    schema : {type: 'number', default: 0},

    init: function() {

        setObjectLayer = function(object, layer) {
            if (!object.el ||
                !object.el.hasAttribute('keep-default-layer')) {
                object.layers.set(layer);
            }
            object.children.forEach(o => setObjectLayer(o, layer));
        }

        this.el.addEventListener("loaded", () => {
            setObjectLayer(this.el.object3D, this.data);
        });

        if (this.el.hasAttribute('text')) {
            this.el.addEventListener("textfontset", () => {
                setObjectLayer(this.el.object3D, this.data);
            });
        }
    }
});

/* This component has code in common with viewpoint-selector-renderer
   However it's a completely generic stripped-down version, which
   just delivers the 2nd camera function.
   i.e. it is missing:
   - The positioning of the viewpoint-selector entity.
   - The cursor / raycaster elements.
*/

AFRAME.registerComponent('secondary-camera', {
    schema: {
        output: {type: 'string', oneOf: ['screen', 'plane'], default: 'screen'},
        outputElement: {type: 'selector'},
        cameraType: {type: 'string', oneOf: ['perspective, orthographic'], default: 'perspective'},
        sequence: {type: 'string', oneOf: ['before', 'after', 'replace'], default: 'after'},
        quality: {type: 'string', oneOf: ['high, low'], default: 'high'}
    },

    init() {

        if (!this.el.id) {
          console.error("No id specified on entity.  secondary-camera only works on entities with an id")
        }

        this.savedViewport = new THREE.Vector4();
        this.sceneInfo = this.prepareScene();
        this.activeRenderTarget = 0;



        // add the render call to the scene
        this.el.sceneEl.setAttribute(`add-render-call__${this.el.id}`,
                                     {entity: `#${this.el.id}`,
                                      componentName: "secondary-camera",
                                      sequence: this.data.sequence});

        // if there is a cursor on this entity, set it up to read this camera.
        if (this.el.hasAttribute('cursor')) {
          this.el.setAttribute("cursor", "canvas: user; camera: user");

          this.el.addEventListener('loaded', () => {
                this.el.components['raycaster'].raycaster.layers.mask = this.el.object3D.layers.mask;

                const cursor = this.el.components['cursor'];
                cursor.removeEventListeners();
                cursor.camera = this.camera;
                cursor.canvas = this.data.outputElement;
                cursor.canvasBounds = cursor.canvas.getBoundingClientRect();
                cursor.addEventListeners();
                cursor.updateMouseEventListeners();
            });
        }

        if (this.data.output === 'plane') {
          if (!this.data.outputElement.hasLoaded) {
            this.data.outputElement.addEventListener("loaded", () => {
              this.configureCameraToPlane()
            });
          } else {
            this.configureCameraToPlane()
          }
        }
    },

    configureCameraToPlane() {
      const object = this.data.outputElement.getObject3D('mesh');
      function nearestPowerOf2(n) {
        return 1 << 31 - Math.clz32(n);
      }
      // 2 * nearest power of 2 gives a nice look, but at a perf cost.
      const factor = (this.data.quality === 'high') ? 2 : 1;

      const width = factor * nearestPowerOf2(window.innerWidth * window.devicePixelRatio);
      const height = factor * nearestPowerOf2(window.innerHeight * window.devicePixelRatio);

      function newRenderTarget() {
        const target = new THREE.WebGLRenderTarget(width,
                                                   height,
                                                   {
                                                      minFilter: THREE.LinearFilter,
                                                      magFilter: THREE.LinearFilter,
                                                      stencilBuffer: false,
                                                      generateMipmaps: false
                                                    });

         return target;
      }
      // We use 2 render targets, and alternate each frame, so that we are
      // never rendering to a target that is actually in front of the camera.
      this.renderTargets = [newRenderTarget(),
                            newRenderTarget()]

      this.camera.aspect = object.geometry.parameters.width /
                           object.geometry.parameters.height;

    },

    remove() {

      this.el.sceneEl.removeAttribute(`add-render-call__${this.el.id}`);
      if (this.renderTargets) {
        this.renderTargets[0].dispose();
        this.renderTargets[1].dispose();
      }

      // "Remove" code does not tidy up adjustments made to cursor component.
      // rarely necessary as cursor is typically put in place at the same time
      // as the secondary camera, and so will be disposed of at the same time.
    },

    prepareScene() {
        this.scene = this.el.sceneEl.object3D;

        const width = 2;
        const height = 2;

        if (this.data.cameraType === "orthographic") {
            this.camera = new THREE.OrthographicCamera( width / - 2, width / 2, height / 2, height / - 2, 1, 1000 );
        }
        else {
            this.camera = new THREE.PerspectiveCamera( 45, width / height, 1, 1000);
        }

        this.scene.add(this.camera);
        return;
    },

    render(renderer, renderFunction) {

        // don't bother rendering to screen in VR mode.
        if (this.data.output === "screen" && this.el.sceneEl.is('vr-mode')) return;

        var elemRect;

        if (this.data.output === "screen") {
           const elem = this.data.outputElement;

           // get the viewport relative position of this element
           elemRect = elem.getBoundingClientRect();
           this.camera.aspect = elemRect.width / elemRect.height;
        }

        // Camera position & layers match this entity.
        this.el.object3D.getWorldPosition(this.camera.position);
        this.el.object3D.getWorldQuaternion(this.camera.quaternion);
        this.camera.layers.mask = this.el.object3D.layers.mask;

        this.camera.updateProjectionMatrix();

        if (this.data.output === "screen") {
          // "bottom" position is relative to the whole viewport, not just the canvas.
          // We need to turn this into a distance from the bottom of the canvas.
          // We need to consider the header bar above the canvas, and the size of the canvas.
          const mainRect = renderer.domElement.getBoundingClientRect();

          renderer.getViewport(this.savedViewport);

          renderer.setViewport(elemRect.left - mainRect.left,
                               mainRect.bottom - elemRect.bottom,
                               elemRect.width,
                               elemRect.height);

          renderFunction.call(renderer, this.scene, this.camera);
          renderer.setViewport(this.savedViewport);
        }
        else {
          // target === "plane"

          // store off current renderer properties so that they can be restored.
          const currentRenderTarget = renderer.getRenderTarget();
          const currentXrEnabled = renderer.xr.enabled;
          const currentShadowAutoUpdate = renderer.shadowMap.autoUpdate;

          // temporarily override renderer proeperties for rendering to a texture.
          renderer.xr.enabled = false; // Avoid camera modification
          renderer.shadowMap.autoUpdate = false; // Avoid re-computing shadows

          const renderTarget = this.renderTargets[this.activeRenderTarget];
          renderTarget.texture.encoding = renderer.outputEncoding;
          renderer.setRenderTarget(renderTarget);
          renderer.state.buffers.depth.setMask( true ); // make sure the depth buffer is writable so it can be properly cleared, see #18897
          renderer.clear();

          renderFunction.call(renderer, this.scene, this.camera);

          this.data.outputElement.getObject3D('mesh').material.map = renderTarget.texture;

          // restore original renderer settings.
          renderer.setRenderTarget(currentRenderTarget);
          renderer.xr.enabled = currentXrEnabled;
          renderer.shadowMap.autoUpdate = currentShadowAutoUpdate;

          this.activeRenderTarget = 1 - this.activeRenderTarget;
        }
    }
});