From 5948f6e18d35e66cdbef6b85c014f6594a982b1a Mon Sep 17 00:00:00 2001 From: Michael Bashurov Date: Fri, 9 Jun 2023 19:09:46 +0300 Subject: [PATCH 01/18] wip --- examples/package.json | 4 +- .../src/assets/brown_photostudio_04_256.hdr | Bin 0 -> 104760 bytes examples/src/demos/ControllersEnvMap.tsx | 56 +++++++++++++ examples/src/demos/index.tsx | 3 +- examples/src/global.d.ts | 4 + examples/vite.config.ts | 3 +- examples/yarn.lock | 77 ++++++++++-------- src/Controllers.tsx | 62 ++++++-------- src/XRController.tsx | 36 ++++++++ src/XRControllerModelFactory.ts | 25 +++++- vite.config.js | 1 + vitest.config.ts | 1 + 12 files changed, 195 insertions(+), 77 deletions(-) create mode 100644 examples/src/assets/brown_photostudio_04_256.hdr create mode 100644 examples/src/demos/ControllersEnvMap.tsx create mode 100644 examples/src/global.d.ts diff --git a/examples/package.json b/examples/package.json index b71e161..9b72aa8 100644 --- a/examples/package.json +++ b/examples/package.json @@ -8,8 +8,8 @@ "serve": "vite preview" }, "dependencies": { - "@react-three/drei": "^9.53.0", - "@react-three/fiber": "^8.10.0", + "@react-three/drei": "^9.74.7", + "@react-three/fiber": "^8.13.0", "@vanilla-extract/css": "^1.9.5", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/examples/src/assets/brown_photostudio_04_256.hdr b/examples/src/assets/brown_photostudio_04_256.hdr new file mode 100644 index 0000000000000000000000000000000000000000..8342846bc3351459c10fac77a9c188c6b4f0d9bc GIT binary patch literal 104760 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 L8$YT4=#&3H4XsQ} literal 0 HcmV?d00001 diff --git a/examples/src/demos/ControllersEnvMap.tsx b/examples/src/demos/ControllersEnvMap.tsx new file mode 100644 index 0000000..9e4e17e --- /dev/null +++ b/examples/src/demos/ControllersEnvMap.tsx @@ -0,0 +1,56 @@ +import {Canvas, dispose, useThree} from '@react-three/fiber' +import {XR, VRButton, Controllers, useXR} from '@react-three/xr' +import { PMREMGenerator, Texture } from 'three' +import { RGBELoader } from 'three-stdlib' +import {useEffect, useRef, useState} from 'react' +import EnvMap from "../assets/brown_photostudio_04_256.hdr"; +const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) +function ControllersWithEnvMap() { + const renderer = useThree(({ gl }) => gl) + const isPresenting = useXR(({ isPresenting }) => isPresenting) + const [envMap, setEnvMap] = useState() + const g = useRef() + useEffect(() => { + const pmremGenerator = new PMREMGenerator(renderer) + pmremGenerator.compileEquirectangularShader() + g.current = pmremGenerator + }, []) + useEffect(() => { + const foo = async () => { + const pmremGenerator = g.current! + if (!isPresenting) { + return + } + // if (isPresenting) return + await delay(100) + const rgbeLoader = new RGBELoader() + /** + * Чтобы воспроизвести, нажми Enter VR раньше, чем 5 сек + * Чтобы починилось, нажми позже, когда карта загрузилась + */ + const dataTexture = await rgbeLoader.loadAsync(EnvMap) + const radianceMap = pmremGenerator.fromEquirectangular(dataTexture).texture + setEnvMap(radianceMap) + pmremGenerator.dispose() + console.log('done radianceMap') + } + foo() + }, [isPresenting]) + + return +} + +export default function () { + return ( + <> + console.error(e)} /> + + + {/**/} + {/**/} + + + + + ) +} diff --git a/examples/src/demos/index.tsx b/examples/src/demos/index.tsx index dc69ba6..77bd846 100644 --- a/examples/src/demos/index.tsx +++ b/examples/src/demos/index.tsx @@ -5,5 +5,6 @@ const HitTest = { Component: lazy(() => import('./HitTest')) } const Player = { Component: lazy(() => import('./Player')) } const Text = { Component: lazy(() => import('./Text')) } const Hands = { Component: lazy(() => import('./Hands')) } +const ControllersEnvMap = { Component: lazy(() => import('./ControllersEnvMap')) } -export { Interactive, HitTest, Player, Text, Hands } +export { Interactive, HitTest, Player, Text, Hands, ControllersEnvMap } diff --git a/examples/src/global.d.ts b/examples/src/global.d.ts new file mode 100644 index 0000000..6804e0f --- /dev/null +++ b/examples/src/global.d.ts @@ -0,0 +1,4 @@ +declare module '*.hdr' { + const path: string + export = path +} \ No newline at end of file diff --git a/examples/vite.config.ts b/examples/vite.config.ts index 1dafc4d..66cbd81 100644 --- a/examples/vite.config.ts +++ b/examples/vite.config.ts @@ -1,8 +1,9 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' -import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin'; +import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin' // https://vitejs.dev/config/ export default defineConfig({ + assetsInclude: ['**/*.hdr', '**/*.gltf'], plugins: [react(), vanillaExtractPlugin()] }) diff --git a/examples/yarn.lock b/examples/yarn.lock index 7516b06..30dbbef 100644 --- a/examples/yarn.lock +++ b/examples/yarn.lock @@ -249,7 +249,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.19.0" -"@babel/runtime@^7.11.2", "@babel/runtime@^7.16.7", "@babel/runtime@^7.17.8": +"@babel/runtime@^7.11.2", "@babel/runtime@^7.17.8": version "7.20.13" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.13.tgz#7055ab8a7cff2b8f6058bf6ae45ff84ad2aded4b" integrity sha512-gt3PKXs0DBoL9xCvOIIZ2NEqAGZqHjAnmVbfQtB620V0uReIQutpel14KcneZuer7UioY8ALKZ7iocavvzTNFA== @@ -504,6 +504,11 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" +"@mediapipe/tasks-vision@^0.10.0": + version "0.10.1" + resolved "https://registry.yarnpkg.com/@mediapipe/tasks-vision/-/tasks-vision-0.10.1.tgz#68047459352019cc141dc9c1d15c05b8ab689423" + integrity sha512-/zIKjOAIABx+KVfqe8hA6X2pxBGsBYlEtvD7/gpXecvzKefo/JQO6XaggmJul7+noaqiPYM0CVGZxmFJ2oTdSQ== + "@react-spring/animated@~9.6.1": version "9.6.1" resolved "https://registry.yarnpkg.com/@react-spring/animated/-/animated-9.6.1.tgz#ccc626d847cbe346f5f8815d0928183c647eb425" @@ -535,7 +540,7 @@ "@react-spring/rafz" "~9.6.1" "@react-spring/types" "~9.6.1" -"@react-spring/three@^9.3.1": +"@react-spring/three@~9.6.1": version "9.6.1" resolved "https://registry.yarnpkg.com/@react-spring/three/-/three-9.6.1.tgz#095fcd1dc6509127c33c14486d88289b89baeb9d" integrity sha512-Tyw2YhZPKJAX3t2FcqvpLRb71CyTe1GvT3V+i+xJzfALgpk10uPGdGaQQ5Xrzmok1340DAeg2pR/MCfaW7b8AA== @@ -550,16 +555,17 @@ resolved "https://registry.yarnpkg.com/@react-spring/types/-/types-9.6.1.tgz#913d3a68c5cbc1124fdb18eff919432f7b6abdde" integrity sha512-POu8Mk0hIU3lRXB3bGIGe4VHIwwDsQyoD1F394OK7STTiX9w4dG3cTLljjYswkQN+hDSHRrj4O36kuVa7KPU8Q== -"@react-three/drei@^9.53.0": - version "9.56.19" - resolved "https://registry.yarnpkg.com/@react-three/drei/-/drei-9.56.19.tgz#cc52febde5ca35de7218bb54c97439edc823a529" - integrity sha512-vbBEbxDrC1VZ16QFtJQoC0QK1d3HSFV0WxPL5wxXZcL1zI0GN9RvoWj9CF9z2ImsaAQYzCnEurPJbsKJopxwzQ== +"@react-three/drei@^9.74.7": + version "9.74.7" + resolved "https://registry.yarnpkg.com/@react-three/drei/-/drei-9.74.7.tgz#0b6d8316baa43307fd43a48c3d1812d126710695" + integrity sha512-rGl1WHEbVk83KDtRzpuQZkrOLGSmvpuu9uGEZpZzy1csxlVjOA8EWWwSCSKwmY/spWwcLnSIQ30haUMmL8ZSCw== dependencies: "@babel/runtime" "^7.11.2" - "@react-spring/three" "^9.3.1" + "@mediapipe/tasks-vision" "^0.10.0" + "@react-spring/three" "~9.6.1" "@use-gesture/react" "^10.2.24" - camera-controls "^2.0.1" - detect-gpu "^5.0.9" + camera-controls "^2.3.1" + detect-gpu "^5.0.14" glsl-noise "^0.0.0" lodash.clamp "^4.0.3" lodash.omit "^4.5.0" @@ -571,15 +577,15 @@ stats.js "^0.17.0" suspend-react "^0.0.8" three-mesh-bvh "^0.5.23" - three-stdlib "^2.21.8" + three-stdlib "^2.23.5" troika-three-text "^0.47.1" utility-types "^3.10.0" zustand "^3.5.13" -"@react-three/fiber@^8.10.0": - version "8.11.0" - resolved "https://registry.yarnpkg.com/@react-three/fiber/-/fiber-8.11.0.tgz#3ed67cc3a7951c2aed00fe02ee694a95744c50a5" - integrity sha512-n9eM7hVsHbecexKK0isvUOPq1SYMHcLhUTZsMZQSYo5RT1yjbgQbbrVtF9bXN9rQgrD9l3V3Ho3ckPp0cNNs1w== +"@react-three/fiber@^8.13.0": + version "8.13.0" + resolved "https://registry.yarnpkg.com/@react-three/fiber/-/fiber-8.13.0.tgz#c9eabe60f2276a66d7ce9a3b927083894f4202f9" + integrity sha512-hPFzFNgikEMyEbL+NpSA7q+UWZxInrrkJldWaCR2w34Fwf20x9p68bsyN0/yn9oM2VlWoJcJjR8hw1tN9AxHuA== dependencies: "@babel/runtime" "^7.17.8" "@types/react-reconciler" "^0.26.7" @@ -607,6 +613,11 @@ estree-walker "^2.0.2" picomatch "^2.3.1" +"@types/draco3d@^1.4.0": + version "1.4.2" + resolved "https://registry.yarnpkg.com/@types/draco3d/-/draco3d-1.4.2.tgz#7faccb809db2a5e19b9efb97c5f2eb9d64d527ea" + integrity sha512-goh23EGr6CLV6aKPwN1p8kBD/7tT5V/bLpToSbarKrwVejqNrspVrv8DhliteYkkhZYrlq/fwKZRRUzH4XN88w== + "@types/estree@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.0.tgz#5fb2e536c1ae9bf35366eed879e827fa59ca41c2" @@ -669,6 +680,11 @@ resolved "https://registry.yarnpkg.com/@types/webxr/-/webxr-0.5.0.tgz#aae1cef3210d88fd4204f8c33385a0bbc4da07c9" integrity sha512-IUMDPSXnYIbEO2IereEFcgcqfDREOgmbGqtrMpVPpACTU6pltYLwHgVkrnYv0XhWEcjio9sYEfIEzgn3c7nDqA== +"@types/webxr@^0.5.2": + version "0.5.2" + resolved "https://registry.yarnpkg.com/@types/webxr/-/webxr-0.5.2.tgz#5d9627b0ffe223aa3b166de7112ac8a9460dc54f" + integrity sha512-szL74BnIcok9m7QwYtVmQ+EdIKwbjPANudfuvDrAF8Cljg9MKUlIoc1w5tjj9PMpeSH3U1Xnx//czQybJ0EfSw== + "@use-gesture/core@10.2.24": version "10.2.24" resolved "https://registry.yarnpkg.com/@use-gesture/core/-/core-10.2.24.tgz#88d13a60954ba62463c774acb92d12bf7b3d810c" @@ -748,11 +764,6 @@ magic-string "^0.27.0" react-refresh "^0.14.0" -"@webgpu/glslang@^0.0.15": - version "0.0.15" - resolved "https://registry.yarnpkg.com/@webgpu/glslang/-/glslang-0.0.15.tgz#f5ccaf6015241e6175f4b90906b053f88483d1f2" - integrity sha512-niT+Prh3Aff8Uf1MVBVUsaNjFj9rJAKDXuoHIKiQbB+6IUP/3J3JIhBNyZ7lDhytvXxw6ppgnwKZdDJ08UMj4Q== - acorn@^8.8.2: version "8.8.2" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" @@ -794,10 +805,10 @@ browserslist@^4.21.3: node-releases "^2.0.8" update-browserslist-db "^1.0.10" -camera-controls@^2.0.1: - version "2.1.0" - resolved "https://registry.yarnpkg.com/camera-controls/-/camera-controls-2.1.0.tgz#75edd73935270af76b4202dc794cf022afcee4b0" - integrity sha512-9b2dpUZp+3Rfkh/E8dU9O9/rBbPDzyB5DBINktedRAF4I5ldZUgBiSYtFac7wF3yXNf4UH2pjP3uRcoAtXTh4A== +camera-controls@^2.3.1: + version "2.4.2" + resolved "https://registry.yarnpkg.com/camera-controls/-/camera-controls-2.4.2.tgz#815aa5d7c4c43054fc55fb8b6cc685a56540fea2" + integrity sha512-blYDPECYFT/4egDMNWqKc2lBrpOfIAjPPRUNVswQELPi8naGBXUvZM3sDJSNuIRaHqid+JKPtlcoZk+Cb+X5qg== caniuse-lite@^1.0.30001449: version "1.0.30001451" @@ -899,10 +910,10 @@ deepmerge@^4.2.2: resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.0.tgz#65491893ec47756d44719ae520e0e2609233b59b" integrity sha512-z2wJZXrmeHdvYJp/Ux55wIjqo81G5Bp4c+oELTW+7ar6SogWHajt5a9gO3s3IDaGSAXjDk0vlQKN3rms8ab3og== -detect-gpu@^5.0.9: - version "5.0.10" - resolved "https://registry.yarnpkg.com/detect-gpu/-/detect-gpu-5.0.10.tgz#4010b89a7f91a24ed8d2a136ee6ac816b39d0801" - integrity sha512-V0g0RhtlItrhgBM4/T/lTpjephr9b/xDAOtJZztGTvQxaPSMQ4EAiV9tdHL+4GcT1ATvYmMCm4QbrhyjdEH6Fw== +detect-gpu@^5.0.14: + version "5.0.27" + resolved "https://registry.yarnpkg.com/detect-gpu/-/detect-gpu-5.0.27.tgz#821d9331c87e32568c483d85e12a9adee43d7bb2" + integrity sha512-IDjjqTkS+f0xm/ntbD21IPYiF0srzpePC/hhUMmctEsoklZwJwStJiMi/KN0pnH0LjSsgjwbP+QwW7y+Qf4/SQ== dependencies: webgl-constants "^1.1.1" @@ -1413,14 +1424,14 @@ three-mesh-bvh@^0.5.23: resolved "https://registry.yarnpkg.com/three-mesh-bvh/-/three-mesh-bvh-0.5.23.tgz#08e5b629144b48b11acbd433519680e457d398ed" integrity sha512-nyk+MskdyDgECqkxdv57UjazqqhrMi+Al9PxJN6yFtx1CTW4r0eCQ27FtyYKY5gCIWhxjtNfWYDPVy8lzx6LkA== -three-stdlib@^2.21.8: - version "2.21.8" - resolved "https://registry.yarnpkg.com/three-stdlib/-/three-stdlib-2.21.8.tgz#37b11b7f62d07b10742c212153b14db21433b3c6" - integrity sha512-kqisiKvO4mSy59v5vWqBQSH8famLxp7Z51LxpMJI9GwDxqODaW02rhIwmjYDEzZWNFpjZpoDHVGbdpeHf8h3SA== +three-stdlib@^2.23.5: + version "2.23.9" + resolved "https://registry.yarnpkg.com/three-stdlib/-/three-stdlib-2.23.9.tgz#09c74fc6acced3d124e4f9d695156136c587a355" + integrity sha512-fYBClVGQptD7UZcoRZGNlR3sKcUW37hVPoEW1v68E4XuiwD0Ml/VqDUJ0yEMVE2DlooDvqgqv/rIcHC/B4N5pg== dependencies: - "@babel/runtime" "^7.16.7" + "@types/draco3d" "^1.4.0" "@types/offscreencanvas" "^2019.6.4" - "@webgpu/glslang" "^0.0.15" + "@types/webxr" "^0.5.2" chevrotain "^10.1.2" draco3d "^1.4.1" fflate "^0.6.9" diff --git a/src/Controllers.tsx b/src/Controllers.tsx index f323dbe..69e78c3 100644 --- a/src/Controllers.tsx +++ b/src/Controllers.tsx @@ -2,10 +2,11 @@ import * as React from 'react' import * as THREE from 'three' import { useFrame, Object3DNode, extend, createPortal } from '@react-three/fiber' import { useXR } from './XR' -import { XRController } from './XRController' +import { ControllerModel, XRController } from './XRController' import { useIsomorphicLayoutEffect } from './utils' -import { XRControllerModel, XRControllerModelFactory } from './XRControllerModelFactory' -import { XRControllerEvent } from './XREvents' +import { XRControllerModelFactory } from './XRControllerModelFactory' +import { useCallback, useEffect } from 'react' +import { Texture } from 'three' export interface RayProps extends Partial { /** The XRController to attach the ray to */ @@ -13,6 +14,7 @@ export interface RayProps extends Partial { /** Whether to hide the ray on controller blur. Default is `false` */ hideOnBlur?: boolean } + export const Ray = React.forwardRef(function Ray({ target, hideOnBlur = false, ...props }, forwardedRef) { const hoverState = useXR((state) => state.hoverState) const ray = React.useRef(null!) @@ -46,37 +48,6 @@ export const Ray = React.forwardRef(function Ray({ target, const modelFactory = new XRControllerModelFactory() -class ControllerModel extends THREE.Group { - readonly target: XRController - readonly xrControllerModel: XRControllerModel - - constructor(target: XRController) { - super() - this.xrControllerModel = new XRControllerModel() - this.target = target - this.add(this.xrControllerModel) - - this._onConnected = this._onConnected.bind(this) - this._onDisconnected = this._onDisconnected.bind(this) - - this.target.controller.addEventListener('connected', this._onConnected) - this.target.controller.addEventListener('disconnected', this._onDisconnected) - } - - private _onConnected(event: XRControllerEvent) { - modelFactory.initializeControllerModel(this.xrControllerModel, event) - } - - private _onDisconnected(_event: XRControllerEvent) { - this.xrControllerModel.disconnect() - } - - dispose() { - this.target.controller.removeEventListener('connected', this._onConnected) - this.target.controller.removeEventListener('disconnected', this._onDisconnected) - } -} - declare global { namespace JSX { interface IntrinsicElements { @@ -90,8 +61,11 @@ export interface ControllersProps { rayMaterial?: JSX.IntrinsicElements['meshBasicMaterial'] /** Whether to hide controllers' rays on blur. Default is `false` */ hideRaysOnBlur?: boolean + envMap?: Texture + envMapIntensity?: number } -export function Controllers({ rayMaterial = {}, hideRaysOnBlur = false }: ControllersProps) { + +export function Controllers({ rayMaterial = {}, hideRaysOnBlur = false, envMap, envMapIntensity }: ControllersProps) { const controllers = useXR((state) => state.controllers) const isHandTracking = useXR((state) => state.isHandTracking) const rayMaterialProps = React.useMemo( @@ -114,11 +88,27 @@ export function Controllers({ rayMaterial = {}, hideRaysOnBlur = false }: Contro } }, [controllers]) + const handleControllerModel = useCallback((r: ControllerModel | null, target: XRController) => { + target.controllerModel = r + }, []) + + useEffect(() => { + if (!envMap) { + return + } + for (const target of controllers) { + if (target.controllerModel) { + target.controllerModel.xrControllerModel.setEnvironmentMap(envMap, envMapIntensity) + console.log('updated environment map') + } + } + }, [envMap, envMapIntensity, controllers]) + return ( <> {controllers.map((target, i) => ( - {createPortal(, target.grip)} + {createPortal( handleControllerModel(r, target)} args={[target, modelFactory]} />, target.grip)} {createPortal( , target.controller diff --git a/src/XRController.tsx b/src/XRController.tsx index 8c85e8e..8d9b06d 100644 --- a/src/XRController.tsx +++ b/src/XRController.tsx @@ -1,5 +1,40 @@ import * as THREE from 'three' import { XRControllerEvent } from './XREvents' +import {XRControllerModel, XRControllerModelFactory} from "./XRControllerModelFactory"; + + +export class ControllerModel extends THREE.Group { + readonly target: XRController + readonly xrControllerModel: XRControllerModel + private modelFactory: XRControllerModelFactory; + + constructor(target: XRController, modelFactory: XRControllerModelFactory) { + super() + this.xrControllerModel = new XRControllerModel() + this.target = target + this.modelFactory = modelFactory + this.add(this.xrControllerModel) + + this._onConnected = this._onConnected.bind(this) + this._onDisconnected = this._onDisconnected.bind(this) + + this.target.controller.addEventListener('connected', this._onConnected) + this.target.controller.addEventListener('disconnected', this._onDisconnected) + } + + private _onConnected(event: XRControllerEvent) { + this.modelFactory.initializeControllerModel(this.xrControllerModel, event) + } + + private _onDisconnected(_event: XRControllerEvent) { + this.xrControllerModel.disconnect() + } + + dispose() { + this.target.controller.removeEventListener('connected', this._onConnected) + this.target.controller.removeEventListener('disconnected', this._onDisconnected) + } +} export class XRController extends THREE.Group { readonly index: number @@ -7,6 +42,7 @@ export class XRController extends THREE.Group { readonly grip: THREE.XRGripSpace readonly hand: THREE.XRHandSpace public inputSource!: XRInputSource + public controllerModel: ControllerModel | null = null; constructor(index: number, gl: THREE.WebGLRenderer) { super() diff --git a/src/XRControllerModelFactory.ts b/src/XRControllerModelFactory.ts index 4578434..ef48299 100644 --- a/src/XRControllerModelFactory.ts +++ b/src/XRControllerModelFactory.ts @@ -1,15 +1,27 @@ -import { Mesh, Object3D, SphereGeometry, MeshBasicMaterial } from 'three' +import { + Mesh, + Object3D, + SphereGeometry, + MeshBasicMaterial, + MeshStandardMaterial, + MeshPhongMaterial, + MeshLambertMaterial +} from 'three' import type { Texture } from 'three' import { fetchProfile, GLTFLoader, MotionController, MotionControllerConstants } from 'three-stdlib' const DEFAULT_PROFILES_PATH = 'https://cdn.jsdelivr.net/npm/@webxr-input-profiles/assets@1.0/dist/profiles' const DEFAULT_PROFILE = 'generic-trigger' +const isEnvMapApplicable = ( + material: any +): material is MeshBasicMaterial | MeshStandardMaterial | MeshPhongMaterial | MeshLambertMaterial => 'envMap' in material + const applyEnvironmentMap = (envMap: Texture, envMapIntensity: number, obj: Object3D): void => { obj.traverse((child) => { - if (child instanceof Mesh && 'envMap' in child.material) { + if (child instanceof Mesh && isEnvMapApplicable(child.material)) { child.material.envMap = envMap - child.material.envMapIntensity = envMapIntensity + if ('envMapIntensity' in child.material) child.material.envMapIntensity = envMapIntensity child.material.needsUpdate = true } }) @@ -37,7 +49,7 @@ export class XRControllerModel extends Object3D { this.envMap = envMap this.envMapIntensity = envMapIntensity - applyEnvironmentMap(this.envMap, envMapIntensity, this) + applyEnvironmentMap(envMap, envMapIntensity, this) return this } @@ -48,12 +60,16 @@ export class XRControllerModel extends Object3D { return } + + this.scene = scene addAssetSceneToControllerModel(this, scene) this.dispatchEvent({ type: 'modelconnected', data: scene }) + + console.log('model connected') } connectMotionController(motionController: MotionController): void { @@ -194,6 +210,7 @@ export class XRControllerModelFactory { gltfLoader: GLTFLoader path: string private _assetCache: Record + constructor(gltfLoader: GLTFLoader | null = null, path = DEFAULT_PROFILES_PATH) { this.gltfLoader = gltfLoader ?? new GLTFLoader() this.path = path diff --git a/vite.config.js b/vite.config.js index 4187520..c627648 100644 --- a/vite.config.js +++ b/vite.config.js @@ -6,6 +6,7 @@ import { defineConfig } from 'vite' const config = { serve: defineConfig({ + assetsInclude: ['**/*.hdr', '**/*.gltf'], root: 'examples', plugins: [react(), ssl(), vanillaExtractPlugin()], server: { host: '0.0.0.0', https: true }, diff --git a/vitest.config.ts b/vitest.config.ts index 8faa4fe..4afcd13 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,6 +1,7 @@ import { defineConfig } from 'vitest/config' export default defineConfig({ + test: { environment: 'happy-dom' }, From b6940d360042920df06a37f287f5914dab1324c8 Mon Sep 17 00:00:00 2001 From: Michael Bashurov Date: Tue, 13 Jun 2023 14:21:08 +0300 Subject: [PATCH 02/18] wip 2 --- examples/src/demos/ControllersEnvMap.tsx | 34 +- examples/src/demos/PMREMGenerator.js | 757 +++++++++++++++++++++++ 2 files changed, 774 insertions(+), 17 deletions(-) create mode 100644 examples/src/demos/PMREMGenerator.js diff --git a/examples/src/demos/ControllersEnvMap.tsx b/examples/src/demos/ControllersEnvMap.tsx index 9e4e17e..e8baa04 100644 --- a/examples/src/demos/ControllersEnvMap.tsx +++ b/examples/src/demos/ControllersEnvMap.tsx @@ -1,26 +1,23 @@ -import {Canvas, dispose, useThree} from '@react-three/fiber' -import {XR, VRButton, Controllers, useXR} from '@react-three/xr' -import { PMREMGenerator, Texture } from 'three' +import { Canvas, dispose, useThree } from '@react-three/fiber' +import { XR, VRButton, Controllers, useXR } from '@react-three/xr' +import { + Texture, +} from 'three' import { RGBELoader } from 'three-stdlib' -import {useEffect, useRef, useState} from 'react' -import EnvMap from "../assets/brown_photostudio_04_256.hdr"; +import { useEffect, useRef, useState } from 'react' +import EnvMap from '../assets/brown_photostudio_04_256.hdr' +import {PMREMGenerator} from "./PMREMGenerator"; + const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) + + function ControllersWithEnvMap() { const renderer = useThree(({ gl }) => gl) const isPresenting = useXR(({ isPresenting }) => isPresenting) const [envMap, setEnvMap] = useState() - const g = useRef() - useEffect(() => { - const pmremGenerator = new PMREMGenerator(renderer) - pmremGenerator.compileEquirectangularShader() - g.current = pmremGenerator - }, []) useEffect(() => { const foo = async () => { - const pmremGenerator = g.current! - if (!isPresenting) { - return - } + if (!isPresenting) return // if (isPresenting) return await delay(100) const rgbeLoader = new RGBELoader() @@ -29,10 +26,13 @@ function ControllersWithEnvMap() { * Чтобы починилось, нажми позже, когда карта загрузилась */ const dataTexture = await rgbeLoader.loadAsync(EnvMap) - const radianceMap = pmremGenerator.fromEquirectangular(dataTexture).texture + const pmremGenerator = new PMREMGenerator(renderer) + pmremGenerator.compileEquirectangularShader() + const rt = pmremGenerator.fromEquirectangular(dataTexture) + const radianceMap = rt.texture setEnvMap(radianceMap) pmremGenerator.dispose() - console.log('done radianceMap') + console.log('done radianceMap', radianceMap.encoding, rt.isXRRenderTarget) } foo() }, [isPresenting]) diff --git a/examples/src/demos/PMREMGenerator.js b/examples/src/demos/PMREMGenerator.js new file mode 100644 index 0000000..554692c --- /dev/null +++ b/examples/src/demos/PMREMGenerator.js @@ -0,0 +1,757 @@ +import { + CubeReflectionMapping, + CubeRefractionMapping, + CubeUVReflectionMapping, + LinearEncoding, + LinearFilter, + NoToneMapping, + NoBlending, + RGBAFormat, + HalfFloatType, + BufferAttribute, + BufferGeometry, + Mesh, + OrthographicCamera, + PerspectiveCamera, + ShaderMaterial, + Vector3, + Color, + WebGLRenderTarget, + MeshBasicMaterial, + BoxGeometry, + BackSide, sRGBEncoding +} from 'three' + +const LOD_MIN = 4 + +// The standard deviations (radians) associated with the extra mips. These are +// chosen to approximate a Trowbridge-Reitz distribution function times the +// geometric shadowing function. These sigma values squared must match the +// variance #defines in cube_uv_reflection_fragment.glsl.js. +const EXTRA_LOD_SIGMA = [0.125, 0.215, 0.35, 0.446, 0.526, 0.582] + +// The maximum length of the blur for loop. Smaller sigmas will use fewer +// samples and exit early, but not recompile the shader. +const MAX_SAMPLES = 20 + +const _flatCamera = /*@__PURE__*/ new OrthographicCamera() +const _clearColor = /*@__PURE__*/ new Color() +let _oldTarget = null + +// Golden Ratio +const PHI = (1 + Math.sqrt(5)) / 2 +const INV_PHI = 1 / PHI + +// Vertices of a dodecahedron (except the opposites, which represent the +// same axis), used as axis directions evenly spread on a sphere. +const _axisDirections = [ + /*@__PURE__*/ new Vector3(1, 1, 1), + /*@__PURE__*/ new Vector3(-1, 1, 1), + /*@__PURE__*/ new Vector3(1, 1, -1), + /*@__PURE__*/ new Vector3(-1, 1, -1), + /*@__PURE__*/ new Vector3(0, PHI, INV_PHI), + /*@__PURE__*/ new Vector3(0, PHI, -INV_PHI), + /*@__PURE__*/ new Vector3(INV_PHI, 0, PHI), + /*@__PURE__*/ new Vector3(-INV_PHI, 0, PHI), + /*@__PURE__*/ new Vector3(PHI, INV_PHI, 0), + /*@__PURE__*/ new Vector3(-PHI, INV_PHI, 0) +] + +/** + * This class generates a Prefiltered, Mipmapped Radiance Environment Map + * (PMREM) from a cubeMap environment texture. This allows different levels of + * blur to be quickly accessed based on material roughness. It is packed into a + * special CubeUV format that allows us to perform custom interpolation so that + * we can support nonlinear formats such as RGBE. Unlike a traditional mipmap + * chain, it only goes down to the LOD_MIN level (above), and then creates extra + * even more filtered 'mips' at the same LOD_MIN resolution, associated with + * higher roughness levels. In this way we maintain resolution to smoothly + * interpolate diffuse lighting while limiting sampling computation. + * + * Paper: Fast, Accurate Image-Based Lighting + * https://drive.google.com/file/d/15y8r_UpKlU9SvV4ILb0C3qCPecS8pvLz/view + */ + +class PMREMGenerator { + constructor(renderer) { + this._renderer = renderer + this._pingPongRenderTarget = null + + this._lodMax = 0 + this._cubeSize = 0 + this._lodPlanes = [] + this._sizeLods = [] + this._sigmas = [] + + this._blurMaterial = null + this._cubemapMaterial = null + this._equirectMaterial = null + + this._compileMaterial(this._blurMaterial) + } + + /** + * Generates a PMREM from a supplied Scene, which can be faster than using an + * image if networking bandwidth is low. Optional sigma specifies a blur radius + * in radians to be applied to the scene before PMREM generation. Optional near + * and far planes ensure the scene is rendered in its entirety (the cubeCamera + * is placed at the origin). + */ + fromScene(scene, sigma = 0, near = 0.1, far = 100) { + _oldTarget = this._renderer.getRenderTarget() + + this._setSize(256) + + const cubeUVRenderTarget = this._allocateTargets() + cubeUVRenderTarget.depthBuffer = true + + this._sceneToCubeUV(scene, near, far, cubeUVRenderTarget) + + if (sigma > 0) { + this._blur(cubeUVRenderTarget, 0, 0, sigma) + } + + this._applyPMREM(cubeUVRenderTarget) + this._cleanup(cubeUVRenderTarget) + + return cubeUVRenderTarget + } + + /** + * Generates a PMREM from an equirectangular texture, which can be either LDR + * or HDR. The ideal input image size is 1k (1024 x 512), + * as this matches best with the 256 x 256 cubemap output. + */ + fromEquirectangular(equirectangular, renderTarget = null) { + return this._fromTexture(equirectangular, renderTarget) + } + + /** + * Generates a PMREM from an cubemap texture, which can be either LDR + * or HDR. The ideal input cube size is 256 x 256, + * as this matches best with the 256 x 256 cubemap output. + */ + fromCubemap(cubemap, renderTarget = null) { + return this._fromTexture(cubemap, renderTarget) + } + + /** + * Pre-compiles the cubemap shader. You can get faster start-up by invoking this method during + * your texture's network fetch for increased concurrency. + */ + compileCubemapShader() { + if (this._cubemapMaterial === null) { + this._cubemapMaterial = _getCubemapMaterial() + this._compileMaterial(this._cubemapMaterial) + } + } + + /** + * Pre-compiles the equirectangular shader. You can get faster start-up by invoking this method during + * your texture's network fetch for increased concurrency. + */ + compileEquirectangularShader() { + if (this._equirectMaterial === null) { + this._equirectMaterial = _getEquirectMaterial() + this._compileMaterial(this._equirectMaterial) + } + } + + /** + * Disposes of the PMREMGenerator's internal memory. Note that PMREMGenerator is a static class, + * so you should not need more than one PMREMGenerator object. If you do, calling dispose() on + * one of them will cause any others to also become unusable. + */ + dispose() { + this._dispose() + + if (this._cubemapMaterial !== null) this._cubemapMaterial.dispose() + if (this._equirectMaterial !== null) this._equirectMaterial.dispose() + } + + // private interface + + _setSize(cubeSize) { + this._lodMax = Math.floor(Math.log2(cubeSize)) + this._cubeSize = Math.pow(2, this._lodMax) + } + + _dispose() { + if (this._blurMaterial !== null) this._blurMaterial.dispose() + + if (this._pingPongRenderTarget !== null) this._pingPongRenderTarget.dispose() + + for (let i = 0; i < this._lodPlanes.length; i++) { + this._lodPlanes[i].dispose() + } + } + + _cleanup(outputTarget) { + this._renderer.setRenderTarget(_oldTarget) + outputTarget.scissorTest = false + _setViewport(outputTarget, 0, 0, outputTarget.width, outputTarget.height) + } + + _fromTexture(texture, renderTarget) { + if (texture.mapping === CubeReflectionMapping || texture.mapping === CubeRefractionMapping) { + this._setSize(texture.image.length === 0 ? 16 : texture.image[0].width || texture.image[0].image.width) + } else { + // Equirectangular + + this._setSize(texture.image.width / 4) + } + + _oldTarget = this._renderer.getRenderTarget() + + const cubeUVRenderTarget = renderTarget || this._allocateTargets() + this._textureToCubeUV(texture, cubeUVRenderTarget) + this._applyPMREM(cubeUVRenderTarget) + this._cleanup(cubeUVRenderTarget) + + return cubeUVRenderTarget + } + + _allocateTargets() { + const width = 3 * Math.max(this._cubeSize, 16 * 7) + const height = 4 * this._cubeSize + + const params = { + magFilter: LinearFilter, + minFilter: LinearFilter, + generateMipmaps: false, + type: HalfFloatType, + format: RGBAFormat, + encoding: sRGBEncoding, + // encoding: LinearEncoding, + depthBuffer: false + } + + const cubeUVRenderTarget = _createRenderTarget(width, height, params) + cubeUVRenderTarget.isXRRenderTarget = true + + if (this._pingPongRenderTarget === null || this._pingPongRenderTarget.width !== width) { + if (this._pingPongRenderTarget !== null) { + this._dispose() + } + + this._pingPongRenderTarget = _createRenderTarget(width, height, params) + // this._pingPongRenderTarget.isXRRenderTarget = true + + const { _lodMax } = this + ;({ sizeLods: this._sizeLods, lodPlanes: this._lodPlanes, sigmas: this._sigmas } = _createPlanes(_lodMax)) + + this._blurMaterial = _getBlurShader(_lodMax, width, height) + } + + return cubeUVRenderTarget + } + + _compileMaterial(material) { + const tmpMesh = new Mesh(this._lodPlanes[0], material) + this._renderer.compile(tmpMesh, _flatCamera) + } + + _sceneToCubeUV(scene, near, far, cubeUVRenderTarget) { + const fov = 90 + const aspect = 1 + const cubeCamera = new PerspectiveCamera(fov, aspect, near, far) + const upSign = [1, -1, 1, 1, 1, 1] + const forwardSign = [1, 1, 1, -1, -1, -1] + const renderer = this._renderer + + const originalAutoClear = renderer.autoClear + const toneMapping = renderer.toneMapping + renderer.getClearColor(_clearColor) + + renderer.toneMapping = NoToneMapping + renderer.autoClear = false + + const backgroundMaterial = new MeshBasicMaterial({ + name: 'PMREM.Background', + side: BackSide, + depthWrite: false, + depthTest: false + }) + + const backgroundBox = new Mesh(new BoxGeometry(), backgroundMaterial) + + let useSolidColor = false + const background = scene.background + + if (background) { + if (background.isColor) { + backgroundMaterial.color.copy(background) + scene.background = null + useSolidColor = true + } + } else { + backgroundMaterial.color.copy(_clearColor) + useSolidColor = true + } + + for (let i = 0; i < 6; i++) { + const col = i % 3 + + if (col === 0) { + cubeCamera.up.set(0, upSign[i], 0) + cubeCamera.lookAt(forwardSign[i], 0, 0) + } else if (col === 1) { + cubeCamera.up.set(0, 0, upSign[i]) + cubeCamera.lookAt(0, forwardSign[i], 0) + } else { + cubeCamera.up.set(0, upSign[i], 0) + cubeCamera.lookAt(0, 0, forwardSign[i]) + } + + const size = this._cubeSize + + _setViewport(cubeUVRenderTarget, col * size, i > 2 ? size : 0, size, size) + + renderer.setRenderTarget(cubeUVRenderTarget) + + if (useSolidColor) { + renderer.render(backgroundBox, cubeCamera) + } + + renderer.render(scene, cubeCamera) + } + + backgroundBox.geometry.dispose() + backgroundBox.material.dispose() + + renderer.toneMapping = toneMapping + renderer.autoClear = originalAutoClear + scene.background = background + } + + _textureToCubeUV(texture, cubeUVRenderTarget) { + const renderer = this._renderer + + const isCubeTexture = texture.mapping === CubeReflectionMapping || texture.mapping === CubeRefractionMapping + + if (isCubeTexture) { + if (this._cubemapMaterial === null) { + this._cubemapMaterial = _getCubemapMaterial() + } + + this._cubemapMaterial.uniforms.flipEnvMap.value = texture.isRenderTargetTexture === false ? -1 : 1 + } else { + if (this._equirectMaterial === null) { + this._equirectMaterial = _getEquirectMaterial() + } + } + + const material = isCubeTexture ? this._cubemapMaterial : this._equirectMaterial + const mesh = new Mesh(this._lodPlanes[0], material) + + const uniforms = material.uniforms + + uniforms['envMap'].value = texture + + const size = this._cubeSize + + _setViewport(cubeUVRenderTarget, 0, 0, 3 * size, 2 * size) + + renderer.setRenderTarget(cubeUVRenderTarget) + renderer.render(mesh, _flatCamera) + } + + _applyPMREM(cubeUVRenderTarget) { + const renderer = this._renderer + const autoClear = renderer.autoClear + renderer.autoClear = false + + for (let i = 1; i < this._lodPlanes.length; i++) { + const sigma = Math.sqrt(this._sigmas[i] * this._sigmas[i] - this._sigmas[i - 1] * this._sigmas[i - 1]) + + const poleAxis = _axisDirections[(i - 1) % _axisDirections.length] + + this._blur(cubeUVRenderTarget, i - 1, i, sigma, poleAxis) + } + + renderer.autoClear = autoClear + } + + /** + * This is a two-pass Gaussian blur for a cubemap. Normally this is done + * vertically and horizontally, but this breaks down on a cube. Here we apply + * the blur latitudinally (around the poles), and then longitudinally (towards + * the poles) to approximate the orthogonally-separable blur. It is least + * accurate at the poles, but still does a decent job. + */ + _blur(cubeUVRenderTarget, lodIn, lodOut, sigma, poleAxis) { + const pingPongRenderTarget = this._pingPongRenderTarget + + this._halfBlur(cubeUVRenderTarget, pingPongRenderTarget, lodIn, lodOut, sigma, 'latitudinal', poleAxis) + + this._halfBlur(pingPongRenderTarget, cubeUVRenderTarget, lodOut, lodOut, sigma, 'longitudinal', poleAxis) + } + + _halfBlur(targetIn, targetOut, lodIn, lodOut, sigmaRadians, direction, poleAxis) { + const renderer = this._renderer + const blurMaterial = this._blurMaterial + + if (direction !== 'latitudinal' && direction !== 'longitudinal') { + console.error('blur direction must be either latitudinal or longitudinal!') + } + + // Number of standard deviations at which to cut off the discrete approximation. + const STANDARD_DEVIATIONS = 3 + + const blurMesh = new Mesh(this._lodPlanes[lodOut], blurMaterial) + const blurUniforms = blurMaterial.uniforms + + const pixels = this._sizeLods[lodIn] - 1 + const radiansPerPixel = isFinite(sigmaRadians) ? Math.PI / (2 * pixels) : (2 * Math.PI) / (2 * MAX_SAMPLES - 1) + const sigmaPixels = sigmaRadians / radiansPerPixel + const samples = isFinite(sigmaRadians) ? 1 + Math.floor(STANDARD_DEVIATIONS * sigmaPixels) : MAX_SAMPLES + + if (samples > MAX_SAMPLES) { + console.warn( + `sigmaRadians, ${sigmaRadians}, is too large and will clip, as it requested ${samples} samples when the maximum is set to ${MAX_SAMPLES}` + ) + } + + const weights = [] + let sum = 0 + + for (let i = 0; i < MAX_SAMPLES; ++i) { + const x = i / sigmaPixels + const weight = Math.exp((-x * x) / 2) + weights.push(weight) + + if (i === 0) { + sum += weight + } else if (i < samples) { + sum += 2 * weight + } + } + + for (let i = 0; i < weights.length; i++) { + weights[i] = weights[i] / sum + } + + blurUniforms['envMap'].value = targetIn.texture + blurUniforms['samples'].value = samples + blurUniforms['weights'].value = weights + blurUniforms['latitudinal'].value = direction === 'latitudinal' + + if (poleAxis) { + blurUniforms['poleAxis'].value = poleAxis + } + + const { _lodMax } = this + blurUniforms['dTheta'].value = radiansPerPixel + blurUniforms['mipInt'].value = _lodMax - lodIn + + const outputSize = this._sizeLods[lodOut] + const x = 3 * outputSize * (lodOut > _lodMax - LOD_MIN ? lodOut - _lodMax + LOD_MIN : 0) + const y = 4 * (this._cubeSize - outputSize) + + _setViewport(targetOut, x, y, 3 * outputSize, 2 * outputSize) + renderer.setRenderTarget(targetOut) + renderer.render(blurMesh, _flatCamera) + } +} + +function _createPlanes(lodMax) { + const lodPlanes = [] + const sizeLods = [] + const sigmas = [] + + let lod = lodMax + + const totalLods = lodMax - LOD_MIN + 1 + EXTRA_LOD_SIGMA.length + + for (let i = 0; i < totalLods; i++) { + const sizeLod = Math.pow(2, lod) + sizeLods.push(sizeLod) + let sigma = 1.0 / sizeLod + + if (i > lodMax - LOD_MIN) { + sigma = EXTRA_LOD_SIGMA[i - lodMax + LOD_MIN - 1] + } else if (i === 0) { + sigma = 0 + } + + sigmas.push(sigma) + + const texelSize = 1.0 / (sizeLod - 2) + const min = -texelSize + const max = 1 + texelSize + const uv1 = [min, min, max, min, max, max, min, min, max, max, min, max] + + const cubeFaces = 6 + const vertices = 6 + const positionSize = 3 + const uvSize = 2 + const faceIndexSize = 1 + + const position = new Float32Array(positionSize * vertices * cubeFaces) + const uv = new Float32Array(uvSize * vertices * cubeFaces) + const faceIndex = new Float32Array(faceIndexSize * vertices * cubeFaces) + + for (let face = 0; face < cubeFaces; face++) { + const x = ((face % 3) * 2) / 3 - 1 + const y = face > 2 ? 0 : -1 + const coordinates = [x, y, 0, x + 2 / 3, y, 0, x + 2 / 3, y + 1, 0, x, y, 0, x + 2 / 3, y + 1, 0, x, y + 1, 0] + position.set(coordinates, positionSize * vertices * face) + uv.set(uv1, uvSize * vertices * face) + const fill = [face, face, face, face, face, face] + faceIndex.set(fill, faceIndexSize * vertices * face) + } + + const planes = new BufferGeometry() + planes.setAttribute('position', new BufferAttribute(position, positionSize)) + planes.setAttribute('uv', new BufferAttribute(uv, uvSize)) + planes.setAttribute('faceIndex', new BufferAttribute(faceIndex, faceIndexSize)) + lodPlanes.push(planes) + + if (lod > LOD_MIN) { + lod-- + } + } + + return { lodPlanes, sizeLods, sigmas } +} + +function _createRenderTarget(width, height, params) { + const cubeUVRenderTarget = new WebGLRenderTarget(width, height, params) + cubeUVRenderTarget.texture.mapping = CubeUVReflectionMapping + cubeUVRenderTarget.texture.name = 'PMREM.cubeUv' + cubeUVRenderTarget.scissorTest = true + return cubeUVRenderTarget +} + +function _setViewport(target, x, y, width, height) { + target.viewport.set(x, y, width, height) + target.scissor.set(x, y, width, height) +} + +function _getBlurShader(lodMax, width, height) { + const weights = new Float32Array(MAX_SAMPLES) + const poleAxis = new Vector3(0, 1, 0) + const shaderMaterial = new ShaderMaterial({ + name: 'SphericalGaussianBlur', + + defines: { + n: MAX_SAMPLES, + CUBEUV_TEXEL_WIDTH: 1.0 / width, + CUBEUV_TEXEL_HEIGHT: 1.0 / height, + CUBEUV_MAX_MIP: `${lodMax}.0` + }, + + uniforms: { + envMap: { value: null }, + samples: { value: 1 }, + weights: { value: weights }, + latitudinal: { value: false }, + dTheta: { value: 0 }, + mipInt: { value: 0 }, + poleAxis: { value: poleAxis } + }, + + vertexShader: _getCommonVertexShader(), + + fragmentShader: /* glsl */ ` + + precision mediump float; + precision mediump int; + + varying vec3 vOutputDirection; + + uniform sampler2D envMap; + uniform int samples; + uniform float weights[ n ]; + uniform bool latitudinal; + uniform float dTheta; + uniform float mipInt; + uniform vec3 poleAxis; + + #define ENVMAP_TYPE_CUBE_UV + #include + + vec3 getSample( float theta, vec3 axis ) { + + float cosTheta = cos( theta ); + // Rodrigues' axis-angle rotation + vec3 sampleDirection = vOutputDirection * cosTheta + + cross( axis, vOutputDirection ) * sin( theta ) + + axis * dot( axis, vOutputDirection ) * ( 1.0 - cosTheta ); + + return bilinearCubeUV( envMap, sampleDirection, mipInt ); + + } + + void main() { + + vec3 axis = latitudinal ? poleAxis : cross( poleAxis, vOutputDirection ); + + if ( all( equal( axis, vec3( 0.0 ) ) ) ) { + + axis = vec3( vOutputDirection.z, 0.0, - vOutputDirection.x ); + + } + + axis = normalize( axis ); + + gl_FragColor = vec4( 0.0, 0.0, 0.0, 1.0 ); + gl_FragColor.rgb += weights[ 0 ] * getSample( 0.0, axis ); + + for ( int i = 1; i < n; i++ ) { + + if ( i >= samples ) { + + break; + + } + + float theta = dTheta * float( i ); + gl_FragColor.rgb += weights[ i ] * getSample( -1.0 * theta, axis ); + gl_FragColor.rgb += weights[ i ] * getSample( theta, axis ); + + } + + } + `, + + blending: NoBlending, + depthTest: false, + depthWrite: false + }) + + return shaderMaterial +} + +function _getEquirectMaterial() { + return new ShaderMaterial({ + name: 'EquirectangularToCubeUV', + + uniforms: { + envMap: { value: null } + }, + + vertexShader: _getCommonVertexShader(), + + fragmentShader: /* glsl */ ` + + precision mediump float; + precision mediump int; + + varying vec3 vOutputDirection; + + uniform sampler2D envMap; + + #include + + void main() { + + vec3 outputDirection = normalize( vOutputDirection ); + vec2 uv = equirectUv( outputDirection ); + + gl_FragColor = vec4( texture2D ( envMap, uv ).rgb, 1.0 ); + + } + `, + + blending: NoBlending, + depthTest: false, + depthWrite: false + }) +} + +function _getCubemapMaterial() { + return new ShaderMaterial({ + name: 'CubemapToCubeUV', + + uniforms: { + envMap: { value: null }, + flipEnvMap: { value: -1 } + }, + + vertexShader: _getCommonVertexShader(), + + fragmentShader: /* glsl */ ` + + precision mediump float; + precision mediump int; + + uniform float flipEnvMap; + + varying vec3 vOutputDirection; + + uniform samplerCube envMap; + + void main() { + + gl_FragColor = textureCube( envMap, vec3( flipEnvMap * vOutputDirection.x, vOutputDirection.yz ) ); + + } + `, + + blending: NoBlending, + depthTest: false, + depthWrite: false + }) +} + +function _getCommonVertexShader() { + return /* glsl */ ` + + precision mediump float; + precision mediump int; + + attribute float faceIndex; + + varying vec3 vOutputDirection; + + // RH coordinate system; PMREM face-indexing convention + vec3 getDirection( vec2 uv, float face ) { + + uv = 2.0 * uv - 1.0; + + vec3 direction = vec3( uv, 1.0 ); + + if ( face == 0.0 ) { + + direction = direction.zyx; // ( 1, v, u ) pos x + + } else if ( face == 1.0 ) { + + direction = direction.xzy; + direction.xz *= -1.0; // ( -u, 1, -v ) pos y + + } else if ( face == 2.0 ) { + + direction.x *= -1.0; // ( -u, v, 1 ) pos z + + } else if ( face == 3.0 ) { + + direction = direction.zyx; + direction.xz *= -1.0; // ( -1, v, -u ) neg x + + } else if ( face == 4.0 ) { + + direction = direction.xzy; + direction.xy *= -1.0; // ( -u, -1, v ) neg y + + } else if ( face == 5.0 ) { + + direction.z *= -1.0; // ( u, v, -1 ) neg z + + } + + return direction; + + } + + void main() { + + vOutputDirection = getDirection( uv, faceIndex ); + gl_Position = vec4( position, 1.0 ); + + } + ` +} + +export {PMREMGenerator}; From b8167d9d89303c43931b1f8dc86014d23cccb982 Mon Sep 17 00:00:00 2001 From: Michael Bashurov Date: Tue, 4 Jul 2023 17:55:56 +0300 Subject: [PATCH 03/18] wip --- examples/src/demos/ControllersEnvMap.tsx | 25 +- examples/src/demos/PMREMGenerator.js | 757 ----------------------- src/Controllers.tsx | 1 - 3 files changed, 5 insertions(+), 778 deletions(-) delete mode 100644 examples/src/demos/PMREMGenerator.js diff --git a/examples/src/demos/ControllersEnvMap.tsx b/examples/src/demos/ControllersEnvMap.tsx index e8baa04..35ff313 100644 --- a/examples/src/demos/ControllersEnvMap.tsx +++ b/examples/src/demos/ControllersEnvMap.tsx @@ -1,30 +1,16 @@ -import { Canvas, dispose, useThree } from '@react-three/fiber' -import { XR, VRButton, Controllers, useXR } from '@react-three/xr' -import { - Texture, -} from 'three' +import { Canvas, useThree } from '@react-three/fiber' +import { XR, VRButton, Controllers } from '@react-three/xr' +import { PMREMGenerator, Texture } from 'three' import { RGBELoader } from 'three-stdlib' -import { useEffect, useRef, useState } from 'react' +import { useEffect, useState } from 'react' import EnvMap from '../assets/brown_photostudio_04_256.hdr' -import {PMREMGenerator} from "./PMREMGenerator"; - -const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) - function ControllersWithEnvMap() { const renderer = useThree(({ gl }) => gl) - const isPresenting = useXR(({ isPresenting }) => isPresenting) const [envMap, setEnvMap] = useState() useEffect(() => { const foo = async () => { - if (!isPresenting) return - // if (isPresenting) return - await delay(100) const rgbeLoader = new RGBELoader() - /** - * Чтобы воспроизвести, нажми Enter VR раньше, чем 5 сек - * Чтобы починилось, нажми позже, когда карта загрузилась - */ const dataTexture = await rgbeLoader.loadAsync(EnvMap) const pmremGenerator = new PMREMGenerator(renderer) pmremGenerator.compileEquirectangularShader() @@ -32,10 +18,9 @@ function ControllersWithEnvMap() { const radianceMap = rt.texture setEnvMap(radianceMap) pmremGenerator.dispose() - console.log('done radianceMap', radianceMap.encoding, rt.isXRRenderTarget) } foo() - }, [isPresenting]) + }, [renderer]) return } diff --git a/examples/src/demos/PMREMGenerator.js b/examples/src/demos/PMREMGenerator.js deleted file mode 100644 index 554692c..0000000 --- a/examples/src/demos/PMREMGenerator.js +++ /dev/null @@ -1,757 +0,0 @@ -import { - CubeReflectionMapping, - CubeRefractionMapping, - CubeUVReflectionMapping, - LinearEncoding, - LinearFilter, - NoToneMapping, - NoBlending, - RGBAFormat, - HalfFloatType, - BufferAttribute, - BufferGeometry, - Mesh, - OrthographicCamera, - PerspectiveCamera, - ShaderMaterial, - Vector3, - Color, - WebGLRenderTarget, - MeshBasicMaterial, - BoxGeometry, - BackSide, sRGBEncoding -} from 'three' - -const LOD_MIN = 4 - -// The standard deviations (radians) associated with the extra mips. These are -// chosen to approximate a Trowbridge-Reitz distribution function times the -// geometric shadowing function. These sigma values squared must match the -// variance #defines in cube_uv_reflection_fragment.glsl.js. -const EXTRA_LOD_SIGMA = [0.125, 0.215, 0.35, 0.446, 0.526, 0.582] - -// The maximum length of the blur for loop. Smaller sigmas will use fewer -// samples and exit early, but not recompile the shader. -const MAX_SAMPLES = 20 - -const _flatCamera = /*@__PURE__*/ new OrthographicCamera() -const _clearColor = /*@__PURE__*/ new Color() -let _oldTarget = null - -// Golden Ratio -const PHI = (1 + Math.sqrt(5)) / 2 -const INV_PHI = 1 / PHI - -// Vertices of a dodecahedron (except the opposites, which represent the -// same axis), used as axis directions evenly spread on a sphere. -const _axisDirections = [ - /*@__PURE__*/ new Vector3(1, 1, 1), - /*@__PURE__*/ new Vector3(-1, 1, 1), - /*@__PURE__*/ new Vector3(1, 1, -1), - /*@__PURE__*/ new Vector3(-1, 1, -1), - /*@__PURE__*/ new Vector3(0, PHI, INV_PHI), - /*@__PURE__*/ new Vector3(0, PHI, -INV_PHI), - /*@__PURE__*/ new Vector3(INV_PHI, 0, PHI), - /*@__PURE__*/ new Vector3(-INV_PHI, 0, PHI), - /*@__PURE__*/ new Vector3(PHI, INV_PHI, 0), - /*@__PURE__*/ new Vector3(-PHI, INV_PHI, 0) -] - -/** - * This class generates a Prefiltered, Mipmapped Radiance Environment Map - * (PMREM) from a cubeMap environment texture. This allows different levels of - * blur to be quickly accessed based on material roughness. It is packed into a - * special CubeUV format that allows us to perform custom interpolation so that - * we can support nonlinear formats such as RGBE. Unlike a traditional mipmap - * chain, it only goes down to the LOD_MIN level (above), and then creates extra - * even more filtered 'mips' at the same LOD_MIN resolution, associated with - * higher roughness levels. In this way we maintain resolution to smoothly - * interpolate diffuse lighting while limiting sampling computation. - * - * Paper: Fast, Accurate Image-Based Lighting - * https://drive.google.com/file/d/15y8r_UpKlU9SvV4ILb0C3qCPecS8pvLz/view - */ - -class PMREMGenerator { - constructor(renderer) { - this._renderer = renderer - this._pingPongRenderTarget = null - - this._lodMax = 0 - this._cubeSize = 0 - this._lodPlanes = [] - this._sizeLods = [] - this._sigmas = [] - - this._blurMaterial = null - this._cubemapMaterial = null - this._equirectMaterial = null - - this._compileMaterial(this._blurMaterial) - } - - /** - * Generates a PMREM from a supplied Scene, which can be faster than using an - * image if networking bandwidth is low. Optional sigma specifies a blur radius - * in radians to be applied to the scene before PMREM generation. Optional near - * and far planes ensure the scene is rendered in its entirety (the cubeCamera - * is placed at the origin). - */ - fromScene(scene, sigma = 0, near = 0.1, far = 100) { - _oldTarget = this._renderer.getRenderTarget() - - this._setSize(256) - - const cubeUVRenderTarget = this._allocateTargets() - cubeUVRenderTarget.depthBuffer = true - - this._sceneToCubeUV(scene, near, far, cubeUVRenderTarget) - - if (sigma > 0) { - this._blur(cubeUVRenderTarget, 0, 0, sigma) - } - - this._applyPMREM(cubeUVRenderTarget) - this._cleanup(cubeUVRenderTarget) - - return cubeUVRenderTarget - } - - /** - * Generates a PMREM from an equirectangular texture, which can be either LDR - * or HDR. The ideal input image size is 1k (1024 x 512), - * as this matches best with the 256 x 256 cubemap output. - */ - fromEquirectangular(equirectangular, renderTarget = null) { - return this._fromTexture(equirectangular, renderTarget) - } - - /** - * Generates a PMREM from an cubemap texture, which can be either LDR - * or HDR. The ideal input cube size is 256 x 256, - * as this matches best with the 256 x 256 cubemap output. - */ - fromCubemap(cubemap, renderTarget = null) { - return this._fromTexture(cubemap, renderTarget) - } - - /** - * Pre-compiles the cubemap shader. You can get faster start-up by invoking this method during - * your texture's network fetch for increased concurrency. - */ - compileCubemapShader() { - if (this._cubemapMaterial === null) { - this._cubemapMaterial = _getCubemapMaterial() - this._compileMaterial(this._cubemapMaterial) - } - } - - /** - * Pre-compiles the equirectangular shader. You can get faster start-up by invoking this method during - * your texture's network fetch for increased concurrency. - */ - compileEquirectangularShader() { - if (this._equirectMaterial === null) { - this._equirectMaterial = _getEquirectMaterial() - this._compileMaterial(this._equirectMaterial) - } - } - - /** - * Disposes of the PMREMGenerator's internal memory. Note that PMREMGenerator is a static class, - * so you should not need more than one PMREMGenerator object. If you do, calling dispose() on - * one of them will cause any others to also become unusable. - */ - dispose() { - this._dispose() - - if (this._cubemapMaterial !== null) this._cubemapMaterial.dispose() - if (this._equirectMaterial !== null) this._equirectMaterial.dispose() - } - - // private interface - - _setSize(cubeSize) { - this._lodMax = Math.floor(Math.log2(cubeSize)) - this._cubeSize = Math.pow(2, this._lodMax) - } - - _dispose() { - if (this._blurMaterial !== null) this._blurMaterial.dispose() - - if (this._pingPongRenderTarget !== null) this._pingPongRenderTarget.dispose() - - for (let i = 0; i < this._lodPlanes.length; i++) { - this._lodPlanes[i].dispose() - } - } - - _cleanup(outputTarget) { - this._renderer.setRenderTarget(_oldTarget) - outputTarget.scissorTest = false - _setViewport(outputTarget, 0, 0, outputTarget.width, outputTarget.height) - } - - _fromTexture(texture, renderTarget) { - if (texture.mapping === CubeReflectionMapping || texture.mapping === CubeRefractionMapping) { - this._setSize(texture.image.length === 0 ? 16 : texture.image[0].width || texture.image[0].image.width) - } else { - // Equirectangular - - this._setSize(texture.image.width / 4) - } - - _oldTarget = this._renderer.getRenderTarget() - - const cubeUVRenderTarget = renderTarget || this._allocateTargets() - this._textureToCubeUV(texture, cubeUVRenderTarget) - this._applyPMREM(cubeUVRenderTarget) - this._cleanup(cubeUVRenderTarget) - - return cubeUVRenderTarget - } - - _allocateTargets() { - const width = 3 * Math.max(this._cubeSize, 16 * 7) - const height = 4 * this._cubeSize - - const params = { - magFilter: LinearFilter, - minFilter: LinearFilter, - generateMipmaps: false, - type: HalfFloatType, - format: RGBAFormat, - encoding: sRGBEncoding, - // encoding: LinearEncoding, - depthBuffer: false - } - - const cubeUVRenderTarget = _createRenderTarget(width, height, params) - cubeUVRenderTarget.isXRRenderTarget = true - - if (this._pingPongRenderTarget === null || this._pingPongRenderTarget.width !== width) { - if (this._pingPongRenderTarget !== null) { - this._dispose() - } - - this._pingPongRenderTarget = _createRenderTarget(width, height, params) - // this._pingPongRenderTarget.isXRRenderTarget = true - - const { _lodMax } = this - ;({ sizeLods: this._sizeLods, lodPlanes: this._lodPlanes, sigmas: this._sigmas } = _createPlanes(_lodMax)) - - this._blurMaterial = _getBlurShader(_lodMax, width, height) - } - - return cubeUVRenderTarget - } - - _compileMaterial(material) { - const tmpMesh = new Mesh(this._lodPlanes[0], material) - this._renderer.compile(tmpMesh, _flatCamera) - } - - _sceneToCubeUV(scene, near, far, cubeUVRenderTarget) { - const fov = 90 - const aspect = 1 - const cubeCamera = new PerspectiveCamera(fov, aspect, near, far) - const upSign = [1, -1, 1, 1, 1, 1] - const forwardSign = [1, 1, 1, -1, -1, -1] - const renderer = this._renderer - - const originalAutoClear = renderer.autoClear - const toneMapping = renderer.toneMapping - renderer.getClearColor(_clearColor) - - renderer.toneMapping = NoToneMapping - renderer.autoClear = false - - const backgroundMaterial = new MeshBasicMaterial({ - name: 'PMREM.Background', - side: BackSide, - depthWrite: false, - depthTest: false - }) - - const backgroundBox = new Mesh(new BoxGeometry(), backgroundMaterial) - - let useSolidColor = false - const background = scene.background - - if (background) { - if (background.isColor) { - backgroundMaterial.color.copy(background) - scene.background = null - useSolidColor = true - } - } else { - backgroundMaterial.color.copy(_clearColor) - useSolidColor = true - } - - for (let i = 0; i < 6; i++) { - const col = i % 3 - - if (col === 0) { - cubeCamera.up.set(0, upSign[i], 0) - cubeCamera.lookAt(forwardSign[i], 0, 0) - } else if (col === 1) { - cubeCamera.up.set(0, 0, upSign[i]) - cubeCamera.lookAt(0, forwardSign[i], 0) - } else { - cubeCamera.up.set(0, upSign[i], 0) - cubeCamera.lookAt(0, 0, forwardSign[i]) - } - - const size = this._cubeSize - - _setViewport(cubeUVRenderTarget, col * size, i > 2 ? size : 0, size, size) - - renderer.setRenderTarget(cubeUVRenderTarget) - - if (useSolidColor) { - renderer.render(backgroundBox, cubeCamera) - } - - renderer.render(scene, cubeCamera) - } - - backgroundBox.geometry.dispose() - backgroundBox.material.dispose() - - renderer.toneMapping = toneMapping - renderer.autoClear = originalAutoClear - scene.background = background - } - - _textureToCubeUV(texture, cubeUVRenderTarget) { - const renderer = this._renderer - - const isCubeTexture = texture.mapping === CubeReflectionMapping || texture.mapping === CubeRefractionMapping - - if (isCubeTexture) { - if (this._cubemapMaterial === null) { - this._cubemapMaterial = _getCubemapMaterial() - } - - this._cubemapMaterial.uniforms.flipEnvMap.value = texture.isRenderTargetTexture === false ? -1 : 1 - } else { - if (this._equirectMaterial === null) { - this._equirectMaterial = _getEquirectMaterial() - } - } - - const material = isCubeTexture ? this._cubemapMaterial : this._equirectMaterial - const mesh = new Mesh(this._lodPlanes[0], material) - - const uniforms = material.uniforms - - uniforms['envMap'].value = texture - - const size = this._cubeSize - - _setViewport(cubeUVRenderTarget, 0, 0, 3 * size, 2 * size) - - renderer.setRenderTarget(cubeUVRenderTarget) - renderer.render(mesh, _flatCamera) - } - - _applyPMREM(cubeUVRenderTarget) { - const renderer = this._renderer - const autoClear = renderer.autoClear - renderer.autoClear = false - - for (let i = 1; i < this._lodPlanes.length; i++) { - const sigma = Math.sqrt(this._sigmas[i] * this._sigmas[i] - this._sigmas[i - 1] * this._sigmas[i - 1]) - - const poleAxis = _axisDirections[(i - 1) % _axisDirections.length] - - this._blur(cubeUVRenderTarget, i - 1, i, sigma, poleAxis) - } - - renderer.autoClear = autoClear - } - - /** - * This is a two-pass Gaussian blur for a cubemap. Normally this is done - * vertically and horizontally, but this breaks down on a cube. Here we apply - * the blur latitudinally (around the poles), and then longitudinally (towards - * the poles) to approximate the orthogonally-separable blur. It is least - * accurate at the poles, but still does a decent job. - */ - _blur(cubeUVRenderTarget, lodIn, lodOut, sigma, poleAxis) { - const pingPongRenderTarget = this._pingPongRenderTarget - - this._halfBlur(cubeUVRenderTarget, pingPongRenderTarget, lodIn, lodOut, sigma, 'latitudinal', poleAxis) - - this._halfBlur(pingPongRenderTarget, cubeUVRenderTarget, lodOut, lodOut, sigma, 'longitudinal', poleAxis) - } - - _halfBlur(targetIn, targetOut, lodIn, lodOut, sigmaRadians, direction, poleAxis) { - const renderer = this._renderer - const blurMaterial = this._blurMaterial - - if (direction !== 'latitudinal' && direction !== 'longitudinal') { - console.error('blur direction must be either latitudinal or longitudinal!') - } - - // Number of standard deviations at which to cut off the discrete approximation. - const STANDARD_DEVIATIONS = 3 - - const blurMesh = new Mesh(this._lodPlanes[lodOut], blurMaterial) - const blurUniforms = blurMaterial.uniforms - - const pixels = this._sizeLods[lodIn] - 1 - const radiansPerPixel = isFinite(sigmaRadians) ? Math.PI / (2 * pixels) : (2 * Math.PI) / (2 * MAX_SAMPLES - 1) - const sigmaPixels = sigmaRadians / radiansPerPixel - const samples = isFinite(sigmaRadians) ? 1 + Math.floor(STANDARD_DEVIATIONS * sigmaPixels) : MAX_SAMPLES - - if (samples > MAX_SAMPLES) { - console.warn( - `sigmaRadians, ${sigmaRadians}, is too large and will clip, as it requested ${samples} samples when the maximum is set to ${MAX_SAMPLES}` - ) - } - - const weights = [] - let sum = 0 - - for (let i = 0; i < MAX_SAMPLES; ++i) { - const x = i / sigmaPixels - const weight = Math.exp((-x * x) / 2) - weights.push(weight) - - if (i === 0) { - sum += weight - } else if (i < samples) { - sum += 2 * weight - } - } - - for (let i = 0; i < weights.length; i++) { - weights[i] = weights[i] / sum - } - - blurUniforms['envMap'].value = targetIn.texture - blurUniforms['samples'].value = samples - blurUniforms['weights'].value = weights - blurUniforms['latitudinal'].value = direction === 'latitudinal' - - if (poleAxis) { - blurUniforms['poleAxis'].value = poleAxis - } - - const { _lodMax } = this - blurUniforms['dTheta'].value = radiansPerPixel - blurUniforms['mipInt'].value = _lodMax - lodIn - - const outputSize = this._sizeLods[lodOut] - const x = 3 * outputSize * (lodOut > _lodMax - LOD_MIN ? lodOut - _lodMax + LOD_MIN : 0) - const y = 4 * (this._cubeSize - outputSize) - - _setViewport(targetOut, x, y, 3 * outputSize, 2 * outputSize) - renderer.setRenderTarget(targetOut) - renderer.render(blurMesh, _flatCamera) - } -} - -function _createPlanes(lodMax) { - const lodPlanes = [] - const sizeLods = [] - const sigmas = [] - - let lod = lodMax - - const totalLods = lodMax - LOD_MIN + 1 + EXTRA_LOD_SIGMA.length - - for (let i = 0; i < totalLods; i++) { - const sizeLod = Math.pow(2, lod) - sizeLods.push(sizeLod) - let sigma = 1.0 / sizeLod - - if (i > lodMax - LOD_MIN) { - sigma = EXTRA_LOD_SIGMA[i - lodMax + LOD_MIN - 1] - } else if (i === 0) { - sigma = 0 - } - - sigmas.push(sigma) - - const texelSize = 1.0 / (sizeLod - 2) - const min = -texelSize - const max = 1 + texelSize - const uv1 = [min, min, max, min, max, max, min, min, max, max, min, max] - - const cubeFaces = 6 - const vertices = 6 - const positionSize = 3 - const uvSize = 2 - const faceIndexSize = 1 - - const position = new Float32Array(positionSize * vertices * cubeFaces) - const uv = new Float32Array(uvSize * vertices * cubeFaces) - const faceIndex = new Float32Array(faceIndexSize * vertices * cubeFaces) - - for (let face = 0; face < cubeFaces; face++) { - const x = ((face % 3) * 2) / 3 - 1 - const y = face > 2 ? 0 : -1 - const coordinates = [x, y, 0, x + 2 / 3, y, 0, x + 2 / 3, y + 1, 0, x, y, 0, x + 2 / 3, y + 1, 0, x, y + 1, 0] - position.set(coordinates, positionSize * vertices * face) - uv.set(uv1, uvSize * vertices * face) - const fill = [face, face, face, face, face, face] - faceIndex.set(fill, faceIndexSize * vertices * face) - } - - const planes = new BufferGeometry() - planes.setAttribute('position', new BufferAttribute(position, positionSize)) - planes.setAttribute('uv', new BufferAttribute(uv, uvSize)) - planes.setAttribute('faceIndex', new BufferAttribute(faceIndex, faceIndexSize)) - lodPlanes.push(planes) - - if (lod > LOD_MIN) { - lod-- - } - } - - return { lodPlanes, sizeLods, sigmas } -} - -function _createRenderTarget(width, height, params) { - const cubeUVRenderTarget = new WebGLRenderTarget(width, height, params) - cubeUVRenderTarget.texture.mapping = CubeUVReflectionMapping - cubeUVRenderTarget.texture.name = 'PMREM.cubeUv' - cubeUVRenderTarget.scissorTest = true - return cubeUVRenderTarget -} - -function _setViewport(target, x, y, width, height) { - target.viewport.set(x, y, width, height) - target.scissor.set(x, y, width, height) -} - -function _getBlurShader(lodMax, width, height) { - const weights = new Float32Array(MAX_SAMPLES) - const poleAxis = new Vector3(0, 1, 0) - const shaderMaterial = new ShaderMaterial({ - name: 'SphericalGaussianBlur', - - defines: { - n: MAX_SAMPLES, - CUBEUV_TEXEL_WIDTH: 1.0 / width, - CUBEUV_TEXEL_HEIGHT: 1.0 / height, - CUBEUV_MAX_MIP: `${lodMax}.0` - }, - - uniforms: { - envMap: { value: null }, - samples: { value: 1 }, - weights: { value: weights }, - latitudinal: { value: false }, - dTheta: { value: 0 }, - mipInt: { value: 0 }, - poleAxis: { value: poleAxis } - }, - - vertexShader: _getCommonVertexShader(), - - fragmentShader: /* glsl */ ` - - precision mediump float; - precision mediump int; - - varying vec3 vOutputDirection; - - uniform sampler2D envMap; - uniform int samples; - uniform float weights[ n ]; - uniform bool latitudinal; - uniform float dTheta; - uniform float mipInt; - uniform vec3 poleAxis; - - #define ENVMAP_TYPE_CUBE_UV - #include - - vec3 getSample( float theta, vec3 axis ) { - - float cosTheta = cos( theta ); - // Rodrigues' axis-angle rotation - vec3 sampleDirection = vOutputDirection * cosTheta - + cross( axis, vOutputDirection ) * sin( theta ) - + axis * dot( axis, vOutputDirection ) * ( 1.0 - cosTheta ); - - return bilinearCubeUV( envMap, sampleDirection, mipInt ); - - } - - void main() { - - vec3 axis = latitudinal ? poleAxis : cross( poleAxis, vOutputDirection ); - - if ( all( equal( axis, vec3( 0.0 ) ) ) ) { - - axis = vec3( vOutputDirection.z, 0.0, - vOutputDirection.x ); - - } - - axis = normalize( axis ); - - gl_FragColor = vec4( 0.0, 0.0, 0.0, 1.0 ); - gl_FragColor.rgb += weights[ 0 ] * getSample( 0.0, axis ); - - for ( int i = 1; i < n; i++ ) { - - if ( i >= samples ) { - - break; - - } - - float theta = dTheta * float( i ); - gl_FragColor.rgb += weights[ i ] * getSample( -1.0 * theta, axis ); - gl_FragColor.rgb += weights[ i ] * getSample( theta, axis ); - - } - - } - `, - - blending: NoBlending, - depthTest: false, - depthWrite: false - }) - - return shaderMaterial -} - -function _getEquirectMaterial() { - return new ShaderMaterial({ - name: 'EquirectangularToCubeUV', - - uniforms: { - envMap: { value: null } - }, - - vertexShader: _getCommonVertexShader(), - - fragmentShader: /* glsl */ ` - - precision mediump float; - precision mediump int; - - varying vec3 vOutputDirection; - - uniform sampler2D envMap; - - #include - - void main() { - - vec3 outputDirection = normalize( vOutputDirection ); - vec2 uv = equirectUv( outputDirection ); - - gl_FragColor = vec4( texture2D ( envMap, uv ).rgb, 1.0 ); - - } - `, - - blending: NoBlending, - depthTest: false, - depthWrite: false - }) -} - -function _getCubemapMaterial() { - return new ShaderMaterial({ - name: 'CubemapToCubeUV', - - uniforms: { - envMap: { value: null }, - flipEnvMap: { value: -1 } - }, - - vertexShader: _getCommonVertexShader(), - - fragmentShader: /* glsl */ ` - - precision mediump float; - precision mediump int; - - uniform float flipEnvMap; - - varying vec3 vOutputDirection; - - uniform samplerCube envMap; - - void main() { - - gl_FragColor = textureCube( envMap, vec3( flipEnvMap * vOutputDirection.x, vOutputDirection.yz ) ); - - } - `, - - blending: NoBlending, - depthTest: false, - depthWrite: false - }) -} - -function _getCommonVertexShader() { - return /* glsl */ ` - - precision mediump float; - precision mediump int; - - attribute float faceIndex; - - varying vec3 vOutputDirection; - - // RH coordinate system; PMREM face-indexing convention - vec3 getDirection( vec2 uv, float face ) { - - uv = 2.0 * uv - 1.0; - - vec3 direction = vec3( uv, 1.0 ); - - if ( face == 0.0 ) { - - direction = direction.zyx; // ( 1, v, u ) pos x - - } else if ( face == 1.0 ) { - - direction = direction.xzy; - direction.xz *= -1.0; // ( -u, 1, -v ) pos y - - } else if ( face == 2.0 ) { - - direction.x *= -1.0; // ( -u, v, 1 ) pos z - - } else if ( face == 3.0 ) { - - direction = direction.zyx; - direction.xz *= -1.0; // ( -1, v, -u ) neg x - - } else if ( face == 4.0 ) { - - direction = direction.xzy; - direction.xy *= -1.0; // ( -u, -1, v ) neg y - - } else if ( face == 5.0 ) { - - direction.z *= -1.0; // ( u, v, -1 ) neg z - - } - - return direction; - - } - - void main() { - - vOutputDirection = getDirection( uv, faceIndex ); - gl_Position = vec4( position, 1.0 ); - - } - ` -} - -export {PMREMGenerator}; diff --git a/src/Controllers.tsx b/src/Controllers.tsx index 69e78c3..7b2eb6a 100644 --- a/src/Controllers.tsx +++ b/src/Controllers.tsx @@ -99,7 +99,6 @@ export function Controllers({ rayMaterial = {}, hideRaysOnBlur = false, envMap, for (const target of controllers) { if (target.controllerModel) { target.controllerModel.xrControllerModel.setEnvironmentMap(envMap, envMapIntensity) - console.log('updated environment map') } } }, [envMap, envMapIntensity, controllers]) From 254dd08c663814b4c1473f7d325f574784b71c43 Mon Sep 17 00:00:00 2001 From: Michael Bashurov Date: Tue, 11 Jul 2023 14:59:04 +0300 Subject: [PATCH 04/18] wip --- src/Controllers.tsx | 38 +++++++++++++++----------- src/XRController.tsx | 47 +++++++-------------------------- src/XRControllerModelFactory.ts | 21 +++------------ 3 files changed, 35 insertions(+), 71 deletions(-) diff --git a/src/Controllers.tsx b/src/Controllers.tsx index 7b2eb6a..ab55d17 100644 --- a/src/Controllers.tsx +++ b/src/Controllers.tsx @@ -2,9 +2,8 @@ import * as React from 'react' import * as THREE from 'three' import { useFrame, Object3DNode, extend, createPortal } from '@react-three/fiber' import { useXR } from './XR' -import { ControllerModel, XRController } from './XRController' -import { useIsomorphicLayoutEffect } from './utils' -import { XRControllerModelFactory } from './XRControllerModelFactory' +import { XRController } from './XRController' +import { XRControllerModel, XRControllerModelFactory } from './XRControllerModelFactory' import { useCallback, useEffect } from 'react' import { Texture } from 'three' @@ -26,6 +25,10 @@ export const Ray = React.forwardRef(function Ray({ target, // Show ray line when hovering objects useFrame(() => { + if (!target.inputSource) { + return + } + let rayLength = 1 const intersection: THREE.Intersection = hoverState[target.inputSource.handedness].values().next().value @@ -51,7 +54,7 @@ const modelFactory = new XRControllerModelFactory() declare global { namespace JSX { interface IntrinsicElements { - controllerModel: Object3DNode + xRControllerModel: Object3DNode } } } @@ -79,17 +82,20 @@ export function Controllers({ rayMaterial = {}, hideRaysOnBlur = false, envMap, ), [JSON.stringify(rayMaterial)] // eslint-disable-line react-hooks/exhaustive-deps ) - React.useMemo(() => extend({ ControllerModel }), []) + React.useMemo(() => extend({ XRControllerModel }), []) - // Send fake connected event (no-op) so models start loading - useIsomorphicLayoutEffect(() => { - for (const target of controllers) { - target.controller.dispatchEvent({ type: 'connected', data: target.inputSource, fake: true }) + const handleControllerModel = useCallback((xrControllerModel: XRControllerModel | null, target: XRController) => { + if (xrControllerModel) { + target.xrControllerModel = xrControllerModel + if (target.inputSource) { + modelFactory.initializeControllerModel(xrControllerModel, target.inputSource) + } else { + console.warn('no input source on XRController when handleControllerModel') + } + } else { + target.xrControllerModel?.disconnect() + target.xrControllerModel = null } - }, [controllers]) - - const handleControllerModel = useCallback((r: ControllerModel | null, target: XRController) => { - target.controllerModel = r }, []) useEffect(() => { @@ -97,8 +103,8 @@ export function Controllers({ rayMaterial = {}, hideRaysOnBlur = false, envMap, return } for (const target of controllers) { - if (target.controllerModel) { - target.controllerModel.xrControllerModel.setEnvironmentMap(envMap, envMapIntensity) + if (target.xrControllerModel) { + target.xrControllerModel.setEnvironmentMap(envMap, envMapIntensity) } } }, [envMap, envMapIntensity, controllers]) @@ -107,7 +113,7 @@ export function Controllers({ rayMaterial = {}, hideRaysOnBlur = false, envMap, <> {controllers.map((target, i) => ( - {createPortal( handleControllerModel(r, target)} args={[target, modelFactory]} />, target.grip)} + {createPortal( handleControllerModel(r, target)} args={[]} />, target.grip)} {createPortal( , target.controller diff --git a/src/XRController.tsx b/src/XRController.tsx index 8d9b06d..df1835c 100644 --- a/src/XRController.tsx +++ b/src/XRController.tsx @@ -1,48 +1,17 @@ import * as THREE from 'three' import { XRControllerEvent } from './XREvents' -import {XRControllerModel, XRControllerModelFactory} from "./XRControllerModelFactory"; - - -export class ControllerModel extends THREE.Group { - readonly target: XRController - readonly xrControllerModel: XRControllerModel - private modelFactory: XRControllerModelFactory; - - constructor(target: XRController, modelFactory: XRControllerModelFactory) { - super() - this.xrControllerModel = new XRControllerModel() - this.target = target - this.modelFactory = modelFactory - this.add(this.xrControllerModel) - - this._onConnected = this._onConnected.bind(this) - this._onDisconnected = this._onDisconnected.bind(this) - - this.target.controller.addEventListener('connected', this._onConnected) - this.target.controller.addEventListener('disconnected', this._onDisconnected) - } - - private _onConnected(event: XRControllerEvent) { - this.modelFactory.initializeControllerModel(this.xrControllerModel, event) - } - - private _onDisconnected(_event: XRControllerEvent) { - this.xrControllerModel.disconnect() - } - - dispose() { - this.target.controller.removeEventListener('connected', this._onConnected) - this.target.controller.removeEventListener('disconnected', this._onDisconnected) - } -} +import { XRControllerModel } from './XRControllerModelFactory' +/** Counterpart of WebXRController from three ks + * in a sense that it's long living */ export class XRController extends THREE.Group { readonly index: number + // TODO rename it? readonly controller: THREE.XRTargetRaySpace readonly grip: THREE.XRGripSpace readonly hand: THREE.XRHandSpace - public inputSource!: XRInputSource - public controllerModel: ControllerModel | null = null; + public inputSource: XRInputSource | null = null + public xrControllerModel: XRControllerModel | null = null constructor(index: number, gl: THREE.WebGLRenderer) { super() @@ -57,6 +26,7 @@ export class XRController extends THREE.Group { this.hand.userData.name = 'hand' this.visible = false + // TODO is this needed? this.add(this.controller, this.grip, this.hand) this._onConnected = this._onConnected.bind(this) @@ -68,9 +38,10 @@ export class XRController extends THREE.Group { _onConnected(event: XRControllerEvent) { if (event.fake) return + if (!event.data) return this.visible = true - this.inputSource = event.data! + this.inputSource = event.data this.dispatchEvent(event) } diff --git a/src/XRControllerModelFactory.ts b/src/XRControllerModelFactory.ts index ef48299..6dd08fe 100644 --- a/src/XRControllerModelFactory.ts +++ b/src/XRControllerModelFactory.ts @@ -1,12 +1,4 @@ -import { - Mesh, - Object3D, - SphereGeometry, - MeshBasicMaterial, - MeshStandardMaterial, - MeshPhongMaterial, - MeshLambertMaterial -} from 'three' +import { Mesh, Object3D, SphereGeometry, MeshBasicMaterial, MeshStandardMaterial, MeshPhongMaterial, MeshLambertMaterial, Group } from 'three' import type { Texture } from 'three' import { fetchProfile, GLTFLoader, MotionController, MotionControllerConstants } from 'three-stdlib' @@ -27,7 +19,7 @@ const applyEnvironmentMap = (envMap: Texture, envMapIntensity: number, obj: Obje }) } -export class XRControllerModel extends Object3D { +export class XRControllerModel extends Group { envMap: Texture | null envMapIntensity: number motionController: MotionController | null @@ -60,16 +52,12 @@ export class XRControllerModel extends Object3D { return } - - this.scene = scene addAssetSceneToControllerModel(this, scene) this.dispatchEvent({ type: 'modelconnected', data: scene }) - - console.log('model connected') } connectMotionController(motionController: MotionController): void { @@ -217,9 +205,8 @@ export class XRControllerModelFactory { this._assetCache = {} } - initializeControllerModel(controllerModel: XRControllerModel, event: any): void { - const xrInputSource = event.data - + initializeControllerModel(controllerModel: XRControllerModel, xrInputSource: XRInputSource): void { + // TODO check gamepad in other condition if (xrInputSource.targetRayMode !== 'tracked-pointer' || !xrInputSource.gamepad) return fetchProfile(xrInputSource, this.path, DEFAULT_PROFILE) From 0327bd8c2dc55db8a9513848cda94864738859be Mon Sep 17 00:00:00 2001 From: Michael Bashurov Date: Tue, 11 Jul 2023 15:03:56 +0300 Subject: [PATCH 05/18] fix --- src/Controllers.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Controllers.tsx b/src/Controllers.tsx index f654d98..4cb5b44 100644 --- a/src/Controllers.tsx +++ b/src/Controllers.tsx @@ -87,12 +87,18 @@ export function Controllers({ rayMaterial = {}, hideRaysOnBlur = false, envMap, const handleControllerModel = useCallback((xrControllerModel: XRControllerModel | null, target: XRController) => { if (xrControllerModel) { target.xrControllerModel = xrControllerModel - if (target.inputSource && !!target.inputSource.hand) { + if (target.inputSource?.hand) { + return + } + if (target.inputSource) { modelFactory.initializeControllerModel(xrControllerModel, target.inputSource) } else { console.warn('no input source on XRController when handleControllerModel') } } else { + if (target.inputSource?.hand) { + return + } target.xrControllerModel?.disconnect() target.xrControllerModel = null } From 9d57937d746da9bdf24e9c763cf7610f54c8db8c Mon Sep 17 00:00:00 2001 From: Michael Bashurov Date: Tue, 11 Jul 2023 15:04:37 +0300 Subject: [PATCH 06/18] fix --- vitest.config.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/vitest.config.ts b/vitest.config.ts index 4afcd13..7d45fd8 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,8 +1,7 @@ import { defineConfig } from 'vitest/config' export default defineConfig({ - test: { environment: 'happy-dom' - }, -}) \ No newline at end of file + } +}) From 004fce2ddcc9f952bbb0d47aad06c9dd537b79d4 Mon Sep 17 00:00:00 2001 From: Michael Bashurov Date: Tue, 11 Jul 2023 15:08:53 +0300 Subject: [PATCH 07/18] ts fixes --- src/Interactions.tsx | 6 ++++++ src/Teleportation.tsx | 2 +- src/XR.tsx | 2 +- src/XRController.tsx | 1 + src/XREvents.ts | 2 +- src/mocks/XRControllerMock.ts | 5 ++++- 6 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/Interactions.tsx b/src/Interactions.tsx index 2265fc4..11ce5a3 100644 --- a/src/Interactions.tsx +++ b/src/Interactions.tsx @@ -56,6 +56,9 @@ export function InteractionManager({ children }: { children: React.ReactNode }) if (interactions.size === 0) return for (const target of controllers) { + if (!target.inputSource?.handedness) { + return + } const hovering = hoverState[target.inputSource.handedness] const hits = new Set() let intersections = intersect(target.controller) @@ -109,6 +112,9 @@ export function InteractionManager({ children }: { children: React.ReactNode }) const triggerEvent = React.useCallback( (interaction: XRInteractionType) => (e: XREvent) => { + if (!e.target.inputSource?.handedness) { + return + } const hovering = hoverState[e.target.inputSource.handedness] const intersections = Array.from(new Set(hovering.values())) diff --git a/src/Teleportation.tsx b/src/Teleportation.tsx index dbb02a0..38d4e0c 100644 --- a/src/Teleportation.tsx +++ b/src/Teleportation.tsx @@ -73,7 +73,7 @@ export const TeleportationPlane = React.forwardRef { - const { handedness } = e.target.inputSource + const handedness = e.target.inputSource?.handedness return !!((handedness !== 'left' || leftHand) && (handedness !== 'right' || rightHand)) }, [leftHand, rightHand] diff --git a/src/XR.tsx b/src/XR.tsx index 2e0991b..ee59cdc 100644 --- a/src/XR.tsx +++ b/src/XR.tsx @@ -476,7 +476,7 @@ export function useXR( export function useController(handedness: XRHandedness) { const controllers = useXR((state) => state.controllers) const controller = React.useMemo( - () => controllers.find(({ inputSource }) => inputSource.handedness === handedness), + () => controllers.find(({ inputSource }) => inputSource?.handedness && inputSource.handedness === handedness), [handedness, controllers] ) diff --git a/src/XRController.tsx b/src/XRController.tsx index df1835c..c988b15 100644 --- a/src/XRController.tsx +++ b/src/XRController.tsx @@ -49,6 +49,7 @@ export class XRController extends THREE.Group { if (event.fake) return this.visible = false + this.inputSource = null this.dispatchEvent(event) } diff --git a/src/XREvents.ts b/src/XREvents.ts index 61418d9..f9cd7d3 100644 --- a/src/XREvents.ts +++ b/src/XREvents.ts @@ -30,7 +30,7 @@ export function useXREvent(event: XRControllerEventType, handler: XREventHandler useIsomorphicLayoutEffect(() => { const listeners = controllers.map((target) => { - if (handedness && target.inputSource.handedness !== handedness) return + if (handedness && target.inputSource && target.inputSource.handedness !== handedness) return const listener = (nativeEvent: XRControllerEvent) => handlerRef.current({ nativeEvent, target }) target.controller.addEventListener(event, listener) diff --git a/src/mocks/XRControllerMock.ts b/src/mocks/XRControllerMock.ts index 36e21a6..4ffdac8 100644 --- a/src/mocks/XRControllerMock.ts +++ b/src/mocks/XRControllerMock.ts @@ -1,4 +1,4 @@ -import { XRController, XRControllerEvent } from '@react-three/xr' +import { XRController, XRControllerEvent, XRControllerModel } from '@react-three/xr' import { Group, XRTargetRaySpace, XRGripSpace, XRHandSpace, Vector3, XRHandInputState, XRHandJoints } from 'three' export class XRTargetRaySpaceMock extends Group implements XRTargetRaySpace { @@ -29,6 +29,9 @@ export class XRControllerMock extends Group implements XRController { // TODO Implement mocks for inputSource // @ts-ignore inputSource: XRInputSource + // TODO Implement mocks for xrControllerModel + // @ts-ignore + public xrControllerModel: XRControllerModel | null constructor(index: number) { super() From c715086da32c6a810aab8e0983d32fdd950f6a0a Mon Sep 17 00:00:00 2001 From: Michael Bashurov Date: Tue, 29 Aug 2023 12:13:47 +0300 Subject: [PATCH 08/18] wip --- examples/src/demos/ControllersEnvMap.tsx | 8 ++++---- src/Controllers.tsx | 22 +++++++++++++++------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/examples/src/demos/ControllersEnvMap.tsx b/examples/src/demos/ControllersEnvMap.tsx index 35ff313..34a10e3 100644 --- a/examples/src/demos/ControllersEnvMap.tsx +++ b/examples/src/demos/ControllersEnvMap.tsx @@ -8,8 +8,9 @@ import EnvMap from '../assets/brown_photostudio_04_256.hdr' function ControllersWithEnvMap() { const renderer = useThree(({ gl }) => gl) const [envMap, setEnvMap] = useState() + useEffect(() => { - const foo = async () => { + const generateEnvMap = async () => { const rgbeLoader = new RGBELoader() const dataTexture = await rgbeLoader.loadAsync(EnvMap) const pmremGenerator = new PMREMGenerator(renderer) @@ -19,7 +20,8 @@ function ControllersWithEnvMap() { setEnvMap(radianceMap) pmremGenerator.dispose() } - foo() + + generateEnvMap() }, [renderer]) return @@ -31,8 +33,6 @@ export default function () { console.error(e)} /> - {/**/} - {/**/} diff --git a/src/Controllers.tsx b/src/Controllers.tsx index 403bbc5..4a97c9a 100644 --- a/src/Controllers.tsx +++ b/src/Controllers.tsx @@ -5,6 +5,7 @@ import { useXR } from './XR' import { XRController } from './XRController' import { XRControllerModelFactory } from './XRControllerModelFactory' import { XRControllerModel } from './XRControllerModel' +import { useCallbackRef } from './utils' export interface RayProps extends Partial { /** The XRController to attach the ray to */ @@ -77,12 +78,19 @@ const ControllerModel = ({ envMapIntensity?: number }) => { const xrControllerModelRef = React.useRef(null) - + const setEnvironmentMapRef = useCallbackRef((xrControllerModel: XRControllerModel) => xrControllerModel.setEnvironmentMap(envMap ?? null)) + const setEnvironmentMapIntensityRef = useCallbackRef((xrControllerModel: XRControllerModel) => { + if (envMapIntensity == null) return + xrControllerModel.setEnvironmentMapIntensity(envMapIntensity) + }) + const handleControllerModel = React.useCallback( (xrControllerModel: XRControllerModel | null) => { xrControllerModelRef.current = xrControllerModel if (xrControllerModel) { target.xrControllerModel = xrControllerModel + setEnvironmentMapRef.current(xrControllerModel) + setEnvironmentMapIntensityRef.current(xrControllerModel) if (target.inputSource?.hand) { return } @@ -99,20 +107,20 @@ const ControllerModel = ({ target.xrControllerModel = null } }, - [target] + [setEnvironmentMapIntensityRef, setEnvironmentMapRef, target] ) React.useLayoutEffect(() => { if (xrControllerModelRef.current) { - xrControllerModelRef.current.setEnvironmentMap(envMap ?? null) + setEnvironmentMapRef.current(xrControllerModelRef.current) } - }, [envMap]) + }, [envMap, setEnvironmentMapRef]) React.useLayoutEffect(() => { - if (xrControllerModelRef.current && envMapIntensity != null) { - xrControllerModelRef.current.setEnvironmentMapIntensity(envMapIntensity) + if (xrControllerModelRef.current) { + setEnvironmentMapIntensityRef.current(xrControllerModelRef.current) } - }, [envMapIntensity]) + }, [envMapIntensity, setEnvironmentMapIntensityRef]) return } From c5b8406f3ea887ebeba9a2d9522585d84270d2cb Mon Sep 17 00:00:00 2001 From: Michael Bashurov Date: Tue, 29 Aug 2023 12:14:53 +0300 Subject: [PATCH 09/18] wip --- src/XRController.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/XRController.ts b/src/XRController.ts index 0ed0013..ec6b18e 100644 --- a/src/XRController.ts +++ b/src/XRController.ts @@ -6,7 +6,6 @@ import { XRControllerModel } from './XRControllerModel' * in a sense that it's long living */ export class XRController extends THREE.Group { readonly index: number - // TODO rename it? readonly controller: THREE.XRTargetRaySpace readonly grip: THREE.XRGripSpace readonly hand: THREE.XRHandSpace @@ -26,7 +25,6 @@ export class XRController extends THREE.Group { this.hand.userData.name = 'hand' this.visible = false - // TODO is this needed? this.add(this.controller, this.grip, this.hand) this._onConnected = this._onConnected.bind(this) From 11389d4b355e93bbf420dc931ed75eccd9db5b78 Mon Sep 17 00:00:00 2001 From: Michael Bashurov Date: Tue, 29 Aug 2023 12:27:51 +0300 Subject: [PATCH 10/18] wip --- src/XRControllerModel.test.ts | 76 ++++++++++++++++++++++++++++-- src/XRControllerModel.ts | 7 +-- src/mocks/XRControllerModelMock.ts | 5 +- 3 files changed, 81 insertions(+), 7 deletions(-) diff --git a/src/XRControllerModel.test.ts b/src/XRControllerModel.test.ts index f92ac95..ea2b502 100644 --- a/src/XRControllerModel.test.ts +++ b/src/XRControllerModel.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, vi } from 'vitest' import { XRControllerModel } from './XRControllerModel' -import { Object3D, Texture } from 'three' +import { BoxBufferGeometry, Mesh, MeshStandardMaterial, Object3D, Texture } from 'three' import { MotionControllerMock } from './mocks/MotionControllerMock' describe('XRControllerModel', () => { @@ -35,18 +35,88 @@ describe('XRControllerModel', () => { expect(xrControllerModel.children).toContain(sceneMock) }) - it('should set and apply environment map when setEnvironment map is called', () => { + it('should set and apply environment map when setEnvironment map is called after scene is loaded', () => { const xrControllerModel = new XRControllerModel() const motionControllerMock = new MotionControllerMock() const sceneMock = new Object3D() + const mesh = new Mesh(new BoxBufferGeometry(), new MeshStandardMaterial()) + sceneMock.add(mesh) + const materialNeedsUpdateSpy = vi.spyOn(mesh.material, 'needsUpdate', 'set') const envMapMock = new Texture() xrControllerModel.connectMotionController(motionControllerMock) xrControllerModel.connectModel(sceneMock) - xrControllerModel.setEnvironmentMap(envMapMock, 0.5) + xrControllerModel.setEnvironmentMap(envMapMock) + xrControllerModel.setEnvironmentMapIntensity(0.5) expect(xrControllerModel.envMap).toBe(envMapMock) expect(xrControllerModel.envMapIntensity).toBe(0.5) + + expect(mesh.material.envMap).toBe(envMapMock) + expect(mesh.material.envMapIntensity).toBe(0.5) + expect(materialNeedsUpdateSpy).toBeCalledWith(true) + }) + + + it('should set and apply environment map when setEnvironment map is called before scene is loaded', () => { + const xrControllerModel = new XRControllerModel() + const motionControllerMock = new MotionControllerMock() + const sceneMock = new Object3D() + const mesh = new Mesh(new BoxBufferGeometry(), new MeshStandardMaterial()) + sceneMock.add(mesh) + const materialNeedsUpdateSpy = vi.spyOn(mesh.material, 'needsUpdate', 'set') + const envMapMock = new Texture() + + xrControllerModel.connectMotionController(motionControllerMock) + xrControllerModel.setEnvironmentMap(envMapMock) + xrControllerModel.setEnvironmentMapIntensity(0.5) + xrControllerModel.connectModel(sceneMock) + + expect(xrControllerModel.envMap).toBe(envMapMock) + expect(xrControllerModel.envMapIntensity).toBe(0.5) + + expect(mesh.material.envMap).toBe(envMapMock) + expect(mesh.material.envMapIntensity).toBe(0.5) + expect(materialNeedsUpdateSpy).toBeCalledWith(true) + }) + + it('should set environment map intensity when setEnvironment map is called before scene is loaded', () => { + const xrControllerModel = new XRControllerModel() + const motionControllerMock = new MotionControllerMock() + const sceneMock = new Object3D() + const mesh = new Mesh(new BoxBufferGeometry(), new MeshStandardMaterial()) + sceneMock.add(mesh) + const materialNeedsUpdateSpy = vi.spyOn(mesh.material, 'needsUpdate', 'set') + + xrControllerModel.connectMotionController(motionControllerMock) + xrControllerModel.setEnvironmentMapIntensity(0.5) + xrControllerModel.connectModel(sceneMock) + + expect(xrControllerModel.envMap).toBe(null) + expect(xrControllerModel.envMapIntensity).toBe(0.5) + + expect(mesh.material.envMap).toBe(null) + expect(mesh.material.envMapIntensity).toBe(0.5) + expect(materialNeedsUpdateSpy).toBeCalledWith(true) + }) + + it('should remove environment map when setEnvironment map is called with null', () => { + const xrControllerModel = new XRControllerModel() + const motionControllerMock = new MotionControllerMock() + const sceneMock = new Object3D() + const mesh = new Mesh(new BoxBufferGeometry(), new MeshStandardMaterial()) + sceneMock.add(mesh) + const materialNeedsUpdateSpy = vi.spyOn(mesh.material, 'needsUpdate', 'set') + const envMapMock = new Texture() + + xrControllerModel.connectMotionController(motionControllerMock) + xrControllerModel.connectModel(sceneMock) + xrControllerModel.setEnvironmentMap(envMapMock) + xrControllerModel.setEnvironmentMap(null) + + expect(xrControllerModel.envMap).toBe(null) + expect(mesh.material.envMap).toBe(null) + expect(materialNeedsUpdateSpy).toBeCalledWith(true) }) it('should update motioncontroller from gamepad on updateMatrixWorld', () => { diff --git a/src/XRControllerModel.ts b/src/XRControllerModel.ts index 5d34871..c0031be 100644 --- a/src/XRControllerModel.ts +++ b/src/XRControllerModel.ts @@ -93,9 +93,10 @@ function addAssetSceneToControllerModel(controllerModel: XRControllerModel, scen // Apply any environment map that the mesh already has set. if (controllerModel.envMap) { applyEnvironmentMap(controllerModel.envMap, scene) - if (controllerModel.envMapIntensity != null) { - applyEnvironmentMapIntensity(controllerModel.envMapIntensity, scene) - } + } + + if (controllerModel.envMapIntensity != null) { + applyEnvironmentMapIntensity(controllerModel.envMapIntensity, scene) } // Add the glTF scene to the controllerModel. diff --git a/src/mocks/XRControllerModelMock.ts b/src/mocks/XRControllerModelMock.ts index e555457..364fee5 100644 --- a/src/mocks/XRControllerModelMock.ts +++ b/src/mocks/XRControllerModelMock.ts @@ -8,7 +8,10 @@ export class XRControllerModelMock extends Group implements XRControllerModel { envMapIntensity = 1 motionController: MotionController | null = null scene: Object3D | null = null - setEnvironmentMap(_envMap: Texture, _envMapIntensity?: number): XRControllerModel { + setEnvironmentMap(_envMap: Texture): XRControllerModel { + throw new Error('Method not implemented.') + } + setEnvironmentMapIntensity( _envMapIntensity?: number): XRControllerModel { throw new Error('Method not implemented.') } connectModel = vi.fn<[scene: Object3D], void>() From be8ae887ef4ed36a12f480fa066d606866184b95 Mon Sep 17 00:00:00 2001 From: Michael Bashurov Date: Tue, 29 Aug 2023 12:43:24 +0300 Subject: [PATCH 11/18] tests --- src/Controllers.test.tsx | 56 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/src/Controllers.test.tsx b/src/Controllers.test.tsx index c567ecf..9ad6c39 100644 --- a/src/Controllers.test.tsx +++ b/src/Controllers.test.tsx @@ -8,6 +8,7 @@ import { XRControllerModel } from './XRControllerModel' import { XRControllerModelFactoryMock } from './mocks/XRControllerModelFactoryMock' import { XRInputSourceMock } from './mocks/XRInputSourceMock' import { act } from '@react-three/test-renderer' +import { Texture } from 'three' vi.mock('./XRControllerModelFactory', async () => { const { XRControllerModelFactoryMock } = await vi.importActual( @@ -141,4 +142,59 @@ describe('Controllers', () => { expect(disconnectSpy).not.toBeCalled() expect(xrControllerModelFactory?.initializeControllerModel).toBeCalledTimes(1) }) + + describe('envMap', () => { + it("should not set env map if it's not provided in props", async () => { + const store = createStoreMock() + const xrControllerMock = new XRControllerMock(0) + store.setState({ controllers: [xrControllerMock] }) + + await render(, { wrapper: createStoreProvider(store) }) + + const xrControllerModel = xrControllerMock.xrControllerModel + + expect(xrControllerModel!.envMap).toBeNull() + expect(xrControllerModel!.envMapIntensity).toBe(1) + }) + + it("should set env map if it's provided in props", async () => { + const store = createStoreMock() + const xrControllerMock = new XRControllerMock(0) + store.setState({ controllers: [xrControllerMock] }) + const envMap = new Texture() + + await render(, { wrapper: createStoreProvider(store) }) + + const xrControllerModel = xrControllerMock.xrControllerModel + + expect(xrControllerModel!.envMap).toBe(envMap) + expect(xrControllerModel!.envMapIntensity).toBe(1) + }) + + it("should only set env map intensity if it's provided in props", async () => { + const store = createStoreMock() + const xrControllerMock = new XRControllerMock(0) + store.setState({ controllers: [xrControllerMock] }) + + await render(, { wrapper: createStoreProvider(store) }) + + const xrControllerModel = xrControllerMock.xrControllerModel + + expect(xrControllerModel!.envMap).toBeNull() + expect(xrControllerModel!.envMapIntensity).toBe(0.5) + }) + + it("should remove env map if it's provided in props first, and then removed", async () => { + const store = createStoreMock() + const xrControllerMock = new XRControllerMock(0) + store.setState({ controllers: [xrControllerMock] }) + const envMap = new Texture() + + const { rerender } = await render(, { wrapper: createStoreProvider(store) }) + const xrControllerModel = xrControllerMock.xrControllerModel + await rerender() + + expect(xrControllerModel!.envMap).toBeNull() + }) + }) }) From 3270c36d5ef9eb579a2337c32310623e69251795 Mon Sep 17 00:00:00 2001 From: Michael Bashurov Date: Tue, 29 Aug 2023 12:47:10 +0300 Subject: [PATCH 12/18] docs --- src/Controllers.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Controllers.tsx b/src/Controllers.tsx index 4a97c9a..c9c0310 100644 --- a/src/Controllers.tsx +++ b/src/Controllers.tsx @@ -64,7 +64,16 @@ export interface ControllersProps { rayMaterial?: JSX.IntrinsicElements['meshBasicMaterial'] /** Whether to hide controllers' rays on blur. Default is `false` */ hideRaysOnBlur?: boolean + /** + * Optional environment map to apply to controllers' models + * Useful for make controllers look more realistic + * if you don't want to apply env map globally on a scene + */ envMap?: THREE.Texture + /** + * Optional environment map intensity to apply to controllers' models + * Useful for tweaking the env map intensity if they look too bright or too dark + */ envMapIntensity?: number } From d781114c4bafe65f3458fc1f133c09f9b81f025f Mon Sep 17 00:00:00 2001 From: Michael Bashurov Date: Tue, 29 Aug 2023 12:52:15 +0300 Subject: [PATCH 13/18] docs --- README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 51ce446..8df624e 100644 --- a/README.md +++ b/README.md @@ -160,6 +160,10 @@ Controllers can be added with `` for [motion-controllers](https:/ /> ``` +### Environment map + +You can set environment map and/or it's intensity on controller models via props on ``. See [ControllerEnvMap](./examples/src/demos/ControllersEnvMap.tsx) to find out how to do it. + ### useController `useController` references an `XRController` by handedness, exposing position and orientation info. @@ -172,14 +176,15 @@ const gazeController = useController('none') ### XRController -`XRController` is an `Object3D` that represents an [`XRInputSource`](https://developer.mozilla.org/en-US/docs/Web/API/XRInputSource) with the following properties: +`XRController` is an long-living `Object3D` that represents an [`XRInputSource`](https://developer.mozilla.org/en-US/docs/Web/API/XRInputSource) with the following properties: ```jsx index: number controller: THREE.XRTargetRaySpace grip: THREE.XRGripSpace hand: THREE.XRHandSpace -inputSource: XRInputSource +inputSource: XRInputSource | null +xrControllerModel: XRControllerModel ``` ## Interactions From 7eff42ec17d019b75aca3b2338c5e56f3d45a934 Mon Sep 17 00:00:00 2001 From: Michael Bashurov Date: Tue, 29 Aug 2023 12:53:34 +0300 Subject: [PATCH 14/18] fix docs types --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8df624e..088e480 100644 --- a/README.md +++ b/README.md @@ -184,7 +184,7 @@ controller: THREE.XRTargetRaySpace grip: THREE.XRGripSpace hand: THREE.XRHandSpace inputSource: XRInputSource | null -xrControllerModel: XRControllerModel +xrControllerModel: XRControllerModel | null ``` ## Interactions From 73370f1e6fa373f550066d56f042f3f814c109e6 Mon Sep 17 00:00:00 2001 From: Michael Bashurov Date: Tue, 29 Aug 2023 12:56:19 +0300 Subject: [PATCH 15/18] tests --- src/Controllers.test.tsx | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Controllers.test.tsx b/src/Controllers.test.tsx index 9ad6c39..c85e9f4 100644 --- a/src/Controllers.test.tsx +++ b/src/Controllers.test.tsx @@ -184,6 +184,19 @@ describe('Controllers', () => { expect(xrControllerModel!.envMapIntensity).toBe(0.5) }) + it("should change env map intensity if it's provided in props then updated to a different value", async () => { + const store = createStoreMock() + const xrControllerMock = new XRControllerMock(0) + store.setState({ controllers: [xrControllerMock] }) + + const { rerender } = await render(, { wrapper: createStoreProvider(store) }) + const xrControllerModel = xrControllerMock.xrControllerModel + await rerender() + + expect(xrControllerModel!.envMap).toBeNull() + expect(xrControllerModel!.envMapIntensity).toBe(0.6) + }) + it("should remove env map if it's provided in props first, and then removed", async () => { const store = createStoreMock() const xrControllerMock = new XRControllerMock(0) From dbcf95595adea1913552cca8b189b8c50413541f Mon Sep 17 00:00:00 2001 From: Michael Bashurov Date: Tue, 29 Aug 2023 12:58:09 +0300 Subject: [PATCH 16/18] test refactoring --- src/XRControllerModel.test.ts | 159 +++++++++++++++++----------------- 1 file changed, 80 insertions(+), 79 deletions(-) diff --git a/src/XRControllerModel.test.ts b/src/XRControllerModel.test.ts index ea2b502..181c776 100644 --- a/src/XRControllerModel.test.ts +++ b/src/XRControllerModel.test.ts @@ -35,97 +35,98 @@ describe('XRControllerModel', () => { expect(xrControllerModel.children).toContain(sceneMock) }) - it('should set and apply environment map when setEnvironment map is called after scene is loaded', () => { - const xrControllerModel = new XRControllerModel() - const motionControllerMock = new MotionControllerMock() - const sceneMock = new Object3D() - const mesh = new Mesh(new BoxBufferGeometry(), new MeshStandardMaterial()) - sceneMock.add(mesh) - const materialNeedsUpdateSpy = vi.spyOn(mesh.material, 'needsUpdate', 'set') - const envMapMock = new Texture() - - xrControllerModel.connectMotionController(motionControllerMock) - xrControllerModel.connectModel(sceneMock) - xrControllerModel.setEnvironmentMap(envMapMock) - xrControllerModel.setEnvironmentMapIntensity(0.5) - - expect(xrControllerModel.envMap).toBe(envMapMock) - expect(xrControllerModel.envMapIntensity).toBe(0.5) - - expect(mesh.material.envMap).toBe(envMapMock) - expect(mesh.material.envMapIntensity).toBe(0.5) - expect(materialNeedsUpdateSpy).toBeCalledWith(true) - }) - - - it('should set and apply environment map when setEnvironment map is called before scene is loaded', () => { + it('should update motioncontroller from gamepad on updateMatrixWorld', () => { const xrControllerModel = new XRControllerModel() const motionControllerMock = new MotionControllerMock() - const sceneMock = new Object3D() - const mesh = new Mesh(new BoxBufferGeometry(), new MeshStandardMaterial()) - sceneMock.add(mesh) - const materialNeedsUpdateSpy = vi.spyOn(mesh.material, 'needsUpdate', 'set') - const envMapMock = new Texture() xrControllerModel.connectMotionController(motionControllerMock) - xrControllerModel.setEnvironmentMap(envMapMock) - xrControllerModel.setEnvironmentMapIntensity(0.5) - xrControllerModel.connectModel(sceneMock) - - expect(xrControllerModel.envMap).toBe(envMapMock) - expect(xrControllerModel.envMapIntensity).toBe(0.5) + xrControllerModel.updateMatrixWorld(false) - expect(mesh.material.envMap).toBe(envMapMock) - expect(mesh.material.envMapIntensity).toBe(0.5) - expect(materialNeedsUpdateSpy).toBeCalledWith(true) + expect(xrControllerModel.motionController?.updateFromGamepad).toBeCalled() }) - it('should set environment map intensity when setEnvironment map is called before scene is loaded', () => { - const xrControllerModel = new XRControllerModel() - const motionControllerMock = new MotionControllerMock() - const sceneMock = new Object3D() - const mesh = new Mesh(new BoxBufferGeometry(), new MeshStandardMaterial()) - sceneMock.add(mesh) - const materialNeedsUpdateSpy = vi.spyOn(mesh.material, 'needsUpdate', 'set') - - xrControllerModel.connectMotionController(motionControllerMock) - xrControllerModel.setEnvironmentMapIntensity(0.5) - xrControllerModel.connectModel(sceneMock) + describe('envMap', () => { + it('should set and apply environment map when setEnvironment map is called after scene is loaded', () => { + const xrControllerModel = new XRControllerModel() + const motionControllerMock = new MotionControllerMock() + const sceneMock = new Object3D() + const mesh = new Mesh(new BoxBufferGeometry(), new MeshStandardMaterial()) + sceneMock.add(mesh) + const materialNeedsUpdateSpy = vi.spyOn(mesh.material, 'needsUpdate', 'set') + const envMapMock = new Texture() + + xrControllerModel.connectMotionController(motionControllerMock) + xrControllerModel.connectModel(sceneMock) + xrControllerModel.setEnvironmentMap(envMapMock) + xrControllerModel.setEnvironmentMapIntensity(0.5) + + expect(xrControllerModel.envMap).toBe(envMapMock) + expect(xrControllerModel.envMapIntensity).toBe(0.5) + + expect(mesh.material.envMap).toBe(envMapMock) + expect(mesh.material.envMapIntensity).toBe(0.5) + expect(materialNeedsUpdateSpy).toBeCalledWith(true) + }) - expect(xrControllerModel.envMap).toBe(null) - expect(xrControllerModel.envMapIntensity).toBe(0.5) + it('should set and apply environment map when setEnvironment map is called before scene is loaded', () => { + const xrControllerModel = new XRControllerModel() + const motionControllerMock = new MotionControllerMock() + const sceneMock = new Object3D() + const mesh = new Mesh(new BoxBufferGeometry(), new MeshStandardMaterial()) + sceneMock.add(mesh) + const materialNeedsUpdateSpy = vi.spyOn(mesh.material, 'needsUpdate', 'set') + const envMapMock = new Texture() + + xrControllerModel.connectMotionController(motionControllerMock) + xrControllerModel.setEnvironmentMap(envMapMock) + xrControllerModel.setEnvironmentMapIntensity(0.5) + xrControllerModel.connectModel(sceneMock) + + expect(xrControllerModel.envMap).toBe(envMapMock) + expect(xrControllerModel.envMapIntensity).toBe(0.5) + + expect(mesh.material.envMap).toBe(envMapMock) + expect(mesh.material.envMapIntensity).toBe(0.5) + expect(materialNeedsUpdateSpy).toBeCalledWith(true) + }) - expect(mesh.material.envMap).toBe(null) - expect(mesh.material.envMapIntensity).toBe(0.5) - expect(materialNeedsUpdateSpy).toBeCalledWith(true) - }) + it('should set environment map intensity when setEnvironment map is called before scene is loaded', () => { + const xrControllerModel = new XRControllerModel() + const motionControllerMock = new MotionControllerMock() + const sceneMock = new Object3D() + const mesh = new Mesh(new BoxBufferGeometry(), new MeshStandardMaterial()) + sceneMock.add(mesh) + const materialNeedsUpdateSpy = vi.spyOn(mesh.material, 'needsUpdate', 'set') - it('should remove environment map when setEnvironment map is called with null', () => { - const xrControllerModel = new XRControllerModel() - const motionControllerMock = new MotionControllerMock() - const sceneMock = new Object3D() - const mesh = new Mesh(new BoxBufferGeometry(), new MeshStandardMaterial()) - sceneMock.add(mesh) - const materialNeedsUpdateSpy = vi.spyOn(mesh.material, 'needsUpdate', 'set') - const envMapMock = new Texture() + xrControllerModel.connectMotionController(motionControllerMock) + xrControllerModel.setEnvironmentMapIntensity(0.5) + xrControllerModel.connectModel(sceneMock) - xrControllerModel.connectMotionController(motionControllerMock) - xrControllerModel.connectModel(sceneMock) - xrControllerModel.setEnvironmentMap(envMapMock) - xrControllerModel.setEnvironmentMap(null) + expect(xrControllerModel.envMap).toBe(null) + expect(xrControllerModel.envMapIntensity).toBe(0.5) - expect(xrControllerModel.envMap).toBe(null) - expect(mesh.material.envMap).toBe(null) - expect(materialNeedsUpdateSpy).toBeCalledWith(true) - }) - - it('should update motioncontroller from gamepad on updateMatrixWorld', () => { - const xrControllerModel = new XRControllerModel() - const motionControllerMock = new MotionControllerMock() - - xrControllerModel.connectMotionController(motionControllerMock) - xrControllerModel.updateMatrixWorld(false) + expect(mesh.material.envMap).toBe(null) + expect(mesh.material.envMapIntensity).toBe(0.5) + expect(materialNeedsUpdateSpy).toBeCalledWith(true) + }) - expect(xrControllerModel.motionController?.updateFromGamepad).toBeCalled() + it('should remove environment map when setEnvironment map is called with null', () => { + const xrControllerModel = new XRControllerModel() + const motionControllerMock = new MotionControllerMock() + const sceneMock = new Object3D() + const mesh = new Mesh(new BoxBufferGeometry(), new MeshStandardMaterial()) + sceneMock.add(mesh) + const materialNeedsUpdateSpy = vi.spyOn(mesh.material, 'needsUpdate', 'set') + const envMapMock = new Texture() + + xrControllerModel.connectMotionController(motionControllerMock) + xrControllerModel.connectModel(sceneMock) + xrControllerModel.setEnvironmentMap(envMapMock) + xrControllerModel.setEnvironmentMap(null) + + expect(xrControllerModel.envMap).toBe(null) + expect(mesh.material.envMap).toBe(null) + expect(materialNeedsUpdateSpy).toBeCalledWith(true) + }) }) }) From 75e4fe3067d0d11d18c6e84383c0e59c92015442 Mon Sep 17 00:00:00 2001 From: Michael Bashurov Date: Wed, 30 Aug 2023 16:42:06 +0300 Subject: [PATCH 17/18] review * supported array materials * returned api of setEnvironmentMap on XRControllerModel --- package.json | 2 +- src/Controllers.test.tsx | 14 ++ src/Controllers.tsx | 22 ++- src/XRControllerModel.test.ts | 63 ++++++- src/XRControllerModel.ts | 70 +++++--- yarn.lock | 309 +++++++++++++++++----------------- 6 files changed, 288 insertions(+), 192 deletions(-) diff --git a/package.json b/package.json index 6737bd7..5130bc9 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "three": "^0.141.0", "typescript": "^4.5.5", "vite": "^3.0.5", - "vitest": "^0.29.1" + "vitest": "^0.34.3" }, "dependencies": { "@types/webxr": "*", diff --git a/src/Controllers.test.tsx b/src/Controllers.test.tsx index c85e9f4..25c60be 100644 --- a/src/Controllers.test.tsx +++ b/src/Controllers.test.tsx @@ -209,5 +209,19 @@ describe('Controllers', () => { expect(xrControllerModel!.envMap).toBeNull() }) + + it("should change env map intensity if it's provided in props then updated to a different value but envMap stays the same", async () => { + const store = createStoreMock() + const xrControllerMock = new XRControllerMock(0) + store.setState({ controllers: [xrControllerMock] }) + const envMap = new Texture() + + const { rerender } = await render(, { wrapper: createStoreProvider(store) }) + const xrControllerModel = xrControllerMock.xrControllerModel + await rerender() + + expect(xrControllerModel!.envMap).toBe(envMap) + expect(xrControllerModel!.envMapIntensity).toBe(0.6) + }) }) }) diff --git a/src/Controllers.tsx b/src/Controllers.tsx index c9c0310..74a7164 100644 --- a/src/Controllers.tsx +++ b/src/Controllers.tsx @@ -87,7 +87,12 @@ const ControllerModel = ({ envMapIntensity?: number }) => { const xrControllerModelRef = React.useRef(null) - const setEnvironmentMapRef = useCallbackRef((xrControllerModel: XRControllerModel) => xrControllerModel.setEnvironmentMap(envMap ?? null)) + const setEnvironmentMapRef = useCallbackRef((xrControllerModel: XRControllerModel) => { + if (envMap == null) return + xrControllerModel.setEnvironmentMap(envMap ?? null) + }) + const clearEnvironmentMapRef = useCallbackRef((xrControllerModel: XRControllerModel) => xrControllerModel.setEnvironmentMap(null)) + const setEnvironmentMapIntensityRef = useCallbackRef((xrControllerModel: XRControllerModel) => { if (envMapIntensity == null) return xrControllerModel.setEnvironmentMapIntensity(envMapIntensity) @@ -98,11 +103,12 @@ const ControllerModel = ({ xrControllerModelRef.current = xrControllerModel if (xrControllerModel) { target.xrControllerModel = xrControllerModel - setEnvironmentMapRef.current(xrControllerModel) - setEnvironmentMapIntensityRef.current(xrControllerModel) if (target.inputSource?.hand) { return } + + setEnvironmentMapRef.current(xrControllerModel) + setEnvironmentMapIntensityRef.current(xrControllerModel) if (target.inputSource) { modelFactory.initializeControllerModel(xrControllerModel, target.inputSource) } else { @@ -116,14 +122,18 @@ const ControllerModel = ({ target.xrControllerModel = null } }, - [setEnvironmentMapIntensityRef, setEnvironmentMapRef, target] + [target, setEnvironmentMapIntensityRef, setEnvironmentMapRef] ) React.useLayoutEffect(() => { if (xrControllerModelRef.current) { - setEnvironmentMapRef.current(xrControllerModelRef.current) + if (envMap) { + setEnvironmentMapRef.current(xrControllerModelRef.current) + } else { + clearEnvironmentMapRef.current(xrControllerModelRef.current) + } } - }, [envMap, setEnvironmentMapRef]) + }, [envMap, setEnvironmentMapRef, clearEnvironmentMapRef]) React.useLayoutEffect(() => { if (xrControllerModelRef.current) { diff --git a/src/XRControllerModel.test.ts b/src/XRControllerModel.test.ts index 181c776..d7c5ca8 100644 --- a/src/XRControllerModel.test.ts +++ b/src/XRControllerModel.test.ts @@ -46,7 +46,7 @@ describe('XRControllerModel', () => { }) describe('envMap', () => { - it('should set and apply environment map when setEnvironment map is called after scene is loaded', () => { + it('should set and apply environment map when setEnvironmentMap is called after scene is loaded', () => { const xrControllerModel = new XRControllerModel() const motionControllerMock = new MotionControllerMock() const sceneMock = new Object3D() @@ -57,8 +57,7 @@ describe('XRControllerModel', () => { xrControllerModel.connectMotionController(motionControllerMock) xrControllerModel.connectModel(sceneMock) - xrControllerModel.setEnvironmentMap(envMapMock) - xrControllerModel.setEnvironmentMapIntensity(0.5) + xrControllerModel.setEnvironmentMap(envMapMock, 0.5) expect(xrControllerModel.envMap).toBe(envMapMock) expect(xrControllerModel.envMapIntensity).toBe(0.5) @@ -68,7 +67,7 @@ describe('XRControllerModel', () => { expect(materialNeedsUpdateSpy).toBeCalledWith(true) }) - it('should set and apply environment map when setEnvironment map is called before scene is loaded', () => { + it('should set and apply environment map when setEnvironmentMap is called after scene is loaded', () => { const xrControllerModel = new XRControllerModel() const motionControllerMock = new MotionControllerMock() const sceneMock = new Object3D() @@ -78,8 +77,56 @@ describe('XRControllerModel', () => { const envMapMock = new Texture() xrControllerModel.connectMotionController(motionControllerMock) + xrControllerModel.connectModel(sceneMock) xrControllerModel.setEnvironmentMap(envMapMock) - xrControllerModel.setEnvironmentMapIntensity(0.5) + + expect(xrControllerModel.envMap).toBe(envMapMock) + expect(xrControllerModel.envMapIntensity).toBe(1) + + expect(mesh.material.envMap).toBe(envMapMock) + expect(mesh.material.envMapIntensity).toBe(1) + expect(materialNeedsUpdateSpy).toBeCalledWith(true) + }) + + it('should set and apply environment map to an array material when setEnvironment map is called', () => { + const xrControllerModel = new XRControllerModel() + const motionControllerMock = new MotionControllerMock() + const sceneMock = new Object3D() + const material1 = new MeshStandardMaterial() + const material2 = new MeshStandardMaterial() + const mesh = new Mesh(new BoxBufferGeometry(), [material1, material2]) + sceneMock.add(mesh) + const material1NeedsUpdateSpy = vi.spyOn(material1, 'needsUpdate', 'set') + const material2NeedsUpdateSpy = vi.spyOn(material2, 'needsUpdate', 'set') + const envMapMock = new Texture() + + xrControllerModel.connectMotionController(motionControllerMock) + xrControllerModel.connectModel(sceneMock) + xrControllerModel.setEnvironmentMap(envMapMock, 0.5) + + expect(xrControllerModel.envMap).toBe(envMapMock) + expect(xrControllerModel.envMapIntensity).toBe(0.5) + + expect(material1.envMap).toBe(envMapMock) + expect(material1.envMapIntensity).toBe(0.5) + expect(material1NeedsUpdateSpy).toBeCalledWith(true) + + expect(material2.envMap).toBe(envMapMock) + expect(material2.envMapIntensity).toBe(0.5) + expect(material2NeedsUpdateSpy).toBeCalledWith(true) + }) + + it('should set and apply environment map when setEnvironment map is called before scene is loaded', () => { + const xrControllerModel = new XRControllerModel() + const motionControllerMock = new MotionControllerMock() + const sceneMock = new Object3D() + const mesh = new Mesh(new BoxBufferGeometry(), new MeshStandardMaterial()) + sceneMock.add(mesh) + const materialNeedsUpdateSpy = vi.spyOn(mesh.material, 'needsUpdate', 'set') + const envMapMock = new Texture() + + xrControllerModel.connectMotionController(motionControllerMock) + xrControllerModel.setEnvironmentMap(envMapMock, 0.5) xrControllerModel.connectModel(sceneMock) expect(xrControllerModel.envMap).toBe(envMapMock) @@ -90,7 +137,7 @@ describe('XRControllerModel', () => { expect(materialNeedsUpdateSpy).toBeCalledWith(true) }) - it('should set environment map intensity when setEnvironment map is called before scene is loaded', () => { + it('should set environment map intensity when setEnvironmentMapIntensity is called before scene is loaded', () => { const xrControllerModel = new XRControllerModel() const motionControllerMock = new MotionControllerMock() const sceneMock = new Object3D() @@ -110,7 +157,7 @@ describe('XRControllerModel', () => { expect(materialNeedsUpdateSpy).toBeCalledWith(true) }) - it('should remove environment map when setEnvironment map is called with null', () => { + it('should remove environment map when setEnvironmentMap is called with null', () => { const xrControllerModel = new XRControllerModel() const motionControllerMock = new MotionControllerMock() const sceneMock = new Object3D() @@ -122,6 +169,8 @@ describe('XRControllerModel', () => { xrControllerModel.connectMotionController(motionControllerMock) xrControllerModel.connectModel(sceneMock) xrControllerModel.setEnvironmentMap(envMapMock) + expect.soft(xrControllerModel).toBe(envMapMock) + xrControllerModel.setEnvironmentMap(null) expect(xrControllerModel.envMap).toBe(null) diff --git a/src/XRControllerModel.ts b/src/XRControllerModel.ts index c0031be..3701135 100644 --- a/src/XRControllerModel.ts +++ b/src/XRControllerModel.ts @@ -7,30 +7,47 @@ import { MeshLambertMaterial, MeshPhongMaterial, MeshStandardMaterial, - SphereGeometry + SphereGeometry, + Material } from 'three' import { MotionController, MotionControllerConstants } from 'three-stdlib' -const isEnvMapApplicable = ( - material: any -): material is MeshBasicMaterial | MeshStandardMaterial | MeshPhongMaterial | MeshLambertMaterial => 'envMap' in material +type MaterialsWithEnvMap = MeshBasicMaterial | MeshStandardMaterial | MeshPhongMaterial | MeshLambertMaterial + +const isEnvMapApplicable = (material: Material): material is MaterialsWithEnvMap => 'envMap' in material + +const updateEnvMap = (material: MaterialsWithEnvMap, envMap: Texture | null) => { + material.envMap = envMap + material.needsUpdate = true +} const applyEnvironmentMap = (envMap: Texture | null, obj: Object3D): void => { - obj.traverse((child) => { - if (child instanceof Mesh && isEnvMapApplicable(child.material)) { - child.material.envMap = envMap - child.material.needsUpdate = true + if (obj instanceof Mesh) { + if (Array.isArray(obj.material)) { + obj.material.forEach((m) => (isEnvMapApplicable(m) ? updateEnvMap(m, envMap) : undefined)) + } else if (isEnvMapApplicable(obj.material)) { + updateEnvMap(obj.material, envMap) } - }) + } +} + +type MaterialsWithEnvMapIntensity = Material & { envMapIntensity: any } + +const isEnvMapIntensityApplicable = (material: Material): material is MaterialsWithEnvMapIntensity => 'envMapIntensity' in material + +const updateEnvMapIntensity = (material: MaterialsWithEnvMapIntensity, envMapIntensity: number) => { + material.envMapIntensity = envMapIntensity + material.needsUpdate = true } const applyEnvironmentMapIntensity = (envMapIntensity: number, obj: Object3D): void => { - obj.traverse((child) => { - if (child instanceof Mesh && 'envMapIntensity' in child.material) { - child.material.envMapIntensity = envMapIntensity - child.material.needsUpdate = true + if (obj instanceof Mesh) { + if (Array.isArray(obj.material)) { + obj.material.forEach((m) => (isEnvMapIntensityApplicable(m) ? updateEnvMapIntensity(m, envMapIntensity) : undefined)) + } else if (isEnvMapIntensityApplicable(obj.material)) { + updateEnvMapIntensity(obj.material, envMapIntensity) } - }) + } } /** @@ -91,12 +108,11 @@ function addAssetSceneToControllerModel(controllerModel: XRControllerModel, scen findNodes(controllerModel.motionController!, scene) // Apply any environment map that the mesh already has set. - if (controllerModel.envMap) { - applyEnvironmentMap(controllerModel.envMap, scene) - } - - if (controllerModel.envMapIntensity != null) { - applyEnvironmentMapIntensity(controllerModel.envMapIntensity, scene) + if (controllerModel.envMap || controllerModel.envMapIntensity != null) { + scene.traverse((c) => { + if (controllerModel.envMap) applyEnvironmentMap(controllerModel.envMap, c) + if (controllerModel.envMapIntensity != null) applyEnvironmentMapIntensity(controllerModel.envMapIntensity, c) + }) } // Add the glTF scene to the controllerModel. @@ -118,24 +134,28 @@ export class XRControllerModel extends Group { this.scene = null } - setEnvironmentMap(envMap: Texture | null): XRControllerModel { - if (this.envMap === envMap) { + setEnvironmentMap(envMap: Texture | null, envMapIntensity = 1): XRControllerModel { + if (this.envMap === envMap && this.envMapIntensity === envMapIntensity) { return this } this.envMap = envMap - applyEnvironmentMap(envMap, this) + this.envMapIntensity = envMapIntensity + this.scene?.traverse((c) => { + applyEnvironmentMap(envMap, c) + applyEnvironmentMapIntensity(envMapIntensity, c) + }) return this } - setEnvironmentMapIntensity(envMapIntensity = 1): XRControllerModel { + setEnvironmentMapIntensity(envMapIntensity: number): XRControllerModel { if (this.envMapIntensity === envMapIntensity) { return this } this.envMapIntensity = envMapIntensity - applyEnvironmentMapIntensity(envMapIntensity, this) + this.scene?.traverse((c) => applyEnvironmentMapIntensity(envMapIntensity, c)) return this } diff --git a/yarn.lock b/yarn.lock index 3236158..017f5f5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -576,6 +576,13 @@ resolved "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz" integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== +"@jest/schemas@^29.6.3": + version "29.6.3" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.3.tgz#430b5ce8a4e0044a7e3819663305a7b3091c8e03" + integrity sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA== + dependencies: + "@sinclair/typebox" "^0.27.8" + "@jridgewell/gen-mapping@^0.1.0": version "0.1.1" resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz" @@ -623,6 +630,11 @@ resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.13.tgz" integrity sha512-GryiOJmNcWbovBxTfZSF71V/mXbgcV3MewDe3kIMCLyIh5e7SKAeUZs+rMnJ8jkMolZ/4/VsdBmMrw3l+VdZ3w== +"@jridgewell/sourcemap-codec@^1.4.15": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + "@jridgewell/trace-mapping@^0.3.17": version "0.3.17" resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz" @@ -747,6 +759,11 @@ resolved "https://registry.yarnpkg.com/@react-three/test-renderer/-/test-renderer-8.2.0.tgz#ac69e4f9abc0f21f341378f3235bfeece4a7f782" integrity sha512-sYTW/9AkU0f03M/rilYaCB9ORD3tS96bUhM+WRsx/QLtOKdUNCWrWwnxutm5M0orON0A0O84gbUosnqvCAKTsw== +"@sinclair/typebox@^0.27.8": + version "0.27.8" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" + integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== + "@types/chai-subset@^1.3.3": version "1.3.3" resolved "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.3.tgz" @@ -754,11 +771,16 @@ dependencies: "@types/chai" "*" -"@types/chai@*", "@types/chai@^4.3.4": +"@types/chai@*": version "4.3.4" resolved "https://registry.npmjs.org/@types/chai/-/chai-4.3.4.tgz" integrity sha512-KnRanxnpfpjUTqTCXslZSEdLfXExwgNxYPdiO2WGUj8+HDjFi8R3k5RVKPeSCzLjCcshCAtVO2QBbVuAV4kTnw== +"@types/chai@^4.3.5": + version "4.3.5" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.5.tgz#ae69bcbb1bebb68c4ac0b11e9d8ed04526b3562b" + integrity sha512-mEo1sAde+UCE6b2hxn332f1g1E8WfYRu6p5SvTKr2ZKC1f7gFJXk4h5PyGP9Dt6gCaG8y8XhwnXWC6Iy2cmBng== + "@types/draco3d@^1.4.0": version "1.4.2" resolved "https://registry.yarnpkg.com/@types/draco3d/-/draco3d-1.4.2.tgz#7faccb809db2a5e19b9efb97c5f2eb9d64d527ea" @@ -1007,41 +1029,48 @@ magic-string "^0.26.2" react-refresh "^0.14.0" -"@vitest/expect@0.29.1": - version "0.29.1" - resolved "https://registry.npmjs.org/@vitest/expect/-/expect-0.29.1.tgz" - integrity sha512-VFt1u34D+/L4pqjLA8VGPdHbdF8dgjX9Nq573L9KG6/7MIAL9jmbEIKpXudmxjoTwcyczOXRyDuUWBQHZafjoA== +"@vitest/expect@0.34.3": + version "0.34.3" + resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-0.34.3.tgz#576e1fd6a3a8b8b7a79a06477f3d450a77d67852" + integrity sha512-F8MTXZUYRBVsYL1uoIft1HHWhwDbSzwAU9Zgh8S6WFC3YgVb4AnFV2GXO3P5Em8FjEYaZtTnQYoNwwBrlOMXgg== dependencies: - "@vitest/spy" "0.29.1" - "@vitest/utils" "0.29.1" + "@vitest/spy" "0.34.3" + "@vitest/utils" "0.34.3" chai "^4.3.7" -"@vitest/runner@0.29.1": - version "0.29.1" - resolved "https://registry.npmjs.org/@vitest/runner/-/runner-0.29.1.tgz" - integrity sha512-VZ6D+kWpd/LVJjvxkt79OA29FUpyrI5L/EEwoBxH5m9KmKgs1QWNgobo/CGQtIWdifLQLvZdzYEK7Qj96w/ixQ== +"@vitest/runner@0.34.3": + version "0.34.3" + resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-0.34.3.tgz#ce09b777d133bbcf843e1a67f4a743365764e097" + integrity sha512-lYNq7N3vR57VMKMPLVvmJoiN4bqwzZ1euTW+XXYH5kzr3W/+xQG3b41xJn9ChJ3AhYOSoweu974S1V3qDcFESA== dependencies: - "@vitest/utils" "0.29.1" + "@vitest/utils" "0.34.3" p-limit "^4.0.0" - pathe "^1.1.0" + pathe "^1.1.1" + +"@vitest/snapshot@0.34.3": + version "0.34.3" + resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-0.34.3.tgz#cb4767aa44711a1072bd2e06204b659275c4f0f2" + integrity sha512-QyPaE15DQwbnIBp/yNJ8lbvXTZxS00kRly0kfFgAD5EYmCbYcA+1EEyRalc93M0gosL/xHeg3lKAClIXYpmUiQ== + dependencies: + magic-string "^0.30.1" + pathe "^1.1.1" + pretty-format "^29.5.0" -"@vitest/spy@0.29.1": - version "0.29.1" - resolved "https://registry.npmjs.org/@vitest/spy/-/spy-0.29.1.tgz" - integrity sha512-sRXXK44pPzaizpiZOIQP7YMhxIs80J/b6v1yR3SItpxG952c8tdA7n0O2j4OsVkjiO/ZDrjAYFrXL3gq6hLx6Q== +"@vitest/spy@0.34.3": + version "0.34.3" + resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-0.34.3.tgz#d4cf25e6ca9230991a0223ecd4ec2df30f0784ff" + integrity sha512-N1V0RFQ6AI7CPgzBq9kzjRdPIgThC340DGjdKdPSE8r86aUSmeliTUgkTqLSgtEwWWsGfBQ+UetZWhK0BgJmkQ== dependencies: - tinyspy "^1.0.2" + tinyspy "^2.1.1" -"@vitest/utils@0.29.1": - version "0.29.1" - resolved "https://registry.npmjs.org/@vitest/utils/-/utils-0.29.1.tgz" - integrity sha512-6npOEpmyE6zPS2wsWb7yX5oDpp6WY++cC5BX6/qaaMhGC3ZlPd8BbTz3RtGPi1PfPerPvfs4KqS/JDOIaB6J3w== +"@vitest/utils@0.34.3": + version "0.34.3" + resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-0.34.3.tgz#6e243189a358b736b9fc0216e6b6979bc857e897" + integrity sha512-kiSnzLG6m/tiT0XEl4U2H8JDBjFtwVlaE8I3QfGiMFR0QvnRDfYfdP3YvTBWM/6iJDAyaPY6yVQiCTUc7ZzTHA== dependencies: - cli-truncate "^3.1.0" - diff "^5.1.0" + diff-sequences "^29.4.3" loupe "^2.3.6" - picocolors "^1.0.0" - pretty-format "^27.5.1" + pretty-format "^29.5.0" "@webgpu/glslang@^0.0.15": version "0.0.15" @@ -1058,12 +1087,17 @@ acorn-walk@^8.2.0: resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz" integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== +acorn@^8.10.0, acorn@^8.9.0: + version "8.10.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" + integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== + acorn@^8.7.1: version "8.7.1" resolved "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz" integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A== -acorn@^8.8.1, acorn@^8.8.2: +acorn@^8.8.2: version "8.8.2" resolved "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz" integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== @@ -1088,11 +1122,6 @@ ansi-regex@^5.0.1: resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== -ansi-regex@^6.0.1: - version "6.0.1" - resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz" - integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== - ansi-styles@^3.2.1: version "3.2.1" resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz" @@ -1112,11 +1141,6 @@ ansi-styles@^5.0.0: resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz" integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== -ansi-styles@^6.0.0: - version "6.2.1" - resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz" - integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== - argparse@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" @@ -1286,14 +1310,6 @@ chevrotain@^10.1.2: lodash "4.17.21" regexp-to-ast "0.5.0" -cli-truncate@^3.1.0: - version "3.1.0" - resolved "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz" - integrity sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA== - dependencies: - slice-ansi "^5.0.0" - string-width "^5.0.0" - color-convert@^1.9.0: version "1.9.3" resolved "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz" @@ -1427,10 +1443,10 @@ detect-gpu@^4.0.19: dependencies: webgl-constants "^1.1.1" -diff@^5.1.0: - version "5.1.0" - resolved "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz" - integrity sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw== +diff-sequences@^29.4.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" + integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q== dir-glob@^3.0.1: version "3.0.1" @@ -1458,11 +1474,6 @@ draco3d@^1.4.1: resolved "https://registry.npmjs.org/draco3d/-/draco3d-1.5.2.tgz" integrity sha512-AeRQ25Fb29c14vpjnh167UGW0nGY0ZpEM3ld+zEXoEySlmEXcXfsCHZeTgo5qXH925V1JsdjrzasdaQ22/vXog== -eastasianwidth@^0.2.0: - version "0.2.0" - resolved "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz" - integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== - electron-to-chromium@^1.4.147: version "1.4.161" resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.161.tgz" @@ -1473,11 +1484,6 @@ electron-to-chromium@^1.4.284: resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.313.tgz" integrity sha512-QckB9OVqr2oybjIrbMI99uF+b9+iTja5weFe0ePbqLb5BHqXOJUO1SG6kDj/1WtWPRIBr51N153AEq8m7HuIaA== -emoji-regex@^9.2.2: - version "9.2.2" - resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz" - integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== - es-abstract@^1.19.0, es-abstract@^1.19.1, es-abstract@^1.19.2, es-abstract@^1.19.5: version "1.20.1" resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.1.tgz" @@ -2219,11 +2225,6 @@ is-extglob@^2.1.1: resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== -is-fullwidth-code-point@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz" - integrity sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ== - is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3: version "4.0.3" resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" @@ -2369,9 +2370,9 @@ lilconfig@^2.0.5: resolved "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz" integrity sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg== -local-pkg@^0.4.2: +local-pkg@^0.4.3: version "0.4.3" - resolved "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.3.tgz" + resolved "https://registry.yarnpkg.com/local-pkg/-/local-pkg-0.4.3.tgz#0ff361ab3ae7f1c19113d9bb97b98b905dbc4963" integrity sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g== locate-path@^2.0.0: @@ -2444,6 +2445,13 @@ magic-string@^0.26.2: dependencies: sourcemap-codec "^1.4.8" +magic-string@^0.30.1: + version "0.30.3" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.3.tgz#403755dfd9d6b398dfa40635d52e96c5ac095b85" + integrity sha512-B7xGbll2fG/VjP+SWg4sX3JynwIU0mjoTc6MPpKNuIvftk6u6vqhDnk1R80b8C2GBR6ywqy+1DcKBrevBg+bmw== + dependencies: + "@jridgewell/sourcemap-codec" "^1.4.15" + media-query-parser@^2.0.2: version "2.0.2" resolved "https://registry.npmjs.org/media-query-parser/-/media-query-parser-2.0.2.tgz" @@ -2491,6 +2499,16 @@ mlly@^1.1.0, mlly@^1.1.1: pkg-types "^1.0.1" ufo "^1.1.0" +mlly@^1.2.0, mlly@^1.4.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.4.1.tgz#7ab9cbb040bf8bd8205a0c341ce9acc3ae0c3a74" + integrity sha512-SCDs78Q2o09jiZiE2WziwVBEqXQ02XkGdUy45cbJf+BpYRIjArXRJ1Wbowxkb+NaM9DWvS3UC9GiO/6eqvQ/pg== + dependencies: + acorn "^8.10.0" + pathe "^1.1.1" + pkg-types "^1.0.3" + ufo "^1.3.0" + mmd-parser@^1.0.4: version "1.0.4" resolved "https://registry.npmjs.org/mmd-parser/-/mmd-parser-1.0.4.tgz" @@ -2712,6 +2730,11 @@ pathe@^1.1.0: resolved "https://registry.npmjs.org/pathe/-/pathe-1.1.0.tgz" integrity sha512-ODbEPR0KKHqECXW1GoxdDb+AZvULmXjVPy4rt+pGo2+TnjJTIPJQSVS6N63n8T2Ip+syHhbn52OewKicV0373w== +pathe@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.1.tgz#1dd31d382b974ba69809adc9a7a347e65d84829a" + integrity sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q== + pathval@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz" @@ -2736,6 +2759,15 @@ pkg-types@^1.0.1: mlly "^1.1.1" pathe "^1.1.0" +pkg-types@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/pkg-types/-/pkg-types-1.0.3.tgz#988b42ab19254c01614d13f4f65a2cfc7880f868" + integrity sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A== + dependencies: + jsonc-parser "^3.2.0" + mlly "^1.2.0" + pathe "^1.1.0" + postcss-load-config@^3.1.0: version "3.1.4" resolved "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz" @@ -2777,14 +2809,14 @@ prettier@^2.5.1: resolved "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz" integrity sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g== -pretty-format@^27.5.1: - version "27.5.1" - resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz" - integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ== +pretty-format@^29.5.0: + version "29.6.3" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.6.3.tgz#d432bb4f1ca6f9463410c3fb25a0ba88e594ace7" + integrity sha512-ZsBgjVhFAj5KeK+nHfF1305/By3lechHQSMWCTl8iHSbfOm2TN5nHEtFc/+W7fAyUeCs2n5iow72gld4gW0xDw== dependencies: - ansi-regex "^5.0.1" + "@jest/schemas" "^29.6.3" ansi-styles "^5.0.0" - react-is "^17.0.1" + react-is "^18.0.0" prop-types@^15.6.0, prop-types@^15.8.1: version "15.8.1" @@ -2820,7 +2852,7 @@ react-dom@^18.2.0: loose-envify "^1.1.0" scheduler "^0.23.0" -"react-is@^16.12.0 || ^17.0.0 || ^18.0.0", react-is@^18.2.0: +"react-is@^16.12.0 || ^17.0.0 || ^18.0.0", react-is@^18.0.0, react-is@^18.2.0: version "18.2.0" resolved "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz" integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== @@ -2830,11 +2862,6 @@ react-is@^16.13.1: resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== -react-is@^17.0.1: - version "17.0.2" - resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz" - integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== - react-merge-refs@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/react-merge-refs/-/react-merge-refs-1.1.0.tgz" @@ -3046,24 +3073,11 @@ slash@^3.0.0: resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== -slice-ansi@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz" - integrity sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ== - dependencies: - ansi-styles "^6.0.0" - is-fullwidth-code-point "^4.0.0" - source-map-js@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz" integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== -source-map@^0.6.1: - version "0.6.1" - resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - sourcemap-codec@^1.4.8: version "1.4.8" resolved "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz" @@ -3079,19 +3093,10 @@ stats.js@^0.17.0: resolved "https://registry.npmjs.org/stats.js/-/stats.js-0.17.0.tgz" integrity sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw== -std-env@^3.3.1: - version "3.3.2" - resolved "https://registry.npmjs.org/std-env/-/std-env-3.3.2.tgz" - integrity sha512-uUZI65yrV2Qva5gqE0+A7uVAvO40iPo6jGhs7s8keRfHCmtg+uB2X6EiLGCI9IgL1J17xGhvoOqSz79lzICPTA== - -string-width@^5.0.0: - version "5.1.2" - resolved "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz" - integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== - dependencies: - eastasianwidth "^0.2.0" - emoji-regex "^9.2.2" - strip-ansi "^7.0.1" +std-env@^3.3.3: + version "3.4.3" + resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.4.3.tgz#326f11db518db751c83fd58574f449b7c3060910" + integrity sha512-f9aPhy8fYBuMN+sNfakZV18U39PbalgjXG3lLB9WkaYTxijru61wb57V9wxxNthXM5Sd88ETBWi29qLAsHO52Q== string.prototype.codepointat@^0.2.1: version "0.2.1" @@ -3137,13 +3142,6 @@ strip-ansi@^6.0.1: dependencies: ansi-regex "^5.0.1" -strip-ansi@^7.0.1: - version "7.0.1" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz" - integrity sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw== - dependencies: - ansi-regex "^6.0.1" - strip-bom@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz" @@ -3154,12 +3152,12 @@ strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== -strip-literal@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/strip-literal/-/strip-literal-1.0.1.tgz" - integrity sha512-QZTsipNpa2Ppr6v1AmJHESqJ3Uz247MUS0OjrnnZjFAvEoWqxuyFuXn2xLgMtRnijJShAa1HL0gtJyUs7u7n3Q== +strip-literal@^1.0.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/strip-literal/-/strip-literal-1.3.0.tgz#db3942c2ec1699e6836ad230090b84bb458e3a07" + integrity sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg== dependencies: - acorn "^8.8.2" + acorn "^8.10.0" supports-color@^5.3.0: version "5.5.0" @@ -3239,20 +3237,20 @@ tiny-inflate@^1.0.3: resolved "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz" integrity sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw== -tinybench@^2.3.1: - version "2.3.1" - resolved "https://registry.npmjs.org/tinybench/-/tinybench-2.3.1.tgz" - integrity sha512-hGYWYBMPr7p4g5IarQE7XhlyWveh1EKhy4wUBS1LrHXCKYgvz+4/jCqgmJqZxxldesn05vccrtME2RLLZNW7iA== +tinybench@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.5.0.tgz#4711c99bbf6f3e986f67eb722fed9cddb3a68ba5" + integrity sha512-kRwSG8Zx4tjF9ZiyH4bhaebu+EDz1BOx9hOigYHlUW4xxI/wKIUQUqo018UlU4ar6ATPBsaMrdbKZ+tmPdohFA== -tinypool@^0.3.1: - version "0.3.1" - resolved "https://registry.npmjs.org/tinypool/-/tinypool-0.3.1.tgz" - integrity sha512-zLA1ZXlstbU2rlpA4CIeVaqvWq41MTWqLY3FfsAXgC8+f7Pk7zroaJQxDgxn1xNudKW6Kmj4808rPFShUlIRmQ== +tinypool@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-0.7.0.tgz#88053cc99b4a594382af23190c609d93fddf8021" + integrity sha512-zSYNUlYSMhJ6Zdou4cJwo/p7w5nmAH17GRfU/ui3ctvjXFErXXkruT4MWW6poDeXgCaIBlGLrfU6TbTXxyGMww== -tinyspy@^1.0.2: - version "1.1.1" - resolved "https://registry.npmjs.org/tinyspy/-/tinyspy-1.1.1.tgz" - integrity sha512-UVq5AXt/gQlti7oxoIg5oi/9r0WpF7DGEVwXgqWSMmyN16+e3tl5lIvTaOpJ3TAtu5xFzWccFRM4R5NaWHF+4g== +tinyspy@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-2.1.1.tgz#9e6371b00c259e5c5b301917ca18c01d40ae558c" + integrity sha512-XPJL2uSzcOyBMky6OFrusqWlzfFrXtE0hPuMgW8A2HmaqrPo4ZQHRN/V0QXN3FSjKxpsbRrFc5LI7KOwBsT1/w== to-fast-properties@^2.0.0: version "2.0.0" @@ -3340,6 +3338,11 @@ ufo@^1.1.0: resolved "https://registry.npmjs.org/ufo/-/ufo-1.1.0.tgz" integrity sha512-LQc2s/ZDMaCN3QLpa+uzHUOQ7SdV0qgv3VBXOolQGXTaaZpIur6PwUclF5nN2hNkiTRcUugXd1zFOW3FLJ135Q== +ufo@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.3.0.tgz#c92f8ac209daff607c57bbd75029e190930a0019" + integrity sha512-bRn3CsoojyNStCZe0BG0Mt4Nr/4KF+rhFlnNXybgqt5pXHNFRlqinSoQaTrGyzE4X8aHplSb+TorH+COin9Yxw== + unbox-primitive@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz" @@ -3375,15 +3378,15 @@ v8-compile-cache@^2.0.3: resolved "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz" integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== -vite-node@0.29.1: - version "0.29.1" - resolved "https://registry.npmjs.org/vite-node/-/vite-node-0.29.1.tgz" - integrity sha512-Ey9bTlQOQrCxQN0oJ7sTg+GrU4nTMLg44iKTFCKf31ry60csqQz4E+Q04hdWhwE4cTgpxUC+zEB1kHbf5jNkFA== +vite-node@0.34.3: + version "0.34.3" + resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-0.34.3.tgz#de134fe38bc1555ac8ab5e489d7df6159a3e1a4c" + integrity sha512-+0TzJf1g0tYXj6tR2vEyiA42OPq68QkRZCu/ERSo2PtsDJfBpDyEfuKbRvLmZqi/CgC7SCBtyC+WjTGNMRIaig== dependencies: cac "^6.7.14" debug "^4.3.4" - mlly "^1.1.0" - pathe "^1.1.0" + mlly "^1.4.0" + pathe "^1.1.1" picocolors "^1.0.0" vite "^3.0.0 || ^4.0.0" @@ -3411,34 +3414,34 @@ vite@^3.0.5: optionalDependencies: fsevents "~2.3.2" -vitest@^0.29.1: - version "0.29.1" - resolved "https://registry.npmjs.org/vitest/-/vitest-0.29.1.tgz" - integrity sha512-iSy6d9VwsIn7pz5I8SjVwdTLDRGKNZCRJVzROwjt0O0cffoymKwazIZ2epyMpRGpeL5tsXAl1cjXiT7agTyVug== +vitest@^0.34.3: + version "0.34.3" + resolved "https://registry.yarnpkg.com/vitest/-/vitest-0.34.3.tgz#863d61c133d01b16e49fd52d380c09fa5ac03188" + integrity sha512-7+VA5Iw4S3USYk+qwPxHl8plCMhA5rtfwMjgoQXMT7rO5ldWcdsdo3U1QD289JgglGK4WeOzgoLTsGFu6VISyQ== dependencies: - "@types/chai" "^4.3.4" + "@types/chai" "^4.3.5" "@types/chai-subset" "^1.3.3" "@types/node" "*" - "@vitest/expect" "0.29.1" - "@vitest/runner" "0.29.1" - "@vitest/spy" "0.29.1" - "@vitest/utils" "0.29.1" - acorn "^8.8.1" + "@vitest/expect" "0.34.3" + "@vitest/runner" "0.34.3" + "@vitest/snapshot" "0.34.3" + "@vitest/spy" "0.34.3" + "@vitest/utils" "0.34.3" + acorn "^8.9.0" acorn-walk "^8.2.0" cac "^6.7.14" chai "^4.3.7" debug "^4.3.4" - local-pkg "^0.4.2" - pathe "^1.1.0" + local-pkg "^0.4.3" + magic-string "^0.30.1" + pathe "^1.1.1" picocolors "^1.0.0" - source-map "^0.6.1" - std-env "^3.3.1" - strip-literal "^1.0.0" - tinybench "^2.3.1" - tinypool "^0.3.1" - tinyspy "^1.0.2" + std-env "^3.3.3" + strip-literal "^1.0.1" + tinybench "^2.5.0" + tinypool "^0.7.0" vite "^3.0.0 || ^4.0.0" - vite-node "0.29.1" + vite-node "0.34.3" why-is-node-running "^2.2.2" webgl-constants@^1.1.1: From f8bd39f4c14040a893c80c71b20dd436b89b1f1e Mon Sep 17 00:00:00 2001 From: Michael Bashurov Date: Wed, 30 Aug 2023 16:45:02 +0300 Subject: [PATCH 18/18] fixed broken test --- src/XRControllerModel.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/XRControllerModel.test.ts b/src/XRControllerModel.test.ts index d7c5ca8..6f7ba40 100644 --- a/src/XRControllerModel.test.ts +++ b/src/XRControllerModel.test.ts @@ -169,7 +169,7 @@ describe('XRControllerModel', () => { xrControllerModel.connectMotionController(motionControllerMock) xrControllerModel.connectModel(sceneMock) xrControllerModel.setEnvironmentMap(envMapMock) - expect.soft(xrControllerModel).toBe(envMapMock) + expect.soft(xrControllerModel.envMap).toBe(envMapMock) xrControllerModel.setEnvironmentMap(null)