Search code examples
three.jswebvr

Positioning a three.js object in front of the camera, but not tied to the camera, using webvr-boilerplate


I have an object3D which is invisible. I can freely look around by using the webvr-boilerplate. On click, I want to position this object3D in front of the camera, make it visible, but at a certain y position above the "ground".

I tried the following but it places the object3D in mid-air if I look there, and no at a certain position above the "ground":

var updateMatrix = camera.matrixWorld.clone(); object3D.applyMatrix(updateMatrix);

I think I need to extract certain camera rotation and position components from it and apply it to my object3D in order to get what I want.

Any hints are very much welcome!


Solution

  • One way of doing this is to put your object onto a pivot at the position that you want it to appear and then rotate and position the pivot instead of trying to calculate the object's rotation and position simultaneously.

    var pivot = new THREE.Object3D();
    scene.add(pivot);
    pivot.add(object3D);
    // position the object on the pivot, so that it appears 5 meters 
    // in front of the user.
    object3D.position.z = -5;
    

    Next, generally speaking, you want to decompose the camera's quaternion into its components with respect to a plane. In your case the plane is the ground (X-Z, I assume).

    Deriving from another Stack Overflow answer, you can do this in three.js with a few operations:

    var yaxis = new THREE.Vector3(0, 1, 0);
    var zaxis = new THREE.Vector3(0, 0, 1);
    var direction = zaxis.clone();
    // Apply the camera's quaternion onto the unit vector of one of the axes
    // of our desired rotation plane (the z axis of the xz plane, in this case).
    direction.applyQuaternion(camera.quaternion);
    // Project the direction vector onto the y axis to get the y component
    // of the direction.
    var ycomponent = yaxis.clone().multiplyScalar(direction.dot(yaxis));
    // Subtract the y component from the direction vector so that we are
    // left with the x and z components.
    direction.sub(ycomponent);
    // Normalize the direction into a unit vector again.
    direction.normalize();
    // Set the pivot's quaternion to the rotation required to get from the z axis
    // to the xz component of the camera's direction.
    pivot.quaternion.setFromUnitVectors(zaxis, direction);
    // Finally, set the pivot's position as well, so that it follows the camera.
    pivot.position.copy(camera.position);
    

    Full example code:

    var renderer = new THREE.WebGLRenderer({antialias: true});
    renderer.setPixelRatio(window.devicePixelRatio);
    document.body.appendChild(renderer.domElement);
    var scene = new THREE.Scene();
    var camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 10000);
    var controls = new THREE.VRControls(camera);
    var effect = new THREE.VREffect(renderer);
    effect.setSize(window.innerWidth, window.innerHeight);
    var manager = new WebVRManager(renderer, effect, {hideButton: false});
    
    // Add a repeating grid as a skybox.
    var boxWidth = 5;
    THREE.ImageUtils.crossOrigin = '';
    var texture = THREE.ImageUtils.loadTexture(
      'http://cdn.rawgit.com/borismus/webvr-boilerplate/0.3.3/img/box.png'
    );
    texture.wrapS = THREE.RepeatWrapping;
    texture.wrapT = THREE.RepeatWrapping;
    texture.repeat.set(boxWidth, boxWidth);
    var skybox = new THREE.Mesh(
      new THREE.BoxGeometry(boxWidth, boxWidth, boxWidth),
      new THREE.MeshBasicMaterial({
        map: texture,
        color: 0x01BE00,
        side: THREE.BackSide
      })
    );
    scene.add(skybox);
    
    var floorPos = -1.5;
    var floor = new THREE.Mesh(
      new THREE.PlaneGeometry(100, 100, 2, 2),
      new THREE.MeshBasicMaterial({color: 'white'})
    );
    floor.position.set(0, floorPos, 0);
    floor.rotation.x = -Math.PI / 2;
    scene.add(floor);
    
    var thingDistance = -1;
    
    var box = new THREE.Mesh(
      new THREE.BoxGeometry(0.5, 0.5, 0.5),
      new THREE.MeshNormalMaterial()
      );
    box.position.set(-0.5, floorPos, thingDistance);
    var boxPos = box.position.clone();
    scene.add(box);
    
    var ball = new THREE.Mesh(
      new THREE.SphereGeometry(0.3),
      new THREE.MeshNormalMaterial()
      );
    ball.position.set(0.5, floorPos, thingDistance);
    var ballPos = ball.position.clone();
    scene.add(ball);
    
    var pivot = new THREE.Object3D();
    scene.add(pivot);
    
    var ballInFront = false;
    document.addEventListener('click', function () {
      if (ballInFront) {
        scene.add(ball);
        ball.position.copy(ballPos);
        
        pivot.add(box);
        box.position.set(0, 0, thingDistance);
        ballInFront = false;
      }
      else {
        scene.add(box);
        box.position.copy(boxPos);
        
        pivot.add(ball);
        ball.position.set(0, 0, thingDistance);
        ballInFront = true;
      }
    });
    
    
    var yaxis = new THREE.Vector3(0, 1, 0);
    var zaxis = new THREE.Vector3(0, 0, 1);
    function animate(timestamp) {
      controls.update();
      
      var direction = zaxis.clone();
      // Apply the camera's quaternion onto the unit vector of one of the axes
      // of our desired rotation plane (the z axis of the xz plane, in this case).
      direction.applyQuaternion(camera.quaternion);
      // Project the direction vector onto the y axis to get the y component
      // of the direction.
      var ycomponent = yaxis.clone().multiplyScalar(direction.dot(yaxis));
      // Subtract the y component from the direction vector so that we are
      // left with the x and z components.
      direction.sub(ycomponent);
      // Normalize the direction into a unit vector again.
      direction.normalize();
      // Set the pivot's quaternion to the rotation required to get from the z axis
      // to the xz component of the camera's direction.
      pivot.quaternion.setFromUnitVectors(zaxis, direction);
      
      pivot.position.copy(camera.position);
      
      manager.render(scene, camera, timestamp);
      requestAnimationFrame(animate);
    }
    animate();
    
    function onKey(event) {
      if (event.keyCode == 90) { // z
        controls.resetSensor();
      }
    }
    window.addEventListener('keydown', onKey, true);
    <!DOCTYPE html>
    <html>
    <head>
    <meta name="description" content="SO 34447119">
      <meta charset="utf-8">
      <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
      <meta name="mobile-web-app-capable" content="yes">
      <meta name="apple-mobile-web-app-capable" content="yes" />
      <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
      <title>JS Bin</title>
      <script src="https://cdn.rawgit.com/borismus/webvr-boilerplate/0.2.2/bower_components/threejs/build/three.js"></script>
      <script src="https://cdn.rawgit.com/borismus/webvr-boilerplate/0.2.2/bower_components/threejs/examples/js/controls/VRControls.js"></script>
      <script src="https://cdn.rawgit.com/borismus/webvr-boilerplate/0.2.2/bower_components/threejs/examples/js/effects/VREffect.js"></script>
      <script src="https://cdn.rawgit.com/borismus/webvr-boilerplate/0.2.2/bower_components/webvr-polyfill/build/webvr-polyfill.js"></script>
      <script src="https://cdn.rawgit.com/borismus/webvr-boilerplate/0.2.2/build/webvr-manager.js"></script>
    </head>
    <body>
    </body> 
    </html>