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.
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.