three.jswebvrwebxr

Locking the camera position in WebVR / WebXR


What's the best way to lock the camera position to a single point in WebVR/WebXR using three.js?

The user would still need to be able to rotate their head, but their head movement shouldn't change the position (x,y,z) of the camera.


Solution

  • See update for simple solution

    This ability was misguidedly, and explicitly removed from the WebXR spec.

    Their "trivial" stripping of positional data example is in their 360-photos.html example, and it's skyboxMaterial class's vertex shader that's eaten by their convoluted renderer.

    specifically:

    get vertexSource() {
        return `
        uniform int EYE_INDEX;
        uniform vec4 texCoordScaleOffset[2];
        attribute vec3 POSITION;
        attribute vec2 TEXCOORD_0;
        varying vec2 vTexCoord;
    
        vec4 vertex_main(mat4 proj, mat4 view, mat4 model) {
          vec4 scaleOffset = texCoordScaleOffset[EYE_INDEX];
          vTexCoord = (TEXCOORD_0 * scaleOffset.xy) + scaleOffset.zw;
          // Drop the translation portion of the view matrix
          view[3].xyz = vec3(0.0, 0.0, 0.0);
          vec4 out_vec = proj * view * model * vec4(POSITION, 1.0);
    
          // Returning the W component for both Z and W forces the geometry depth to
          // the far plane. When combined with a depth func of LEQUAL this makes the
          // sky write to any depth fragment that has not been written to yet.
          return out_vec.xyww;
        }`;
      }
    

    Nice and trivial... /s

    Hopefully this helps, I'm currently working through the same issue, and if/when I overcome it, I'll update this answer.

    UPDATE 2: As promised, instead of modifying each shader to support this capability. Do the following when processing each xrPose's view:

        //NOTE: Uses the gl-matrix.js library, albeit slightly modified
        //to add vec3.multiplyBy. Which is used to multiply a vector
        //by a single value.
        
        let dist;
        let poseMaxDist = 0.4; //0.4M or 1.2ft
        
        let calculatedViewPos;
        let viewRotAsQuat;
        let vector;
    
        let origin = vec3.create();
        let framePose = vec3.create();
        let poseToBounds = vec3.create();
        let elasticTransformMatrix = mat4.create();
    
        let view = pose.views[viewIdx];
        //If positionDisabled, negate headset position changes, while maintaining
        //eye offset which allows for limited translation as users head does
        //move laterally when looking around.
        if(_positionDisabled){
            //DOMPoint to vec3 easier calculations.
            framePose = vec3.fromValues(
                pose.transform.position.x,
                pose.transform.position.y,
                pose.transform.position.z);
    
            //Distance from the origin
            dist = vec3.distance(origin, framePose);
    
            if(dist >= poseMaxDist){
                //calculation 'origin' == A
                //framePose == B
                let AB = vec3.create();
                let AC = vec3.create();
                let C = vec3.create();
                let CB = vec3.create();
    
                //Vector from origin to pose
                vec3.subtract(AB, framePose, origin);
    
                //Unit vector from origin to pose
                vec3.normalize(AB, AB);
    
                //Max allowed vector from origin to pose
                vec3.multiplyBy(AC, AB, poseMaxDist);
    
                //Limit point from origin to pose using max allowed vector  
                vec3.add(C, origin, AC);
          
                //vector from pose to limit point, use to shift view
                vec3.subtract(poseToBounds, C, framePose);
    
                //vector from limit point to pose, use to shift origin
                vec3.subtract(CB, framePose, C);
    
                //Shift calculation 'origin'
                vec3.add(origin, origin, CB);
    
                //adjust view matrix using the caluclated origin,
                //and the vector from the pose to the limit point.
                calculatedViewPos = vec4.fromValues(
                    view.transform.position.x - origin[0] + poseToBounds[0],
                    view.transform.position.y - origin[1] + poseToBounds[1],
                    view.transform.position.z - origin[2] + poseToBounds[2],
                    view.transform.position.w);
    
            }else{
                //adjust view matrix using the caluclated origin
                calculatedViewPos = vec4.fromValues(
                    view.transform.position.x - origin[0],
                    view.transform.position.y - origin[1],
                    view.transform.position.z - origin[2],
                    view.transform.position.w);
            }
    
            //Changing the DOMPoint to a quat for easier matrix calculation.
            viewRotAsQuat = quat.fromValues(
                view.transform.orientation.x,
                view.transform.orientation.y,
                view.transform.orientation.z,
                view.transform.orientation.w
            );
    
            mat4.fromRotationTranslation(elasticTransformMatrix, viewRotAsQuat, calculatedViewPos)
    
            mat4.invert(elasticTransformMatrix, elasticTransformMatrix);
                
            mat4.multiply(modelViewMatrix, elasticTransformMatrix, entity.transformMatrix);
    
        }else{
            mat4.multiply(modelViewMatrix, view.transform.inverse.matrix, entity.transformMatrix);
        }
    

    FYI: you will want to optimize the variable use to avoid extraneous allocations. I left them in to better visualize what each calculation is using on.