starry / backend /libs /three /renderers /webxr /WebXRManager.js
k-l-lambda's picture
feat: add Python ML services (CPU mode) with model download
2b7aae2
import { ArrayCamera } from '../../cameras/ArrayCamera.js';
import { EventDispatcher } from '../../core/EventDispatcher.js';
import { PerspectiveCamera } from '../../cameras/PerspectiveCamera.js';
import { Vector3 } from '../../math/Vector3.js';
import { Vector4 } from '../../math/Vector4.js';
import { WebGLAnimation } from '../webgl/WebGLAnimation.js';
import { WebGLRenderTarget } from '../WebGLRenderTarget.js';
import { WebXRController } from './WebXRController.js';
import { DepthTexture } from '../../textures/DepthTexture.js';
import { WebGLMultisampleRenderTarget } from '../WebGLMultisampleRenderTarget.js';
import { DepthFormat, DepthStencilFormat, RGBAFormat, sRGBEncoding, UnsignedByteType, UnsignedShortType, UnsignedInt248Type } from '../../constants.js';
class WebXRManager extends EventDispatcher {
constructor(renderer, gl) {
super();
const scope = this;
let session = null;
let framebufferScaleFactor = 1.0;
let referenceSpace = null;
let referenceSpaceType = 'local-floor';
const hasMultisampledRenderToTexture = renderer.extensions.has('WEBGL_multisampled_render_to_texture');
let pose = null;
let glBinding = null;
let glProjLayer = null;
let glBaseLayer = null;
let isMultisample = false;
let xrFrame = null;
const attributes = gl.getContextAttributes();
let initialRenderTarget = null;
let newRenderTarget = null;
const controllers = [];
const inputSourcesMap = new Map();
//
const cameraL = new PerspectiveCamera();
cameraL.layers.enable(1);
cameraL.viewport = new Vector4();
const cameraR = new PerspectiveCamera();
cameraR.layers.enable(2);
cameraR.viewport = new Vector4();
const cameras = [cameraL, cameraR];
const cameraVR = new ArrayCamera();
cameraVR.layers.enable(1);
cameraVR.layers.enable(2);
let _currentDepthNear = null;
let _currentDepthFar = null;
//
this.cameraAutoUpdate = true;
this.enabled = false;
this.isPresenting = false;
this.getController = function (index) {
let controller = controllers[index];
if (controller === undefined) {
controller = new WebXRController();
controllers[index] = controller;
}
return controller.getTargetRaySpace();
};
this.getControllerGrip = function (index) {
let controller = controllers[index];
if (controller === undefined) {
controller = new WebXRController();
controllers[index] = controller;
}
return controller.getGripSpace();
};
this.getHand = function (index) {
let controller = controllers[index];
if (controller === undefined) {
controller = new WebXRController();
controllers[index] = controller;
}
return controller.getHandSpace();
};
//
function onSessionEvent(event) {
const controller = inputSourcesMap.get(event.inputSource);
if (controller) {
controller.dispatchEvent({ type: event.type, data: event.inputSource });
}
}
function onSessionEnd() {
inputSourcesMap.forEach(function (controller, inputSource) {
controller.disconnect(inputSource);
});
inputSourcesMap.clear();
_currentDepthNear = null;
_currentDepthFar = null;
// restore framebuffer/rendering state
renderer.setRenderTarget(initialRenderTarget);
glBaseLayer = null;
glProjLayer = null;
glBinding = null;
session = null;
newRenderTarget = null;
//
animation.stop();
scope.isPresenting = false;
scope.dispatchEvent({ type: 'sessionend' });
}
this.setFramebufferScaleFactor = function (value) {
framebufferScaleFactor = value;
if (scope.isPresenting === true) {
console.warn('THREE.WebXRManager: Cannot change framebuffer scale while presenting.');
}
};
this.setReferenceSpaceType = function (value) {
referenceSpaceType = value;
if (scope.isPresenting === true) {
console.warn('THREE.WebXRManager: Cannot change reference space type while presenting.');
}
};
this.getReferenceSpace = function () {
return referenceSpace;
};
this.getBaseLayer = function () {
return glProjLayer !== null ? glProjLayer : glBaseLayer;
};
this.getBinding = function () {
return glBinding;
};
this.getFrame = function () {
return xrFrame;
};
this.getSession = function () {
return session;
};
this.setSession = async function (value) {
session = value;
if (session !== null) {
initialRenderTarget = renderer.getRenderTarget();
session.addEventListener('select', onSessionEvent);
session.addEventListener('selectstart', onSessionEvent);
session.addEventListener('selectend', onSessionEvent);
session.addEventListener('squeeze', onSessionEvent);
session.addEventListener('squeezestart', onSessionEvent);
session.addEventListener('squeezeend', onSessionEvent);
session.addEventListener('end', onSessionEnd);
session.addEventListener('inputsourceschange', onInputSourcesChange);
if (attributes.xrCompatible !== true) {
await gl.makeXRCompatible();
}
if (session.renderState.layers === undefined || renderer.capabilities.isWebGL2 === false) {
const layerInit = {
antialias: session.renderState.layers === undefined ? attributes.antialias : true,
alpha: attributes.alpha,
depth: attributes.depth,
stencil: attributes.stencil,
framebufferScaleFactor: framebufferScaleFactor,
};
glBaseLayer = new XRWebGLLayer(session, gl, layerInit);
session.updateRenderState({ baseLayer: glBaseLayer });
newRenderTarget = new WebGLRenderTarget(glBaseLayer.framebufferWidth, glBaseLayer.framebufferHeight, {
format: RGBAFormat,
type: UnsignedByteType,
encoding: renderer.outputEncoding,
});
} else {
isMultisample = attributes.antialias;
let depthFormat = null;
let depthType = null;
let glDepthFormat = null;
if (attributes.depth) {
glDepthFormat = attributes.stencil ? gl.DEPTH24_STENCIL8 : gl.DEPTH_COMPONENT24;
depthFormat = attributes.stencil ? DepthStencilFormat : DepthFormat;
depthType = attributes.stencil ? UnsignedInt248Type : UnsignedShortType;
}
const projectionlayerInit = {
colorFormat: renderer.outputEncoding === sRGBEncoding ? gl.SRGB8_ALPHA8 : gl.RGBA8,
depthFormat: glDepthFormat,
scaleFactor: framebufferScaleFactor,
};
glBinding = new XRWebGLBinding(session, gl);
glProjLayer = glBinding.createProjectionLayer(projectionlayerInit);
session.updateRenderState({ layers: [glProjLayer] });
if (isMultisample) {
newRenderTarget = new WebGLMultisampleRenderTarget(glProjLayer.textureWidth, glProjLayer.textureHeight, {
format: RGBAFormat,
type: UnsignedByteType,
depthTexture: new DepthTexture(
glProjLayer.textureWidth,
glProjLayer.textureHeight,
depthType,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
depthFormat
),
stencilBuffer: attributes.stencil,
ignoreDepth: glProjLayer.ignoreDepthValues,
useRenderToTexture: hasMultisampledRenderToTexture,
encoding: renderer.outputEncoding,
});
} else {
newRenderTarget = new WebGLRenderTarget(glProjLayer.textureWidth, glProjLayer.textureHeight, {
format: RGBAFormat,
type: UnsignedByteType,
depthTexture: new DepthTexture(
glProjLayer.textureWidth,
glProjLayer.textureHeight,
depthType,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
depthFormat
),
stencilBuffer: attributes.stencil,
ignoreDepth: glProjLayer.ignoreDepthValues,
encoding: renderer.outputEncoding,
});
}
}
// Set foveation to maximum.
this.setFoveation(1.0);
referenceSpace = await session.requestReferenceSpace(referenceSpaceType);
animation.setContext(session);
animation.start();
scope.isPresenting = true;
scope.dispatchEvent({ type: 'sessionstart' });
}
};
function onInputSourcesChange(event) {
const inputSources = session.inputSources;
// Assign inputSources to available controllers
for (let i = 0; i < controllers.length; i++) {
inputSourcesMap.set(inputSources[i], controllers[i]);
}
// Notify disconnected
for (let i = 0; i < event.removed.length; i++) {
const inputSource = event.removed[i];
const controller = inputSourcesMap.get(inputSource);
if (controller) {
controller.dispatchEvent({ type: 'disconnected', data: inputSource });
inputSourcesMap.delete(inputSource);
}
}
// Notify connected
for (let i = 0; i < event.added.length; i++) {
const inputSource = event.added[i];
const controller = inputSourcesMap.get(inputSource);
if (controller) {
controller.dispatchEvent({ type: 'connected', data: inputSource });
}
}
}
//
const cameraLPos = new Vector3();
const cameraRPos = new Vector3();
/**
* Assumes 2 cameras that are parallel and share an X-axis, and that
* the cameras' projection and world matrices have already been set.
* And that near and far planes are identical for both cameras.
* Visualization of this technique: https://computergraphics.stackexchange.com/a/4765
*/
function setProjectionFromUnion(camera, cameraL, cameraR) {
cameraLPos.setFromMatrixPosition(cameraL.matrixWorld);
cameraRPos.setFromMatrixPosition(cameraR.matrixWorld);
const ipd = cameraLPos.distanceTo(cameraRPos);
const projL = cameraL.projectionMatrix.elements;
const projR = cameraR.projectionMatrix.elements;
// VR systems will have identical far and near planes, and
// most likely identical top and bottom frustum extents.
// Use the left camera for these values.
const near = projL[14] / (projL[10] - 1);
const far = projL[14] / (projL[10] + 1);
const topFov = (projL[9] + 1) / projL[5];
const bottomFov = (projL[9] - 1) / projL[5];
const leftFov = (projL[8] - 1) / projL[0];
const rightFov = (projR[8] + 1) / projR[0];
const left = near * leftFov;
const right = near * rightFov;
// Calculate the new camera's position offset from the
// left camera. xOffset should be roughly half `ipd`.
const zOffset = ipd / (-leftFov + rightFov);
const xOffset = zOffset * -leftFov;
// TODO: Better way to apply this offset?
cameraL.matrixWorld.decompose(camera.position, camera.quaternion, camera.scale);
camera.translateX(xOffset);
camera.translateZ(zOffset);
camera.matrixWorld.compose(camera.position, camera.quaternion, camera.scale);
camera.matrixWorldInverse.copy(camera.matrixWorld).invert();
// Find the union of the frustum values of the cameras and scale
// the values so that the near plane's position does not change in world space,
// although must now be relative to the new union camera.
const near2 = near + zOffset;
const far2 = far + zOffset;
const left2 = left - xOffset;
const right2 = right + (ipd - xOffset);
const top2 = ((topFov * far) / far2) * near2;
const bottom2 = ((bottomFov * far) / far2) * near2;
camera.projectionMatrix.makePerspective(left2, right2, top2, bottom2, near2, far2);
}
function updateCamera(camera, parent) {
if (parent === null) {
camera.matrixWorld.copy(camera.matrix);
} else {
camera.matrixWorld.multiplyMatrices(parent.matrixWorld, camera.matrix);
}
camera.matrixWorldInverse.copy(camera.matrixWorld).invert();
}
this.updateCamera = function (camera) {
if (session === null) return;
cameraVR.near = cameraR.near = cameraL.near = camera.near;
cameraVR.far = cameraR.far = cameraL.far = camera.far;
if (_currentDepthNear !== cameraVR.near || _currentDepthFar !== cameraVR.far) {
// Note that the new renderState won't apply until the next frame. See #18320
session.updateRenderState({
depthNear: cameraVR.near,
depthFar: cameraVR.far,
});
_currentDepthNear = cameraVR.near;
_currentDepthFar = cameraVR.far;
}
const parent = camera.parent;
const cameras = cameraVR.cameras;
updateCamera(cameraVR, parent);
for (let i = 0; i < cameras.length; i++) {
updateCamera(cameras[i], parent);
}
cameraVR.matrixWorld.decompose(cameraVR.position, cameraVR.quaternion, cameraVR.scale);
// update user camera and its children
camera.position.copy(cameraVR.position);
camera.quaternion.copy(cameraVR.quaternion);
camera.scale.copy(cameraVR.scale);
camera.matrix.copy(cameraVR.matrix);
camera.matrixWorld.copy(cameraVR.matrixWorld);
const children = camera.children;
for (let i = 0, l = children.length; i < l; i++) {
children[i].updateMatrixWorld(true);
}
// update projection matrix for proper view frustum culling
if (cameras.length === 2) {
setProjectionFromUnion(cameraVR, cameraL, cameraR);
} else {
// assume single camera setup (AR)
cameraVR.projectionMatrix.copy(cameraL.projectionMatrix);
}
};
this.getCamera = function () {
return cameraVR;
};
this.getFoveation = function () {
if (glProjLayer !== null) {
return glProjLayer.fixedFoveation;
}
if (glBaseLayer !== null) {
return glBaseLayer.fixedFoveation;
}
return undefined;
};
this.setFoveation = function (foveation) {
// 0 = no foveation = full resolution
// 1 = maximum foveation = the edges render at lower resolution
if (glProjLayer !== null) {
glProjLayer.fixedFoveation = foveation;
}
if (glBaseLayer !== null && glBaseLayer.fixedFoveation !== undefined) {
glBaseLayer.fixedFoveation = foveation;
}
};
// Animation Loop
let onAnimationFrameCallback = null;
function onAnimationFrame(time, frame) {
pose = frame.getViewerPose(referenceSpace);
xrFrame = frame;
if (pose !== null) {
const views = pose.views;
if (glBaseLayer !== null) {
renderer.setRenderTargetFramebuffer(newRenderTarget, glBaseLayer.framebuffer);
renderer.setRenderTarget(newRenderTarget);
}
let cameraVRNeedsUpdate = false;
// check if it's necessary to rebuild cameraVR's camera list
if (views.length !== cameraVR.cameras.length) {
cameraVR.cameras.length = 0;
cameraVRNeedsUpdate = true;
}
for (let i = 0; i < views.length; i++) {
const view = views[i];
let viewport = null;
if (glBaseLayer !== null) {
viewport = glBaseLayer.getViewport(view);
} else {
const glSubImage = glBinding.getViewSubImage(glProjLayer, view);
viewport = glSubImage.viewport;
// For side-by-side projection, we only produce a single texture for both eyes.
if (i === 0) {
renderer.setRenderTargetTextures(
newRenderTarget,
glSubImage.colorTexture,
glProjLayer.ignoreDepthValues ? undefined : glSubImage.depthStencilTexture
);
renderer.setRenderTarget(newRenderTarget);
}
}
const camera = cameras[i];
camera.matrix.fromArray(view.transform.matrix);
camera.projectionMatrix.fromArray(view.projectionMatrix);
camera.viewport.set(viewport.x, viewport.y, viewport.width, viewport.height);
if (i === 0) {
cameraVR.matrix.copy(camera.matrix);
}
if (cameraVRNeedsUpdate === true) {
cameraVR.cameras.push(camera);
}
}
}
//
const inputSources = session.inputSources;
for (let i = 0; i < controllers.length; i++) {
const controller = controllers[i];
const inputSource = inputSources[i];
controller.update(inputSource, frame, referenceSpace);
}
if (onAnimationFrameCallback) onAnimationFrameCallback(time, frame);
xrFrame = null;
}
const animation = new WebGLAnimation();
animation.setAnimationLoop(onAnimationFrame);
this.setAnimationLoop = function (callback) {
onAnimationFrameCallback = callback;
};
this.dispose = function () {};
}
}
export { WebXRManager };