├── .gitattributes ├── README.md ├── assets ├── OculusHand_L.fbx ├── OculusHand_L_low.fbx ├── OculusHand_R.fbx └── OculusHand_R_low.fbx ├── css └── styles.css ├── handpose.html ├── index.html ├── js ├── main-handpose.js ├── main.js └── trail.js └── third_party ├── FBXLoader.js ├── GLTFLoader.js ├── Maf.js ├── NURBSCurve.js ├── NURBSUtils.js ├── OrbitControls.js ├── VRButton.js ├── XRControllerModelFactory.js ├── XRHandModelFactory.js ├── XRHandOculusMeshModel.js ├── XRHandPrimitiveModel.js ├── gum-av.js ├── inflate.module.min.js ├── motion-controllers.module.js └── three.module.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # webxr-trails 2 | 3 | -------------------------------------------------------------------------------- /assets/OculusHand_L.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spite/webxr-trails/cbbbd2824a6140af5cd749d8c9846b20e6165a44/assets/OculusHand_L.fbx -------------------------------------------------------------------------------- /assets/OculusHand_L_low.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spite/webxr-trails/cbbbd2824a6140af5cd749d8c9846b20e6165a44/assets/OculusHand_L_low.fbx -------------------------------------------------------------------------------- /assets/OculusHand_R.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spite/webxr-trails/cbbbd2824a6140af5cd749d8c9846b20e6165a44/assets/OculusHand_R.fbx -------------------------------------------------------------------------------- /assets/OculusHand_R_low.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spite/webxr-trails/cbbbd2824a6140af5cd749d8c9846b20e6165a44/assets/OculusHand_R_low.fbx -------------------------------------------------------------------------------- /css/styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | body { 8 | } 9 | 10 | canvas { 11 | position: absolute; 12 | width: 100%; 13 | height: 100%; 14 | } 15 | -------------------------------------------------------------------------------- /handpose.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | XR Trails 6 | 10 | 58 | 59 | 60 |

Loading...

61 |
62 | 63 | 64 |
65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | XR Trails 6 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /js/main-handpose.js: -------------------------------------------------------------------------------- 1 | import { 2 | WebGLRenderer, 3 | PerspectiveCamera, 4 | Scene, 5 | Mesh, 6 | MeshNormalMaterial, 7 | OrthographicCamera, 8 | DirectionalLight, 9 | HemisphereLight, 10 | Vector3, 11 | Color, 12 | Raycaster, 13 | PCFSoftShadowMap, 14 | sRGBEncoding, 15 | Vector2, 16 | IcosahedronBufferGeometry, 17 | } from "../third_party/three.module.js"; 18 | import { OrbitControls } from "../third_party/OrbitControls.js"; 19 | 20 | import { trails, Trail, TrailGroup } from "./trail.js"; 21 | 22 | import { VRButton } from "../third_party/VRButton.js"; 23 | import { XRControllerModelFactory } from "../third_party/XRControllerModelFactory.js"; 24 | import { XRHandModelFactory } from "../third_party/XRHandModelFactory.js"; 25 | 26 | const av = document.querySelector("gum-av"); 27 | const canvas = document.querySelector("canvas"); 28 | const status = document.querySelector("#status"); 29 | 30 | const renderer = new WebGLRenderer({ 31 | antialias: true, 32 | alpha: true, 33 | canvas, 34 | preserveDrawingBuffer: false, 35 | powerPreference: "high-performance", 36 | }); 37 | renderer.setPixelRatio(window.devicePixelRatio); 38 | //renderer.setClearColor(0, 0); 39 | renderer.xr.enabled = true; 40 | renderer.shadowMap.enabled = true; 41 | renderer.shadowMap.type = PCFSoftShadowMap; 42 | renderer.outputEncoding = sRGBEncoding; 43 | 44 | document.body.append(renderer.domElement); 45 | 46 | const scene = new Scene(); 47 | const camera = new PerspectiveCamera(60, 1, 0.1, 100); 48 | camera.position.set(0, 0, 2); 49 | 50 | const orthoCamera = new OrthographicCamera(1, 1, 1, 1, -1000, 1000); 51 | 52 | const controls = new OrbitControls(camera, renderer.domElement); 53 | controls.screenSpacePanning = true; 54 | 55 | const geo = new IcosahedronBufferGeometry(0.5, 3); 56 | const mat = new MeshNormalMaterial({ 57 | wireframe: true, 58 | opacity: 0.1, 59 | transparent: true, 60 | depthWrite: false, 61 | }); 62 | const mesh = new Mesh(geo, mat); 63 | scene.add(mesh); 64 | 65 | const raycaster = new Raycaster(); 66 | const mouse = new Vector2(); 67 | 68 | function onMouseMove(event) { 69 | mouse.x = (event.clientX / window.innerWidth) * 2 - 1; 70 | mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; 71 | } 72 | window.addEventListener("mousemove", onMouseMove, false); 73 | 74 | const POINTS = 50; 75 | 76 | const palette = [ 77 | "#CA0045", 78 | "#052269", 79 | "#FFC068", 80 | "#114643", 81 | "#9BC2B5", 82 | "#CE8D3D", 83 | "#BD3E30", 84 | ]; 85 | 86 | //const group = new TrailGroup(50, POINTS); 87 | const up = new Vector3(0, 1, 0); 88 | 89 | const fingerNames = [ 90 | "thumb", 91 | "indexFinger", 92 | "middleFinger", 93 | "ringFinger", 94 | "pinky", 95 | ]; 96 | 97 | const hands = 2; 98 | const trailsPerFinger = 1; 99 | 100 | for (let j = 0; j < fingerNames.length * hands * trailsPerFinger; j++) { 101 | const trail = new Trail(POINTS, 10); 102 | trails.push(trail); 103 | scene.add(trail.mesh); 104 | } 105 | 106 | for (let trail of trails) { 107 | const ptr = ~~(Math.random() * palette.length); 108 | trail.ribbonMesh.material.color = new Color(palette[ptr]); 109 | scene.add(trail.ribbonMesh); 110 | } 111 | 112 | let width = 0; 113 | let height = 0; 114 | let flipCamera = true; 115 | 116 | function resize() { 117 | const videoAspectRatio = width / height; 118 | const windowWidth = window.innerWidth; 119 | const windowHeight = window.innerHeight; 120 | const windowAspectRatio = windowWidth / windowHeight; 121 | let adjustedWidth; 122 | let adjustedHeight; 123 | if (videoAspectRatio > windowAspectRatio) { 124 | adjustedWidth = windowWidth; 125 | adjustedHeight = windowWidth / videoAspectRatio; 126 | } else { 127 | adjustedWidth = windowHeight * videoAspectRatio; 128 | adjustedHeight = windowHeight; 129 | } 130 | renderer.setSize(adjustedWidth, adjustedHeight); 131 | } 132 | 133 | let playing = true; 134 | window.addEventListener("keydown", (e) => { 135 | if (e.keyCode === 32) { 136 | playing = !playing; 137 | } 138 | }); 139 | 140 | async function render() { 141 | // Flip video element horizontally if necessary. 142 | av.video.style.transform = flipCamera ? "scaleX(-1)" : "scaleX(1)"; 143 | 144 | if (width !== av.video.videoWidth || height !== av.video.videoHeight) { 145 | const w = av.video.videoWidth; 146 | const h = av.video.videoHeight; 147 | orthoCamera.left = -0.5 * w; 148 | orthoCamera.right = 0.5 * w; 149 | orthoCamera.top = 0.5 * h; 150 | orthoCamera.bottom = -0.5 * h; 151 | orthoCamera.updateProjectionMatrix(); 152 | width = w; 153 | height = h; 154 | resize(); 155 | } 156 | 157 | if (playing) { 158 | for (const trail of trails) { 159 | trail.step(); 160 | trail.render(); 161 | } 162 | } 163 | renderer.render(scene, orthoCamera); 164 | renderer.setAnimationLoop(render); 165 | } 166 | 167 | async function estimate() { 168 | const predictions = await model.estimateHands(av.video); 169 | 170 | av.style.opacity = 1; 171 | status.textContent = ""; 172 | 173 | if (predictions.length) { 174 | let ptr = 0; 175 | for (let j = 0; j < fingerNames.length; j++) { 176 | const joint = predictions[0].annotations[fingerNames[j]][3]; 177 | const jointN = predictions[0].annotations[fingerNames[j]][2]; 178 | const x = -1 * (joint[0] - 0.5 * width); 179 | const y = height - joint[1] - 0.5 * height; 180 | const z = joint[2]; 181 | const xn = -1 * (jointN[0] - 0.5 * width); 182 | const yn = height - jointN[1] - 0.5 * height; 183 | const zn = jointN[2]; 184 | const v = new Vector3(x, y, z); 185 | const n = v.clone(); 186 | n.x -= xn; 187 | n.y -= yn; 188 | n.z -= zn; 189 | n.normalize(); 190 | trails[ptr].update(v, n); 191 | ptr++; 192 | } 193 | if (predictions.length > 1) { 194 | for (let j = 0; j < fingerNames.length; j++) { 195 | const joint = predictions[1].annotations[fingerNames[j]][3]; 196 | const jointN = predictions[1].annotations[fingerNames[j]][2]; 197 | const x = joint[0] - 0.5 * width; 198 | const y = height - joint[1] - 0.5 * height; 199 | const z = joint[2]; 200 | const xn = jointN[0] - 0.5 * width; 201 | const yn = height - jointN[1] - 0.5 * height; 202 | const zn = jointN[2]; 203 | const v = new Vector3(x, y, z); 204 | const n = v.clone(); 205 | n.x -= xn; 206 | n.y -= yn; 207 | n.z -= zn; 208 | n.normalize(); 209 | trails[ptr].update(v, n); 210 | ptr++; 211 | } 212 | } 213 | } 214 | requestAnimationFrame(estimate); 215 | } 216 | 217 | const light = new DirectionalLight(0xffffff); 218 | light.position.set(0, 6, 0); 219 | light.castShadow = true; 220 | light.shadow.camera.top = 2; 221 | light.shadow.camera.bottom = -2; 222 | light.shadow.camera.right = 2; 223 | light.shadow.camera.left = -2; 224 | light.shadow.bias = -0.00001; 225 | light.shadow.mapSize.set(4096, 4096); 226 | scene.add(light); 227 | 228 | const hemiLight = new HemisphereLight(0xffffbb, 0x080820, 1); 229 | scene.add(hemiLight); 230 | 231 | let model; 232 | 233 | async function init() { 234 | await Promise.all([tf.setBackend("webgl"), av.ready()]); 235 | status.textContent = "Loading model..."; 236 | model = await handpose.load(); 237 | document.body.appendChild(VRButton.createButton(renderer)); 238 | render(); 239 | estimate(); 240 | } 241 | 242 | window.addEventListener("resize", resize); 243 | 244 | resize(); 245 | init(); 246 | -------------------------------------------------------------------------------- /js/main.js: -------------------------------------------------------------------------------- 1 | import { 2 | WebGLRenderer, 3 | PerspectiveCamera, 4 | Scene, 5 | Mesh, 6 | MeshNormalMaterial, 7 | OrthographicCamera, 8 | DirectionalLight, 9 | HemisphereLight, 10 | Vector3, 11 | Color, 12 | Raycaster, 13 | Vector2, 14 | IcosahedronBufferGeometry, 15 | } from "../third_party/three.module.js"; 16 | import { OrbitControls } from "../third_party/OrbitControls.js"; 17 | 18 | import { trails, Trail, TrailGroup } from "./trail.js"; 19 | 20 | import { VRButton } from "../third_party/VRButton.js"; 21 | import { XRControllerModelFactory } from "../third_party/XRControllerModelFactory.js"; 22 | import { XRHandModelFactory } from "../third_party/XRHandModelFactory.js"; 23 | 24 | const av = document.querySelector("gum-av"); 25 | const status = document.querySelector("#status"); 26 | 27 | const renderer = new WebGLRenderer({ 28 | preserveDrawingBuffer: false, 29 | antialias: true, 30 | powerPreference: "high-performance", 31 | }); 32 | renderer.setPixelRatio(window.devicePixelRatio); 33 | renderer.setClearColor(0, 0); 34 | renderer.xr.enabled = true; 35 | 36 | document.body.append(renderer.domElement); 37 | 38 | const scene = new Scene(); 39 | const camera = new PerspectiveCamera(60, 1, 0.1, 100); 40 | camera.position.set(0, 0, 2); 41 | 42 | const orthoCamera = new OrthographicCamera(1, 1, 1, 1, -1000, 1000); 43 | 44 | const controls = new OrbitControls(camera, renderer.domElement); 45 | controls.screenSpacePanning = true; 46 | 47 | const geo = new IcosahedronBufferGeometry(0.5, 3); 48 | const mat = new MeshNormalMaterial({ 49 | wireframe: true, 50 | opacity: 0.1, 51 | transparent: true, 52 | depthWrite: false, 53 | }); 54 | const mesh = new Mesh(geo, mat); 55 | scene.add(mesh); 56 | 57 | const raycaster = new Raycaster(); 58 | const mouse = new Vector2(); 59 | 60 | function onMouseMove(event) { 61 | mouse.x = (event.clientX / window.innerWidth) * 2 - 1; 62 | mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; 63 | } 64 | window.addEventListener("mousemove", onMouseMove, false); 65 | 66 | const POINTS = 50; 67 | 68 | const palette = [ 69 | "#CA0045", 70 | "#052269", 71 | "#FFC068", 72 | "#114643", 73 | "#9BC2B5", 74 | "#CE8D3D", 75 | "#BD3E30", 76 | ]; 77 | 78 | const group = new TrailGroup(50, POINTS, 0.02); 79 | const up = new Vector3(0, 1, 0); 80 | 81 | for (let trail of trails) { 82 | const ptr = ~~(Math.random() * palette.length); 83 | trail.ribbonMesh.material.color = new Color(palette[ptr]); 84 | scene.add(trail.ribbonMesh); 85 | } 86 | 87 | const fingers = []; 88 | 89 | if ("XRHand" in window) { 90 | fingers.push(XRHand.THUMB_PHALANX_TIP); 91 | fingers.push(XRHand.INDEX_PHALANX_TIP); 92 | fingers.push(XRHand.MIDDLE_PHALANX_TIP); 93 | fingers.push(XRHand.RING_PHALANX_TIP); 94 | fingers.push(XRHand.LITTLE_PHALANX_TIP); 95 | } 96 | 97 | const hands = 2; 98 | const trailsPerFinger = 5; 99 | 100 | // for (let j = 0; j < fingers.length * hands * trailsPerFinger; j++) { 101 | // const trail = new Trail(); 102 | // trails.push(trail); 103 | // scene.add(trail.mesh); 104 | // } 105 | 106 | const trail = new Trail(); 107 | trails.push(trail); 108 | scene.add(trail.mesh); 109 | 110 | function resize() { 111 | let w = window.innerWidth; 112 | let h = window.innerHeight; 113 | renderer.setSize(w, h); 114 | renderer.domElement.style.width = "100%"; 115 | renderer.domElement.style.height = "100%"; 116 | 117 | camera.aspect = w / h; 118 | camera.updateProjectionMatrix(); 119 | } 120 | 121 | let playing = true; 122 | window.addEventListener("keydown", (e) => { 123 | if (e.keyCode === 32) { 124 | playing = !playing; 125 | } 126 | }); 127 | 128 | let width = 0; 129 | let height = 0; 130 | 131 | async function render() { 132 | renderer.setAnimationLoop(render); 133 | 134 | const dir = new Vector3(); 135 | for (let j = 0; j < fingers.length; j++) { 136 | const pos = hand1.joints[fingers[j]].position; 137 | const pos2 = hand2.joints[fingers[j]].position; 138 | const nextJoinPos = hand1.joints[fingers[j] - 1].position; 139 | const nextJoinPos2 = hand2.joints[fingers[j] - 1].position; 140 | const n = pos.clone().sub(nextJoinPos).normalize(); 141 | const n2 = pos2.clone().sub(nextJoinPos2).normalize(); 142 | 143 | for (let i = 0; i < trailsPerFinger; i++) { 144 | const pts = trails[j + i * 10].points; 145 | // if (pts.length) { 146 | // dir.copy(pts[0].position).sub(pts[1].position).normalize(); 147 | // n.crossVectors(dir, up).normalize(); 148 | // n.cross(dir); 149 | // } 150 | trails[j + i * 10].update(pos, n); 151 | // const pts2 = trails[j + 5 + i * 10].points; 152 | // if (pts2.length) { 153 | // dir.copy(pts2[0].position).sub(pts2[1].position).normalize(); 154 | // n2.crossVectors(dir, up).normalize(); 155 | // n2.cross(dir); 156 | // } 157 | trails[j + 5 + i * 10].update(pos2, n2); 158 | } 159 | } 160 | raycaster.setFromCamera(mouse, camera); 161 | const intersects = raycaster.intersectObject(mesh); 162 | if (intersects.length) { 163 | const p = intersects[0]; 164 | const n = p.face.normal; 165 | group.update(p.point, n); 166 | } 167 | if (playing) { 168 | for (const trail of trails) { 169 | trail.step(); 170 | trail.render(); 171 | } 172 | } 173 | renderer.render(scene, camera); 174 | } 175 | 176 | const controller1 = renderer.xr.getController(0); 177 | scene.add(controller1); 178 | 179 | const controller2 = renderer.xr.getController(1); 180 | scene.add(controller2); 181 | 182 | const controllerModelFactory = new XRControllerModelFactory(); 183 | const handModelFactory = new XRHandModelFactory().setPath("./assets/"); 184 | 185 | // Hand 1 186 | const controllerGrip1 = renderer.xr.getControllerGrip(0); 187 | controllerGrip1.add( 188 | controllerModelFactory.createControllerModel(controllerGrip1) 189 | ); 190 | scene.add(controllerGrip1); 191 | 192 | const hand1 = renderer.xr.getHand(0); 193 | hand1.add(handModelFactory.createHandModel(hand1, "oculus")); 194 | scene.add(hand1); 195 | 196 | // Hand 2 197 | const controllerGrip2 = renderer.xr.getControllerGrip(1); 198 | controllerGrip2.add( 199 | controllerModelFactory.createControllerModel(controllerGrip2) 200 | ); 201 | scene.add(controllerGrip2); 202 | 203 | const hand2 = renderer.xr.getHand(1); 204 | hand2.add(handModelFactory.createHandModel(hand2, "oculus")); 205 | scene.add(hand2); 206 | 207 | const light = new DirectionalLight(0xffffff); 208 | light.position.set(0, 6, 0); 209 | light.castShadow = true; 210 | light.shadow.camera.top = 2; 211 | light.shadow.camera.bottom = -2; 212 | light.shadow.camera.right = 2; 213 | light.shadow.camera.left = -2; 214 | light.shadow.bias = -0.00001; 215 | light.shadow.mapSize.set(4096, 4096); 216 | scene.add(light); 217 | 218 | const hemiLight = new HemisphereLight(0xffffbb, 0x080820, 1); 219 | scene.add(hemiLight); 220 | 221 | renderer.shadowMap.enabled = true; 222 | 223 | async function init() { 224 | //await Promise.all([tf.setBackend("webgl"), av.ready()]); 225 | //status.textContent = "Loading model..."; 226 | //model = await handpose.load(); 227 | document.body.appendChild(VRButton.createButton(renderer)); 228 | render(); 229 | } 230 | 231 | window.addEventListener("resize", resize); 232 | 233 | resize(); 234 | init(); 235 | -------------------------------------------------------------------------------- /js/trail.js: -------------------------------------------------------------------------------- 1 | import { 2 | Mesh, 3 | Line, 4 | DoubleSide, 5 | Vector3, 6 | BufferAttribute, 7 | BufferGeometry, 8 | LineBasicMaterial, 9 | AdditiveBlending, 10 | PlaneBufferGeometry, 11 | MeshStandardMaterial, 12 | } from "../third_party/three.module.js"; 13 | import Maf from "../third_party/Maf.js"; 14 | 15 | const trails = []; 16 | 17 | const up = new Vector3(0, 1, 0); 18 | 19 | class Point { 20 | constructor(position, normal) { 21 | this.normal = new Vector3().copy(normal); 22 | this.position = new Vector3().copy(position); 23 | this.velocity = new Vector3(0, 0, 0); 24 | this.tmp = new Vector3(); 25 | } 26 | step(pos, force) { 27 | this.tmp.copy(pos).sub(this.position).multiplyScalar(force); 28 | this.velocity.add(this.tmp); 29 | this.velocity.multiplyScalar(0.5); 30 | this.position.add(this.velocity); 31 | } 32 | } 33 | 34 | class Trail { 35 | constructor(numPoints, width) { 36 | this.width = width; 37 | this.numPoints = numPoints; 38 | this.spring = Maf.randomInRange(0.4, 0.5); // 0.45 39 | this.dampening = Maf.randomInRange(0.2, 0.25); // .25 40 | this.friction = Maf.randomInRange(0.45, 0.5); // .5 41 | this.tension = Maf.randomInRange(0.98, 0.99); // 0.98; 42 | 43 | this.points = []; 44 | this.initialised = false; 45 | this.vertices = new Float32Array(this.numPoints * 3); 46 | this.geometry = new BufferGeometry(); 47 | this.geometry.setAttribute( 48 | "position", 49 | new BufferAttribute(this.vertices, 3) 50 | ); 51 | this.material = new LineBasicMaterial({ 52 | color: 0xffffff, 53 | opacity: 1, 54 | linewidth: 4, 55 | transparent: true, 56 | blending: AdditiveBlending, 57 | }); 58 | this.mesh = new Line(this.geometry, this.material); 59 | this.mesh.frustumCulled = false; 60 | 61 | this.ribbonGeometry = new PlaneBufferGeometry(1, 1, this.numPoints - 1, 1); 62 | this.ribbonMaterial = new MeshStandardMaterial({ 63 | wireframe: !true, 64 | side: DoubleSide, 65 | color: 0xffffff, 66 | roughness: 0.9, 67 | metalness: 0.1, 68 | //emissive: 0xffffff, 69 | }); 70 | this.ribbonMesh = new Mesh(this.ribbonGeometry, this.ribbonMaterial); 71 | this.ribbonMesh.frustumCulled = false; 72 | this.ribbonMesh.receiveShadow = this.ribbonMesh.castShadow = true; 73 | 74 | this.ptr = 0; 75 | } 76 | 77 | update(point, normal) { 78 | if (!this.initialised) { 79 | this.initialised = true; 80 | for (let j = 0; j < this.numPoints; j++) { 81 | const p = new Point(point, normal); 82 | this.points[j] = p; 83 | } 84 | } 85 | this.points[0].position.copy(point); 86 | this.points[0].normal.copy(normal); 87 | } 88 | 89 | step() { 90 | let spring = this.spring; 91 | const dampening = this.dampening; 92 | const friction = this.friction; 93 | const tension = this.tension; 94 | 95 | for (let j = 1; j < this.points.length; j++) { 96 | const prev = this.points[j - 1]; 97 | const cur = this.points[j]; 98 | 99 | const prevNormal = this.points[j - 1].normal; 100 | const normal = this.points[j].normal; 101 | normal.lerp(prevNormal, dampening); 102 | 103 | cur.velocity.x += (prev.position.x - cur.position.x) * spring; 104 | cur.velocity.y += (prev.position.y - cur.position.y) * spring; 105 | cur.velocity.z += (prev.position.z - cur.position.z) * spring; 106 | cur.velocity.x += prev.velocity.x * dampening; 107 | cur.velocity.y += prev.velocity.y * dampening; 108 | cur.velocity.z += prev.velocity.z * dampening; 109 | cur.velocity.multiplyScalar(friction); 110 | // cur.velocity.x += 0.001 * normal.x; 111 | // cur.velocity.y += 0.001 * normal.y; 112 | // cur.velocity.z += 0.001 * normal.z; 113 | 114 | cur.position.add(cur.velocity); 115 | spring *= tension; 116 | } 117 | } 118 | 119 | render() { 120 | for (let j = 0; j < this.points.length; j++) { 121 | const p = this.points[j].position; 122 | this.vertices[j * 3] = p.x; 123 | this.vertices[j * 3 + 1] = p.y; 124 | this.vertices[j * 3 + 2] = p.z; 125 | } 126 | this.geometry.attributes.position.needsUpdate = true; 127 | 128 | const vertices = this.ribbonGeometry.attributes.position.array; 129 | const w = this.width; 130 | const n = new Vector3(); 131 | const t = new Vector3(); 132 | for (let j = 0; j < this.points.length; j++) { 133 | const p = this.points[j].position; 134 | n.copy(this.points[j].normal); 135 | n.normalize(); 136 | vertices[j * 3] = p.x - 0.01 * n.x; 137 | vertices[j * 3 + 1] = p.y - 0.01 * n.y; 138 | vertices[j * 3 + 2] = p.z - 0.01 * n.z; 139 | vertices[(j + this.points.length) * 3 + 0] = p.x - (0.01 + w) * n.x; 140 | vertices[(j + this.points.length) * 3 + 1] = p.y - (0.01 + w) * n.y; 141 | vertices[(j + this.points.length) * 3 + 2] = p.z - (0.01 + w) * n.z; 142 | } 143 | this.ribbonGeometry.attributes.position.needsUpdate = true; 144 | this.ribbonGeometry.computeVertexNormals(); 145 | this.ribbonGeometry.computeFaceNormals(); 146 | } 147 | } 148 | 149 | class TrailGroup { 150 | constructor(num, numPoints, width) { 151 | this.trails = []; 152 | for (let j = 0; j < num; j++) { 153 | const trail = new Trail(numPoints, width); 154 | this.trails.push(trail); 155 | trails.push(trail); 156 | } 157 | } 158 | 159 | update(point, normal) { 160 | for (let trail of this.trails) { 161 | trail.update(point, normal); 162 | } 163 | } 164 | } 165 | 166 | export { Trail, TrailGroup, trails }; 167 | -------------------------------------------------------------------------------- /third_party/Maf.js: -------------------------------------------------------------------------------- 1 | const Maf = {}; 2 | 3 | // Current version. 4 | Maf.VERSION = "1.0.0"; 5 | 6 | Maf.PI = Math.PI; 7 | Maf.TAU = 2 * Maf.PI; 8 | 9 | // https://www.opengl.org/sdk/docs/man/html/clamp.xhtml 10 | 11 | Maf.clamp = function (v, minVal, maxVal) { 12 | return Math.min(maxVal, Math.max(minVal, v)); 13 | }; 14 | 15 | // https://www.opengl.org/sdk/docs/man/html/step.xhtml 16 | 17 | Maf.step = function (edge, v) { 18 | return v < edge ? 0 : 1; 19 | }; 20 | 21 | // https://www.opengl.org/sdk/docs/man/html/smoothstep.xhtml 22 | 23 | Maf.smoothStep = function (edge0, edge1, v) { 24 | var t = Maf.clamp((v - edge0) / (edge1 - edge0), 0.0, 1.0); 25 | return t * t * (3.0 - 2.0 * t); 26 | }; 27 | 28 | // http://docs.unity3d.com/ScriptReference/Mathf.html 29 | // http://www.shaderific.com/glsl-functions/ 30 | // https://www.opengl.org/sdk/docs/man4/html/ 31 | // https://msdn.microsoft.com/en-us/library/windows/desktop/ff471376(v=vs.85).aspx 32 | // http://moutjs.com/docs/v0.11/math.html#map 33 | // https://code.google.com/p/kuda/source/browse/public/js/hemi/utils/mathUtils.js?r=8d581c02651077c4ac3f5fc4725323210b6b13cc 34 | 35 | // Converts from degrees to radians. 36 | Maf.deg2Rad = function (degrees) { 37 | return (degrees * Math.PI) / 180; 38 | }; 39 | 40 | Maf.toRadians = Maf.deg2Rad; 41 | 42 | // Converts from radians to degrees. 43 | Maf.rad2Deg = function (radians) { 44 | return (radians * 180) / Math.PI; 45 | }; 46 | 47 | Maf.toDegrees = Maf.rad2Deg; 48 | 49 | Maf.clamp01 = function (v) { 50 | return Maf.clamp(v, 0, 1); 51 | }; 52 | 53 | // https://www.opengl.org/sdk/docs/man/html/mix.xhtml 54 | 55 | Maf.mix = function (x, y, a) { 56 | if (a <= 0) return x; 57 | if (a >= 1) return y; 58 | return x + a * (y - x); 59 | }; 60 | 61 | Maf.lerp = Maf.mix; 62 | 63 | Maf.inverseMix = function (a, b, v) { 64 | return (v - a) / (b - a); 65 | }; 66 | 67 | Maf.inverseLerp = Maf.inverseMix; 68 | 69 | Maf.mixUnclamped = function (x, y, a) { 70 | if (a <= 0) return x; 71 | if (a >= 1) return y; 72 | return x + a * (y - x); 73 | }; 74 | 75 | Maf.lerpUnclamped = Maf.mixUnclamped; 76 | 77 | // https://www.opengl.org/sdk/docs/man/html/fract.xhtml 78 | 79 | Maf.fract = function (v) { 80 | return v - Math.floor(v); 81 | }; 82 | 83 | Maf.frac = Maf.fract; 84 | 85 | // http://stackoverflow.com/questions/4965301/finding-if-a-number-is-a-power-of-2 86 | 87 | Maf.isPowerOfTwo = function (v) { 88 | return ((v - 1) & v) == 0; 89 | }; 90 | 91 | // https://bocoup.com/weblog/find-the-closest-power-of-2-with-javascript 92 | 93 | Maf.closestPowerOfTwo = function (v) { 94 | return Math.pow(2, Math.round(Math.log(v) / Math.log(2))); 95 | }; 96 | 97 | Maf.nextPowerOfTwo = function (v) { 98 | return Math.pow(2, Math.ceil(Math.log(v) / Math.log(2))); 99 | }; 100 | 101 | // http://stackoverflow.com/questions/1878907/the-smallest-difference-between-2-angles 102 | 103 | //function mod(a, n) { return a - Math.floor(a/n) * n; } 104 | Maf.mod = function (a, n) { 105 | return ((a % n) + n) % n; 106 | }; 107 | 108 | Maf.deltaAngle = function (a, b) { 109 | var d = Maf.mod(b - a, 360); 110 | if (d > 180) d = Math.abs(d - 360); 111 | return d; 112 | }; 113 | 114 | Maf.deltaAngleDeg = Maf.deltaAngle; 115 | 116 | Maf.deltaAngleRad = function (a, b) { 117 | return Maf.toRadians(Maf.deltaAngle(Maf.toDegrees(a), Maf.toDegrees(b))); 118 | }; 119 | 120 | Maf.lerpAngle = function (a, b, t) { 121 | var angle = Maf.deltaAngle(a, b); 122 | return Maf.mod(a + Maf.lerp(0, angle, t), 360); 123 | }; 124 | 125 | Maf.lerpAngleDeg = Maf.lerpAngle; 126 | 127 | Maf.lerpAngleRad = function (a, b, t) { 128 | return Maf.toRadians(Maf.lerpAngleDeg(Maf.toDegrees(a), Maf.toDegrees(b), t)); 129 | }; 130 | 131 | // http://gamedev.stackexchange.com/questions/74324/gamma-space-and-linear-space-with-shader 132 | 133 | Maf.gammaToLinearSpace = function (v) { 134 | return Math.pow(v, 2.2); 135 | }; 136 | 137 | Maf.linearToGammaSpace = function (v) { 138 | return Math.pow(v, 1 / 2.2); 139 | }; 140 | 141 | Maf.map = function (from1, to1, from2, to2, v) { 142 | return from2 + ((v - from1) * (to2 - from2)) / (to1 - from1); 143 | }; 144 | 145 | Maf.scale = Maf.map; 146 | 147 | // http://www.iquilezles.org/www/articles/functions/functions.htm 148 | 149 | Maf.almostIdentity = function (x, m, n) { 150 | if (x > m) return x; 151 | 152 | var a = 2 * n - m; 153 | var b = 2 * m - 3 * n; 154 | var t = x / m; 155 | 156 | return (a * t + b) * t * t + n; 157 | }; 158 | 159 | Maf.impulse = function (k, x) { 160 | var h = k * x; 161 | return h * Math.exp(1 - h); 162 | }; 163 | 164 | Maf.cubicPulse = function (c, w, x) { 165 | x = Math.abs(x - c); 166 | if (x > w) return 0; 167 | x /= w; 168 | return 1 - x * x * (3 - 2 * x); 169 | }; 170 | 171 | Maf.expStep = function (x, k, n) { 172 | return Math.exp(-k * Math.pow(x, n)); 173 | }; 174 | 175 | Maf.parabola = function (x, k) { 176 | return Math.pow(4 * x * (1 - x), k); 177 | }; 178 | 179 | Maf.powerCurve = function (x, a, b) { 180 | var k = Math.pow(a + b, a + b) / (Math.pow(a, a) * Math.pow(b, b)); 181 | return k * Math.pow(x, a) * Math.pow(1 - x, b); 182 | }; 183 | 184 | // http://iquilezles.org/www/articles/smin/smin.htm ? 185 | 186 | Maf.latLonToCartesian = function (lat, lon) { 187 | lon += 180; 188 | lat = Maf.clamp(lat, -85, 85); 189 | var phi = Maf.toRadians(90 - lat); 190 | var theta = Maf.toRadians(180 - lon); 191 | var x = Math.sin(phi) * Math.cos(theta); 192 | var y = Math.cos(phi); 193 | var z = Math.sin(phi) * Math.sin(theta); 194 | 195 | return { x: x, y: y, z: z }; 196 | }; 197 | 198 | Maf.cartesianToLatLon = function (x, y, z) { 199 | var n = Math.sqrt(x * x + y * y + z * z); 200 | return { lat: Math.asin(z / n), lon: Math.atan2(y, x) }; 201 | }; 202 | 203 | Maf.randomInRange = function (min, max) { 204 | return min + Math.random() * (max - min); 205 | }; 206 | 207 | Maf.norm = function (v, minVal, maxVal) { 208 | return (v - minVal) / (maxVal - minVal); 209 | }; 210 | 211 | Maf.hash = function (n) { 212 | return Maf.fract((1.0 + Math.cos(n)) * 415.92653); 213 | }; 214 | 215 | Maf.noise2d = function (x, y) { 216 | var xhash = Maf.hash(x * 37.0); 217 | var yhash = Maf.hash(y * 57.0); 218 | return Maf.fract(xhash + yhash); 219 | }; 220 | 221 | // http://iquilezles.org/www/articles/smin/smin.htm 222 | 223 | Maf.smoothMin = function (a, b, k) { 224 | var res = Math.exp(-k * a) + Math.exp(-k * b); 225 | return -Math.log(res) / k; 226 | }; 227 | 228 | Maf.smoothMax = function (a, b, k) { 229 | return Math.log(Math.exp(a) + Math.exp(b)) / k; 230 | }; 231 | 232 | Maf.almost = function (a, b) { 233 | return Math.abs(a - b) < 0.0001; 234 | }; 235 | 236 | export default Maf; 237 | -------------------------------------------------------------------------------- /third_party/NURBSCurve.js: -------------------------------------------------------------------------------- 1 | import { Curve, Vector3, Vector4 } from "./three.module.js"; 2 | import { NURBSUtils } from "./NURBSUtils.js"; 3 | /** 4 | * NURBS curve object 5 | * 6 | * Derives from Curve, overriding getPoint and getTangent. 7 | * 8 | * Implementation is based on (x, y [, z=0 [, w=1]]) control points with w=weight. 9 | * 10 | **/ 11 | 12 | var NURBSCurve = function ( 13 | degree, 14 | knots /* array of reals */, 15 | controlPoints /* array of Vector(2|3|4) */, 16 | startKnot /* index in knots */, 17 | endKnot /* index in knots */ 18 | ) { 19 | Curve.call(this); 20 | 21 | this.degree = degree; 22 | this.knots = knots; 23 | this.controlPoints = []; 24 | // Used by periodic NURBS to remove hidden spans 25 | this.startKnot = startKnot || 0; 26 | this.endKnot = endKnot || this.knots.length - 1; 27 | for (var i = 0; i < controlPoints.length; ++i) { 28 | // ensure Vector4 for control points 29 | var point = controlPoints[i]; 30 | this.controlPoints[i] = new Vector4(point.x, point.y, point.z, point.w); 31 | } 32 | }; 33 | 34 | NURBSCurve.prototype = Object.create(Curve.prototype); 35 | NURBSCurve.prototype.constructor = NURBSCurve; 36 | 37 | NURBSCurve.prototype.getPoint = function (t, optionalTarget) { 38 | var point = optionalTarget || new Vector3(); 39 | 40 | var u = 41 | this.knots[this.startKnot] + 42 | t * (this.knots[this.endKnot] - this.knots[this.startKnot]); // linear mapping t->u 43 | 44 | // following results in (wx, wy, wz, w) homogeneous point 45 | var hpoint = NURBSUtils.calcBSplinePoint( 46 | this.degree, 47 | this.knots, 48 | this.controlPoints, 49 | u 50 | ); 51 | 52 | if (hpoint.w != 1.0) { 53 | // project to 3D space: (wx, wy, wz, w) -> (x, y, z, 1) 54 | hpoint.divideScalar(hpoint.w); 55 | } 56 | 57 | return point.set(hpoint.x, hpoint.y, hpoint.z); 58 | }; 59 | 60 | NURBSCurve.prototype.getTangent = function (t, optionalTarget) { 61 | var tangent = optionalTarget || new Vector3(); 62 | 63 | var u = 64 | this.knots[0] + t * (this.knots[this.knots.length - 1] - this.knots[0]); 65 | var ders = NURBSUtils.calcNURBSDerivatives( 66 | this.degree, 67 | this.knots, 68 | this.controlPoints, 69 | u, 70 | 1 71 | ); 72 | tangent.copy(ders[1]).normalize(); 73 | 74 | return tangent; 75 | }; 76 | 77 | export { NURBSCurve }; 78 | -------------------------------------------------------------------------------- /third_party/NURBSUtils.js: -------------------------------------------------------------------------------- 1 | import { Vector3, Vector4 } from "./three.module.js"; 2 | /** 3 | * NURBS utils 4 | * 5 | * See NURBSCurve and NURBSSurface. 6 | **/ 7 | 8 | /************************************************************** 9 | * NURBS Utils 10 | **************************************************************/ 11 | 12 | var NURBSUtils = { 13 | /* 14 | Finds knot vector span. 15 | 16 | p : degree 17 | u : parametric value 18 | U : knot vector 19 | 20 | returns the span 21 | */ 22 | findSpan: function (p, u, U) { 23 | var n = U.length - p - 1; 24 | 25 | if (u >= U[n]) { 26 | return n - 1; 27 | } 28 | 29 | if (u <= U[p]) { 30 | return p; 31 | } 32 | 33 | var low = p; 34 | var high = n; 35 | var mid = Math.floor((low + high) / 2); 36 | 37 | while (u < U[mid] || u >= U[mid + 1]) { 38 | if (u < U[mid]) { 39 | high = mid; 40 | } else { 41 | low = mid; 42 | } 43 | 44 | mid = Math.floor((low + high) / 2); 45 | } 46 | 47 | return mid; 48 | }, 49 | 50 | /* 51 | Calculate basis functions. See The NURBS Book, page 70, algorithm A2.2 52 | 53 | span : span in which u lies 54 | u : parametric point 55 | p : degree 56 | U : knot vector 57 | 58 | returns array[p+1] with basis functions values. 59 | */ 60 | calcBasisFunctions: function (span, u, p, U) { 61 | var N = []; 62 | var left = []; 63 | var right = []; 64 | N[0] = 1.0; 65 | 66 | for (var j = 1; j <= p; ++j) { 67 | left[j] = u - U[span + 1 - j]; 68 | right[j] = U[span + j] - u; 69 | 70 | var saved = 0.0; 71 | 72 | for (var r = 0; r < j; ++r) { 73 | var rv = right[r + 1]; 74 | var lv = left[j - r]; 75 | var temp = N[r] / (rv + lv); 76 | N[r] = saved + rv * temp; 77 | saved = lv * temp; 78 | } 79 | 80 | N[j] = saved; 81 | } 82 | 83 | return N; 84 | }, 85 | 86 | /* 87 | Calculate B-Spline curve points. See The NURBS Book, page 82, algorithm A3.1. 88 | 89 | p : degree of B-Spline 90 | U : knot vector 91 | P : control points (x, y, z, w) 92 | u : parametric point 93 | 94 | returns point for given u 95 | */ 96 | calcBSplinePoint: function (p, U, P, u) { 97 | var span = this.findSpan(p, u, U); 98 | var N = this.calcBasisFunctions(span, u, p, U); 99 | var C = new Vector4(0, 0, 0, 0); 100 | 101 | for (var j = 0; j <= p; ++j) { 102 | var point = P[span - p + j]; 103 | var Nj = N[j]; 104 | var wNj = point.w * Nj; 105 | C.x += point.x * wNj; 106 | C.y += point.y * wNj; 107 | C.z += point.z * wNj; 108 | C.w += point.w * Nj; 109 | } 110 | 111 | return C; 112 | }, 113 | 114 | /* 115 | Calculate basis functions derivatives. See The NURBS Book, page 72, algorithm A2.3. 116 | 117 | span : span in which u lies 118 | u : parametric point 119 | p : degree 120 | n : number of derivatives to calculate 121 | U : knot vector 122 | 123 | returns array[n+1][p+1] with basis functions derivatives 124 | */ 125 | calcBasisFunctionDerivatives: function (span, u, p, n, U) { 126 | var zeroArr = []; 127 | for (var i = 0; i <= p; ++i) zeroArr[i] = 0.0; 128 | 129 | var ders = []; 130 | for (var i = 0; i <= n; ++i) ders[i] = zeroArr.slice(0); 131 | 132 | var ndu = []; 133 | for (var i = 0; i <= p; ++i) ndu[i] = zeroArr.slice(0); 134 | 135 | ndu[0][0] = 1.0; 136 | 137 | var left = zeroArr.slice(0); 138 | var right = zeroArr.slice(0); 139 | 140 | for (var j = 1; j <= p; ++j) { 141 | left[j] = u - U[span + 1 - j]; 142 | right[j] = U[span + j] - u; 143 | 144 | var saved = 0.0; 145 | 146 | for (var r = 0; r < j; ++r) { 147 | var rv = right[r + 1]; 148 | var lv = left[j - r]; 149 | ndu[j][r] = rv + lv; 150 | 151 | var temp = ndu[r][j - 1] / ndu[j][r]; 152 | ndu[r][j] = saved + rv * temp; 153 | saved = lv * temp; 154 | } 155 | 156 | ndu[j][j] = saved; 157 | } 158 | 159 | for (var j = 0; j <= p; ++j) { 160 | ders[0][j] = ndu[j][p]; 161 | } 162 | 163 | for (var r = 0; r <= p; ++r) { 164 | var s1 = 0; 165 | var s2 = 1; 166 | 167 | var a = []; 168 | for (var i = 0; i <= p; ++i) { 169 | a[i] = zeroArr.slice(0); 170 | } 171 | 172 | a[0][0] = 1.0; 173 | 174 | for (var k = 1; k <= n; ++k) { 175 | var d = 0.0; 176 | var rk = r - k; 177 | var pk = p - k; 178 | 179 | if (r >= k) { 180 | a[s2][0] = a[s1][0] / ndu[pk + 1][rk]; 181 | d = a[s2][0] * ndu[rk][pk]; 182 | } 183 | 184 | var j1 = rk >= -1 ? 1 : -rk; 185 | var j2 = r - 1 <= pk ? k - 1 : p - r; 186 | 187 | for (var j = j1; j <= j2; ++j) { 188 | a[s2][j] = (a[s1][j] - a[s1][j - 1]) / ndu[pk + 1][rk + j]; 189 | d += a[s2][j] * ndu[rk + j][pk]; 190 | } 191 | 192 | if (r <= pk) { 193 | a[s2][k] = -a[s1][k - 1] / ndu[pk + 1][r]; 194 | d += a[s2][k] * ndu[r][pk]; 195 | } 196 | 197 | ders[k][r] = d; 198 | 199 | var j = s1; 200 | s1 = s2; 201 | s2 = j; 202 | } 203 | } 204 | 205 | var r = p; 206 | 207 | for (var k = 1; k <= n; ++k) { 208 | for (var j = 0; j <= p; ++j) { 209 | ders[k][j] *= r; 210 | } 211 | 212 | r *= p - k; 213 | } 214 | 215 | return ders; 216 | }, 217 | 218 | /* 219 | Calculate derivatives of a B-Spline. See The NURBS Book, page 93, algorithm A3.2. 220 | 221 | p : degree 222 | U : knot vector 223 | P : control points 224 | u : Parametric points 225 | nd : number of derivatives 226 | 227 | returns array[d+1] with derivatives 228 | */ 229 | calcBSplineDerivatives: function (p, U, P, u, nd) { 230 | var du = nd < p ? nd : p; 231 | var CK = []; 232 | var span = this.findSpan(p, u, U); 233 | var nders = this.calcBasisFunctionDerivatives(span, u, p, du, U); 234 | var Pw = []; 235 | 236 | for (var i = 0; i < P.length; ++i) { 237 | var point = P[i].clone(); 238 | var w = point.w; 239 | 240 | point.x *= w; 241 | point.y *= w; 242 | point.z *= w; 243 | 244 | Pw[i] = point; 245 | } 246 | 247 | for (var k = 0; k <= du; ++k) { 248 | var point = Pw[span - p].clone().multiplyScalar(nders[k][0]); 249 | 250 | for (var j = 1; j <= p; ++j) { 251 | point.add(Pw[span - p + j].clone().multiplyScalar(nders[k][j])); 252 | } 253 | 254 | CK[k] = point; 255 | } 256 | 257 | for (var k = du + 1; k <= nd + 1; ++k) { 258 | CK[k] = new Vector4(0, 0, 0); 259 | } 260 | 261 | return CK; 262 | }, 263 | 264 | /* 265 | Calculate "K over I" 266 | 267 | returns k!/(i!(k-i)!) 268 | */ 269 | calcKoverI: function (k, i) { 270 | var nom = 1; 271 | 272 | for (var j = 2; j <= k; ++j) { 273 | nom *= j; 274 | } 275 | 276 | var denom = 1; 277 | 278 | for (var j = 2; j <= i; ++j) { 279 | denom *= j; 280 | } 281 | 282 | for (var j = 2; j <= k - i; ++j) { 283 | denom *= j; 284 | } 285 | 286 | return nom / denom; 287 | }, 288 | 289 | /* 290 | Calculate derivatives (0-nd) of rational curve. See The NURBS Book, page 127, algorithm A4.2. 291 | 292 | Pders : result of function calcBSplineDerivatives 293 | 294 | returns array with derivatives for rational curve. 295 | */ 296 | calcRationalCurveDerivatives: function (Pders) { 297 | var nd = Pders.length; 298 | var Aders = []; 299 | var wders = []; 300 | 301 | for (var i = 0; i < nd; ++i) { 302 | var point = Pders[i]; 303 | Aders[i] = new Vector3(point.x, point.y, point.z); 304 | wders[i] = point.w; 305 | } 306 | 307 | var CK = []; 308 | 309 | for (var k = 0; k < nd; ++k) { 310 | var v = Aders[k].clone(); 311 | 312 | for (var i = 1; i <= k; ++i) { 313 | v.sub( 314 | CK[k - i].clone().multiplyScalar(this.calcKoverI(k, i) * wders[i]) 315 | ); 316 | } 317 | 318 | CK[k] = v.divideScalar(wders[0]); 319 | } 320 | 321 | return CK; 322 | }, 323 | 324 | /* 325 | Calculate NURBS curve derivatives. See The NURBS Book, page 127, algorithm A4.2. 326 | 327 | p : degree 328 | U : knot vector 329 | P : control points in homogeneous space 330 | u : parametric points 331 | nd : number of derivatives 332 | 333 | returns array with derivatives. 334 | */ 335 | calcNURBSDerivatives: function (p, U, P, u, nd) { 336 | var Pders = this.calcBSplineDerivatives(p, U, P, u, nd); 337 | return this.calcRationalCurveDerivatives(Pders); 338 | }, 339 | 340 | /* 341 | Calculate rational B-Spline surface point. See The NURBS Book, page 134, algorithm A4.3. 342 | 343 | p1, p2 : degrees of B-Spline surface 344 | U1, U2 : knot vectors 345 | P : control points (x, y, z, w) 346 | u, v : parametric values 347 | 348 | returns point for given (u, v) 349 | */ 350 | calcSurfacePoint: function (p, q, U, V, P, u, v, target) { 351 | var uspan = this.findSpan(p, u, U); 352 | var vspan = this.findSpan(q, v, V); 353 | var Nu = this.calcBasisFunctions(uspan, u, p, U); 354 | var Nv = this.calcBasisFunctions(vspan, v, q, V); 355 | var temp = []; 356 | 357 | for (var l = 0; l <= q; ++l) { 358 | temp[l] = new Vector4(0, 0, 0, 0); 359 | for (var k = 0; k <= p; ++k) { 360 | var point = P[uspan - p + k][vspan - q + l].clone(); 361 | var w = point.w; 362 | point.x *= w; 363 | point.y *= w; 364 | point.z *= w; 365 | temp[l].add(point.multiplyScalar(Nu[k])); 366 | } 367 | } 368 | 369 | var Sw = new Vector4(0, 0, 0, 0); 370 | for (var l = 0; l <= q; ++l) { 371 | Sw.add(temp[l].multiplyScalar(Nv[l])); 372 | } 373 | 374 | Sw.divideScalar(Sw.w); 375 | target.set(Sw.x, Sw.y, Sw.z); 376 | }, 377 | }; 378 | 379 | export { NURBSUtils }; 380 | -------------------------------------------------------------------------------- /third_party/OrbitControls.js: -------------------------------------------------------------------------------- 1 | import { Vector2, Vector3, Spherical, MOUSE, Quaternion, EventDispatcher } from '../third_party/three.module.js'; 2 | 3 | /** 4 | * @author qiao / https://github.com/qiao 5 | * @author mrdoob / http://mrdoob.com 6 | * @author alteredq / http://alteredqualia.com/ 7 | * @author WestLangley / http://github.com/WestLangley 8 | * @author erich666 / http://erichaines.com 9 | */ 10 | 11 | // This set of controls performs orbiting, dollying (zooming), and panning. 12 | // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). 13 | // 14 | // Orbit - left mouse / touch: one-finger move 15 | // Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish 16 | // Pan - right mouse, or left mouse + ctrl/metaKey, or arrow keys / touch: two-finger move 17 | 18 | const OrbitControls = function(object, domElement) { 19 | 20 | this.object = object; 21 | 22 | this.domElement = (domElement !== undefined) ? domElement : document; 23 | 24 | // Set to false to disable this control 25 | this.enabled = true; 26 | 27 | // "target" sets the location of focus, where the object orbits around 28 | this.target = new Vector3(); 29 | 30 | // How far you can dolly in and out ( PerspectiveCamera only ) 31 | this.minDistance = 0; 32 | this.maxDistance = Infinity; 33 | 34 | // How far you can zoom in and out ( OrthographicCamera only ) 35 | this.minZoom = 0; 36 | this.maxZoom = Infinity; 37 | 38 | // How far you can orbit vertically, upper and lower limits. 39 | // Range is 0 to Math.PI radians. 40 | this.minPolarAngle = 0; // radians 41 | this.maxPolarAngle = Math.PI; // radians 42 | 43 | // How far you can orbit horizontally, upper and lower limits. 44 | // If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ]. 45 | this.minAzimuthAngle = -Infinity; // radians 46 | this.maxAzimuthAngle = Infinity; // radians 47 | 48 | // Set to true to enable damping (inertia) 49 | // If damping is enabled, you must call controls.update() in your animation loop 50 | this.enableDamping = false; 51 | this.dampingFactor = 0.25; 52 | 53 | // This option actually enables dollying in and out; left as "zoom" for backwards compatibility. 54 | // Set to false to disable zooming 55 | this.enableZoom = true; 56 | this.zoomSpeed = 1.0; 57 | 58 | // Set to false to disable rotating 59 | this.enableRotate = true; 60 | this.rotateSpeed = 1.0; 61 | 62 | // Set to false to disable panning 63 | this.enablePan = true; 64 | this.panSpeed = 1.0; 65 | this.screenSpacePanning = false; // if true, pan in screen-space 66 | this.keyPanSpeed = 7.0; // pixels moved per arrow key push 67 | 68 | // Set to true to automatically rotate around the target 69 | // If auto-rotate is enabled, you must call controls.update() in your animation loop 70 | this.autoRotate = false; 71 | this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60 72 | 73 | // Set to false to disable use of the keys 74 | this.enableKeys = true; 75 | 76 | // The four arrow keys 77 | this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 }; 78 | 79 | // Mouse buttons 80 | this.mouseButtons = { LEFT: MOUSE.LEFT, MIDDLE: MOUSE.MIDDLE, RIGHT: MOUSE.RIGHT }; 81 | 82 | // for reset 83 | this.target0 = this.target.clone(); 84 | this.position0 = this.object.position.clone(); 85 | this.zoom0 = this.object.zoom; 86 | 87 | // 88 | // public methods 89 | // 90 | 91 | this.getPolarAngle = function() { 92 | 93 | return spherical.phi; 94 | 95 | }; 96 | 97 | this.getAzimuthalAngle = function() { 98 | 99 | return spherical.theta; 100 | 101 | }; 102 | 103 | this.saveState = function() { 104 | 105 | scope.target0.copy(scope.target); 106 | scope.position0.copy(scope.object.position); 107 | scope.zoom0 = scope.object.zoom; 108 | 109 | }; 110 | 111 | this.reset = function() { 112 | 113 | scope.target.copy(scope.target0); 114 | scope.object.position.copy(scope.position0); 115 | scope.object.zoom = scope.zoom0; 116 | 117 | scope.object.updateProjectionMatrix(); 118 | scope.dispatchEvent(changeEvent); 119 | 120 | scope.update(); 121 | 122 | state = STATE.NONE; 123 | 124 | }; 125 | 126 | // this method is exposed, but perhaps it would be better if we can make it private... 127 | this.update = function() { 128 | 129 | var offset = new Vector3(); 130 | 131 | // so camera.up is the orbit axis 132 | var quat = new Quaternion().setFromUnitVectors(object.up, new Vector3(0, 1, 0)); 133 | var quatInverse = quat.clone().inverse(); 134 | 135 | var lastPosition = new Vector3(); 136 | var lastQuaternion = new Quaternion(); 137 | 138 | return function update() { 139 | 140 | var position = scope.object.position; 141 | 142 | offset.copy(position).sub(scope.target); 143 | 144 | // rotate offset to "y-axis-is-up" space 145 | offset.applyQuaternion(quat); 146 | 147 | // angle from z-axis around y-axis 148 | spherical.setFromVector3(offset); 149 | 150 | if (scope.autoRotate && state === STATE.NONE) { 151 | 152 | rotateLeft(getAutoRotationAngle()); 153 | 154 | } 155 | 156 | spherical.theta += sphericalDelta.theta; 157 | spherical.phi += sphericalDelta.phi; 158 | 159 | // restrict theta to be between desired limits 160 | spherical.theta = Math.max(scope.minAzimuthAngle, Math.min(scope.maxAzimuthAngle, spherical.theta)); 161 | 162 | // restrict phi to be between desired limits 163 | spherical.phi = Math.max(scope.minPolarAngle, Math.min(scope.maxPolarAngle, spherical.phi)); 164 | 165 | spherical.makeSafe(); 166 | 167 | 168 | spherical.radius *= scale; 169 | 170 | // restrict radius to be between desired limits 171 | spherical.radius = Math.max(scope.minDistance, Math.min(scope.maxDistance, spherical.radius)); 172 | 173 | // move target to panned location 174 | scope.target.add(panOffset); 175 | 176 | offset.setFromSpherical(spherical); 177 | 178 | // rotate offset back to "camera-up-vector-is-up" space 179 | offset.applyQuaternion(quatInverse); 180 | 181 | position.copy(scope.target).add(offset); 182 | 183 | scope.object.lookAt(scope.target); 184 | 185 | if (scope.enableDamping === true) { 186 | 187 | sphericalDelta.theta *= (1 - scope.dampingFactor); 188 | sphericalDelta.phi *= (1 - scope.dampingFactor); 189 | 190 | panOffset.multiplyScalar(1 - scope.dampingFactor); 191 | 192 | } else { 193 | 194 | sphericalDelta.set(0, 0, 0); 195 | 196 | panOffset.set(0, 0, 0); 197 | 198 | } 199 | 200 | scale = 1; 201 | 202 | // update condition is: 203 | // min(camera displacement, camera rotation in radians)^2 > EPS 204 | // using small-angle approximation cos(x/2) = 1 - x^2 / 8 205 | 206 | if (zoomChanged || 207 | lastPosition.distanceToSquared(scope.object.position) > EPS || 208 | 8 * (1 - lastQuaternion.dot(scope.object.quaternion)) > EPS) { 209 | 210 | scope.dispatchEvent(changeEvent); 211 | 212 | lastPosition.copy(scope.object.position); 213 | lastQuaternion.copy(scope.object.quaternion); 214 | zoomChanged = false; 215 | 216 | return true; 217 | 218 | } 219 | 220 | return false; 221 | 222 | }; 223 | 224 | }(); 225 | 226 | this.dispose = function() { 227 | 228 | scope.domElement.removeEventListener('contextmenu', onContextMenu, false); 229 | scope.domElement.removeEventListener('mousedown', onMouseDown, false); 230 | scope.domElement.removeEventListener('wheel', onMouseWheel, false); 231 | 232 | scope.domElement.removeEventListener('touchstart', onTouchStart, false); 233 | scope.domElement.removeEventListener('touchend', onTouchEnd, false); 234 | scope.domElement.removeEventListener('touchmove', onTouchMove, false); 235 | 236 | document.removeEventListener('mousemove', onMouseMove, false); 237 | document.removeEventListener('mouseup', onMouseUp, false); 238 | 239 | window.removeEventListener('keydown', onKeyDown, false); 240 | 241 | //scope.dispatchEvent( { type: 'dispose' } ); // should this be added here? 242 | 243 | }; 244 | 245 | // 246 | // internals 247 | // 248 | 249 | var scope = this; 250 | 251 | var changeEvent = { type: 'change' }; 252 | var startEvent = { type: 'start' }; 253 | var endEvent = { type: 'end' }; 254 | 255 | var STATE = { NONE: -1, ROTATE: 0, DOLLY: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_DOLLY_PAN: 4 }; 256 | 257 | var state = STATE.NONE; 258 | 259 | var EPS = 0.000001; 260 | 261 | // current position in spherical coordinates 262 | var spherical = new Spherical(); 263 | var sphericalDelta = new Spherical(); 264 | 265 | var scale = 1; 266 | var panOffset = new Vector3(); 267 | var zoomChanged = false; 268 | 269 | var rotateStart = new Vector2(); 270 | var rotateEnd = new Vector2(); 271 | var rotateDelta = new Vector2(); 272 | 273 | var panStart = new Vector2(); 274 | var panEnd = new Vector2(); 275 | var panDelta = new Vector2(); 276 | 277 | var dollyStart = new Vector2(); 278 | var dollyEnd = new Vector2(); 279 | var dollyDelta = new Vector2(); 280 | 281 | function getAutoRotationAngle() { 282 | 283 | return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; 284 | 285 | } 286 | 287 | function getZoomScale() { 288 | 289 | return Math.pow(0.95, scope.zoomSpeed); 290 | 291 | } 292 | 293 | function rotateLeft(angle) { 294 | 295 | sphericalDelta.theta -= angle; 296 | 297 | } 298 | 299 | function rotateUp(angle) { 300 | 301 | sphericalDelta.phi -= angle; 302 | 303 | } 304 | 305 | var panLeft = function() { 306 | 307 | var v = new Vector3(); 308 | 309 | return function panLeft(distance, objectMatrix) { 310 | 311 | v.setFromMatrixColumn(objectMatrix, 0); // get X column of objectMatrix 312 | v.multiplyScalar(-distance); 313 | 314 | panOffset.add(v); 315 | 316 | }; 317 | 318 | }(); 319 | 320 | var panUp = function() { 321 | 322 | var v = new Vector3(); 323 | 324 | return function panUp(distance, objectMatrix) { 325 | 326 | if (scope.screenSpacePanning === true) { 327 | 328 | v.setFromMatrixColumn(objectMatrix, 1); 329 | 330 | } else { 331 | 332 | v.setFromMatrixColumn(objectMatrix, 0); 333 | v.crossVectors(scope.object.up, v); 334 | 335 | } 336 | 337 | v.multiplyScalar(distance); 338 | 339 | panOffset.add(v); 340 | 341 | }; 342 | 343 | }(); 344 | 345 | // deltaX and deltaY are in pixels; right and down are positive 346 | var pan = function() { 347 | 348 | var offset = new Vector3(); 349 | 350 | return function pan(deltaX, deltaY) { 351 | 352 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement; 353 | 354 | if (scope.object.isPerspectiveCamera) { 355 | 356 | // perspective 357 | var position = scope.object.position; 358 | offset.copy(position).sub(scope.target); 359 | var targetDistance = offset.length(); 360 | 361 | // half of the fov is center to top of screen 362 | targetDistance *= Math.tan((scope.object.fov / 2) * Math.PI / 180.0); 363 | 364 | // we use only clientHeight here so aspect ratio does not distort speed 365 | panLeft(2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix); 366 | panUp(2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix); 367 | 368 | } else if (scope.object.isOrthographicCamera) { 369 | 370 | // orthographic 371 | panLeft(deltaX * (scope.object.right - scope.object.left) / scope.object.zoom / element.clientWidth, 372 | scope.object.matrix); 373 | panUp(deltaY * (scope.object.top - scope.object.bottom) / scope.object.zoom / element.clientHeight, scope 374 | .object.matrix); 375 | 376 | } else { 377 | 378 | // camera neither orthographic nor perspective 379 | console.warn('WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.'); 380 | scope.enablePan = false; 381 | 382 | } 383 | 384 | }; 385 | 386 | }(); 387 | 388 | function dollyIn(dollyScale) { 389 | 390 | if (scope.object.isPerspectiveCamera) { 391 | 392 | scale /= dollyScale; 393 | 394 | } else if (scope.object.isOrthographicCamera) { 395 | 396 | scope.object.zoom = Math.max(scope.minZoom, Math.min(scope.maxZoom, scope.object.zoom * dollyScale)); 397 | scope.object.updateProjectionMatrix(); 398 | zoomChanged = true; 399 | 400 | } else { 401 | 402 | console.warn('WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.'); 403 | scope.enableZoom = false; 404 | 405 | } 406 | 407 | } 408 | 409 | function dollyOut(dollyScale) { 410 | 411 | if (scope.object.isPerspectiveCamera) { 412 | 413 | scale *= dollyScale; 414 | 415 | } else if (scope.object.isOrthographicCamera) { 416 | 417 | scope.object.zoom = Math.max(scope.minZoom, Math.min(scope.maxZoom, scope.object.zoom / dollyScale)); 418 | scope.object.updateProjectionMatrix(); 419 | zoomChanged = true; 420 | 421 | } else { 422 | 423 | console.warn('WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.'); 424 | scope.enableZoom = false; 425 | 426 | } 427 | 428 | } 429 | 430 | // 431 | // event callbacks - update the object state 432 | // 433 | 434 | function handleMouseDownRotate(event) { 435 | 436 | //console.log( 'handleMouseDownRotate' ); 437 | 438 | rotateStart.set(event.clientX, event.clientY); 439 | 440 | } 441 | 442 | function handleMouseDownDolly(event) { 443 | 444 | //console.log( 'handleMouseDownDolly' ); 445 | 446 | dollyStart.set(event.clientX, event.clientY); 447 | 448 | } 449 | 450 | function handleMouseDownPan(event) { 451 | 452 | //console.log( 'handleMouseDownPan' ); 453 | 454 | panStart.set(event.clientX, event.clientY); 455 | 456 | } 457 | 458 | function handleMouseMoveRotate(event) { 459 | 460 | //console.log( 'handleMouseMoveRotate' ); 461 | 462 | rotateEnd.set(event.clientX, event.clientY); 463 | 464 | rotateDelta.subVectors(rotateEnd, rotateStart).multiplyScalar(scope.rotateSpeed); 465 | 466 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement; 467 | 468 | rotateLeft(2 * Math.PI * rotateDelta.x / element.clientHeight); // yes, height 469 | 470 | rotateUp(2 * Math.PI * rotateDelta.y / element.clientHeight); 471 | 472 | rotateStart.copy(rotateEnd); 473 | 474 | scope.update(); 475 | 476 | } 477 | 478 | function handleMouseMoveDolly(event) { 479 | 480 | //console.log( 'handleMouseMoveDolly' ); 481 | 482 | dollyEnd.set(event.clientX, event.clientY); 483 | 484 | dollyDelta.subVectors(dollyEnd, dollyStart); 485 | 486 | if (dollyDelta.y > 0) { 487 | 488 | dollyIn(getZoomScale()); 489 | 490 | } else if (dollyDelta.y < 0) { 491 | 492 | dollyOut(getZoomScale()); 493 | 494 | } 495 | 496 | dollyStart.copy(dollyEnd); 497 | 498 | scope.update(); 499 | 500 | } 501 | 502 | function handleMouseMovePan(event) { 503 | 504 | //console.log( 'handleMouseMovePan' ); 505 | 506 | panEnd.set(event.clientX, event.clientY); 507 | 508 | panDelta.subVectors(panEnd, panStart).multiplyScalar(scope.panSpeed); 509 | 510 | pan(panDelta.x, panDelta.y); 511 | 512 | panStart.copy(panEnd); 513 | 514 | scope.update(); 515 | 516 | } 517 | 518 | function handleMouseUp(event) { 519 | 520 | // console.log( 'handleMouseUp' ); 521 | 522 | } 523 | 524 | function handleMouseWheel(event) { 525 | 526 | // console.log( 'handleMouseWheel' ); 527 | 528 | if (event.deltaY < 0) { 529 | 530 | dollyOut(getZoomScale()); 531 | 532 | } else if (event.deltaY > 0) { 533 | 534 | dollyIn(getZoomScale()); 535 | 536 | } 537 | 538 | scope.update(); 539 | 540 | } 541 | 542 | function handleKeyDown(event) { 543 | 544 | //console.log( 'handleKeyDown' ); 545 | 546 | switch (event.keyCode) { 547 | 548 | case scope.keys.UP: 549 | pan(0, scope.keyPanSpeed); 550 | scope.update(); 551 | break; 552 | 553 | case scope.keys.BOTTOM: 554 | pan(0, -scope.keyPanSpeed); 555 | scope.update(); 556 | break; 557 | 558 | case scope.keys.LEFT: 559 | pan(scope.keyPanSpeed, 0); 560 | scope.update(); 561 | break; 562 | 563 | case scope.keys.RIGHT: 564 | pan(-scope.keyPanSpeed, 0); 565 | scope.update(); 566 | break; 567 | 568 | } 569 | 570 | } 571 | 572 | function handleTouchStartRotate(event) { 573 | 574 | //console.log( 'handleTouchStartRotate' ); 575 | 576 | rotateStart.set(event.touches[0].pageX, event.touches[0].pageY); 577 | 578 | } 579 | 580 | function handleTouchStartDollyPan(event) { 581 | 582 | //console.log( 'handleTouchStartDollyPan' ); 583 | 584 | if (scope.enableZoom) { 585 | 586 | var dx = event.touches[0].pageX - event.touches[1].pageX; 587 | var dy = event.touches[0].pageY - event.touches[1].pageY; 588 | 589 | var distance = Math.sqrt(dx * dx + dy * dy); 590 | 591 | dollyStart.set(0, distance); 592 | 593 | } 594 | 595 | if (scope.enablePan) { 596 | 597 | var x = 0.5 * (event.touches[0].pageX + event.touches[1].pageX); 598 | var y = 0.5 * (event.touches[0].pageY + event.touches[1].pageY); 599 | 600 | panStart.set(x, y); 601 | 602 | } 603 | 604 | } 605 | 606 | function handleTouchMoveRotate(event) { 607 | 608 | //console.log( 'handleTouchMoveRotate' ); 609 | 610 | rotateEnd.set(event.touches[0].pageX, event.touches[0].pageY); 611 | 612 | rotateDelta.subVectors(rotateEnd, rotateStart).multiplyScalar(scope.rotateSpeed); 613 | 614 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement; 615 | 616 | rotateLeft(2 * Math.PI * rotateDelta.x / element.clientHeight); // yes, height 617 | 618 | rotateUp(2 * Math.PI * rotateDelta.y / element.clientHeight); 619 | 620 | rotateStart.copy(rotateEnd); 621 | 622 | scope.update(); 623 | 624 | } 625 | 626 | function handleTouchMoveDollyPan(event) { 627 | 628 | //console.log( 'handleTouchMoveDollyPan' ); 629 | 630 | if (scope.enableZoom) { 631 | 632 | var dx = event.touches[0].pageX - event.touches[1].pageX; 633 | var dy = event.touches[0].pageY - event.touches[1].pageY; 634 | 635 | var distance = Math.sqrt(dx * dx + dy * dy); 636 | 637 | dollyEnd.set(0, distance); 638 | 639 | dollyDelta.set(0, Math.pow(dollyEnd.y / dollyStart.y, scope.zoomSpeed)); 640 | 641 | dollyIn(dollyDelta.y); 642 | 643 | dollyStart.copy(dollyEnd); 644 | 645 | } 646 | 647 | if (scope.enablePan) { 648 | 649 | var x = 0.5 * (event.touches[0].pageX + event.touches[1].pageX); 650 | var y = 0.5 * (event.touches[0].pageY + event.touches[1].pageY); 651 | 652 | panEnd.set(x, y); 653 | 654 | panDelta.subVectors(panEnd, panStart).multiplyScalar(scope.panSpeed); 655 | 656 | pan(panDelta.x, panDelta.y); 657 | 658 | panStart.copy(panEnd); 659 | 660 | } 661 | 662 | scope.update(); 663 | 664 | } 665 | 666 | function handleTouchEnd(event) { 667 | 668 | //console.log( 'handleTouchEnd' ); 669 | 670 | } 671 | 672 | // 673 | // event handlers - FSM: listen for events and reset state 674 | // 675 | 676 | function onMouseDown(event) { 677 | 678 | if (scope.enabled === false) return; 679 | 680 | event.preventDefault(); 681 | 682 | switch (event.button) { 683 | 684 | case scope.mouseButtons.LEFT: 685 | 686 | if (event.ctrlKey || event.metaKey) { 687 | 688 | if (scope.enablePan === false) return; 689 | 690 | handleMouseDownPan(event); 691 | 692 | state = STATE.PAN; 693 | 694 | } else { 695 | 696 | if (scope.enableRotate === false) return; 697 | 698 | handleMouseDownRotate(event); 699 | 700 | state = STATE.ROTATE; 701 | 702 | } 703 | 704 | break; 705 | 706 | case scope.mouseButtons.MIDDLE: 707 | 708 | if (scope.enableZoom === false) return; 709 | 710 | handleMouseDownDolly(event); 711 | 712 | state = STATE.DOLLY; 713 | 714 | break; 715 | 716 | case scope.mouseButtons.RIGHT: 717 | 718 | if (scope.enablePan === false) return; 719 | 720 | handleMouseDownPan(event); 721 | 722 | state = STATE.PAN; 723 | 724 | break; 725 | 726 | } 727 | 728 | if (state !== STATE.NONE) { 729 | 730 | document.addEventListener('mousemove', onMouseMove, false); 731 | document.addEventListener('mouseup', onMouseUp, false); 732 | 733 | scope.dispatchEvent(startEvent); 734 | 735 | } 736 | 737 | } 738 | 739 | function onMouseMove(event) { 740 | 741 | if (scope.enabled === false) return; 742 | 743 | event.preventDefault(); 744 | 745 | switch (state) { 746 | 747 | case STATE.ROTATE: 748 | 749 | if (scope.enableRotate === false) return; 750 | 751 | handleMouseMoveRotate(event); 752 | 753 | break; 754 | 755 | case STATE.DOLLY: 756 | 757 | if (scope.enableZoom === false) return; 758 | 759 | handleMouseMoveDolly(event); 760 | 761 | break; 762 | 763 | case STATE.PAN: 764 | 765 | if (scope.enablePan === false) return; 766 | 767 | handleMouseMovePan(event); 768 | 769 | break; 770 | 771 | } 772 | 773 | } 774 | 775 | function onMouseUp(event) { 776 | 777 | if (scope.enabled === false) return; 778 | 779 | handleMouseUp(event); 780 | 781 | document.removeEventListener('mousemove', onMouseMove, false); 782 | document.removeEventListener('mouseup', onMouseUp, false); 783 | 784 | scope.dispatchEvent(endEvent); 785 | 786 | state = STATE.NONE; 787 | 788 | } 789 | 790 | function onMouseWheel(event) { 791 | 792 | if (scope.enabled === false || scope.enableZoom === false || (state !== STATE.NONE && state !== STATE.ROTATE)) 793 | return; 794 | 795 | event.preventDefault(); 796 | event.stopPropagation(); 797 | 798 | scope.dispatchEvent(startEvent); 799 | 800 | handleMouseWheel(event); 801 | 802 | scope.dispatchEvent(endEvent); 803 | 804 | } 805 | 806 | function onKeyDown(event) { 807 | 808 | if (scope.enabled === false || scope.enableKeys === false || scope.enablePan === false) return; 809 | 810 | handleKeyDown(event); 811 | 812 | } 813 | 814 | function onTouchStart(event) { 815 | 816 | if (scope.enabled === false) return; 817 | 818 | event.preventDefault(); 819 | 820 | switch (event.touches.length) { 821 | 822 | case 1: // one-fingered touch: rotate 823 | 824 | if (scope.enableRotate === false) return; 825 | 826 | handleTouchStartRotate(event); 827 | 828 | state = STATE.TOUCH_ROTATE; 829 | 830 | break; 831 | 832 | case 2: // two-fingered touch: dolly-pan 833 | 834 | if (scope.enableZoom === false && scope.enablePan === false) return; 835 | 836 | handleTouchStartDollyPan(event); 837 | 838 | state = STATE.TOUCH_DOLLY_PAN; 839 | 840 | break; 841 | 842 | default: 843 | 844 | state = STATE.NONE; 845 | 846 | } 847 | 848 | if (state !== STATE.NONE) { 849 | 850 | scope.dispatchEvent(startEvent); 851 | 852 | } 853 | 854 | } 855 | 856 | function onTouchMove(event) { 857 | 858 | if (scope.enabled === false) return; 859 | 860 | event.preventDefault(); 861 | event.stopPropagation(); 862 | 863 | switch (event.touches.length) { 864 | 865 | case 1: // one-fingered touch: rotate 866 | 867 | if (scope.enableRotate === false) return; 868 | if (state !== STATE.TOUCH_ROTATE) return; // is this needed? 869 | 870 | handleTouchMoveRotate(event); 871 | 872 | break; 873 | 874 | case 2: // two-fingered touch: dolly-pan 875 | 876 | if (scope.enableZoom === false && scope.enablePan === false) return; 877 | if (state !== STATE.TOUCH_DOLLY_PAN) return; // is this needed? 878 | 879 | handleTouchMoveDollyPan(event); 880 | 881 | break; 882 | 883 | default: 884 | 885 | state = STATE.NONE; 886 | 887 | } 888 | 889 | } 890 | 891 | function onTouchEnd(event) { 892 | 893 | if (scope.enabled === false) return; 894 | 895 | handleTouchEnd(event); 896 | 897 | scope.dispatchEvent(endEvent); 898 | 899 | state = STATE.NONE; 900 | 901 | } 902 | 903 | function onContextMenu(event) { 904 | 905 | if (scope.enabled === false) return; 906 | 907 | event.preventDefault(); 908 | 909 | } 910 | 911 | // 912 | 913 | scope.domElement.addEventListener('contextmenu', onContextMenu, false); 914 | 915 | scope.domElement.addEventListener('mousedown', onMouseDown, false); 916 | scope.domElement.addEventListener('wheel', onMouseWheel, false); 917 | 918 | scope.domElement.addEventListener('touchstart', onTouchStart, false); 919 | scope.domElement.addEventListener('touchend', onTouchEnd, false); 920 | scope.domElement.addEventListener('touchmove', onTouchMove, false); 921 | 922 | window.addEventListener('keydown', onKeyDown, false); 923 | 924 | // force an update at start 925 | 926 | this.update(); 927 | 928 | }; 929 | 930 | OrbitControls.prototype = Object.create(EventDispatcher.prototype); 931 | OrbitControls.prototype.constructor = OrbitControls; 932 | 933 | Object.defineProperties(OrbitControls.prototype, { 934 | 935 | center: { 936 | 937 | get: function() { 938 | 939 | console.warn('OrbitControls: .center has been renamed to .target'); 940 | return this.target; 941 | 942 | } 943 | 944 | }, 945 | 946 | // backward compatibility 947 | 948 | noZoom: { 949 | 950 | get: function() { 951 | 952 | console.warn('OrbitControls: .noZoom has been deprecated. Use .enableZoom instead.'); 953 | return !this.enableZoom; 954 | 955 | }, 956 | 957 | set: function(value) { 958 | 959 | console.warn('OrbitControls: .noZoom has been deprecated. Use .enableZoom instead.'); 960 | this.enableZoom = !value; 961 | 962 | } 963 | 964 | }, 965 | 966 | noRotate: { 967 | 968 | get: function() { 969 | 970 | console.warn('OrbitControls: .noRotate has been deprecated. Use .enableRotate instead.'); 971 | return !this.enableRotate; 972 | 973 | }, 974 | 975 | set: function(value) { 976 | 977 | console.warn('OrbitControls: .noRotate has been deprecated. Use .enableRotate instead.'); 978 | this.enableRotate = !value; 979 | 980 | } 981 | 982 | }, 983 | 984 | noPan: { 985 | 986 | get: function() { 987 | 988 | console.warn('OrbitControls: .noPan has been deprecated. Use .enablePan instead.'); 989 | return !this.enablePan; 990 | 991 | }, 992 | 993 | set: function(value) { 994 | 995 | console.warn('OrbitControls: .noPan has been deprecated. Use .enablePan instead.'); 996 | this.enablePan = !value; 997 | 998 | } 999 | 1000 | }, 1001 | 1002 | noKeys: { 1003 | 1004 | get: function() { 1005 | 1006 | console.warn('OrbitControls: .noKeys has been deprecated. Use .enableKeys instead.'); 1007 | return !this.enableKeys; 1008 | 1009 | }, 1010 | 1011 | set: function(value) { 1012 | 1013 | console.warn('OrbitControls: .noKeys has been deprecated. Use .enableKeys instead.'); 1014 | this.enableKeys = !value; 1015 | 1016 | } 1017 | 1018 | }, 1019 | 1020 | staticMoving: { 1021 | 1022 | get: function() { 1023 | 1024 | console.warn('OrbitControls: .staticMoving has been deprecated. Use .enableDamping instead.'); 1025 | return !this.enableDamping; 1026 | 1027 | }, 1028 | 1029 | set: function(value) { 1030 | 1031 | console.warn('OrbitControls: .staticMoving has been deprecated. Use .enableDamping instead.'); 1032 | this.enableDamping = !value; 1033 | 1034 | } 1035 | 1036 | }, 1037 | 1038 | dynamicDampingFactor: { 1039 | 1040 | get: function() { 1041 | 1042 | console.warn('OrbitControls: .dynamicDampingFactor has been renamed. Use .dampingFactor instead.'); 1043 | return this.dampingFactor; 1044 | 1045 | }, 1046 | 1047 | set: function(value) { 1048 | 1049 | console.warn('OrbitControls: .dynamicDampingFactor has been renamed. Use .dampingFactor instead.'); 1050 | this.dampingFactor = value; 1051 | 1052 | } 1053 | 1054 | } 1055 | 1056 | }); 1057 | 1058 | export { OrbitControls }; -------------------------------------------------------------------------------- /third_party/VRButton.js: -------------------------------------------------------------------------------- 1 | var VRButton = { 2 | 3 | createButton: function ( renderer, options ) { 4 | 5 | if ( options ) { 6 | 7 | console.error( 'THREE.VRButton: The "options" parameter has been removed. Please set the reference space type via renderer.xr.setReferenceSpaceType() instead.' ); 8 | 9 | } 10 | 11 | function showEnterVR( /*device*/ ) { 12 | 13 | var currentSession = null; 14 | 15 | function onSessionStarted( session ) { 16 | 17 | session.addEventListener( 'end', onSessionEnded ); 18 | 19 | renderer.xr.setSession( session ); 20 | button.textContent = 'EXIT VR'; 21 | 22 | currentSession = session; 23 | 24 | } 25 | 26 | function onSessionEnded( /*event*/ ) { 27 | 28 | currentSession.removeEventListener( 'end', onSessionEnded ); 29 | 30 | button.textContent = 'ENTER VR'; 31 | 32 | currentSession = null; 33 | 34 | } 35 | 36 | // 37 | 38 | button.style.display = ''; 39 | 40 | button.style.cursor = 'pointer'; 41 | button.style.left = 'calc(50% - 50px)'; 42 | button.style.width = '100px'; 43 | 44 | button.textContent = 'ENTER VR'; 45 | 46 | button.onmouseenter = function () { 47 | 48 | button.style.opacity = '1.0'; 49 | 50 | }; 51 | 52 | button.onmouseleave = function () { 53 | 54 | button.style.opacity = '0.5'; 55 | 56 | }; 57 | 58 | button.onclick = function () { 59 | 60 | if ( currentSession === null ) { 61 | 62 | // WebXR's requestReferenceSpace only works if the corresponding feature 63 | // was requested at session creation time. For simplicity, just ask for 64 | // the interesting ones as optional features, but be aware that the 65 | // requestReferenceSpace call will fail if it turns out to be unavailable. 66 | // ('local' is always available for immersive sessions and doesn't need to 67 | // be requested separately.) 68 | 69 | var sessionInit = { optionalFeatures: [ 'local-floor', 'bounded-floor', 'hand-tracking' ] }; 70 | navigator.xr.requestSession( 'immersive-vr', sessionInit ).then( onSessionStarted ); 71 | 72 | } else { 73 | 74 | currentSession.end(); 75 | 76 | } 77 | 78 | }; 79 | 80 | } 81 | 82 | function disableButton() { 83 | 84 | button.style.display = ''; 85 | 86 | button.style.cursor = 'auto'; 87 | button.style.left = 'calc(50% - 75px)'; 88 | button.style.width = '150px'; 89 | 90 | button.onmouseenter = null; 91 | button.onmouseleave = null; 92 | 93 | button.onclick = null; 94 | 95 | } 96 | 97 | function showWebXRNotFound() { 98 | 99 | disableButton(); 100 | 101 | button.textContent = 'VR NOT SUPPORTED'; 102 | 103 | } 104 | 105 | function stylizeElement( element ) { 106 | 107 | element.style.position = 'absolute'; 108 | element.style.bottom = '20px'; 109 | element.style.padding = '12px 6px'; 110 | element.style.border = '1px solid #fff'; 111 | element.style.borderRadius = '4px'; 112 | element.style.background = 'rgba(0,0,0,0.1)'; 113 | element.style.color = '#fff'; 114 | element.style.font = 'normal 13px sans-serif'; 115 | element.style.textAlign = 'center'; 116 | element.style.opacity = '0.5'; 117 | element.style.outline = 'none'; 118 | element.style.zIndex = '999'; 119 | 120 | } 121 | 122 | if ( 'xr' in navigator ) { 123 | 124 | var button = document.createElement( 'button' ); 125 | button.id = 'VRButton'; 126 | button.style.display = 'none'; 127 | 128 | stylizeElement( button ); 129 | 130 | navigator.xr.isSessionSupported( 'immersive-vr' ).then( function ( supported ) { 131 | 132 | supported ? showEnterVR() : showWebXRNotFound(); 133 | 134 | } ); 135 | 136 | return button; 137 | 138 | } else { 139 | 140 | var message = document.createElement( 'a' ); 141 | 142 | if ( window.isSecureContext === false ) { 143 | 144 | message.href = document.location.href.replace( /^http:/, 'https:' ); 145 | message.innerHTML = 'WEBXR NEEDS HTTPS'; // TODO Improve message 146 | 147 | } else { 148 | 149 | message.href = 'https://immersiveweb.dev/'; 150 | message.innerHTML = 'WEBXR NOT AVAILABLE'; 151 | 152 | } 153 | 154 | message.style.left = 'calc(50% - 90px)'; 155 | message.style.width = '180px'; 156 | message.style.textDecoration = 'none'; 157 | 158 | stylizeElement( message ); 159 | 160 | return message; 161 | 162 | } 163 | 164 | } 165 | 166 | }; 167 | 168 | export { VRButton }; 169 | -------------------------------------------------------------------------------- /third_party/XRControllerModelFactory.js: -------------------------------------------------------------------------------- 1 | import { 2 | Mesh, 3 | MeshBasicMaterial, 4 | Object3D, 5 | Quaternion, 6 | SphereBufferGeometry, 7 | } from "./three.module.js"; 8 | 9 | import { GLTFLoader } from "./GLTFLoader.js"; 10 | 11 | import { 12 | Constants as MotionControllerConstants, 13 | fetchProfile, 14 | MotionController, 15 | } from "./motion-controllers.module.js"; 16 | 17 | const DEFAULT_PROFILES_PATH = 18 | "https://cdn.jsdelivr.net/npm/@webxr-input-profiles/assets@1.0/dist/profiles"; 19 | const DEFAULT_PROFILE = "generic-trigger"; 20 | 21 | function XRControllerModel() { 22 | Object3D.call(this); 23 | 24 | this.motionController = null; 25 | this.envMap = null; 26 | } 27 | 28 | XRControllerModel.prototype = Object.assign(Object.create(Object3D.prototype), { 29 | constructor: XRControllerModel, 30 | 31 | setEnvironmentMap: function (envMap) { 32 | if (this.envMap == envMap) { 33 | return this; 34 | } 35 | 36 | this.envMap = envMap; 37 | this.traverse((child) => { 38 | if (child.isMesh) { 39 | child.material.envMap = this.envMap; 40 | child.material.needsUpdate = true; 41 | } 42 | }); 43 | 44 | return this; 45 | }, 46 | 47 | /** 48 | * Polls data from the XRInputSource and updates the model's components to match 49 | * the real world data 50 | */ 51 | updateMatrixWorld: function (force) { 52 | Object3D.prototype.updateMatrixWorld.call(this, force); 53 | 54 | if (!this.motionController) return; 55 | 56 | // Cause the MotionController to poll the Gamepad for data 57 | this.motionController.updateFromGamepad(); 58 | 59 | // Update the 3D model to reflect the button, thumbstick, and touchpad state 60 | Object.values(this.motionController.components).forEach((component) => { 61 | // Update node data based on the visual responses' current states 62 | Object.values(component.visualResponses).forEach((visualResponse) => { 63 | const { 64 | valueNode, 65 | minNode, 66 | maxNode, 67 | value, 68 | valueNodeProperty, 69 | } = visualResponse; 70 | 71 | // Skip if the visual response node is not found. No error is needed, 72 | // because it will have been reported at load time. 73 | if (!valueNode) return; 74 | 75 | // Calculate the new properties based on the weight supplied 76 | if ( 77 | valueNodeProperty === 78 | MotionControllerConstants.VisualResponseProperty.VISIBILITY 79 | ) { 80 | valueNode.visible = value; 81 | } else if ( 82 | valueNodeProperty === 83 | MotionControllerConstants.VisualResponseProperty.TRANSFORM 84 | ) { 85 | Quaternion.slerp( 86 | minNode.quaternion, 87 | maxNode.quaternion, 88 | valueNode.quaternion, 89 | value 90 | ); 91 | 92 | valueNode.position.lerpVectors( 93 | minNode.position, 94 | maxNode.position, 95 | value 96 | ); 97 | } 98 | }); 99 | }); 100 | }, 101 | }); 102 | 103 | /** 104 | * Walks the model's tree to find the nodes needed to animate the components and 105 | * saves them to the motionContoller components for use in the frame loop. When 106 | * touchpads are found, attaches a touch dot to them. 107 | */ 108 | function findNodes(motionController, scene) { 109 | // Loop through the components and find the nodes needed for each components' visual responses 110 | Object.values(motionController.components).forEach((component) => { 111 | const { type, touchPointNodeName, visualResponses } = component; 112 | 113 | if (type === MotionControllerConstants.ComponentType.TOUCHPAD) { 114 | component.touchPointNode = scene.getObjectByName(touchPointNodeName); 115 | if (component.touchPointNode) { 116 | // Attach a touch dot to the touchpad. 117 | const sphereGeometry = new SphereBufferGeometry(0.001); 118 | const material = new MeshBasicMaterial({ color: 0x0000ff }); 119 | const sphere = new Mesh(sphereGeometry, material); 120 | component.touchPointNode.add(sphere); 121 | } else { 122 | console.warn( 123 | `Could not find touch dot, ${component.touchPointNodeName}, in touchpad component ${component.id}` 124 | ); 125 | } 126 | } 127 | 128 | // Loop through all the visual responses to be applied to this component 129 | Object.values(visualResponses).forEach((visualResponse) => { 130 | const { 131 | valueNodeName, 132 | minNodeName, 133 | maxNodeName, 134 | valueNodeProperty, 135 | } = visualResponse; 136 | 137 | // If animating a transform, find the two nodes to be interpolated between. 138 | if ( 139 | valueNodeProperty === 140 | MotionControllerConstants.VisualResponseProperty.TRANSFORM 141 | ) { 142 | visualResponse.minNode = scene.getObjectByName(minNodeName); 143 | visualResponse.maxNode = scene.getObjectByName(maxNodeName); 144 | 145 | // If the extents cannot be found, skip this animation 146 | if (!visualResponse.minNode) { 147 | console.warn(`Could not find ${minNodeName} in the model`); 148 | return; 149 | } 150 | 151 | if (!visualResponse.maxNode) { 152 | console.warn(`Could not find ${maxNodeName} in the model`); 153 | return; 154 | } 155 | } 156 | 157 | // If the target node cannot be found, skip this animation 158 | visualResponse.valueNode = scene.getObjectByName(valueNodeName); 159 | if (!visualResponse.valueNode) { 160 | console.warn(`Could not find ${valueNodeName} in the model`); 161 | } 162 | }); 163 | }); 164 | } 165 | 166 | function addAssetSceneToControllerModel(controllerModel, scene) { 167 | // Find the nodes needed for animation and cache them on the motionController. 168 | findNodes(controllerModel.motionController, scene); 169 | 170 | // Apply any environment map that the mesh already has set. 171 | if (controllerModel.envMap) { 172 | scene.traverse((child) => { 173 | if (child.isMesh) { 174 | child.material.envMap = controllerModel.envMap; 175 | child.material.needsUpdate = true; 176 | } 177 | }); 178 | } 179 | 180 | // Add the glTF scene to the controllerModel. 181 | controllerModel.add(scene); 182 | } 183 | 184 | var XRControllerModelFactory = (function () { 185 | function XRControllerModelFactory(gltfLoader = null) { 186 | this.gltfLoader = gltfLoader; 187 | this.path = DEFAULT_PROFILES_PATH; 188 | this._assetCache = {}; 189 | 190 | // If a GLTFLoader wasn't supplied to the constructor create a new one. 191 | if (!this.gltfLoader) { 192 | this.gltfLoader = new GLTFLoader(); 193 | } 194 | } 195 | 196 | XRControllerModelFactory.prototype = { 197 | constructor: XRControllerModelFactory, 198 | 199 | createControllerModel: function (controller) { 200 | const controllerModel = new XRControllerModel(); 201 | let scene = null; 202 | 203 | controller.addEventListener("connected", (event) => { 204 | const xrInputSource = event.data; 205 | 206 | if ( 207 | xrInputSource.targetRayMode !== "tracked-pointer" || 208 | !xrInputSource.gamepad 209 | ) 210 | return; 211 | 212 | fetchProfile(xrInputSource, this.path, DEFAULT_PROFILE) 213 | .then(({ profile, assetPath }) => { 214 | controllerModel.motionController = new MotionController( 215 | xrInputSource, 216 | profile, 217 | assetPath 218 | ); 219 | 220 | let cachedAsset = this._assetCache[ 221 | controllerModel.motionController.assetUrl 222 | ]; 223 | if (cachedAsset) { 224 | scene = cachedAsset.scene.clone(); 225 | 226 | addAssetSceneToControllerModel(controllerModel, scene); 227 | } else { 228 | if (!this.gltfLoader) { 229 | throw new Error(`GLTFLoader not set.`); 230 | } 231 | 232 | this.gltfLoader.setPath(""); 233 | this.gltfLoader.load( 234 | controllerModel.motionController.assetUrl, 235 | (asset) => { 236 | this._assetCache[ 237 | controllerModel.motionController.assetUrl 238 | ] = asset; 239 | 240 | scene = asset.scene.clone(); 241 | 242 | addAssetSceneToControllerModel(controllerModel, scene); 243 | }, 244 | null, 245 | () => { 246 | throw new Error( 247 | `Asset ${controllerModel.motionController.assetUrl} missing or malformed.` 248 | ); 249 | } 250 | ); 251 | } 252 | }) 253 | .catch((err) => { 254 | console.warn(err); 255 | }); 256 | }); 257 | 258 | controller.addEventListener("disconnected", () => { 259 | controllerModel.motionController = null; 260 | controllerModel.remove(scene); 261 | scene = null; 262 | }); 263 | 264 | return controllerModel; 265 | }, 266 | }; 267 | 268 | return XRControllerModelFactory; 269 | })(); 270 | 271 | export { XRControllerModelFactory }; 272 | -------------------------------------------------------------------------------- /third_party/XRHandModelFactory.js: -------------------------------------------------------------------------------- 1 | import { Object3D } from "./three.module.js"; 2 | 3 | import { XRHandPrimitiveModel } from "./XRHandPrimitiveModel.js"; 4 | 5 | import { XRHandOculusMeshModel } from "./XRHandOculusMeshModel.js"; 6 | 7 | function XRHandModel(controller) { 8 | Object3D.call(this); 9 | 10 | this.controller = controller; 11 | this.motionController = null; 12 | this.envMap = null; 13 | 14 | this.mesh = null; 15 | } 16 | 17 | XRHandModel.prototype = Object.assign(Object.create(Object3D.prototype), { 18 | constructor: XRHandModel, 19 | 20 | updateMatrixWorld: function (force) { 21 | Object3D.prototype.updateMatrixWorld.call(this, force); 22 | 23 | if (this.motionController) { 24 | this.motionController.updateMesh(); 25 | } 26 | }, 27 | }); 28 | 29 | const XRHandModelFactory = (function () { 30 | function XRHandModelFactory() { 31 | this.path = ""; 32 | } 33 | 34 | XRHandModelFactory.prototype = { 35 | constructor: XRHandModelFactory, 36 | 37 | setPath: function (path) { 38 | this.path = path; 39 | return this; 40 | }, 41 | 42 | createHandModel: function (controller, profile, options) { 43 | const handModel = new XRHandModel(controller); 44 | 45 | controller.addEventListener("connected", (event) => { 46 | const xrInputSource = event.data; 47 | 48 | if (xrInputSource.hand && !handModel.motionController) { 49 | handModel.visible = true; 50 | handModel.xrInputSource = xrInputSource; 51 | 52 | // @todo Detect profile if not provided 53 | if (profile === undefined || profile === "spheres") { 54 | handModel.motionController = new XRHandPrimitiveModel( 55 | handModel, 56 | controller, 57 | this.path, 58 | xrInputSource.handedness, 59 | { primitive: "sphere" } 60 | ); 61 | } else if (profile === "boxes") { 62 | handModel.motionController = new XRHandPrimitiveModel( 63 | handModel, 64 | controller, 65 | this.path, 66 | xrInputSource.handedness, 67 | { primitive: "box" } 68 | ); 69 | } else if (profile === "oculus") { 70 | handModel.motionController = new XRHandOculusMeshModel( 71 | handModel, 72 | controller, 73 | this.path, 74 | xrInputSource.handedness, 75 | options 76 | ); 77 | } 78 | } 79 | }); 80 | 81 | controller.addEventListener("disconnected", () => { 82 | // handModel.motionController = null; 83 | // handModel.remove( scene ); 84 | // scene = null; 85 | }); 86 | 87 | return handModel; 88 | }, 89 | }; 90 | 91 | return XRHandModelFactory; 92 | })(); 93 | 94 | export { XRHandModelFactory }; 95 | -------------------------------------------------------------------------------- /third_party/XRHandOculusMeshModel.js: -------------------------------------------------------------------------------- 1 | import { FBXLoader } from "./FBXLoader.js"; 2 | 3 | class XRHandOculusMeshModel { 4 | constructor(handModel, controller, path, handedness, options) { 5 | this.controller = controller; 6 | this.handModel = handModel; 7 | 8 | this.bones = []; 9 | const loader = new FBXLoader(); 10 | const low = options && options.model === "lowpoly" ? "_low" : ""; 11 | 12 | loader.setPath(path); 13 | loader.load( 14 | `OculusHand_${handedness === "right" ? "R" : "L"}${low}.fbx`, 15 | (object) => { 16 | this.handModel.add(object); 17 | // Hack because of the scale of the skinnedmesh 18 | object.scale.setScalar(0.01); 19 | 20 | const mesh = object.getObjectByProperty("type", "SkinnedMesh"); 21 | mesh.frustumCulled = false; 22 | mesh.castShadow = true; 23 | mesh.receiveShadow = true; 24 | 25 | const bonesMapping = [ 26 | "b_%_wrist", // XRHand.WRIST, 27 | 28 | "b_%_thumb1", // XRHand.THUMB_METACARPAL, 29 | "b_%_thumb2", // XRHand.THUMB_PHALANX_PROXIMAL, 30 | "b_%_thumb3", // XRHand.THUMB_PHALANX_DISTAL, 31 | "b_%_thumb_null", // XRHand.THUMB_PHALANX_TIP, 32 | 33 | null, //'b_%_index1', // XRHand.INDEX_METACARPAL, 34 | "b_%_index1", // XRHand.INDEX_PHALANX_PROXIMAL, 35 | "b_%_index2", // XRHand.INDEX_PHALANX_INTERMEDIATE, 36 | "b_%_index3", // XRHand.INDEX_PHALANX_DISTAL, 37 | "b_%_index_null", // XRHand.INDEX_PHALANX_TIP, 38 | 39 | null, //'b_%_middle1', // XRHand.MIDDLE_METACARPAL, 40 | "b_%_middle1", // XRHand.MIDDLE_PHALANX_PROXIMAL, 41 | "b_%_middle2", // XRHand.MIDDLE_PHALANX_INTERMEDIATE, 42 | "b_%_middle3", // XRHand.MIDDLE_PHALANX_DISTAL, 43 | "b_%_middlenull", // XRHand.MIDDLE_PHALANX_TIP, 44 | 45 | null, //'b_%_ring1', // XRHand.RING_METACARPAL, 46 | "b_%_ring1", // XRHand.RING_PHALANX_PROXIMAL, 47 | "b_%_ring2", // XRHand.RING_PHALANX_INTERMEDIATE, 48 | "b_%_ring3", // XRHand.RING_PHALANX_DISTAL, 49 | "b_%_ring_inull", // XRHand.RING_PHALANX_TIP, 50 | 51 | "b_%_pinky0", // XRHand.LITTLE_METACARPAL, 52 | "b_%_pinky1", // XRHand.LITTLE_PHALANX_PROXIMAL, 53 | "b_%_pinky2", // XRHand.LITTLE_PHALANX_INTERMEDIATE, 54 | "b_%_pinky3", // XRHand.LITTLE_PHALANX_DISTAL, 55 | "b_%_pinkynull", // XRHand.LITTLE_PHALANX_TIP 56 | ]; 57 | bonesMapping.forEach((boneName) => { 58 | if (boneName) { 59 | const bone = object.getObjectByName( 60 | boneName.replace(/%/g, handedness === "right" ? "r" : "l") 61 | ); 62 | this.bones.push(bone); 63 | } else { 64 | this.bones.push(null); 65 | } 66 | }); 67 | } 68 | ); 69 | } 70 | 71 | updateMesh() { 72 | // XR Joints 73 | const XRJoints = this.controller.joints; 74 | for (let i = 0; i < this.bones.length; i++) { 75 | const bone = this.bones[i]; 76 | const XRJoint = XRJoints[i]; 77 | 78 | if (XRJoint) { 79 | if (XRJoint.visible) { 80 | let position = XRJoint.position; 81 | 82 | if (bone) { 83 | bone.position.copy(position.clone().multiplyScalar(100)); 84 | bone.quaternion.copy(XRJoint.quaternion); 85 | // bone.scale.setScalar( XRJoint.jointRadius || defaultRadius ); 86 | } 87 | } 88 | } 89 | } 90 | } 91 | } 92 | 93 | export { XRHandOculusMeshModel }; 94 | -------------------------------------------------------------------------------- /third_party/XRHandPrimitiveModel.js: -------------------------------------------------------------------------------- 1 | import { 2 | SphereBufferGeometry, 3 | BoxBufferGeometry, 4 | MeshStandardMaterial, 5 | Mesh, 6 | Group, 7 | } from "./three.module.js"; 8 | 9 | class XRHandPrimitiveModel { 10 | constructor(handModel, controller, path, handedness, options) { 11 | this.controller = controller; 12 | this.handModel = handModel; 13 | 14 | this.envMap = null; 15 | 16 | this.handMesh = new Group(); 17 | this.handModel.add(this.handMesh); 18 | 19 | if (window.XRHand) { 20 | let geometry; 21 | 22 | if (!options || !options.primitive || options.primitive === "sphere") { 23 | geometry = new SphereBufferGeometry(1, 10, 10); 24 | } else if (options.primitive === "box") { 25 | geometry = new BoxBufferGeometry(1, 1, 1); 26 | } 27 | 28 | const jointMaterial = new MeshStandardMaterial({ 29 | color: 0xffffff, 30 | roughness: 1, 31 | metalness: 0, 32 | }); 33 | const tipMaterial = new MeshStandardMaterial({ 34 | color: 0x999999, 35 | roughness: 1, 36 | metalness: 0, 37 | }); 38 | 39 | const tipIndexes = [ 40 | window.XRHand.THUMB_PHALANX_TIP, 41 | window.XRHand.INDEX_PHALANX_TIP, 42 | window.XRHand.MIDDLE_PHALANX_TIP, 43 | window.XRHand.RING_PHALANX_TIP, 44 | window.XRHand.LITTLE_PHALANX_TIP, 45 | ]; 46 | for (let i = 0; i <= window.XRHand.LITTLE_PHALANX_TIP; i++) { 47 | var cube = new Mesh( 48 | geometry, 49 | tipIndexes.indexOf(i) !== -1 ? tipMaterial : jointMaterial 50 | ); 51 | cube.castShadow = true; 52 | cube.receiveShadow = true; 53 | this.handMesh.add(cube); 54 | } 55 | } 56 | } 57 | 58 | updateMesh() { 59 | const defaultRadius = 0.008; 60 | const objects = this.handMesh.children; 61 | 62 | // XR Joints 63 | const XRJoints = this.controller.joints; 64 | 65 | for (let i = 0; i < objects.length; i++) { 66 | const jointMesh = objects[i]; 67 | const XRJoint = XRJoints[i]; 68 | 69 | if (XRJoint.visible) { 70 | jointMesh.position.copy(XRJoint.position); 71 | jointMesh.quaternion.copy(XRJoint.quaternion); 72 | jointMesh.scale.setScalar(XRJoint.jointRadius || defaultRadius); 73 | } 74 | 75 | jointMesh.visible = XRJoint.visible; 76 | } 77 | } 78 | } 79 | 80 | export { XRHandPrimitiveModel }; 81 | -------------------------------------------------------------------------------- /third_party/gum-av.js: -------------------------------------------------------------------------------- 1 | // icon from https://www.iconfinder.com/icons/1348651/arrow_forward_next_right_icon 2 | 3 | const template = document.createElement("template"); 4 | template.innerHTML = ` 5 | 55 |
56 |

57 |
58 | 59 |
60 |
61 | `; 62 | 63 | class GumAudioVideo extends HTMLElement { 64 | constructor() { 65 | super(); 66 | 67 | this.invalidateVideoSource(); 68 | 69 | this.attachShadow({ mode: "open" }); 70 | this.shadowRoot.appendChild(template.content.cloneNode(true)); 71 | this.deviceNameLabel = this.shadowRoot.querySelector("#deviceName"); 72 | this.nextDeviceButton = this.shadowRoot.querySelector("#nextDevice"); 73 | this.currentVideoInput = 0; 74 | this.devices = { 75 | audioinput: [], 76 | audiooutput: [], 77 | videoinput: [], 78 | }; 79 | this.nextDeviceButton.addEventListener("click", (e) => { 80 | this.currentVideoInput = 81 | (this.currentVideoInput + 1) % this.devices.videoinput.length; 82 | this.invalidateVideoSource(); 83 | this.getMedia(this.devices.videoinput[this.currentVideoInput]); 84 | }); 85 | 86 | this.init(); 87 | } 88 | 89 | async init() { 90 | await this.enumerateDevices(); 91 | if (this.devices.videoinput.length === 1) { 92 | this.nextDeviceButton.style.display = "none"; 93 | this.currentVideoInput = 0; 94 | this.invalidateVideoSource(); 95 | this.getMedia(this.devices.videoinput[this.currentVideoInput]); 96 | } else { 97 | this.nextDeviceButton.style.display = "block"; 98 | this.currentVideoInput = 0; 99 | this.invalidateVideoSource(); 100 | this.getMedia(this.devices.videoinput[this.currentVideoInput]); 101 | } 102 | } 103 | 104 | async ready() { 105 | await this.videoLoadedData; 106 | } 107 | 108 | async enumerateDevices() { 109 | if (!navigator.mediaDevices) { 110 | this.deviceNameLabel.textContent = `Can't enumerate devices. Make sure the page is HTTPS, and the browser support getUserMedia.`; 111 | return; 112 | } 113 | const devices = await navigator.mediaDevices.enumerateDevices(); 114 | 115 | for (const device of devices) { 116 | let name; 117 | switch (device.kind) { 118 | case "audioinput": 119 | name = device.label || "Microphone"; 120 | break; 121 | case "audiooutput": 122 | name = device.label || "Speakers"; 123 | break; 124 | case "videoinput": 125 | name = device.label || "Camera"; 126 | break; 127 | } 128 | this.devices[device.kind].push(device); 129 | } 130 | } 131 | 132 | invalidateVideoSource() { 133 | this.videoLoadedData = new Promise((resolve, reject) => { 134 | this.resolveLoadedData = resolve; 135 | this.rejectLoadedData = reject; 136 | }); 137 | } 138 | 139 | async getMedia(device) { 140 | const constraints = { 141 | video: { deviceId: device.deviceId, width: 500, height: 500 }, 142 | }; 143 | this.deviceNameLabel.textContent = "Connecting..."; 144 | let stream = null; 145 | 146 | try { 147 | stream = await navigator.mediaDevices.getUserMedia(constraints); 148 | this.deviceNameLabel.textContent = device.label; 149 | this.createVideoElement(); 150 | this.video.srcObject = stream; 151 | } catch (err) { 152 | this.deviceNameLabel.textContent = `${err.name} ${err.message}`; 153 | } 154 | } 155 | 156 | createVideoElement() { 157 | if (this.video && this.video.srcObject) { 158 | this.video.srcObject.getTracks().forEach((track) => { 159 | track.stop(); 160 | }); 161 | } 162 | if (!this.video) { 163 | this.video = document.createElement("video"); 164 | this.video.autoplay = true; 165 | this.video.playsinline = true; 166 | this.video.addEventListener("loadeddata", () => { 167 | this.resolveLoadedData(); 168 | }); 169 | this.shadowRoot.append(this.video); 170 | } 171 | } 172 | } 173 | 174 | customElements.define("gum-av", GumAudioVideo); 175 | -------------------------------------------------------------------------------- /third_party/inflate.module.min.js: -------------------------------------------------------------------------------- 1 | /** @license zlib.js 2012 - imaya [ https://github.com/imaya/zlib.js ] The MIT License */var mod={}, l=void 0,aa=mod;function r(c,d){var a=c.split("."),b=aa;!(a[0]in b)&&b.execScript&&b.execScript("var "+a[0]);for(var e;a.length&&(e=a.shift());)!a.length&&d!==l?b[e]=d:b=b[e]?b[e]:b[e]={}};var t="undefined"!==typeof Uint8Array&&"undefined"!==typeof Uint16Array&&"undefined"!==typeof Uint32Array&&"undefined"!==typeof DataView;function v(c){var d=c.length,a=0,b=Number.POSITIVE_INFINITY,e,f,g,h,k,m,n,p,s,x;for(p=0;pa&&(a=c[p]),c[p]>=1;x=g<<16|p;for(s=m;s>>=1;switch(c){case 0:var d=this.input,a=this.a,b=this.c,e=this.b,f=d.length,g=l,h=l,k=b.length,m=l;this.d=this.f=0;if(a+1>=f)throw Error("invalid uncompressed block header: LEN");g=d[a++]|d[a++]<<8;if(a+1>=f)throw Error("invalid uncompressed block header: NLEN");h=d[a++]|d[a++]<<8;if(g===~h)throw Error("invalid uncompressed block header: length verify");if(a+g>d.length)throw Error("input buffer is broken");switch(this.i){case A:for(;e+ 4 | g>b.length;){m=k-e;g-=m;if(t)b.set(d.subarray(a,a+m),e),e+=m,a+=m;else for(;m--;)b[e++]=d[a++];this.b=e;b=this.e();e=this.b}break;case y:for(;e+g>b.length;)b=this.e({p:2});break;default:throw Error("invalid inflate mode");}if(t)b.set(d.subarray(a,a+g),e),e+=g,a+=g;else for(;g--;)b[e++]=d[a++];this.a=a;this.b=e;this.c=b;break;case 1:this.j(ba,ca);break;case 2:for(var n=C(this,5)+257,p=C(this,5)+1,s=C(this,4)+4,x=new (t?Uint8Array:Array)(D.length),S=l,T=l,U=l,u=l,M=l,F=l,z=l,q=l,V=l,q=0;q=P?8:255>=P?9:279>=P?7:8;var ba=v(O),Q=new (t?Uint8Array:Array)(30),R,ga;R=0;for(ga=Q.length;R=g)throw Error("input buffer is broken");a|=e[f++]<>>d;c.d=b-d;c.a=f;return h} 8 | function E(c,d){for(var a=c.f,b=c.d,e=c.input,f=c.a,g=e.length,h=d[0],k=d[1],m,n;b=g);)a|=e[f++]<>>16;if(n>b)throw Error("invalid code length: "+n);c.f=a>>n;c.d=b-n;c.a=f;return m&65535} 9 | w.prototype.j=function(c,d){var a=this.c,b=this.b;this.o=c;for(var e=a.length-258,f,g,h,k;256!==(f=E(this,c));)if(256>f)b>=e&&(this.b=b,a=this.e(),b=this.b),a[b++]=f;else{g=f-257;k=I[g];0=e&&(this.b=b,a=this.e(),b=this.b);for(;k--;)a[b]=a[b++-h]}for(;8<=this.d;)this.d-=8,this.a--;this.b=b}; 10 | w.prototype.w=function(c,d){var a=this.c,b=this.b;this.o=c;for(var e=a.length,f,g,h,k;256!==(f=E(this,c));)if(256>f)b>=e&&(a=this.e(),e=a.length),a[b++]=f;else{g=f-257;k=I[g];0e&&(a=this.e(),e=a.length);for(;k--;)a[b]=a[b++-h]}for(;8<=this.d;)this.d-=8,this.a--;this.b=b}; 11 | w.prototype.e=function(){var c=new (t?Uint8Array:Array)(this.b-32768),d=this.b-32768,a,b,e=this.c;if(t)c.set(e.subarray(32768,c.length));else{a=0;for(b=c.length;aa;++a)e[a]=e[d+a];this.b=32768;return e}; 12 | w.prototype.z=function(c){var d,a=this.input.length/this.a+1|0,b,e,f,g=this.input,h=this.c;c&&("number"===typeof c.p&&(a=c.p),"number"===typeof c.u&&(a+=c.u));2>a?(b=(g.length-this.a)/this.o[2],f=258*(b/2)|0,e=fd&&(this.c.length=d),c=this.c);return this.buffer=c};function W(c,d){var a,b;this.input=c;this.a=0;if(d||!(d={}))d.index&&(this.a=d.index),d.verify&&(this.A=d.verify);a=c[this.a++];b=c[this.a++];switch(a&15){case ha:this.method=ha;break;default:throw Error("unsupported compression method");}if(0!==((a<<8)+b)%31)throw Error("invalid fcheck flag:"+((a<<8)+b)%31);if(b&32)throw Error("fdict flag is not supported");this.q=new w(c,{index:this.a,bufferSize:d.bufferSize,bufferType:d.bufferType,resize:d.resize})} 15 | W.prototype.k=function(){var c=this.input,d,a;d=this.q.k();this.a=this.q.a;if(this.A){a=(c[this.a++]<<24|c[this.a++]<<16|c[this.a++]<<8|c[this.a++])>>>0;var b=d;if("string"===typeof b){var e=b.split(""),f,g;f=0;for(g=e.length;f>>0;b=e}for(var h=1,k=0,m=b.length,n,p=0;0>>0)throw Error("invalid adler-32 checksum");}return d};var ha=8;r("Zlib.Inflate",W);r("Zlib.Inflate.prototype.decompress",W.prototype.k);var X={ADAPTIVE:B.s,BLOCK:B.t},Y,Z,$,ia;if(Object.keys)Y=Object.keys(X);else for(Z in Y=[],$=0,X)Y[$++]=Z;$=0;for(ia=Y.length;$ { 81 | const supportedProfile = supportedProfilesList[profileId]; 82 | if (supportedProfile) { 83 | match = { 84 | profileId, 85 | profilePath: `${basePath}/${supportedProfile.path}`, 86 | deprecated: !!supportedProfile.deprecated 87 | }; 88 | } 89 | return !!match; 90 | }); 91 | 92 | if (!match) { 93 | if (!defaultProfile) { 94 | throw new Error('No matching profile name found'); 95 | } 96 | 97 | const supportedProfile = supportedProfilesList[defaultProfile]; 98 | if (!supportedProfile) { 99 | throw new Error(`No matching profile name found and default profile "${defaultProfile}" missing.`); 100 | } 101 | 102 | match = { 103 | profileId: defaultProfile, 104 | profilePath: `${basePath}/${supportedProfile.path}`, 105 | deprecated: !!supportedProfile.deprecated 106 | }; 107 | } 108 | 109 | const profile = await fetchJsonFile(match.profilePath); 110 | 111 | let assetPath; 112 | if (getAssetPath) { 113 | let layout; 114 | if (xrInputSource.handedness === 'any') { 115 | layout = profile.layouts[Object.keys(profile.layouts)[0]]; 116 | } else { 117 | layout = profile.layouts[xrInputSource.handedness]; 118 | } 119 | if (!layout) { 120 | throw new Error( 121 | `No matching handedness, ${xrInputSource.handedness}, in profile ${match.profileId}` 122 | ); 123 | } 124 | 125 | if (layout.assetPath) { 126 | assetPath = match.profilePath.replace('profile.json', layout.assetPath); 127 | } 128 | } 129 | 130 | return { profile, assetPath }; 131 | } 132 | 133 | /** @constant {Object} */ 134 | const defaultComponentValues = { 135 | xAxis: 0, 136 | yAxis: 0, 137 | button: 0, 138 | state: Constants.ComponentState.DEFAULT 139 | }; 140 | 141 | /** 142 | * @description Converts an X, Y coordinate from the range -1 to 1 (as reported by the Gamepad 143 | * API) to the range 0 to 1 (for interpolation). Also caps the X, Y values to be bounded within 144 | * a circle. This ensures that thumbsticks are not animated outside the bounds of their physical 145 | * range of motion and touchpads do not report touch locations off their physical bounds. 146 | * @param {number} x The original x coordinate in the range -1 to 1 147 | * @param {number} y The original y coordinate in the range -1 to 1 148 | */ 149 | function normalizeAxes(x = 0, y = 0) { 150 | let xAxis = x; 151 | let yAxis = y; 152 | 153 | // Determine if the point is outside the bounds of the circle 154 | // and, if so, place it on the edge of the circle 155 | const hypotenuse = Math.sqrt((x * x) + (y * y)); 156 | if (hypotenuse > 1) { 157 | const theta = Math.atan2(y, x); 158 | xAxis = Math.cos(theta); 159 | yAxis = Math.sin(theta); 160 | } 161 | 162 | // Scale and move the circle so values are in the interpolation range. The circle's origin moves 163 | // from (0, 0) to (0.5, 0.5). The circle's radius scales from 1 to be 0.5. 164 | const result = { 165 | normalizedXAxis: (xAxis * 0.5) + 0.5, 166 | normalizedYAxis: (yAxis * 0.5) + 0.5 167 | }; 168 | return result; 169 | } 170 | 171 | /** 172 | * Contains the description of how the 3D model should visually respond to a specific user input. 173 | * This is accomplished by initializing the object with the name of a node in the 3D model and 174 | * property that need to be modified in response to user input, the name of the nodes representing 175 | * the allowable range of motion, and the name of the input which triggers the change. In response 176 | * to the named input changing, this object computes the appropriate weighting to use for 177 | * interpolating between the range of motion nodes. 178 | */ 179 | class VisualResponse { 180 | constructor(visualResponseDescription) { 181 | this.componentProperty = visualResponseDescription.componentProperty; 182 | this.states = visualResponseDescription.states; 183 | this.valueNodeName = visualResponseDescription.valueNodeName; 184 | this.valueNodeProperty = visualResponseDescription.valueNodeProperty; 185 | 186 | if (this.valueNodeProperty === Constants.VisualResponseProperty.TRANSFORM) { 187 | this.minNodeName = visualResponseDescription.minNodeName; 188 | this.maxNodeName = visualResponseDescription.maxNodeName; 189 | } 190 | 191 | // Initializes the response's current value based on default data 192 | this.value = 0; 193 | this.updateFromComponent(defaultComponentValues); 194 | } 195 | 196 | /** 197 | * Computes the visual response's interpolation weight based on component state 198 | * @param {Object} componentValues - The component from which to update 199 | * @param {number} xAxis - The reported X axis value of the component 200 | * @param {number} yAxis - The reported Y axis value of the component 201 | * @param {number} button - The reported value of the component's button 202 | * @param {string} state - The component's active state 203 | */ 204 | updateFromComponent({ 205 | xAxis, yAxis, button, state 206 | }) { 207 | const { normalizedXAxis, normalizedYAxis } = normalizeAxes(xAxis, yAxis); 208 | switch (this.componentProperty) { 209 | case Constants.ComponentProperty.X_AXIS: 210 | this.value = (this.states.includes(state)) ? normalizedXAxis : 0.5; 211 | break; 212 | case Constants.ComponentProperty.Y_AXIS: 213 | this.value = (this.states.includes(state)) ? normalizedYAxis : 0.5; 214 | break; 215 | case Constants.ComponentProperty.BUTTON: 216 | this.value = (this.states.includes(state)) ? button : 0; 217 | break; 218 | case Constants.ComponentProperty.STATE: 219 | if (this.valueNodeProperty === Constants.VisualResponseProperty.VISIBILITY) { 220 | this.value = (this.states.includes(state)); 221 | } else { 222 | this.value = this.states.includes(state) ? 1.0 : 0.0; 223 | } 224 | break; 225 | default: 226 | throw new Error(`Unexpected visualResponse componentProperty ${this.componentProperty}`); 227 | } 228 | } 229 | } 230 | 231 | class Component { 232 | /** 233 | * @param {Object} componentId - Id of the component 234 | * @param {Object} componentDescription - Description of the component to be created 235 | */ 236 | constructor(componentId, componentDescription) { 237 | if (!componentId 238 | || !componentDescription 239 | || !componentDescription.visualResponses 240 | || !componentDescription.gamepadIndices 241 | || Object.keys(componentDescription.gamepadIndices).length === 0) { 242 | throw new Error('Invalid arguments supplied'); 243 | } 244 | 245 | this.id = componentId; 246 | this.type = componentDescription.type; 247 | this.rootNodeName = componentDescription.rootNodeName; 248 | this.touchPointNodeName = componentDescription.touchPointNodeName; 249 | 250 | // Build all the visual responses for this component 251 | this.visualResponses = {}; 252 | Object.keys(componentDescription.visualResponses).forEach((responseName) => { 253 | const visualResponse = new VisualResponse(componentDescription.visualResponses[responseName]); 254 | this.visualResponses[responseName] = visualResponse; 255 | }); 256 | 257 | // Set default values 258 | this.gamepadIndices = Object.assign({}, componentDescription.gamepadIndices); 259 | 260 | this.values = { 261 | state: Constants.ComponentState.DEFAULT, 262 | button: (this.gamepadIndices.button !== undefined) ? 0 : undefined, 263 | xAxis: (this.gamepadIndices.xAxis !== undefined) ? 0 : undefined, 264 | yAxis: (this.gamepadIndices.yAxis !== undefined) ? 0 : undefined 265 | }; 266 | } 267 | 268 | get data() { 269 | const data = { id: this.id, ...this.values }; 270 | return data; 271 | } 272 | 273 | /** 274 | * @description Poll for updated data based on current gamepad state 275 | * @param {Object} gamepad - The gamepad object from which the component data should be polled 276 | */ 277 | updateFromGamepad(gamepad) { 278 | // Set the state to default before processing other data sources 279 | this.values.state = Constants.ComponentState.DEFAULT; 280 | 281 | // Get and normalize button 282 | if (this.gamepadIndices.button !== undefined 283 | && gamepad.buttons.length > this.gamepadIndices.button) { 284 | const gamepadButton = gamepad.buttons[this.gamepadIndices.button]; 285 | this.values.button = gamepadButton.value; 286 | this.values.button = (this.values.button < 0) ? 0 : this.values.button; 287 | this.values.button = (this.values.button > 1) ? 1 : this.values.button; 288 | 289 | // Set the state based on the button 290 | if (gamepadButton.pressed || this.values.button === 1) { 291 | this.values.state = Constants.ComponentState.PRESSED; 292 | } else if (gamepadButton.touched || this.values.button > Constants.ButtonTouchThreshold) { 293 | this.values.state = Constants.ComponentState.TOUCHED; 294 | } 295 | } 296 | 297 | // Get and normalize x axis value 298 | if (this.gamepadIndices.xAxis !== undefined 299 | && gamepad.axes.length > this.gamepadIndices.xAxis) { 300 | this.values.xAxis = gamepad.axes[this.gamepadIndices.xAxis]; 301 | this.values.xAxis = (this.values.xAxis < -1) ? -1 : this.values.xAxis; 302 | this.values.xAxis = (this.values.xAxis > 1) ? 1 : this.values.xAxis; 303 | 304 | // If the state is still default, check if the xAxis makes it touched 305 | if (this.values.state === Constants.ComponentState.DEFAULT 306 | && Math.abs(this.values.xAxis) > Constants.AxisTouchThreshold) { 307 | this.values.state = Constants.ComponentState.TOUCHED; 308 | } 309 | } 310 | 311 | // Get and normalize Y axis value 312 | if (this.gamepadIndices.yAxis !== undefined 313 | && gamepad.axes.length > this.gamepadIndices.yAxis) { 314 | this.values.yAxis = gamepad.axes[this.gamepadIndices.yAxis]; 315 | this.values.yAxis = (this.values.yAxis < -1) ? -1 : this.values.yAxis; 316 | this.values.yAxis = (this.values.yAxis > 1) ? 1 : this.values.yAxis; 317 | 318 | // If the state is still default, check if the yAxis makes it touched 319 | if (this.values.state === Constants.ComponentState.DEFAULT 320 | && Math.abs(this.values.yAxis) > Constants.AxisTouchThreshold) { 321 | this.values.state = Constants.ComponentState.TOUCHED; 322 | } 323 | } 324 | 325 | // Update the visual response weights based on the current component data 326 | Object.values(this.visualResponses).forEach((visualResponse) => { 327 | visualResponse.updateFromComponent(this.values); 328 | }); 329 | } 330 | } 331 | 332 | /** 333 | * @description Builds a motion controller with components and visual responses based on the 334 | * supplied profile description. Data is polled from the xrInputSource's gamepad. 335 | * @author Nell Waliczek / https://github.com/NellWaliczek 336 | */ 337 | class MotionController { 338 | /** 339 | * @param {Object} xrInputSource - The XRInputSource to build the MotionController around 340 | * @param {Object} profile - The best matched profile description for the supplied xrInputSource 341 | * @param {Object} assetUrl 342 | */ 343 | constructor(xrInputSource, profile, assetUrl) { 344 | if (!xrInputSource) { 345 | throw new Error('No xrInputSource supplied'); 346 | } 347 | 348 | if (!profile) { 349 | throw new Error('No profile supplied'); 350 | } 351 | 352 | this.xrInputSource = xrInputSource; 353 | this.assetUrl = assetUrl; 354 | this.id = profile.profileId; 355 | 356 | // Build child components as described in the profile description 357 | this.layoutDescription = profile.layouts[xrInputSource.handedness]; 358 | this.components = {}; 359 | Object.keys(this.layoutDescription.components).forEach((componentId) => { 360 | const componentDescription = this.layoutDescription.components[componentId]; 361 | this.components[componentId] = new Component(componentId, componentDescription); 362 | }); 363 | 364 | // Initialize components based on current gamepad state 365 | this.updateFromGamepad(); 366 | } 367 | 368 | get gripSpace() { 369 | return this.xrInputSource.gripSpace; 370 | } 371 | 372 | get targetRaySpace() { 373 | return this.xrInputSource.targetRaySpace; 374 | } 375 | 376 | /** 377 | * @description Returns a subset of component data for simplified debugging 378 | */ 379 | get data() { 380 | const data = []; 381 | Object.values(this.components).forEach((component) => { 382 | data.push(component.data); 383 | }); 384 | return data; 385 | } 386 | 387 | /** 388 | * @description Poll for updated data based on current gamepad state 389 | */ 390 | updateFromGamepad() { 391 | Object.values(this.components).forEach((component) => { 392 | component.updateFromGamepad(this.xrInputSource.gamepad); 393 | }); 394 | } 395 | } 396 | 397 | export { Constants, MotionController, fetchProfile, fetchProfilesList }; 398 | --------------------------------------------------------------------------------