UNPKG

@pmndrs/xr

Version:
198 lines (197 loc) 8.85 kB
import { CylinderGeometry, DepthTexture, HalfFloatType, LinearFilter, Matrix4, PlaneGeometry, Quaternion, SphereGeometry, SRGBColorSpace, Texture, Vector3, VideoTexture, WebGLRenderTarget, } from 'three'; import { getSpaceFromAncestors } from './space.js'; import { 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(); function matrixToRigidTransform(matrix, scaleTarget = scaleHelper) { matrix.decompose(vectorHelper, quaternionHelper, scaleTarget); 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 = 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), }); }