4

最近の v0.3.0 のブログ投稿では、WebVR 1.0 のサポートについて言及されており、「デスクトップ ディスプレイにヘッドセットとは異なるコンテンツを表示できるようになり、非同期のゲームプレイと観戦モードへの扉が開かれました」。これはまさに私が働こうとしているものです。シーン内の 1 つのカメラが HMD の視点を表し、セカンダリ カメラが同じシーンの観客を表し、そのビューを同じ Web ページのキャンバスにレンダリングすることを検討しています。0.3.0 では、埋め込みコンポーネントを優先して、特定のキャンバスにシーンをレンダリングする機能が削除されています。2 台のカメラで 1 つのシーンを同時にレンダリングする方法について何か考えはありますか?

私の意図は、ユーザーが別の視点から何をしているかをデスクトップ ディスプレイに表示させることです。私の最終目標は、複合現実のグリーン スクリーン コンポーネントを構築できるようにすることです。

4

2 に答える 2

6

将来的にこれを行うためのより良い、またはよりクリーンな方法があるかもしれませんが、THREE.js の世界でこれがどのように行われるかの例を見て、2 番目のカメラ レンダリングを取得することができました。

spectator という非アクティブなカメラにコンポーネントを追加します。init 関数で新しいレンダラーをセットアップし、シーン外の div にアタッチして新しいキャンバスを作成します。次に、ライフサイクルの tick() 部分内で render メソッドを呼び出します。

このカメラの動きを分離する方法はまだ考えていません。0.3.0 aframe シーンのデフォルトのルック コントロールは、引き続き両方のカメラを制御します。

観客カムのgif

ソースコード: https://gist.github.com/derickson/334a48eb1f53f6891c59a2c137c180fa

于 2016-08-26T01:34:05.820 に答える
0

これに役立つ一連のコンポーネントを作成しました。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;
        }
    }
});
于 2022-01-21T12:51:04.857 に答える