├── .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 |
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 |
--------------------------------------------------------------------------------