【问题标题】:Rendering from two cameras at the same time in A-Frame在 A-Frame 中同时从两个摄像机渲染
【发布时间】:2022-03-05 23:04:07
【问题描述】:

最近的 v0.3.0 博客文章提到 WebVR 1.0 支持允许“我们在桌面显示器上拥有与耳机不同的内容,为异步游戏和旁观模式打开了大门。”这正是我想要工作的。我希望场景中的一个摄像头代表 HMD 的视点,另一个摄像头代表同一场景的观众,并将该视图呈现到同一网页上的画布上。 0.3.0 删除了将场景渲染到特定画布的能力,以支持嵌入式组件。关于如何完成两个摄像机同时渲染一个场景的任何想法?

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

【问题讨论】:

  • 直到 fernandojsg 重新上线并提供更明确的答案,因为他以前做过,我相信这将涉及一个组件来创建第二个相机/渲染器/画布并使用 @ 连接到渲染循环987654321@。默认画布将在 VR 模式下推送到耳机。然后也许调整 z 索引以在桌面显示器上显示观众画布?只是猜测。
  • 我能够让您的建议发挥作用。解决问题后,我将发布一个 github 存储库。

标签: aframe


【解决方案1】:

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

我向一个名为 spectator 的非活动摄像机添加了一个组件。在初始化函数中,我设置了一个新的渲染器并附加到场景外的 div 以创建一个新的画布。然后我在生命周期的 tick() 部分调用 render 方法。

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

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

【讨论】:

  • 您可以禁用旁观者相机上的外观控制,或者至少设置look-controls="hmdEnabled: false"。或者修改外观控件以添加一个属性以仅在 canvas 而不是 window 上侦听鼠标拖动。它目前在window 上侦听,以防您拖出画布,但不需要使用旁观者模式。
【解决方案2】:

我创建了一组可以帮助解决此问题的组件。 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

还有人建议我在这里发布多摄像头组件的完整源代码。

这里是……

/* 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;
        }
    }
});

【讨论】:

    猜你喜欢
    • 2018-05-04
    • 2017-11-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-05-22
    • 1970-01-01
    • 2016-07-17
    • 1970-01-01
    相关资源
    最近更新 更多