@pmndrs/xr
Version:
VR/AR for threejs
205 lines (204 loc) • 9.13 kB
JavaScript
import { CylinderGeometry, DepthTexture, HalfFloatType, LinearFilter, Matrix4, PlaneGeometry, Quaternion, SphereGeometry, SRGBColorSpace, Texture, Vector3, VideoTexture, WebGLRenderTarget, } from 'three';
import { getSpaceFromAncestors } from './space.js';
import { nanToDefault, toDOMPointInit } from './utils.js';
const DefaultCentralAngle = (60 / 180) * Math.PI;
const DefaultCentralHorizontalAngle = (60 / 180) * Math.PI;
const DefaultLowerVerticalAngle = (-30 / 180) * Math.PI;
const DefaultUpperVerticalAngle = (30 / 180) * Math.PI;
export function createXRLayer(src, state, originReferenceSpace, xrManager, relativeTo, options, properties) {
return src instanceof HTMLVideoElement
? createXRVideoLayer(src, state, originReferenceSpace, relativeTo, options, properties)
: createXRNormalLayer(src, state.origin, originReferenceSpace, xrManager, relativeTo, options, properties);
}
function createXRVideoLayer(src, state, originReferenceSpace, relativeTo, { invertStereo, layout, shape = 'quad' }, properties = {}) {
const space = getSpaceFromAncestors(relativeTo, state.origin, originReferenceSpace, matrixHelper);
const transform = matrixToRigidTransform(matrixHelper, scaleHelper);
const init = {
invertStereo,
layout,
space,
transform,
};
applyXRLayerScale(shape, init, properties.centralAngle, scaleHelper);
const fnName = `create${capitalize(shape)}Layer`;
const layer = state.mediaBinding?.[fnName](src, init);
if (layer == null) {
return undefined;
}
updateXRLayerProperties(layer, properties);
return layer;
}
function createXRNormalLayer(src, origin, originReferenceSpace, xrManager, relativeTo, { shape = 'quad', ...options }, properties = {}) {
const space = getSpaceFromAncestors(relativeTo, origin, originReferenceSpace, matrixHelper);
const transform = matrixToRigidTransform(matrixHelper, scaleHelper);
const init = {
...options,
isStatic: !(src instanceof WebGLRenderTarget),
textureType: 'texture',
viewPixelWidth: options.layout === 'stereo-left-right' ? src.width / 2 : src.width,
viewPixelHeight: options.layout === 'stereo-top-bottom' ? src.height / 2 : src.height,
space,
transform,
};
applyXRLayerScale(shape, init, properties.centralAngle, scaleHelper);
const fnName = `create${capitalize(shape)}Layer`;
const layer = xrManager.getBinding()?.[fnName](init);
if (layer == null) {
return undefined;
}
updateXRLayerProperties(layer, properties);
return layer;
}
const matrixHelper = new Matrix4();
const vectorHelper = new Vector3();
const quaternionHelper = new Quaternion();
const scaleHelper = new Vector3();
/**
* @param matrix is allowed to contain nan values
*/
function matrixToRigidTransform(matrix, scaleTarget = scaleHelper) {
//assume matrix can contain nan values
matrix.decompose(vectorHelper, quaternionHelper, scaleTarget);
scaleTarget.x = nanToDefault(scaleTarget.x);
scaleTarget.y = nanToDefault(scaleTarget.y);
scaleTarget.z = nanToDefault(scaleTarget.z);
return new XRRigidTransform(toDOMPointInit(vectorHelper), toDOMPointInit(quaternionHelper));
}
const segmentPerAngle = 64 / Math.PI;
function computeSegmentAmount(angle) {
return Math.ceil(angle * segmentPerAngle);
}
export function setXRLayerRenderTarget(renderer, renderTarget, layerEntry, frame) {
if (layerEntry != null && frame != null) {
const subImage = renderer.xr.getBinding().getSubImage(layerEntry.layer, frame);
renderer.setRenderTargetTextures(renderTarget, subImage.colorTexture);
}
renderer.setRenderTarget(renderTarget);
}
export function createXRLayerGeometry(shape, properties) {
switch (shape) {
case 'cylinder':
const centralAngle = properties.centralAngle ?? DefaultCentralAngle;
return new CylinderGeometry(1, 1, 1, computeSegmentAmount(centralAngle), 1, true, Math.PI - centralAngle / 2, centralAngle).scale(-1, 1, 1);
case 'equirect': {
const centralHorizontalAngle = properties.centralHorizontalAngle ?? DefaultCentralHorizontalAngle;
const upperVerticalAngle = properties.upperVerticalAngle ?? DefaultUpperVerticalAngle;
const lowerVerticalAngle = properties.lowerVerticalAngle ?? DefaultLowerVerticalAngle;
const centralVerticalAngle = upperVerticalAngle - lowerVerticalAngle;
return new SphereGeometry(1, computeSegmentAmount(centralHorizontalAngle), computeSegmentAmount(centralVerticalAngle), -Math.PI / 2 - centralHorizontalAngle / 2, centralHorizontalAngle, Math.PI / 2 - upperVerticalAngle, centralVerticalAngle).scale(-1, 1, 1);
}
case 'quad':
return new PlaneGeometry();
}
}
function capitalize(text) {
return `${text[0].toUpperCase()}${text.slice(1)}`;
}
export function updateXRLayerProperties(target, properties = {}) {
target.chromaticAberrationCorrection = properties.chromaticAberrationCorrection;
target.quality = properties.quality ?? 'default';
target.blendTextureSourceAlpha = properties.blendTextureSourceAlpha ?? false;
if (target instanceof XRCylinderLayer) {
target.centralAngle = properties?.centralAngle ?? DefaultCentralAngle;
return;
}
if (target instanceof XREquirectLayer) {
target.centralHorizontalAngle = properties?.centralHorizontalAngle ?? DefaultCentralHorizontalAngle;
target.lowerVerticalAngle = properties?.lowerVerticalAngle ?? DefaultLowerVerticalAngle;
target.upperVerticalAngle = properties?.upperVerticalAngle ?? DefaultUpperVerticalAngle;
}
}
export function setupXRImageLayer(renderer, store, layer, src) {
let stop = false;
const draw = async () => {
const frame = await store.requestFrame();
if (stop) {
return;
}
writeContentToXRLayer(renderer, layer, frame, src);
};
layer.addEventListener('redraw', draw);
draw();
return () => {
stop = true;
layer.removeEventListener('redraw', draw);
};
}
export async function waitForXRLayerSrcSize(src) {
if (src instanceof HTMLImageElement && !src.complete) {
await new Promise((resolve) => {
const onResolve = () => {
resolve();
src.removeEventListener('load', onResolve);
};
src.addEventListener('load', onResolve);
});
}
if (src instanceof HTMLVideoElement && src.readyState < 1) {
return new Promise((resolve) => {
const onResolve = () => {
resolve();
src.removeEventListener('loadedmetadata', onResolve);
};
src.addEventListener('loadedmetadata', onResolve);
});
}
}
export function getXRLayerSrcTexture(src) {
if (src instanceof WebGLRenderTarget) {
return src.texture;
}
const texture = src instanceof HTMLVideoElement ? new VideoTexture(src) : new Texture(src);
texture.colorSpace = SRGBColorSpace;
texture.needsUpdate = true;
return texture;
}
function writeContentToXRLayer(renderer, layer, frame, content) {
const context = renderer.getContext();
const subImage = renderer.xr.getBinding().getSubImage(layer, frame);
renderer.state.bindTexture(context.TEXTURE_2D, subImage.colorTexture);
context.pixelStorei(context.UNPACK_FLIP_Y_WEBGL, true);
context.texSubImage2D(context.TEXTURE_2D, 0, 0, 0, content.width, content.height, context.RGBA, context.UNSIGNED_BYTE, content);
}
export function updateXRLayerTransform(state, target, centralAngle, relativeTo) {
if (state.originReferenceSpace == null) {
return;
}
target.space = getSpaceFromAncestors(relativeTo, state.origin, state.originReferenceSpace, matrixHelper);
target.transform = matrixToRigidTransform(matrixHelper, scaleHelper);
applyXRLayerScale(getLayerShape(target), target, centralAngle, scaleHelper);
}
function applyXRLayerScale(shape, target, centralAngle, scale) {
if (shape === 'cylinder') {
//0.5 * avg of x and z axis
const scaleXZ = (scale.x + scale.z) / 2;
const radius = scaleXZ;
const layerWidth = radius * (centralAngle ?? DefaultCentralAngle);
target.radius = radius;
target.aspectRatio = scale.y === 0 ? 1 : layerWidth / scale.y;
}
else if (shape === 'quad') {
target.width = scale.x / 2;
target.height = scale.y / 2;
}
else {
target.radius = (scale.x + scale.y + scale.z) / 3;
}
}
export function getLayerShape(layer) {
if (layer instanceof XRCylinderLayer) {
return 'cylinder';
}
if (layer instanceof XREquirectLayer) {
return 'equirect';
}
return 'quad';
}
export function createXRLayerRenderTarget(pixelWidth, pixelHeight, dpr) {
return new WebGLRenderTarget(pixelWidth * dpr, pixelHeight * dpr, {
minFilter: LinearFilter,
magFilter: LinearFilter,
type: HalfFloatType,
depthTexture: new DepthTexture(pixelWidth, pixelHeight),
});
}