openfree commited on
Commit
f9f2b31
ยท
verified ยท
1 Parent(s): b806e3e

WorldForge: prompt-driven region plan, semantic layout map and composite height field

Browse files
Files changed (8) hide show
  1. README.md +58 -4
  2. app.js +389 -0
  3. index.html +154 -18
  4. vendor/GLTFExporter.js +3595 -0
  5. vendor/OrbitControls.js +1860 -0
  6. vendor/three.core.js +0 -0
  7. vendor/three.module.js +0 -0
  8. world.js +305 -0
README.md CHANGED
@@ -1,10 +1,64 @@
1
  ---
2
  title: WorldForge
3
- emoji: ๐Ÿ“‰
4
- colorFrom: pink
5
- colorTo: green
6
  sdk: static
7
  pinned: false
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  title: WorldForge
3
+ emoji: "\U0001F5FA๏ธ"
4
+ colorFrom: green
5
+ colorTo: gray
6
  sdk: static
7
  pinned: false
8
+ license: apache-2.0
9
  ---
10
 
11
+ # WorldForge โ€” a prompt becomes an explorable world
12
+
13
+ Type a world, get terrain you can fly through and take away as a mesh. Everything
14
+ runs in your browser: no GPU, no server, no account, nothing uploaded.
15
+
16
+ ## What it actually does
17
+
18
+ The pipeline follows the terrain foundation described in
19
+ [WorldClaw](https://arxiv.org/abs/2608.05248) (arXiv 2608.05248, Tencent Hunyuan):
20
+
21
+ 1. **Region plan** โ€” the prompt is read for biome cues, ranked by where each cue
22
+ appears, so the leading noun dominates the map rather than whichever word the
23
+ dictionary happened to list first.
24
+ 2. **Semantic layout map** โ€” regions claim territory by warped Voronoi; the domain
25
+ warp is what keeps borders organic instead of polygonal. This map is shown in the
26
+ sidebar and is exportable.
27
+ 3. **Composite height field** โ€” each region contributes its own base elevation, noise
28
+ bands, and a geomorphic operator (peak, cone, crater, dune, terrace, erosion),
29
+ summed through smoothed region masks so borders blend rather than step.
30
+ 4. **Scatter** โ€” props are placed by region semantics, elevation and slope, oriented
31
+ to the surface. Only rock survives on steep faces.
32
+
33
+ Colour comes from the same masks that shaped the height, with rock revealed on
34
+ slopes and a snow line on worlds tall enough to have one, so the map and the mesh
35
+ cannot drift apart.
36
+
37
+ ## Controls
38
+
39
+ | | |
40
+ | --- | --- |
41
+ | Orbit / pan / zoom | drag, right-drag, wheel |
42
+ | **Fly** | WASD, mouse look, Q/E down/up, Shift boost, Esc to exit |
43
+ | Reroll | new seed, same prompt |
44
+ | Export GLB | the terrain, water and all props, ready for Blender or a game engine |
45
+
46
+ Prompts understand English and Korean cues โ€” `desert`, `canyon`, `volcano`,
47
+ `glacier`, `swamp`, `์‚ฌ๋ง‰`, `ํ˜‘๊ณก`, `์„ค์›`, `์ˆฒ` and so on. Unrecognised prompts fall
48
+ back to a varied temperate world.
49
+
50
+ ## Provenance
51
+
52
+ This is an **independent implementation**, written from the method described in the
53
+ WorldClaw paper. The WorldClaw repository publishes no source code and no model
54
+ weights, and carries no licence; nothing from it is used here.
55
+
56
+ WorldClaw itself depends on Claude Opus 4.8, GPT-Image-2, Hunyuan3D and Blender to
57
+ generate and place *generated* assets. WorldForge deliberately covers only the part
58
+ that needs none of them โ€” the terrain foundation and rule-based scatter โ€” so it runs
59
+ free, offline, and unencumbered. Note that the Tencent Hunyuan3D licence excludes
60
+ the European Union, the United Kingdom and South Korea from its territory, which is
61
+ one reason this implementation avoids that dependency entirely.
62
+
63
+ Rendering by [three.js](https://threejs.org) (MIT), bundled locally.
64
+ Built by [VIDraft](https://huggingface.co/VIDraft).
app.js ADDED
@@ -0,0 +1,389 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as THREE from 'three';
2
+ import { OrbitControls } from './vendor/OrbitControls.js';
3
+ import { GLTFExporter } from './vendor/GLTFExporter.js';
4
+ import { GRID, WORLD, synthesize } from './world.js';
5
+
6
+ const canvas = document.getElementById('view');
7
+ // preserveDrawingBuffer keeps the last frame readable, so the view can be captured.
8
+ const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, preserveDrawingBuffer: true });
9
+ renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
10
+ renderer.outputColorSpace = THREE.SRGBColorSpace;
11
+
12
+ const scene = new THREE.Scene();
13
+ scene.background = new THREE.Color('#9fc4dc');
14
+ scene.fog = new THREE.Fog('#9fc4dc', WORLD * 0.7, WORLD * 2.0);
15
+
16
+ const camera = new THREE.PerspectiveCamera(55, 1, 0.5, WORLD * 4);
17
+ camera.position.set(WORLD * 0.45, WORLD * 0.34, WORLD * 0.45);
18
+
19
+ const controls = new OrbitControls(camera, canvas);
20
+ controls.enableDamping = true;
21
+ controls.maxPolarAngle = Math.PI * 0.495;
22
+ controls.target.set(0, 6, 0);
23
+
24
+ scene.add(new THREE.HemisphereLight('#cfe3f2', '#4a4636', 1.35));
25
+ const sun = new THREE.DirectionalLight('#fff3dd', 1.5);
26
+ sun.position.set(WORLD * 0.6, WORLD * 0.8, WORLD * 0.3);
27
+ scene.add(sun);
28
+
29
+ const world = new THREE.Group();
30
+ scene.add(world);
31
+
32
+ // ------------------------------------------------------------------ prop kit --
33
+ // Deliberately low-poly: these are placement stand-ins for generated assets, and a
34
+ // thousand of them have to stay interactive in a browser tab.
35
+ const PROPS = {
36
+ tree: { trunk: [0.28, 0.35, 3.2], trunkColor: '#584028', crown: 'sphere', crownSize: 2.1, crownColor: '#3f6b34', scale: [0.7, 1.5] },
37
+ pine: { trunk: [0.22, 0.3, 3.6], trunkColor: '#4a3826', crown: 'cone', crownSize: 2.3, crownColor: '#2f5230', scale: [0.7, 1.6] },
38
+ palm: { trunk: [0.2, 0.26, 4.6], trunkColor: '#6b563a', crown: 'cone', crownSize: 2.0, crownColor: '#4a7c3a', scale: [0.8, 1.3] },
39
+ acacia: { trunk: [0.3, 0.4, 2.8], trunkColor: '#5c4a30', crown: 'disc', crownSize: 3.0, crownColor: '#6b7a3a', scale: [0.8, 1.4] },
40
+ cactus: { trunk: [0.42, 0.42, 2.6], trunkColor: '#4a7042', crown: 'none', crownSize: 0, crownColor: '#4a7042', scale: [0.6, 1.3] },
41
+ shrub: { trunk: null, trunkColor: '#000', crown: 'sphere', crownSize: 1.0, crownColor: '#55703a', scale: [0.6, 1.4] },
42
+ rock: { trunk: null, trunkColor: '#000', crown: 'rock', crownSize: 1.2, crownColor: '#7d7a72', scale: [0.5, 2.0] },
43
+ };
44
+
45
+ function crownGeometry(kind, size) {
46
+ switch (kind) {
47
+ case 'cone': return new THREE.ConeGeometry(size * 0.62, size * 1.9, 7);
48
+ case 'disc': return new THREE.SphereGeometry(size * 0.62, 8, 5).scale(1, 0.42, 1);
49
+ case 'rock': return new THREE.IcosahedronGeometry(size * 0.6, 0);
50
+ case 'sphere':
51
+ default: return new THREE.SphereGeometry(size * 0.55, 8, 6);
52
+ }
53
+ }
54
+
55
+ // --------------------------------------------------------------- world build --
56
+ let current = null;
57
+
58
+ function heightAt(field, gx, gy) {
59
+ const x = Math.min(GRID - 1, Math.max(0, gx));
60
+ const y = Math.min(GRID - 1, Math.max(0, gy));
61
+ return field[y * GRID + x];
62
+ }
63
+
64
+ function build(prompt, seed) {
65
+ const t0 = performance.now();
66
+ const data = synthesize(prompt, seed);
67
+ current = data;
68
+
69
+ while (world.children.length) {
70
+ const c = world.children.pop();
71
+ c.traverse?.(o => { o.geometry?.dispose(); o.material?.dispose?.(); });
72
+ }
73
+
74
+ const { regions, masks, height, seaLevel, owner } = data;
75
+ const step = WORLD / (GRID - 1);
76
+
77
+ // --- terrain mesh, vertex-coloured from the same masks that shaped the height
78
+ const geo = new THREE.PlaneGeometry(WORLD, WORLD, GRID - 1, GRID - 1);
79
+ geo.rotateX(-Math.PI / 2);
80
+ const pos = geo.attributes.position;
81
+ const colors = new Float32Array(pos.count * 3);
82
+ const col = new THREE.Color();
83
+ const regColors = regions.map(r => new THREE.Color(r.color));
84
+ const rockColor = new THREE.Color('#6f6b64');
85
+ const snowColor = new THREE.Color('#e8eef2');
86
+
87
+ let maxH = -Infinity;
88
+ for (let i = 0; i < height.length; i++) maxH = Math.max(maxH, height[i]);
89
+
90
+ for (let i = 0; i < pos.count; i++) {
91
+ const gx = i % GRID, gy = Math.floor(i / GRID);
92
+ const h = height[gy * GRID + gx];
93
+ pos.setY(i, h);
94
+
95
+ // Color has no addScaledVector (that is Vector3), so blend the components.
96
+ let cr = 0, cg = 0, cb = 0;
97
+ for (let r = 0; r < regions.length; r++) {
98
+ const m = masks[r][gy * GRID + gx];
99
+ if (m > 0.002) {
100
+ const rc = regColors[r];
101
+ cr += rc.r * m; cg += rc.g * m; cb += rc.b * m;
102
+ }
103
+ }
104
+ col.setRGB(cr, cg, cb);
105
+
106
+ // slope from neighbouring samples -> exposed rock on steep faces
107
+ const dx = heightAt(height, gx + 1, gy) - heightAt(height, gx - 1, gy);
108
+ const dy = heightAt(height, gx, gy + 1) - heightAt(height, gx, gy - 1);
109
+ const slope = Math.min(1, Math.sqrt(dx * dx + dy * dy) / (step * 3.4));
110
+ col.lerp(rockColor, slope * 0.75);
111
+
112
+ // snow line, only on worlds tall enough to have one
113
+ if (maxH > 26) {
114
+ const t = Math.min(1, Math.max(0, (h - maxH * 0.72) / (maxH * 0.28)));
115
+ col.lerp(snowColor, t * (1 - slope * 0.5) * 0.9);
116
+ }
117
+
118
+ colors[i * 3] = col.r; colors[i * 3 + 1] = col.g; colors[i * 3 + 2] = col.b;
119
+ }
120
+ geo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
121
+ geo.computeVertexNormals();
122
+
123
+ const terrain = new THREE.Mesh(
124
+ geo,
125
+ new THREE.MeshStandardMaterial({ vertexColors: true, roughness: 0.95, metalness: 0.0, flatShading: false })
126
+ );
127
+ terrain.name = 'terrain';
128
+ world.add(terrain);
129
+
130
+ // --- water
131
+ if (seaLevel > -900) {
132
+ const water = new THREE.Mesh(
133
+ new THREE.PlaneGeometry(WORLD * 1.6, WORLD * 1.6).rotateX(-Math.PI / 2),
134
+ new THREE.MeshStandardMaterial({
135
+ color: '#2d6e91', transparent: true, opacity: 0.78,
136
+ roughness: 0.16, metalness: 0.25,
137
+ })
138
+ );
139
+ water.position.y = seaLevel;
140
+ water.name = 'water';
141
+ world.add(water);
142
+ }
143
+
144
+ // --- scatter, gated on the same rules the paper uses: region semantics,
145
+ // elevation and slope, with orientation following the surface.
146
+ const buckets = {};
147
+ const rng = (() => { let s = (seed * 2654435761) >>> 0 || 7;
148
+ return () => { s ^= s << 13; s >>>= 0; s ^= s >> 17; s ^= s << 5; s >>>= 0; return s / 4294967296; }; })();
149
+
150
+ const attempts = 9000;
151
+ const capacity = 1600;
152
+ let placed = 0;
153
+ for (let a = 0; a < attempts && placed < capacity; a++) {
154
+ const gx = Math.floor(rng() * GRID), gy = Math.floor(rng() * GRID);
155
+ const reg = regions[owner[gy * GRID + gx]];
156
+ if (!reg.props.length) continue;
157
+
158
+ const h = height[gy * GRID + gx];
159
+ if (h < seaLevel + 0.35) continue;
160
+
161
+ const dx = heightAt(height, gx + 1, gy) - heightAt(height, gx - 1, gy);
162
+ const dy = heightAt(height, gx, gy + 1) - heightAt(height, gx, gy - 1);
163
+ const slope = Math.sqrt(dx * dx + dy * dy) / (step * 2);
164
+ const kind = reg.props[Math.floor(rng() * reg.props.length)];
165
+ if (kind !== 'rock' && slope > 0.85) continue; // only rock clings to cliffs
166
+ if (rng() > 0.62) continue; // thin it out
167
+
168
+ (buckets[kind] ||= []).push({
169
+ x: -WORLD / 2 + gx * step,
170
+ z: -WORLD / 2 + gy * step,
171
+ y: h,
172
+ s: PROPS[kind].scale[0] + rng() * (PROPS[kind].scale[1] - PROPS[kind].scale[0]),
173
+ rot: rng() * Math.PI * 2,
174
+ tilt: kind === 'palm' ? (rng() - 0.5) * 0.35 : 0,
175
+ });
176
+ placed++;
177
+ }
178
+
179
+ const dummy = new THREE.Object3D();
180
+ for (const [kind, list] of Object.entries(buckets)) {
181
+ const spec = PROPS[kind];
182
+ const parts = [];
183
+ if (spec.trunk) {
184
+ const [rt, rb, hh] = spec.trunk;
185
+ parts.push({
186
+ geo: new THREE.CylinderGeometry(rt, rb, hh, 6).translate(0, hh / 2, 0),
187
+ color: spec.trunkColor,
188
+ lift: 0,
189
+ });
190
+ }
191
+ if (spec.crown !== 'none') {
192
+ const lift = spec.trunk ? spec.trunk[2] * 0.92 : spec.crownSize * 0.28;
193
+ parts.push({ geo: crownGeometry(spec.crown, spec.crownSize), color: spec.crownColor, lift });
194
+ }
195
+
196
+ for (const part of parts) {
197
+ const mesh = new THREE.InstancedMesh(
198
+ part.geo,
199
+ new THREE.MeshStandardMaterial({ color: part.color, roughness: 0.9, flatShading: true }),
200
+ list.length
201
+ );
202
+ mesh.name = `${kind}-${part.color}`;
203
+ list.forEach((p, i) => {
204
+ dummy.position.set(p.x, p.y + part.lift * p.s - 0.15, p.z);
205
+ dummy.rotation.set(p.tilt, p.rot, 0);
206
+ dummy.scale.setScalar(p.s);
207
+ dummy.updateMatrix();
208
+ mesh.setMatrixAt(i, dummy.matrix);
209
+ });
210
+ mesh.instanceMatrix.needsUpdate = true;
211
+ world.add(mesh);
212
+ }
213
+ }
214
+
215
+ controls.target.set(0, Math.max(4, maxH * 0.25), 0);
216
+ drawLayout(data);
217
+ renderLegend(regions);
218
+
219
+ const ms = Math.round(performance.now() - t0);
220
+ document.getElementById('stats').textContent =
221
+ `${regions.length} regions ยท ${(GRID * GRID / 1000).toFixed(0)}k vertices ยท ${placed} props ยท ${ms} ms`;
222
+ }
223
+
224
+ // --------------------------------------------------------- semantic layout map --
225
+ function drawLayout(data) {
226
+ const cv = document.getElementById('layout');
227
+ const ctx = cv.getContext('2d');
228
+ const img = ctx.createImageData(GRID, GRID);
229
+ const cols = data.regions.map(r => {
230
+ const c = new THREE.Color(r.color);
231
+ return [c.r * 255, c.g * 255, c.b * 255];
232
+ });
233
+ for (let i = 0; i < GRID * GRID; i++) {
234
+ const [r, g, b] = cols[data.owner[i]];
235
+ img.data[i * 4] = r; img.data[i * 4 + 1] = g; img.data[i * 4 + 2] = b; img.data[i * 4 + 3] = 255;
236
+ }
237
+ cv.width = GRID; cv.height = GRID;
238
+ ctx.putImageData(img, 0, 0);
239
+ }
240
+
241
+ function renderLegend(regions) {
242
+ document.getElementById('legend').innerHTML = regions.map(r =>
243
+ `<li><i style="background:${r.color}"></i>${r.label}<b>${Math.round(r.coverage * 100)}%</b></li>`
244
+ ).join('');
245
+ }
246
+
247
+ // ------------------------------------------------------------------- fly mode --
248
+ let fly = false;
249
+ const keys = new Set();
250
+ const flyState = { yaw: 0, pitch: 0 };
251
+ addEventListener('keydown', e => {
252
+ if (e.code === 'Escape') setFly(false);
253
+ keys.add(e.code);
254
+ });
255
+ addEventListener('keyup', e => keys.delete(e.code));
256
+
257
+ function setFly(on) {
258
+ fly = on;
259
+ controls.enabled = !on;
260
+ document.getElementById('fly').classList.toggle('on', on);
261
+ document.getElementById('hint').style.display = on ? 'block' : 'none';
262
+ if (on) {
263
+ const e = new THREE.Euler().setFromQuaternion(camera.quaternion, 'YXZ');
264
+ flyState.yaw = e.y; flyState.pitch = e.x;
265
+ canvas.requestPointerLock();
266
+ } else if (document.pointerLockElement) {
267
+ document.exitPointerLock();
268
+ }
269
+ }
270
+ document.addEventListener('pointerlockchange', () => {
271
+ if (!document.pointerLockElement && fly) setFly(false);
272
+ });
273
+ addEventListener('mousemove', e => {
274
+ if (!fly || !document.pointerLockElement) return;
275
+ flyState.yaw -= e.movementX * 0.0022;
276
+ flyState.pitch = Math.max(-1.5, Math.min(1.5, flyState.pitch - e.movementY * 0.0022));
277
+ });
278
+
279
+ function stepFly(dt) {
280
+ const speed = (keys.has('ShiftLeft') ? 70 : 26) * dt;
281
+ camera.quaternion.setFromEuler(new THREE.Euler(flyState.pitch, flyState.yaw, 0, 'YXZ'));
282
+ const fwd = new THREE.Vector3(0, 0, -1).applyQuaternion(camera.quaternion);
283
+ const right = new THREE.Vector3(1, 0, 0).applyQuaternion(camera.quaternion);
284
+ if (keys.has('KeyW')) camera.position.addScaledVector(fwd, speed);
285
+ if (keys.has('KeyS')) camera.position.addScaledVector(fwd, -speed);
286
+ if (keys.has('KeyD')) camera.position.addScaledVector(right, speed);
287
+ if (keys.has('KeyA')) camera.position.addScaledVector(right, -speed);
288
+ if (keys.has('KeyE') || keys.has('Space')) camera.position.y += speed;
289
+ if (keys.has('KeyQ')) camera.position.y -= speed;
290
+
291
+ // keep the camera above the ground so flying never ends up inside a hill
292
+ if (current) {
293
+ const step = WORLD / (GRID - 1);
294
+ const gx = Math.round((camera.position.x + WORLD / 2) / step);
295
+ const gy = Math.round((camera.position.z + WORLD / 2) / step);
296
+ if (gx >= 0 && gx < GRID && gy >= 0 && gy < GRID) {
297
+ const floor = current.height[gy * GRID + gx] + 1.8;
298
+ if (camera.position.y < floor) camera.position.y = floor;
299
+ }
300
+ }
301
+ }
302
+
303
+ // ----------------------------------------------------------------------- ui ----
304
+ const promptEl = document.getElementById('prompt');
305
+ const seedEl = document.getElementById('seed');
306
+
307
+ function generate() {
308
+ const seed = parseInt(seedEl.value, 10) || 1;
309
+ document.body.classList.add('busy');
310
+ // setTimeout, not rAF: rAF never fires in a hidden or non-compositing tab, which
311
+ // would leave the world unbuilt until the page happens to become visible.
312
+ setTimeout(() => {
313
+ try {
314
+ build(promptEl.value, seed);
315
+ } catch (err) {
316
+ console.error(err);
317
+ document.getElementById('stats').textContent = `generation failed: ${err.message}`;
318
+ }
319
+ document.body.classList.remove('busy');
320
+ }, 0);
321
+ }
322
+
323
+ document.getElementById('go').onclick = generate;
324
+ promptEl.addEventListener('keydown', e => { if (e.key === 'Enter') generate(); });
325
+ document.getElementById('reseed').onclick = () => {
326
+ seedEl.value = Math.floor(Math.random() * 999999);
327
+ generate();
328
+ };
329
+ document.getElementById('fly').onclick = () => setFly(!fly);
330
+
331
+ document.querySelectorAll('#presets button').forEach(b => {
332
+ b.onclick = () => { promptEl.value = b.dataset.p; generate(); };
333
+ });
334
+
335
+ document.getElementById('glb').onclick = () => {
336
+ const btn = document.getElementById('glb');
337
+ btn.disabled = true; btn.textContent = 'Exportingโ€ฆ';
338
+ new GLTFExporter().parse(world, result => {
339
+ const blob = new Blob([result], { type: 'model/gltf-binary' });
340
+ const a = document.createElement('a');
341
+ a.href = URL.createObjectURL(blob);
342
+ a.download = 'worldforge.glb';
343
+ a.click();
344
+ URL.revokeObjectURL(a.href);
345
+ btn.disabled = false; btn.textContent = 'Export GLB';
346
+ }, err => {
347
+ console.error(err);
348
+ btn.disabled = false; btn.textContent = 'Export GLB';
349
+ }, { binary: true });
350
+ };
351
+
352
+ document.getElementById('png').onclick = () => {
353
+ const a = document.createElement('a');
354
+ a.href = document.getElementById('layout').toDataURL('image/png');
355
+ a.download = 'layout-map.png';
356
+ a.click();
357
+ };
358
+
359
+ // ---------------------------------------------------------------------- loop ---
360
+ function resize() {
361
+ const w = canvas.clientWidth, h = canvas.clientHeight;
362
+ if (canvas.width !== w || canvas.height !== h) {
363
+ renderer.setSize(w, h, false);
364
+ camera.aspect = w / h;
365
+ camera.updateProjectionMatrix();
366
+ }
367
+ }
368
+
369
+ let last = performance.now();
370
+ function loop(now) {
371
+ const dt = Math.min(0.05, (now - last) / 1000);
372
+ last = now;
373
+ resize();
374
+ if (fly) stepFly(dt); else controls.update();
375
+ renderer.render(scene, camera);
376
+ requestAnimationFrame(loop);
377
+ }
378
+
379
+ // Handle for debugging and for driving the app from a console or a test harness
380
+ // (a headless tab gets no rAF, so the render has to be callable directly).
381
+ window.worldforge = {
382
+ renderer, scene, camera, controls,
383
+ build, generate,
384
+ frame: () => { resize(); renderer.render(scene, camera); },
385
+ };
386
+
387
+ seedEl.value = Math.floor(Math.random() * 999999);
388
+ generate();
389
+ requestAnimationFrame(loop);
index.html CHANGED
@@ -1,19 +1,155 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>WorldForge โ€” prompt to explorable terrain</title>
6
+ <meta name="viewport" content="width=device-width, initial-scale=1">
7
+ <style>
8
+ :root {
9
+ --ink: #e8eef3;
10
+ --dim: #93a3b1;
11
+ --panel: #141a20;
12
+ --line: #26313b;
13
+ --accent: #6fd08c;
14
+ }
15
+ * { box-sizing: border-box; }
16
+ html, body { height: 100%; margin: 0; }
17
+ body {
18
+ font: 14px/1.5 ui-sans-serif, system-ui, "Segoe UI", Roboto, sans-serif;
19
+ color: var(--ink); background: #0c1014; overflow: hidden;
20
+ display: grid; grid-template-columns: 320px 1fr;
21
+ }
22
+ body.busy { cursor: progress; }
23
+
24
+ aside {
25
+ background: var(--panel); border-right: 1px solid var(--line);
26
+ padding: 18px; overflow-y: auto; display: flex; flex-direction: column; gap: 16px;
27
+ }
28
+ h1 { font-size: 17px; margin: 0; letter-spacing: -0.01em; }
29
+ h1 span { color: var(--accent); }
30
+ .sub { color: var(--dim); font-size: 12px; margin-top: 4px; }
31
+
32
+ label { display: block; font-size: 11px; text-transform: uppercase;
33
+ letter-spacing: 0.08em; color: var(--dim); margin-bottom: 6px; }
34
+ input[type=text], input[type=number] {
35
+ width: 100%; padding: 9px 10px; border-radius: 7px;
36
+ border: 1px solid var(--line); background: #0e1418; color: var(--ink); font: inherit;
37
+ }
38
+ input:focus { outline: 2px solid var(--accent); outline-offset: -1px; }
39
+
40
+ .row { display: flex; gap: 8px; }
41
+ .row > * { flex: 1; }
42
+ button {
43
+ padding: 9px 12px; border-radius: 7px; border: 1px solid var(--line);
44
+ background: #1b242c; color: var(--ink); font: inherit; cursor: pointer;
45
+ }
46
+ button:hover { background: #23303a; }
47
+ button:disabled { opacity: 0.5; cursor: default; }
48
+ button.primary { background: var(--accent); color: #06110a; border-color: var(--accent); font-weight: 600; }
49
+ button.primary:hover { background: #82dc9d; }
50
+ button.on { background: var(--accent); color: #06110a; border-color: var(--accent); }
51
+
52
+ #presets { display: flex; flex-wrap: wrap; gap: 6px; }
53
+ #presets button { flex: 0 0 auto; padding: 5px 9px; font-size: 12px; border-radius: 20px; }
54
+
55
+ #layout { width: 100%; aspect-ratio: 1; border-radius: 8px;
56
+ border: 1px solid var(--line); image-rendering: pixelated; display: block; }
57
+
58
+ ul#legend { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 5px; }
59
+ #legend li { display: flex; align-items: center; gap: 8px; font-size: 13px; }
60
+ #legend i { width: 13px; height: 13px; border-radius: 3px; flex: 0 0 auto;
61
+ border: 1px solid rgba(255,255,255,.18); }
62
+ #legend b { margin-left: auto; color: var(--dim); font-weight: 500;
63
+ font-variant-numeric: tabular-nums; }
64
+
65
+ #stats { color: var(--dim); font-size: 12px; font-variant-numeric: tabular-nums; }
66
+ footer { margin-top: auto; color: var(--dim); font-size: 11px; line-height: 1.6; }
67
+ footer a { color: #9ec7dd; }
68
+
69
+ main { position: relative; }
70
+ #view { width: 100%; height: 100%; display: block; }
71
+ #hint {
72
+ display: none; position: absolute; left: 50%; bottom: 24px; transform: translateX(-50%);
73
+ background: rgba(10,14,18,.82); border: 1px solid var(--line); border-radius: 8px;
74
+ padding: 8px 14px; font-size: 12px; color: var(--dim);
75
+ }
76
+ @media (max-width: 820px) {
77
+ body { grid-template-columns: 1fr; grid-template-rows: auto 1fr; }
78
+ aside { max-height: 46vh; }
79
+ }
80
+ </style>
81
+ </head>
82
+ <body>
83
+ <aside>
84
+ <div>
85
+ <h1>World<span>Forge</span></h1>
86
+ <div class="sub">A prompt becomes a region plan, a semantic layout map, and an explorable height field.</div>
87
+ </div>
88
+
89
+ <div>
90
+ <label for="prompt">Prompt</label>
91
+ <input id="prompt" type="text" value="a snowy alpine range above a pine forest and a lake"
92
+ placeholder="describe a worldโ€ฆ" autocomplete="off">
93
+ </div>
94
+
95
+ <div id="presets">
96
+ <button data-p="a desert of red canyons and mesas">Canyons</button>
97
+ <button data-p="volcanic island ringed by ocean and jungle">Volcanic island</button>
98
+ <button data-p="snowy alpine peaks over a pine forest">Alps</button>
99
+ <button data-p="savanna plains with scattered acacia">Savanna</button>
100
+ <button data-p="frozen tundra and glaciers">Tundra</button>
101
+ <button data-p="swamp and dense rainforest">Swamp</button>
102
+ </div>
103
+
104
+ <div class="row">
105
+ <div style="flex:1.1">
106
+ <label for="seed">Seed</label>
107
+ <input id="seed" type="number" min="1" step="1" value="1">
108
+ </div>
109
+ <div style="display:flex; align-items:flex-end">
110
+ <button id="reseed" title="New seed, same prompt">Reroll</button>
111
+ </div>
112
+ </div>
113
+
114
+ <div class="row">
115
+ <button id="go" class="primary">Generate</button>
116
+ <button id="fly" title="WASD + mouse look, Esc to exit">Fly</button>
117
+ </div>
118
+
119
+ <div>
120
+ <label>Semantic layout map</label>
121
+ <canvas id="layout"></canvas>
122
+ </div>
123
+
124
+ <div>
125
+ <label>Regions</label>
126
+ <ul id="legend"></ul>
127
+ </div>
128
+
129
+ <div id="stats"></div>
130
+
131
+ <div class="row">
132
+ <button id="glb">Export GLB</button>
133
+ <button id="png">Export map</button>
134
+ </div>
135
+
136
+ <footer>
137
+ An independent implementation of the terrain foundation described in
138
+ <a href="https://arxiv.org/abs/2608.05248" target="_blank" rel="noopener">WorldClaw</a>
139
+ (arXiv&nbsp;2608.05248). That project publishes no code or weights; nothing from it is
140
+ used here. Everything runs in your browser.<br>
141
+ Built by <a href="https://huggingface.co/VIDraft" target="_blank" rel="noopener">VIDraft</a>.
142
+ </footer>
143
+ </aside>
144
+
145
+ <main>
146
+ <canvas id="view"></canvas>
147
+ <div id="hint">WASD move ยท mouse look ยท Q/E down/up ยท Shift boost ยท Esc exit</div>
148
+ </main>
149
+
150
+ <script type="importmap">
151
+ { "imports": { "three": "./vendor/three.module.js" } }
152
+ </script>
153
+ <script type="module" src="./app.js"></script>
154
+ </body>
155
  </html>
vendor/GLTFExporter.js ADDED
@@ -0,0 +1,3595 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ BufferAttribute,
3
+ ClampToEdgeWrapping,
4
+ Color,
5
+ DoubleSide,
6
+ InterpolateDiscrete,
7
+ InterpolateLinear,
8
+ NoColorSpace,
9
+ LinearFilter,
10
+ LinearMipmapLinearFilter,
11
+ LinearMipmapNearestFilter,
12
+ MathUtils,
13
+ Matrix4,
14
+ MirroredRepeatWrapping,
15
+ NearestFilter,
16
+ NearestMipmapLinearFilter,
17
+ NearestMipmapNearestFilter,
18
+ PropertyBinding,
19
+ RGBAFormat,
20
+ RepeatWrapping,
21
+ Scene,
22
+ Source,
23
+ SRGBColorSpace,
24
+ CompressedTexture,
25
+ Vector3,
26
+ Quaternion,
27
+ REVISION,
28
+ ImageUtils
29
+ } from 'three';
30
+
31
+ /**
32
+ * The KHR_mesh_quantization extension allows these extra attribute component types
33
+ *
34
+ * @see https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_mesh_quantization/README.md#extending-mesh-attributes
35
+ */
36
+ const KHR_mesh_quantization_ExtraAttrTypes = {
37
+ POSITION: [
38
+ 'byte',
39
+ 'byte normalized',
40
+ 'unsigned byte',
41
+ 'unsigned byte normalized',
42
+ 'short',
43
+ 'short normalized',
44
+ 'unsigned short',
45
+ 'unsigned short normalized',
46
+ ],
47
+ NORMAL: [
48
+ 'byte normalized',
49
+ 'short normalized',
50
+ ],
51
+ TANGENT: [
52
+ 'byte normalized',
53
+ 'short normalized',
54
+ ],
55
+ TEXCOORD: [
56
+ 'byte',
57
+ 'byte normalized',
58
+ 'unsigned byte',
59
+ 'short',
60
+ 'short normalized',
61
+ 'unsigned short',
62
+ ],
63
+ };
64
+
65
+ /**
66
+ * An exporter for `glTF` 2.0.
67
+ *
68
+ * glTF (GL Transmission Format) is an [open format specification]{@link https://github.com/KhronosGroup/glTF/tree/master/specification/2.0}
69
+ * for efficient delivery and loading of 3D content. Assets may be provided either in JSON (.gltf)
70
+ * or binary (.glb) format. External files store textures (.jpg, .png) and additional binary
71
+ * data (.bin). A glTF asset may deliver one or more scenes, including meshes, materials,
72
+ * textures, skins, skeletons, morph targets, animations, lights, and/or cameras.
73
+ *
74
+ * GLTFExporter supports the [glTF 2.0 extensions]{@link https://github.com/KhronosGroup/glTF/tree/master/extensions/}:
75
+ *
76
+ * - KHR_lights_punctual
77
+ * - KHR_materials_clearcoat
78
+ * - KHR_materials_dispersion
79
+ * - KHR_materials_emissive_strength
80
+ * - KHR_materials_ior
81
+ * - KHR_materials_iridescence
82
+ * - KHR_materials_specular
83
+ * - KHR_materials_sheen
84
+ * - KHR_materials_transmission
85
+ * - KHR_materials_unlit
86
+ * - KHR_materials_volume
87
+ * - KHR_mesh_quantization
88
+ * - KHR_texture_transform
89
+ * - EXT_materials_bump
90
+ * - EXT_mesh_gpu_instancing
91
+ *
92
+ * The following glTF 2.0 extension is supported by an external user plugin:
93
+ *
94
+ * - [KHR_materials_variants]{@link https://github.com/takahirox/three-gltf-extensions}
95
+ *
96
+ * ```js
97
+ * const exporter = new GLTFExporter();
98
+ * const data = await exporter.parseAsync( scene, options );
99
+ * ```
100
+ *
101
+ * @three_import import { GLTFExporter } from 'three/addons/exporters/GLTFExporter.js';
102
+ */
103
+ class GLTFExporter {
104
+
105
+ /**
106
+ * Constructs a new glTF exporter.
107
+ */
108
+ constructor() {
109
+
110
+ /**
111
+ * A reference to a texture utils module.
112
+ *
113
+ * @type {?(WebGLTextureUtils|WebGPUTextureUtils)}
114
+ * @default null
115
+ */
116
+ this.textureUtils = null;
117
+
118
+ this.pluginCallbacks = [];
119
+
120
+ this.register( function ( writer ) {
121
+
122
+ return new GLTFLightExtension( writer );
123
+
124
+ } );
125
+
126
+ this.register( function ( writer ) {
127
+
128
+ return new GLTFMaterialsUnlitExtension( writer );
129
+
130
+ } );
131
+
132
+ this.register( function ( writer ) {
133
+
134
+ return new GLTFMaterialsTransmissionExtension( writer );
135
+
136
+ } );
137
+
138
+ this.register( function ( writer ) {
139
+
140
+ return new GLTFMaterialsVolumeExtension( writer );
141
+
142
+ } );
143
+
144
+ this.register( function ( writer ) {
145
+
146
+ return new GLTFMaterialsIorExtension( writer );
147
+
148
+ } );
149
+
150
+ this.register( function ( writer ) {
151
+
152
+ return new GLTFMaterialsSpecularExtension( writer );
153
+
154
+ } );
155
+
156
+ this.register( function ( writer ) {
157
+
158
+ return new GLTFMaterialsClearcoatExtension( writer );
159
+
160
+ } );
161
+
162
+ this.register( function ( writer ) {
163
+
164
+ return new GLTFMaterialsDispersionExtension( writer );
165
+
166
+ } );
167
+
168
+ this.register( function ( writer ) {
169
+
170
+ return new GLTFMaterialsIridescenceExtension( writer );
171
+
172
+ } );
173
+
174
+ this.register( function ( writer ) {
175
+
176
+ return new GLTFMaterialsSheenExtension( writer );
177
+
178
+ } );
179
+
180
+ this.register( function ( writer ) {
181
+
182
+ return new GLTFMaterialsAnisotropyExtension( writer );
183
+
184
+ } );
185
+
186
+ this.register( function ( writer ) {
187
+
188
+ return new GLTFMaterialsEmissiveStrengthExtension( writer );
189
+
190
+ } );
191
+
192
+ this.register( function ( writer ) {
193
+
194
+ return new GLTFMaterialsBumpExtension( writer );
195
+
196
+ } );
197
+
198
+ this.register( function ( writer ) {
199
+
200
+ return new GLTFMeshGpuInstancing( writer );
201
+
202
+ } );
203
+
204
+ }
205
+
206
+ /**
207
+ * Registers a plugin callback. This API is internally used to implement the various
208
+ * glTF extensions but can also used by third-party code to add additional logic
209
+ * to the exporter.
210
+ *
211
+ * @param {function(writer:GLTFWriter)} callback - The callback function to register.
212
+ * @return {GLTFExporter} A reference to this exporter.
213
+ */
214
+ register( callback ) {
215
+
216
+ if ( this.pluginCallbacks.indexOf( callback ) === - 1 ) {
217
+
218
+ this.pluginCallbacks.push( callback );
219
+
220
+ }
221
+
222
+ return this;
223
+
224
+ }
225
+
226
+ /**
227
+ * Unregisters a plugin callback.
228
+ *
229
+ * @param {Function} callback - The callback function to unregister.
230
+ * @return {GLTFExporter} A reference to this exporter.
231
+ */
232
+ unregister( callback ) {
233
+
234
+ if ( this.pluginCallbacks.indexOf( callback ) !== - 1 ) {
235
+
236
+ this.pluginCallbacks.splice( this.pluginCallbacks.indexOf( callback ), 1 );
237
+
238
+ }
239
+
240
+ return this;
241
+
242
+ }
243
+
244
+ /**
245
+ * Sets the texture utils for this exporter. Only relevant when compressed textures have to be exported.
246
+ *
247
+ * Depending on whether you use {@link WebGLRenderer} or {@link WebGPURenderer}, you must inject the
248
+ * corresponding texture utils {@link WebGLTextureUtils} or {@link WebGPUTextureUtils}.
249
+ *
250
+ * @param {WebGLTextureUtils|WebGPUTextureUtils} utils - The texture utils.
251
+ * @return {GLTFExporter} A reference to this exporter.
252
+ */
253
+ setTextureUtils( utils ) {
254
+
255
+ this.textureUtils = utils;
256
+
257
+ return this;
258
+
259
+ }
260
+
261
+ /**
262
+ * Parses the given scenes and generates the glTF output.
263
+ *
264
+ * @param {Scene|Array<Scene>} input - A scene or an array of scenes.
265
+ * @param {GLTFExporter~OnDone} onDone - A callback function that is executed when the export has finished.
266
+ * @param {GLTFExporter~OnError} onError - A callback function that is executed when an error happens.
267
+ * @param {GLTFExporter~Options} options - options
268
+ */
269
+ parse( input, onDone, onError, options ) {
270
+
271
+ const writer = new GLTFWriter();
272
+ const plugins = [];
273
+
274
+ for ( let i = 0, il = this.pluginCallbacks.length; i < il; i ++ ) {
275
+
276
+ plugins.push( this.pluginCallbacks[ i ]( writer ) );
277
+
278
+ }
279
+
280
+ writer.setPlugins( plugins );
281
+ writer.setTextureUtils( this.textureUtils );
282
+ writer.writeAsync( input, onDone, options ).catch( onError );
283
+
284
+ }
285
+
286
+ /**
287
+ * Async version of {@link GLTFExporter#parse}.
288
+ *
289
+ * @param {Scene|Array<Scene>} input - A scene or an array of scenes.
290
+ * @param {GLTFExporter~Options} options - options.
291
+ * @return {Promise<ArrayBuffer|string>} A Promise that resolved with the exported glTF data.
292
+ */
293
+ parseAsync( input, options ) {
294
+
295
+ const scope = this;
296
+
297
+ return new Promise( function ( resolve, reject ) {
298
+
299
+ scope.parse( input, resolve, reject, options );
300
+
301
+ } );
302
+
303
+ }
304
+
305
+ }
306
+
307
+ //------------------------------------------------------------------------------
308
+ // Constants
309
+ //------------------------------------------------------------------------------
310
+
311
+ const WEBGL_CONSTANTS = {
312
+ POINTS: 0x0000,
313
+ LINES: 0x0001,
314
+ LINE_LOOP: 0x0002,
315
+ LINE_STRIP: 0x0003,
316
+ TRIANGLES: 0x0004,
317
+ TRIANGLE_STRIP: 0x0005,
318
+ TRIANGLE_FAN: 0x0006,
319
+
320
+ BYTE: 0x1400,
321
+ UNSIGNED_BYTE: 0x1401,
322
+ SHORT: 0x1402,
323
+ UNSIGNED_SHORT: 0x1403,
324
+ INT: 0x1404,
325
+ UNSIGNED_INT: 0x1405,
326
+ FLOAT: 0x1406,
327
+
328
+ ARRAY_BUFFER: 0x8892,
329
+ ELEMENT_ARRAY_BUFFER: 0x8893,
330
+
331
+ NEAREST: 0x2600,
332
+ LINEAR: 0x2601,
333
+ NEAREST_MIPMAP_NEAREST: 0x2700,
334
+ LINEAR_MIPMAP_NEAREST: 0x2701,
335
+ NEAREST_MIPMAP_LINEAR: 0x2702,
336
+ LINEAR_MIPMAP_LINEAR: 0x2703,
337
+
338
+ CLAMP_TO_EDGE: 33071,
339
+ MIRRORED_REPEAT: 33648,
340
+ REPEAT: 10497
341
+ };
342
+
343
+ const KHR_MESH_QUANTIZATION = 'KHR_mesh_quantization';
344
+
345
+ const THREE_TO_WEBGL = {};
346
+
347
+ THREE_TO_WEBGL[ NearestFilter ] = WEBGL_CONSTANTS.NEAREST;
348
+ THREE_TO_WEBGL[ NearestMipmapNearestFilter ] = WEBGL_CONSTANTS.NEAREST_MIPMAP_NEAREST;
349
+ THREE_TO_WEBGL[ NearestMipmapLinearFilter ] = WEBGL_CONSTANTS.NEAREST_MIPMAP_LINEAR;
350
+ THREE_TO_WEBGL[ LinearFilter ] = WEBGL_CONSTANTS.LINEAR;
351
+ THREE_TO_WEBGL[ LinearMipmapNearestFilter ] = WEBGL_CONSTANTS.LINEAR_MIPMAP_NEAREST;
352
+ THREE_TO_WEBGL[ LinearMipmapLinearFilter ] = WEBGL_CONSTANTS.LINEAR_MIPMAP_LINEAR;
353
+
354
+ THREE_TO_WEBGL[ ClampToEdgeWrapping ] = WEBGL_CONSTANTS.CLAMP_TO_EDGE;
355
+ THREE_TO_WEBGL[ RepeatWrapping ] = WEBGL_CONSTANTS.REPEAT;
356
+ THREE_TO_WEBGL[ MirroredRepeatWrapping ] = WEBGL_CONSTANTS.MIRRORED_REPEAT;
357
+
358
+ const PATH_PROPERTIES = {
359
+ scale: 'scale',
360
+ position: 'translation',
361
+ quaternion: 'rotation',
362
+ morphTargetInfluences: 'weights'
363
+ };
364
+
365
+ const DEFAULT_SPECULAR_COLOR = new Color();
366
+
367
+ // GLB constants
368
+ // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#glb-file-format-specification
369
+
370
+ const GLB_HEADER_BYTES = 12;
371
+ const GLB_HEADER_MAGIC = 0x46546C67;
372
+ const GLB_VERSION = 2;
373
+
374
+ const GLB_CHUNK_PREFIX_BYTES = 8;
375
+ const GLB_CHUNK_TYPE_JSON = 0x4E4F534A;
376
+ const GLB_CHUNK_TYPE_BIN = 0x004E4942;
377
+
378
+ //------------------------------------------------------------------------------
379
+ // Utility functions
380
+ //------------------------------------------------------------------------------
381
+
382
+ /**
383
+ * Compare two arrays
384
+ *
385
+ * @private
386
+ * @param {Array} array1 Array 1 to compare
387
+ * @param {Array} array2 Array 2 to compare
388
+ * @return {boolean} Returns true if both arrays are equal
389
+ */
390
+ function equalArray( array1, array2 ) {
391
+
392
+ return ( array1.length === array2.length ) && array1.every( function ( element, index ) {
393
+
394
+ return element === array2[ index ];
395
+
396
+ } );
397
+
398
+ }
399
+
400
+ /**
401
+ * Converts a string to an ArrayBuffer.
402
+ *
403
+ * @private
404
+ * @param {string} text
405
+ * @return {ArrayBuffer}
406
+ */
407
+ function stringToArrayBuffer( text ) {
408
+
409
+ return new TextEncoder().encode( text ).buffer;
410
+
411
+ }
412
+
413
+ /**
414
+ * Is identity matrix
415
+ *
416
+ * @private
417
+ * @param {Matrix4} matrix
418
+ * @returns {boolean} Returns true, if parameter is identity matrix
419
+ */
420
+ function isIdentityMatrix( matrix ) {
421
+
422
+ return equalArray( matrix.elements, [ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ] );
423
+
424
+ }
425
+
426
+ /**
427
+ * Get the min and max vectors from the given attribute
428
+ *
429
+ * @private
430
+ * @param {BufferAttribute} attribute Attribute to find the min/max in range from start to start + count
431
+ * @param {number} start Start index
432
+ * @param {number} count Range to cover
433
+ * @return {Object} Object containing the `min` and `max` values (As an array of attribute.itemSize components)
434
+ */
435
+ function getMinMax( attribute, start, count ) {
436
+
437
+ const output = {
438
+
439
+ min: new Array( attribute.itemSize ).fill( Number.POSITIVE_INFINITY ),
440
+ max: new Array( attribute.itemSize ).fill( Number.NEGATIVE_INFINITY )
441
+
442
+ };
443
+
444
+ for ( let i = start; i < start + count; i ++ ) {
445
+
446
+ for ( let a = 0; a < attribute.itemSize; a ++ ) {
447
+
448
+ let value;
449
+
450
+ if ( attribute.itemSize > 4 ) {
451
+
452
+ // no support for interleaved data for itemSize > 4
453
+
454
+ value = attribute.array[ i * attribute.itemSize + a ];
455
+
456
+ } else {
457
+
458
+ if ( a === 0 ) value = attribute.getX( i );
459
+ else if ( a === 1 ) value = attribute.getY( i );
460
+ else if ( a === 2 ) value = attribute.getZ( i );
461
+ else if ( a === 3 ) value = attribute.getW( i );
462
+
463
+ if ( attribute.normalized === true ) {
464
+
465
+ value = MathUtils.normalize( value, attribute.array );
466
+
467
+ }
468
+
469
+ }
470
+
471
+ output.min[ a ] = Math.min( output.min[ a ], value );
472
+ output.max[ a ] = Math.max( output.max[ a ], value );
473
+
474
+ }
475
+
476
+ }
477
+
478
+ return output;
479
+
480
+ }
481
+
482
+ /**
483
+ * Get the required size + padding for a buffer, rounded to the next 4-byte boundary.
484
+ * https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#data-alignment
485
+ *
486
+ * @private
487
+ * @param {number} bufferSize The size the original buffer. Should be an integer.
488
+ * @returns {number} new buffer size with required padding as an integer.
489
+ *
490
+ */
491
+ function getPaddedBufferSize( bufferSize ) {
492
+
493
+ return Math.ceil( bufferSize / 4 ) * 4;
494
+
495
+ }
496
+
497
+ /**
498
+ * Returns a buffer aligned to 4-byte boundary.
499
+ *
500
+ * @private
501
+ * @param {ArrayBuffer} arrayBuffer Buffer to pad
502
+ * @param {number} [paddingByte=0] Should be an integer
503
+ * @returns {ArrayBuffer} The same buffer if it's already aligned to 4-byte boundary or a new buffer
504
+ */
505
+ function getPaddedArrayBuffer( arrayBuffer, paddingByte = 0 ) {
506
+
507
+ const paddedLength = getPaddedBufferSize( arrayBuffer.byteLength );
508
+
509
+ if ( paddedLength !== arrayBuffer.byteLength ) {
510
+
511
+ const array = new Uint8Array( paddedLength );
512
+ array.set( new Uint8Array( arrayBuffer ) );
513
+
514
+ if ( paddingByte !== 0 ) {
515
+
516
+ for ( let i = arrayBuffer.byteLength; i < paddedLength; i ++ ) {
517
+
518
+ array[ i ] = paddingByte;
519
+
520
+ }
521
+
522
+ }
523
+
524
+ return array.buffer;
525
+
526
+ }
527
+
528
+ return arrayBuffer;
529
+
530
+ }
531
+
532
+ function getCanvas() {
533
+
534
+ if ( typeof document === 'undefined' && typeof OffscreenCanvas !== 'undefined' ) {
535
+
536
+ return new OffscreenCanvas( 1, 1 );
537
+
538
+ }
539
+
540
+ return document.createElement( 'canvas' );
541
+
542
+ }
543
+
544
+ function getToBlobPromise( canvas, mimeType ) {
545
+
546
+ if ( typeof OffscreenCanvas !== 'undefined' && canvas instanceof OffscreenCanvas ) {
547
+
548
+ let quality;
549
+
550
+ // Blink's implementation of convertToBlob seems to default to a quality level of 100%
551
+ // Use the Blink default quality levels of toBlob instead so that file sizes are comparable.
552
+ if ( mimeType === 'image/jpeg' ) {
553
+
554
+ quality = 0.92;
555
+
556
+ } else if ( mimeType === 'image/webp' ) {
557
+
558
+ quality = 0.8;
559
+
560
+ }
561
+
562
+ return canvas.convertToBlob( {
563
+
564
+ type: mimeType,
565
+ quality: quality
566
+
567
+ } );
568
+
569
+ } else {
570
+
571
+ // HTMLCanvasElement code path
572
+
573
+ return new Promise( ( resolve ) => canvas.toBlob( resolve, mimeType ) );
574
+
575
+ }
576
+
577
+ }
578
+
579
+ /**
580
+ * Writer
581
+ *
582
+ * @private
583
+ */
584
+ class GLTFWriter {
585
+
586
+ constructor() {
587
+
588
+ this.plugins = [];
589
+
590
+ this.options = {};
591
+ this.pending = [];
592
+ this.buffers = [];
593
+
594
+ this.byteOffset = 0;
595
+ this.buffers = [];
596
+ this.nodeMap = new Map();
597
+ this.skins = [];
598
+
599
+ this.extensionsUsed = {};
600
+ this.extensionsRequired = {};
601
+
602
+ this.uids = new Map();
603
+ this.uid = 0;
604
+
605
+ this.json = {
606
+ asset: {
607
+ version: '2.0',
608
+ generator: 'THREE.GLTFExporter r' + REVISION
609
+ }
610
+ };
611
+
612
+ this.cache = {
613
+ meshes: new Map(),
614
+ attributes: new Map(),
615
+ attributesNormalized: new Map(),
616
+ materials: new Map(),
617
+ textures: new Map(),
618
+ images: new Map()
619
+ };
620
+
621
+ this.textureUtils = null;
622
+
623
+ }
624
+
625
+ setPlugins( plugins ) {
626
+
627
+ this.plugins = plugins;
628
+
629
+ }
630
+
631
+ setTextureUtils( utils ) {
632
+
633
+ this.textureUtils = utils;
634
+
635
+ }
636
+
637
+ /**
638
+ * Parse scenes and generate GLTF output
639
+ *
640
+ * @param {Scene|Array<Scene>} input Scene or Array of THREE.Scenes
641
+ * @param {Function} onDone Callback on completed
642
+ * @param {Object} options options
643
+ */
644
+ async writeAsync( input, onDone, options = {} ) {
645
+
646
+ this.options = Object.assign( {
647
+ // default options
648
+ binary: false,
649
+ trs: false,
650
+ onlyVisible: true,
651
+ maxTextureSize: Infinity,
652
+ animations: [],
653
+ includeCustomExtensions: false
654
+ }, options );
655
+
656
+ if ( this.options.animations.length > 0 ) {
657
+
658
+ // Only TRS properties, and not matrices, may be targeted by animation.
659
+ this.options.trs = true;
660
+
661
+ }
662
+
663
+ await this.processInputAsync( input );
664
+
665
+ await Promise.all( this.pending );
666
+
667
+ const writer = this;
668
+ const buffers = writer.buffers;
669
+ const json = writer.json;
670
+ options = writer.options;
671
+
672
+ const extensionsUsed = writer.extensionsUsed;
673
+ const extensionsRequired = writer.extensionsRequired;
674
+
675
+ // Merge buffers.
676
+ const blob = new Blob( buffers, { type: 'application/octet-stream' } );
677
+
678
+ // Declare extensions.
679
+ const extensionsUsedList = Object.keys( extensionsUsed );
680
+ const extensionsRequiredList = Object.keys( extensionsRequired );
681
+
682
+ if ( extensionsUsedList.length > 0 ) json.extensionsUsed = extensionsUsedList;
683
+ if ( extensionsRequiredList.length > 0 ) json.extensionsRequired = extensionsRequiredList;
684
+
685
+ // Update bytelength of the single buffer.
686
+ if ( json.buffers && json.buffers.length > 0 ) json.buffers[ 0 ].byteLength = blob.size;
687
+
688
+ if ( options.binary === true ) {
689
+
690
+ // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#glb-file-format-specification
691
+
692
+ const reader = new FileReader();
693
+ reader.readAsArrayBuffer( blob );
694
+ reader.onloadend = function () {
695
+
696
+ // Binary chunk.
697
+ const binaryChunk = getPaddedArrayBuffer( reader.result );
698
+ const binaryChunkPrefix = new DataView( new ArrayBuffer( GLB_CHUNK_PREFIX_BYTES ) );
699
+ binaryChunkPrefix.setUint32( 0, binaryChunk.byteLength, true );
700
+ binaryChunkPrefix.setUint32( 4, GLB_CHUNK_TYPE_BIN, true );
701
+
702
+ // JSON chunk.
703
+ const jsonChunk = getPaddedArrayBuffer( stringToArrayBuffer( JSON.stringify( json ) ), 0x20 );
704
+ const jsonChunkPrefix = new DataView( new ArrayBuffer( GLB_CHUNK_PREFIX_BYTES ) );
705
+ jsonChunkPrefix.setUint32( 0, jsonChunk.byteLength, true );
706
+ jsonChunkPrefix.setUint32( 4, GLB_CHUNK_TYPE_JSON, true );
707
+
708
+ // GLB header.
709
+ const header = new ArrayBuffer( GLB_HEADER_BYTES );
710
+ const headerView = new DataView( header );
711
+ headerView.setUint32( 0, GLB_HEADER_MAGIC, true );
712
+ headerView.setUint32( 4, GLB_VERSION, true );
713
+ const totalByteLength = GLB_HEADER_BYTES
714
+ + jsonChunkPrefix.byteLength + jsonChunk.byteLength
715
+ + binaryChunkPrefix.byteLength + binaryChunk.byteLength;
716
+ headerView.setUint32( 8, totalByteLength, true );
717
+
718
+ const glbBlob = new Blob( [
719
+ header,
720
+ jsonChunkPrefix,
721
+ jsonChunk,
722
+ binaryChunkPrefix,
723
+ binaryChunk
724
+ ], { type: 'application/octet-stream' } );
725
+
726
+ const glbReader = new FileReader();
727
+ glbReader.readAsArrayBuffer( glbBlob );
728
+ glbReader.onloadend = function () {
729
+
730
+ onDone( glbReader.result );
731
+
732
+ };
733
+
734
+ };
735
+
736
+ } else {
737
+
738
+ if ( json.buffers && json.buffers.length > 0 ) {
739
+
740
+ const reader = new FileReader();
741
+ reader.readAsDataURL( blob );
742
+ reader.onloadend = function () {
743
+
744
+ const base64data = reader.result;
745
+ json.buffers[ 0 ].uri = base64data;
746
+ onDone( json );
747
+
748
+ };
749
+
750
+ } else {
751
+
752
+ onDone( json );
753
+
754
+ }
755
+
756
+ }
757
+
758
+
759
+ }
760
+
761
+ /**
762
+ * Serializes a userData.
763
+ *
764
+ * @param {THREE.Object3D|THREE.Material|THREE.BufferGeometry|THREE.AnimationClip} object
765
+ * @param {Object} objectDef
766
+ */
767
+ serializeUserData( object, objectDef ) {
768
+
769
+ if ( Object.keys( object.userData ).length === 0 ) return;
770
+
771
+ const options = this.options;
772
+ const extensionsUsed = this.extensionsUsed;
773
+
774
+ try {
775
+
776
+ const json = JSON.parse( JSON.stringify( object.userData ) );
777
+
778
+ if ( options.includeCustomExtensions && json.gltfExtensions ) {
779
+
780
+ if ( objectDef.extensions === undefined ) objectDef.extensions = {};
781
+
782
+ for ( const extensionName in json.gltfExtensions ) {
783
+
784
+ objectDef.extensions[ extensionName ] = json.gltfExtensions[ extensionName ];
785
+ extensionsUsed[ extensionName ] = true;
786
+
787
+ }
788
+
789
+ delete json.gltfExtensions;
790
+
791
+ }
792
+
793
+ if ( Object.keys( json ).length > 0 ) objectDef.extras = json;
794
+
795
+ } catch ( error ) {
796
+
797
+ console.warn( 'THREE.GLTFExporter: userData of \'' + object.name + '\' ' +
798
+ 'won\'t be serialized because of JSON.stringify error - ' + error.message );
799
+
800
+ }
801
+
802
+ }
803
+
804
+ /**
805
+ * Returns ids for buffer attributes.
806
+ *
807
+ * @param {Object} attribute
808
+ * @param {boolean} [isRelativeCopy=false]
809
+ * @return {number} An integer
810
+ */
811
+ getUID( attribute, isRelativeCopy = false ) {
812
+
813
+ if ( this.uids.has( attribute ) === false ) {
814
+
815
+ const uids = new Map();
816
+
817
+ uids.set( true, this.uid ++ );
818
+ uids.set( false, this.uid ++ );
819
+
820
+ this.uids.set( attribute, uids );
821
+
822
+ }
823
+
824
+ const uids = this.uids.get( attribute );
825
+
826
+ return uids.get( isRelativeCopy );
827
+
828
+ }
829
+
830
+ /**
831
+ * Checks if normal attribute values are normalized.
832
+ *
833
+ * @param {BufferAttribute} normal
834
+ * @returns {boolean}
835
+ */
836
+ isNormalizedNormalAttribute( normal ) {
837
+
838
+ const cache = this.cache;
839
+
840
+ if ( cache.attributesNormalized.has( normal ) ) return false;
841
+
842
+ const v = new Vector3();
843
+
844
+ for ( let i = 0, il = normal.count; i < il; i ++ ) {
845
+
846
+ // 0.0005 is from glTF-validator
847
+ if ( Math.abs( v.fromBufferAttribute( normal, i ).length() - 1.0 ) > 0.0005 ) return false;
848
+
849
+ }
850
+
851
+ return true;
852
+
853
+ }
854
+
855
+ /**
856
+ * Creates normalized normal buffer attribute.
857
+ *
858
+ * @param {BufferAttribute} normal
859
+ * @returns {BufferAttribute}
860
+ *
861
+ */
862
+ createNormalizedNormalAttribute( normal ) {
863
+
864
+ const cache = this.cache;
865
+
866
+ if ( cache.attributesNormalized.has( normal ) ) return cache.attributesNormalized.get( normal );
867
+
868
+ const attribute = normal.clone();
869
+ const v = new Vector3();
870
+
871
+ for ( let i = 0, il = attribute.count; i < il; i ++ ) {
872
+
873
+ v.fromBufferAttribute( attribute, i );
874
+
875
+ if ( v.x === 0 && v.y === 0 && v.z === 0 ) {
876
+
877
+ // if values can't be normalized set (1, 0, 0)
878
+ v.setX( 1.0 );
879
+
880
+ } else {
881
+
882
+ v.normalize();
883
+
884
+ }
885
+
886
+ attribute.setXYZ( i, v.x, v.y, v.z );
887
+
888
+ }
889
+
890
+ cache.attributesNormalized.set( normal, attribute );
891
+
892
+ return attribute;
893
+
894
+ }
895
+
896
+ /**
897
+ * Applies a texture transform, if present, to the map definition. Requires
898
+ * the KHR_texture_transform extension.
899
+ *
900
+ * @param {Object} mapDef
901
+ * @param {THREE.Texture} texture
902
+ */
903
+ applyTextureTransform( mapDef, texture ) {
904
+
905
+ let didTransform = false;
906
+ const transformDef = {};
907
+
908
+ if ( texture.offset.x !== 0 || texture.offset.y !== 0 ) {
909
+
910
+ transformDef.offset = texture.offset.toArray();
911
+ didTransform = true;
912
+
913
+ }
914
+
915
+ if ( texture.rotation !== 0 ) {
916
+
917
+ transformDef.rotation = texture.rotation;
918
+ didTransform = true;
919
+
920
+ }
921
+
922
+ if ( texture.repeat.x !== 1 || texture.repeat.y !== 1 ) {
923
+
924
+ transformDef.scale = texture.repeat.toArray();
925
+ didTransform = true;
926
+
927
+ }
928
+
929
+ if ( didTransform ) {
930
+
931
+ mapDef.extensions = mapDef.extensions || {};
932
+ mapDef.extensions[ 'KHR_texture_transform' ] = transformDef;
933
+ this.extensionsUsed[ 'KHR_texture_transform' ] = true;
934
+
935
+ }
936
+
937
+ }
938
+
939
+ async buildMetalRoughTextureAsync( metalnessMap, roughnessMap ) {
940
+
941
+ if ( metalnessMap === roughnessMap ) return metalnessMap;
942
+
943
+ function getEncodingConversion( map ) {
944
+
945
+ if ( map.colorSpace === SRGBColorSpace ) {
946
+
947
+ return function SRGBToLinear( c ) {
948
+
949
+ return ( c < 0.04045 ) ? c * 0.0773993808 : Math.pow( c * 0.9478672986 + 0.0521327014, 2.4 );
950
+
951
+ };
952
+
953
+ }
954
+
955
+ return function LinearToLinear( c ) {
956
+
957
+ return c;
958
+
959
+ };
960
+
961
+ }
962
+
963
+ if ( metalnessMap instanceof CompressedTexture ) {
964
+
965
+ metalnessMap = await this.decompressTextureAsync( metalnessMap );
966
+
967
+ }
968
+
969
+ if ( roughnessMap instanceof CompressedTexture ) {
970
+
971
+ roughnessMap = await this.decompressTextureAsync( roughnessMap );
972
+
973
+ }
974
+
975
+ const metalness = metalnessMap ? metalnessMap.image : null;
976
+ const roughness = roughnessMap ? roughnessMap.image : null;
977
+
978
+ const width = Math.max( metalness ? metalness.width : 0, roughness ? roughness.width : 0 );
979
+ const height = Math.max( metalness ? metalness.height : 0, roughness ? roughness.height : 0 );
980
+
981
+ const canvas = getCanvas();
982
+ canvas.width = width;
983
+ canvas.height = height;
984
+
985
+ const context = canvas.getContext( '2d', {
986
+ willReadFrequently: true,
987
+ } );
988
+ context.fillStyle = '#00ffff';
989
+ context.fillRect( 0, 0, width, height );
990
+
991
+ const composite = context.getImageData( 0, 0, width, height );
992
+
993
+ if ( metalness ) {
994
+
995
+ context.drawImage( metalness, 0, 0, width, height );
996
+
997
+ const convert = getEncodingConversion( metalnessMap );
998
+ const data = context.getImageData( 0, 0, width, height ).data;
999
+
1000
+ for ( let i = 2; i < data.length; i += 4 ) {
1001
+
1002
+ composite.data[ i ] = convert( data[ i ] / 256 ) * 256;
1003
+
1004
+ }
1005
+
1006
+ }
1007
+
1008
+ if ( roughness ) {
1009
+
1010
+ context.drawImage( roughness, 0, 0, width, height );
1011
+
1012
+ const convert = getEncodingConversion( roughnessMap );
1013
+ const data = context.getImageData( 0, 0, width, height ).data;
1014
+
1015
+ for ( let i = 1; i < data.length; i += 4 ) {
1016
+
1017
+ composite.data[ i ] = convert( data[ i ] / 256 ) * 256;
1018
+
1019
+ }
1020
+
1021
+ }
1022
+
1023
+ context.putImageData( composite, 0, 0 );
1024
+
1025
+ //
1026
+
1027
+ const reference = metalnessMap || roughnessMap;
1028
+
1029
+ const texture = reference.clone();
1030
+
1031
+ texture.source = new Source( canvas );
1032
+ texture.colorSpace = NoColorSpace;
1033
+ texture.channel = ( metalnessMap || roughnessMap ).channel;
1034
+
1035
+ if ( metalnessMap && roughnessMap && metalnessMap.channel !== roughnessMap.channel ) {
1036
+
1037
+ console.warn( 'THREE.GLTFExporter: UV channels for metalnessMap and roughnessMap textures must match.' );
1038
+
1039
+ }
1040
+
1041
+ console.warn( 'THREE.GLTFExporter: Merged metalnessMap and roughnessMap textures.' );
1042
+
1043
+ return texture;
1044
+
1045
+ }
1046
+
1047
+
1048
+ async decompressTextureAsync( texture, maxTextureSize = Infinity ) {
1049
+
1050
+ if ( this.textureUtils === null ) {
1051
+
1052
+ throw new Error( 'THREE.GLTFExporter: setTextureUtils() must be called to process compressed textures.' );
1053
+
1054
+ }
1055
+
1056
+ return await this.textureUtils.decompress( texture, maxTextureSize );
1057
+
1058
+ }
1059
+
1060
+ /**
1061
+ * Process a buffer to append to the default one.
1062
+ * @param {ArrayBuffer} buffer
1063
+ * @return {0}
1064
+ */
1065
+ processBuffer( buffer ) {
1066
+
1067
+ const json = this.json;
1068
+ const buffers = this.buffers;
1069
+
1070
+ if ( ! json.buffers ) json.buffers = [ { byteLength: 0 } ];
1071
+
1072
+ // All buffers are merged before export.
1073
+ buffers.push( buffer );
1074
+
1075
+ return 0;
1076
+
1077
+ }
1078
+
1079
+ /**
1080
+ * Process and generate a BufferView
1081
+ * @param {BufferAttribute} attribute
1082
+ * @param {number} componentType
1083
+ * @param {number} start
1084
+ * @param {number} count
1085
+ * @param {number} [target] Target usage of the BufferView
1086
+ * @return {Object}
1087
+ */
1088
+ processBufferView( attribute, componentType, start, count, target ) {
1089
+
1090
+ const json = this.json;
1091
+
1092
+ if ( ! json.bufferViews ) json.bufferViews = [];
1093
+
1094
+ // Create a new dataview and dump the attribute's array into it
1095
+
1096
+ let componentSize;
1097
+
1098
+ switch ( componentType ) {
1099
+
1100
+ case WEBGL_CONSTANTS.BYTE:
1101
+ case WEBGL_CONSTANTS.UNSIGNED_BYTE:
1102
+
1103
+ componentSize = 1;
1104
+
1105
+ break;
1106
+
1107
+ case WEBGL_CONSTANTS.SHORT:
1108
+ case WEBGL_CONSTANTS.UNSIGNED_SHORT:
1109
+
1110
+ componentSize = 2;
1111
+
1112
+ break;
1113
+
1114
+ default:
1115
+
1116
+ componentSize = 4;
1117
+
1118
+ }
1119
+
1120
+ let byteStride = attribute.itemSize * componentSize;
1121
+
1122
+ if ( target === WEBGL_CONSTANTS.ARRAY_BUFFER ) {
1123
+
1124
+ // Each element of a vertex attribute MUST be aligned to 4-byte boundaries
1125
+ // inside a bufferView
1126
+ byteStride = Math.ceil( byteStride / 4 ) * 4;
1127
+
1128
+ }
1129
+
1130
+ const byteLength = getPaddedBufferSize( count * byteStride );
1131
+ const dataView = new DataView( new ArrayBuffer( byteLength ) );
1132
+ let offset = 0;
1133
+
1134
+ for ( let i = start; i < start + count; i ++ ) {
1135
+
1136
+ for ( let a = 0; a < attribute.itemSize; a ++ ) {
1137
+
1138
+ let value;
1139
+
1140
+ if ( attribute.itemSize > 4 ) {
1141
+
1142
+ // no support for interleaved data for itemSize > 4
1143
+
1144
+ value = attribute.array[ i * attribute.itemSize + a ];
1145
+
1146
+ } else {
1147
+
1148
+ if ( a === 0 ) value = attribute.getX( i );
1149
+ else if ( a === 1 ) value = attribute.getY( i );
1150
+ else if ( a === 2 ) value = attribute.getZ( i );
1151
+ else if ( a === 3 ) value = attribute.getW( i );
1152
+
1153
+ if ( attribute.normalized === true ) {
1154
+
1155
+ value = MathUtils.normalize( value, attribute.array );
1156
+
1157
+ }
1158
+
1159
+ }
1160
+
1161
+ if ( componentType === WEBGL_CONSTANTS.FLOAT ) {
1162
+
1163
+ dataView.setFloat32( offset, value, true );
1164
+
1165
+ } else if ( componentType === WEBGL_CONSTANTS.INT ) {
1166
+
1167
+ dataView.setInt32( offset, value, true );
1168
+
1169
+ } else if ( componentType === WEBGL_CONSTANTS.UNSIGNED_INT ) {
1170
+
1171
+ dataView.setUint32( offset, value, true );
1172
+
1173
+ } else if ( componentType === WEBGL_CONSTANTS.SHORT ) {
1174
+
1175
+ dataView.setInt16( offset, value, true );
1176
+
1177
+ } else if ( componentType === WEBGL_CONSTANTS.UNSIGNED_SHORT ) {
1178
+
1179
+ dataView.setUint16( offset, value, true );
1180
+
1181
+ } else if ( componentType === WEBGL_CONSTANTS.BYTE ) {
1182
+
1183
+ dataView.setInt8( offset, value );
1184
+
1185
+ } else if ( componentType === WEBGL_CONSTANTS.UNSIGNED_BYTE ) {
1186
+
1187
+ dataView.setUint8( offset, value );
1188
+
1189
+ }
1190
+
1191
+ offset += componentSize;
1192
+
1193
+ }
1194
+
1195
+ if ( ( offset % byteStride ) !== 0 ) {
1196
+
1197
+ offset += byteStride - ( offset % byteStride );
1198
+
1199
+ }
1200
+
1201
+ }
1202
+
1203
+ const bufferViewDef = {
1204
+
1205
+ buffer: this.processBuffer( dataView.buffer ),
1206
+ byteOffset: this.byteOffset,
1207
+ byteLength: byteLength
1208
+
1209
+ };
1210
+
1211
+ if ( target !== undefined ) bufferViewDef.target = target;
1212
+
1213
+ if ( target === WEBGL_CONSTANTS.ARRAY_BUFFER ) {
1214
+
1215
+ // Only define byteStride for vertex attributes.
1216
+ bufferViewDef.byteStride = byteStride;
1217
+
1218
+ }
1219
+
1220
+ this.byteOffset += byteLength;
1221
+
1222
+ json.bufferViews.push( bufferViewDef );
1223
+
1224
+ // @TODO Merge bufferViews where possible.
1225
+ const output = {
1226
+
1227
+ id: json.bufferViews.length - 1,
1228
+ byteLength: 0
1229
+
1230
+ };
1231
+
1232
+ return output;
1233
+
1234
+ }
1235
+
1236
+ /**
1237
+ * Process and generate a BufferView from an image Blob.
1238
+ * @param {Blob} blob
1239
+ * @return {Promise<number>} An integer
1240
+ */
1241
+ processBufferViewImage( blob ) {
1242
+
1243
+ const writer = this;
1244
+ const json = writer.json;
1245
+
1246
+ if ( ! json.bufferViews ) json.bufferViews = [];
1247
+
1248
+ return new Promise( function ( resolve ) {
1249
+
1250
+ const reader = new FileReader();
1251
+ reader.readAsArrayBuffer( blob );
1252
+ reader.onloadend = function () {
1253
+
1254
+ const buffer = getPaddedArrayBuffer( reader.result );
1255
+
1256
+ const bufferViewDef = {
1257
+ buffer: writer.processBuffer( buffer ),
1258
+ byteOffset: writer.byteOffset,
1259
+ byteLength: buffer.byteLength
1260
+ };
1261
+
1262
+ writer.byteOffset += buffer.byteLength;
1263
+ resolve( json.bufferViews.push( bufferViewDef ) - 1 );
1264
+
1265
+ };
1266
+
1267
+ } );
1268
+
1269
+ }
1270
+
1271
+ /**
1272
+ * Process attribute to generate an accessor
1273
+ * @param {BufferAttribute} attribute Attribute to process
1274
+ * @param {?BufferGeometry} [geometry] Geometry used for truncated draw range
1275
+ * @param {number} [start=0]
1276
+ * @param {number} [count=Infinity]
1277
+ * @return {?number} Index of the processed accessor on the "accessors" array
1278
+ */
1279
+ processAccessor( attribute, geometry, start, count ) {
1280
+
1281
+ const json = this.json;
1282
+
1283
+ const types = {
1284
+
1285
+ 1: 'SCALAR',
1286
+ 2: 'VEC2',
1287
+ 3: 'VEC3',
1288
+ 4: 'VEC4',
1289
+ 9: 'MAT3',
1290
+ 16: 'MAT4'
1291
+
1292
+ };
1293
+
1294
+ let componentType;
1295
+
1296
+ // Detect the component type of the attribute array
1297
+ if ( attribute.array.constructor === Float32Array ) {
1298
+
1299
+ componentType = WEBGL_CONSTANTS.FLOAT;
1300
+
1301
+ } else if ( attribute.array.constructor === Int32Array ) {
1302
+
1303
+ componentType = WEBGL_CONSTANTS.INT;
1304
+
1305
+ } else if ( attribute.array.constructor === Uint32Array ) {
1306
+
1307
+ componentType = WEBGL_CONSTANTS.UNSIGNED_INT;
1308
+
1309
+ } else if ( attribute.array.constructor === Int16Array ) {
1310
+
1311
+ componentType = WEBGL_CONSTANTS.SHORT;
1312
+
1313
+ } else if ( attribute.array.constructor === Uint16Array ) {
1314
+
1315
+ componentType = WEBGL_CONSTANTS.UNSIGNED_SHORT;
1316
+
1317
+ } else if ( attribute.array.constructor === Int8Array ) {
1318
+
1319
+ componentType = WEBGL_CONSTANTS.BYTE;
1320
+
1321
+ } else if ( attribute.array.constructor === Uint8Array ) {
1322
+
1323
+ componentType = WEBGL_CONSTANTS.UNSIGNED_BYTE;
1324
+
1325
+ } else {
1326
+
1327
+ throw new Error( 'THREE.GLTFExporter: Unsupported bufferAttribute component type: ' + attribute.array.constructor.name );
1328
+
1329
+ }
1330
+
1331
+ if ( start === undefined ) start = 0;
1332
+ if ( count === undefined || count === Infinity ) count = attribute.count;
1333
+
1334
+ // Skip creating an accessor if the attribute doesn't have data to export
1335
+ if ( count === 0 ) return null;
1336
+
1337
+ const minMax = getMinMax( attribute, start, count );
1338
+ let bufferViewTarget;
1339
+
1340
+ // If geometry isn't provided, don't infer the target usage of the bufferView. For
1341
+ // animation samplers, target must not be set.
1342
+ if ( geometry !== undefined ) {
1343
+
1344
+ bufferViewTarget = attribute === geometry.index ? WEBGL_CONSTANTS.ELEMENT_ARRAY_BUFFER : WEBGL_CONSTANTS.ARRAY_BUFFER;
1345
+
1346
+ }
1347
+
1348
+ const bufferView = this.processBufferView( attribute, componentType, start, count, bufferViewTarget );
1349
+
1350
+ const accessorDef = {
1351
+
1352
+ bufferView: bufferView.id,
1353
+ byteOffset: bufferView.byteOffset,
1354
+ componentType: componentType,
1355
+ count: count,
1356
+ max: minMax.max,
1357
+ min: minMax.min,
1358
+ type: types[ attribute.itemSize ]
1359
+
1360
+ };
1361
+
1362
+ if ( attribute.normalized === true ) accessorDef.normalized = true;
1363
+ if ( ! json.accessors ) json.accessors = [];
1364
+
1365
+ return json.accessors.push( accessorDef ) - 1;
1366
+
1367
+ }
1368
+
1369
+ /**
1370
+ * Process image
1371
+ * @param {Image} image to process
1372
+ * @param {number} format Identifier of the format (RGBAFormat)
1373
+ * @param {boolean} flipY before writing out the image
1374
+ * @param {string} mimeType export format
1375
+ * @return {number} Index of the processed texture in the "images" array
1376
+ */
1377
+ processImage( image, format, flipY, mimeType = 'image/png' ) {
1378
+
1379
+ if ( image !== null ) {
1380
+
1381
+ const writer = this;
1382
+ const cache = writer.cache;
1383
+ const json = writer.json;
1384
+ const options = writer.options;
1385
+ const pending = writer.pending;
1386
+
1387
+ if ( ! cache.images.has( image ) ) cache.images.set( image, {} );
1388
+
1389
+ const cachedImages = cache.images.get( image );
1390
+
1391
+ const key = mimeType + ':flipY/' + flipY.toString();
1392
+
1393
+ if ( cachedImages[ key ] !== undefined ) return cachedImages[ key ];
1394
+
1395
+ if ( ! json.images ) json.images = [];
1396
+
1397
+ const imageDef = { mimeType: mimeType };
1398
+
1399
+ const canvas = getCanvas();
1400
+
1401
+ canvas.width = Math.min( image.width, options.maxTextureSize );
1402
+ canvas.height = Math.min( image.height, options.maxTextureSize );
1403
+
1404
+ const ctx = canvas.getContext( '2d', {
1405
+ willReadFrequently: true,
1406
+ } );
1407
+
1408
+ if ( flipY === true ) {
1409
+
1410
+ ctx.translate( 0, canvas.height );
1411
+ ctx.scale( 1, - 1 );
1412
+
1413
+ }
1414
+
1415
+ if ( image.data !== undefined ) { // THREE.DataTexture
1416
+
1417
+ if ( format !== RGBAFormat ) {
1418
+
1419
+ console.error( 'GLTFExporter: Only RGBAFormat is supported.', format );
1420
+
1421
+ }
1422
+
1423
+ if ( image.width > options.maxTextureSize || image.height > options.maxTextureSize ) {
1424
+
1425
+ console.warn( 'GLTFExporter: Image size is bigger than maxTextureSize', image );
1426
+
1427
+ }
1428
+
1429
+ const data = new Uint8ClampedArray( image.height * image.width * 4 );
1430
+
1431
+ for ( let i = 0; i < data.length; i += 4 ) {
1432
+
1433
+ data[ i + 0 ] = image.data[ i + 0 ];
1434
+ data[ i + 1 ] = image.data[ i + 1 ];
1435
+ data[ i + 2 ] = image.data[ i + 2 ];
1436
+ data[ i + 3 ] = image.data[ i + 3 ];
1437
+
1438
+ }
1439
+
1440
+ ctx.putImageData( new ImageData( data, image.width, image.height ), 0, 0 );
1441
+
1442
+ } else {
1443
+
1444
+ if ( ( typeof HTMLImageElement !== 'undefined' && image instanceof HTMLImageElement ) ||
1445
+ ( typeof HTMLCanvasElement !== 'undefined' && image instanceof HTMLCanvasElement ) ||
1446
+ ( typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap ) ||
1447
+ ( typeof OffscreenCanvas !== 'undefined' && image instanceof OffscreenCanvas ) ) {
1448
+
1449
+ ctx.drawImage( image, 0, 0, canvas.width, canvas.height );
1450
+
1451
+ } else {
1452
+
1453
+ throw new Error( 'THREE.GLTFExporter: Invalid image type. Use HTMLImageElement, HTMLCanvasElement, ImageBitmap or OffscreenCanvas.' );
1454
+
1455
+ }
1456
+
1457
+ }
1458
+
1459
+ if ( options.binary === true ) {
1460
+
1461
+ pending.push(
1462
+
1463
+ getToBlobPromise( canvas, mimeType )
1464
+ .then( blob => writer.processBufferViewImage( blob ) )
1465
+ .then( bufferViewIndex => {
1466
+
1467
+ imageDef.bufferView = bufferViewIndex;
1468
+
1469
+ } )
1470
+
1471
+ );
1472
+
1473
+ } else {
1474
+
1475
+ imageDef.uri = ImageUtils.getDataURL( canvas, mimeType );
1476
+
1477
+ }
1478
+
1479
+ const index = json.images.push( imageDef ) - 1;
1480
+ cachedImages[ key ] = index;
1481
+ return index;
1482
+
1483
+ } else {
1484
+
1485
+ throw new Error( 'THREE.GLTFExporter: No valid image data found. Unable to process texture.' );
1486
+
1487
+ }
1488
+
1489
+ }
1490
+
1491
+ /**
1492
+ * Process sampler
1493
+ * @param {Texture} map Texture to process
1494
+ * @return {number} Index of the processed texture in the "samplers" array
1495
+ */
1496
+ processSampler( map ) {
1497
+
1498
+ const json = this.json;
1499
+
1500
+ if ( ! json.samplers ) json.samplers = [];
1501
+
1502
+ const samplerDef = {
1503
+ magFilter: THREE_TO_WEBGL[ map.magFilter ],
1504
+ minFilter: THREE_TO_WEBGL[ map.minFilter ],
1505
+ wrapS: THREE_TO_WEBGL[ map.wrapS ],
1506
+ wrapT: THREE_TO_WEBGL[ map.wrapT ]
1507
+ };
1508
+
1509
+ return json.samplers.push( samplerDef ) - 1;
1510
+
1511
+ }
1512
+
1513
+ /**
1514
+ * Process texture
1515
+ * @param {Texture} map Map to process
1516
+ * @return {Promise<number>} Index of the processed texture in the "textures" array
1517
+ */
1518
+ async processTextureAsync( map ) {
1519
+
1520
+ const writer = this;
1521
+ const options = writer.options;
1522
+ const cache = this.cache;
1523
+ const json = this.json;
1524
+
1525
+ if ( cache.textures.has( map ) ) return cache.textures.get( map );
1526
+
1527
+ if ( ! json.textures ) json.textures = [];
1528
+
1529
+ // make non-readable textures (e.g. CompressedTexture) readable by blitting them into a new texture
1530
+ if ( map instanceof CompressedTexture ) {
1531
+
1532
+ map = await this.decompressTextureAsync( map, options.maxTextureSize );
1533
+
1534
+ }
1535
+
1536
+ let mimeType = map.userData.mimeType;
1537
+
1538
+ if ( mimeType === 'image/webp' ) mimeType = 'image/png';
1539
+
1540
+ const textureDef = {
1541
+ sampler: this.processSampler( map ),
1542
+ source: this.processImage( map.image, map.format, map.flipY, mimeType )
1543
+ };
1544
+
1545
+ if ( map.name ) textureDef.name = map.name;
1546
+
1547
+ await this._invokeAllAsync( async function ( ext ) {
1548
+
1549
+ ext.writeTexture && await ext.writeTexture( map, textureDef );
1550
+
1551
+ } );
1552
+
1553
+ const index = json.textures.push( textureDef ) - 1;
1554
+ cache.textures.set( map, index );
1555
+ return index;
1556
+
1557
+ }
1558
+
1559
+ /**
1560
+ * Process material
1561
+ * @param {THREE.Material} material Material to process
1562
+ * @return {Promise<?number>} Index of the processed material in the "materials" array
1563
+ */
1564
+ async processMaterialAsync( material ) {
1565
+
1566
+ const cache = this.cache;
1567
+ const json = this.json;
1568
+
1569
+ if ( cache.materials.has( material ) ) return cache.materials.get( material );
1570
+
1571
+ if ( material.isShaderMaterial ) {
1572
+
1573
+ console.warn( 'GLTFExporter: THREE.ShaderMaterial not supported.' );
1574
+ return null;
1575
+
1576
+ }
1577
+
1578
+ if ( ! json.materials ) json.materials = [];
1579
+
1580
+ // @QUESTION Should we avoid including any attribute that has the default value?
1581
+ const materialDef = { pbrMetallicRoughness: {} };
1582
+
1583
+ if ( material.isMeshStandardMaterial !== true && material.isMeshBasicMaterial !== true ) {
1584
+
1585
+ console.warn( 'GLTFExporter: Use MeshStandardMaterial or MeshBasicMaterial for best results.' );
1586
+
1587
+ }
1588
+
1589
+ // pbrMetallicRoughness.baseColorFactor
1590
+ const color = material.color.toArray().concat( [ material.opacity ] );
1591
+
1592
+ if ( ! equalArray( color, [ 1, 1, 1, 1 ] ) ) {
1593
+
1594
+ materialDef.pbrMetallicRoughness.baseColorFactor = color;
1595
+
1596
+ }
1597
+
1598
+ if ( material.isMeshStandardMaterial ) {
1599
+
1600
+ materialDef.pbrMetallicRoughness.metallicFactor = material.metalness;
1601
+ materialDef.pbrMetallicRoughness.roughnessFactor = material.roughness;
1602
+
1603
+ } else {
1604
+
1605
+ materialDef.pbrMetallicRoughness.metallicFactor = 0;
1606
+ materialDef.pbrMetallicRoughness.roughnessFactor = 1;
1607
+
1608
+ }
1609
+
1610
+ // pbrMetallicRoughness.metallicRoughnessTexture
1611
+ if ( material.metalnessMap || material.roughnessMap ) {
1612
+
1613
+ const metalRoughTexture = await this.buildMetalRoughTextureAsync( material.metalnessMap, material.roughnessMap );
1614
+
1615
+ const metalRoughMapDef = {
1616
+ index: await this.processTextureAsync( metalRoughTexture ),
1617
+ texCoord: metalRoughTexture.channel
1618
+ };
1619
+ this.applyTextureTransform( metalRoughMapDef, metalRoughTexture );
1620
+ materialDef.pbrMetallicRoughness.metallicRoughnessTexture = metalRoughMapDef;
1621
+
1622
+ }
1623
+
1624
+ // pbrMetallicRoughness.baseColorTexture
1625
+ if ( material.map ) {
1626
+
1627
+ const baseColorMapDef = {
1628
+ index: await this.processTextureAsync( material.map ),
1629
+ texCoord: material.map.channel
1630
+ };
1631
+ this.applyTextureTransform( baseColorMapDef, material.map );
1632
+ materialDef.pbrMetallicRoughness.baseColorTexture = baseColorMapDef;
1633
+
1634
+ }
1635
+
1636
+ if ( material.emissive ) {
1637
+
1638
+ const emissive = material.emissive;
1639
+ const maxEmissiveComponent = Math.max( emissive.r, emissive.g, emissive.b );
1640
+
1641
+ if ( maxEmissiveComponent > 0 ) {
1642
+
1643
+ materialDef.emissiveFactor = material.emissive.toArray();
1644
+
1645
+ }
1646
+
1647
+ // emissiveTexture
1648
+ if ( material.emissiveMap ) {
1649
+
1650
+ const emissiveMapDef = {
1651
+ index: await this.processTextureAsync( material.emissiveMap ),
1652
+ texCoord: material.emissiveMap.channel
1653
+ };
1654
+ this.applyTextureTransform( emissiveMapDef, material.emissiveMap );
1655
+ materialDef.emissiveTexture = emissiveMapDef;
1656
+
1657
+ }
1658
+
1659
+ }
1660
+
1661
+ // normalTexture
1662
+ if ( material.normalMap ) {
1663
+
1664
+ const normalMapDef = {
1665
+ index: await this.processTextureAsync( material.normalMap ),
1666
+ texCoord: material.normalMap.channel
1667
+ };
1668
+
1669
+ if ( material.normalScale && material.normalScale.x !== 1 ) {
1670
+
1671
+ // glTF normal scale is univariate. Ignore `y`, which may be flipped.
1672
+ // Context: https://github.com/mrdoob/three.js/issues/11438#issuecomment-507003995
1673
+ normalMapDef.scale = material.normalScale.x;
1674
+
1675
+ }
1676
+
1677
+ this.applyTextureTransform( normalMapDef, material.normalMap );
1678
+ materialDef.normalTexture = normalMapDef;
1679
+
1680
+ }
1681
+
1682
+ // occlusionTexture
1683
+ if ( material.aoMap ) {
1684
+
1685
+ const occlusionMapDef = {
1686
+ index: await this.processTextureAsync( material.aoMap ),
1687
+ texCoord: material.aoMap.channel
1688
+ };
1689
+
1690
+ if ( material.aoMapIntensity !== 1.0 ) {
1691
+
1692
+ occlusionMapDef.strength = material.aoMapIntensity;
1693
+
1694
+ }
1695
+
1696
+ this.applyTextureTransform( occlusionMapDef, material.aoMap );
1697
+ materialDef.occlusionTexture = occlusionMapDef;
1698
+
1699
+ }
1700
+
1701
+ // alphaMode
1702
+ if ( material.transparent ) {
1703
+
1704
+ materialDef.alphaMode = 'BLEND';
1705
+
1706
+ } else {
1707
+
1708
+ if ( material.alphaTest > 0.0 ) {
1709
+
1710
+ materialDef.alphaMode = 'MASK';
1711
+ materialDef.alphaCutoff = material.alphaTest;
1712
+
1713
+ }
1714
+
1715
+ }
1716
+
1717
+ // doubleSided
1718
+ if ( material.side === DoubleSide ) materialDef.doubleSided = true;
1719
+ if ( material.name !== '' ) materialDef.name = material.name;
1720
+
1721
+ this.serializeUserData( material, materialDef );
1722
+
1723
+ await this._invokeAllAsync( async function ( ext ) {
1724
+
1725
+ ext.writeMaterialAsync && await ext.writeMaterialAsync( material, materialDef );
1726
+
1727
+ } );
1728
+
1729
+ const index = json.materials.push( materialDef ) - 1;
1730
+ cache.materials.set( material, index );
1731
+ return index;
1732
+
1733
+ }
1734
+
1735
+ /**
1736
+ * Process mesh
1737
+ * @param {THREE.Mesh} mesh Mesh to process
1738
+ * @return {Promise<?number>} Index of the processed mesh in the "meshes" array
1739
+ */
1740
+ async processMeshAsync( mesh ) {
1741
+
1742
+ const cache = this.cache;
1743
+ const json = this.json;
1744
+
1745
+ const meshCacheKeyParts = [ mesh.geometry.uuid ];
1746
+
1747
+ if ( Array.isArray( mesh.material ) ) {
1748
+
1749
+ for ( let i = 0, l = mesh.material.length; i < l; i ++ ) {
1750
+
1751
+ meshCacheKeyParts.push( mesh.material[ i ].uuid );
1752
+
1753
+ }
1754
+
1755
+ } else {
1756
+
1757
+ meshCacheKeyParts.push( mesh.material.uuid );
1758
+
1759
+ }
1760
+
1761
+ const meshCacheKey = meshCacheKeyParts.join( ':' );
1762
+
1763
+ if ( cache.meshes.has( meshCacheKey ) ) return cache.meshes.get( meshCacheKey );
1764
+
1765
+ const geometry = mesh.geometry;
1766
+
1767
+ let mode;
1768
+
1769
+ // Use the correct mode
1770
+ if ( mesh.isLineSegments ) {
1771
+
1772
+ mode = WEBGL_CONSTANTS.LINES;
1773
+
1774
+ } else if ( mesh.isLineLoop ) {
1775
+
1776
+ mode = WEBGL_CONSTANTS.LINE_LOOP;
1777
+
1778
+ } else if ( mesh.isLine ) {
1779
+
1780
+ mode = WEBGL_CONSTANTS.LINE_STRIP;
1781
+
1782
+ } else if ( mesh.isPoints ) {
1783
+
1784
+ mode = WEBGL_CONSTANTS.POINTS;
1785
+
1786
+ } else {
1787
+
1788
+ mode = mesh.material.wireframe ? WEBGL_CONSTANTS.LINES : WEBGL_CONSTANTS.TRIANGLES;
1789
+
1790
+ }
1791
+
1792
+ const meshDef = {};
1793
+ const attributes = {};
1794
+ const primitives = [];
1795
+ const targets = [];
1796
+
1797
+ // Conversion between attributes names in threejs and gltf spec
1798
+ const nameConversion = {
1799
+ uv: 'TEXCOORD_0',
1800
+ uv1: 'TEXCOORD_1',
1801
+ uv2: 'TEXCOORD_2',
1802
+ uv3: 'TEXCOORD_3',
1803
+ color: 'COLOR_0',
1804
+ skinWeight: 'WEIGHTS_0',
1805
+ skinIndex: 'JOINTS_0'
1806
+ };
1807
+
1808
+ const originalNormal = geometry.getAttribute( 'normal' );
1809
+
1810
+ if ( originalNormal !== undefined && ! this.isNormalizedNormalAttribute( originalNormal ) ) {
1811
+
1812
+ console.warn( 'THREE.GLTFExporter: Creating normalized normal attribute from the non-normalized one.' );
1813
+
1814
+ geometry.setAttribute( 'normal', this.createNormalizedNormalAttribute( originalNormal ) );
1815
+
1816
+ }
1817
+
1818
+ // @QUESTION Detect if .vertexColors = true?
1819
+ // For every attribute create an accessor
1820
+ let modifiedAttribute = null;
1821
+
1822
+ for ( let attributeName in geometry.attributes ) {
1823
+
1824
+ // Ignore morph target attributes, which are exported later.
1825
+ if ( attributeName.slice( 0, 5 ) === 'morph' ) continue;
1826
+
1827
+ const attribute = geometry.attributes[ attributeName ];
1828
+ attributeName = nameConversion[ attributeName ] || attributeName.toUpperCase();
1829
+
1830
+ // Prefix all geometry attributes except the ones specifically
1831
+ // listed in the spec; non-spec attributes are considered custom.
1832
+ const validVertexAttributes =
1833
+ /^(POSITION|NORMAL|TANGENT|TEXCOORD_\d+|COLOR_\d+|JOINTS_\d+|WEIGHTS_\d+)$/;
1834
+
1835
+ if ( ! validVertexAttributes.test( attributeName ) ) attributeName = '_' + attributeName;
1836
+
1837
+ if ( cache.attributes.has( this.getUID( attribute ) ) ) {
1838
+
1839
+ attributes[ attributeName ] = cache.attributes.get( this.getUID( attribute ) );
1840
+ continue;
1841
+
1842
+ }
1843
+
1844
+ // Enforce glTF vertex attribute requirements:
1845
+ // - JOINTS_0 must be UNSIGNED_BYTE or UNSIGNED_SHORT
1846
+ // - Only custom attributes may be INT or UNSIGNED_INT
1847
+ modifiedAttribute = null;
1848
+ const array = attribute.array;
1849
+
1850
+ if ( attributeName === 'JOINTS_0' &&
1851
+ ! ( array instanceof Uint16Array ) &&
1852
+ ! ( array instanceof Uint8Array ) ) {
1853
+
1854
+ console.warn( 'GLTFExporter: Attribute "skinIndex" converted to type UNSIGNED_SHORT.' );
1855
+ modifiedAttribute = new BufferAttribute( new Uint16Array( array ), attribute.itemSize, attribute.normalized );
1856
+
1857
+ } else if ( ( array instanceof Uint32Array || array instanceof Int32Array ) && ! attributeName.startsWith( '_' ) ) {
1858
+
1859
+ console.warn( `GLTFExporter: Attribute "${ attributeName }" converted to type FLOAT.` );
1860
+ modifiedAttribute = GLTFExporter.Utils.toFloat32BufferAttribute( attribute );
1861
+
1862
+ }
1863
+
1864
+ const accessor = this.processAccessor( modifiedAttribute || attribute, geometry );
1865
+
1866
+ if ( accessor !== null ) {
1867
+
1868
+ if ( ! attributeName.startsWith( '_' ) ) {
1869
+
1870
+ this.detectMeshQuantization( attributeName, attribute );
1871
+
1872
+ }
1873
+
1874
+ attributes[ attributeName ] = accessor;
1875
+ cache.attributes.set( this.getUID( attribute ), accessor );
1876
+
1877
+ }
1878
+
1879
+ }
1880
+
1881
+ if ( originalNormal !== undefined ) geometry.setAttribute( 'normal', originalNormal );
1882
+
1883
+ // Skip if no exportable attributes found
1884
+ if ( Object.keys( attributes ).length === 0 ) return null;
1885
+
1886
+ // Morph targets
1887
+ if ( mesh.morphTargetInfluences !== undefined && mesh.morphTargetInfluences.length > 0 ) {
1888
+
1889
+ const weights = [];
1890
+ const targetNames = [];
1891
+ const reverseDictionary = {};
1892
+
1893
+ if ( mesh.morphTargetDictionary !== undefined ) {
1894
+
1895
+ for ( const key in mesh.morphTargetDictionary ) {
1896
+
1897
+ reverseDictionary[ mesh.morphTargetDictionary[ key ] ] = key;
1898
+
1899
+ }
1900
+
1901
+ }
1902
+
1903
+ for ( let i = 0; i < mesh.morphTargetInfluences.length; ++ i ) {
1904
+
1905
+ const target = {};
1906
+ let warned = false;
1907
+
1908
+ for ( const attributeName in geometry.morphAttributes ) {
1909
+
1910
+ // glTF 2.0 morph supports only POSITION/NORMAL/TANGENT.
1911
+ // Three.js doesn't support TANGENT yet.
1912
+
1913
+ if ( attributeName !== 'position' && attributeName !== 'normal' ) {
1914
+
1915
+ if ( ! warned ) {
1916
+
1917
+ console.warn( 'GLTFExporter: Only POSITION and NORMAL morph are supported.' );
1918
+ warned = true;
1919
+
1920
+ }
1921
+
1922
+ continue;
1923
+
1924
+ }
1925
+
1926
+ const attribute = geometry.morphAttributes[ attributeName ][ i ];
1927
+ const gltfAttributeName = attributeName.toUpperCase();
1928
+
1929
+ // Three.js morph attribute has absolute values while the one of glTF has relative values.
1930
+ //
1931
+ // glTF 2.0 Specification:
1932
+ // https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#morph-targets
1933
+
1934
+ const baseAttribute = geometry.attributes[ attributeName ];
1935
+
1936
+ if ( cache.attributes.has( this.getUID( attribute, true ) ) ) {
1937
+
1938
+ target[ gltfAttributeName ] = cache.attributes.get( this.getUID( attribute, true ) );
1939
+ continue;
1940
+
1941
+ }
1942
+
1943
+ // Clones attribute not to override
1944
+ const relativeAttribute = attribute.clone();
1945
+
1946
+ if ( ! geometry.morphTargetsRelative ) {
1947
+
1948
+ for ( let j = 0, jl = attribute.count; j < jl; j ++ ) {
1949
+
1950
+ for ( let a = 0; a < attribute.itemSize; a ++ ) {
1951
+
1952
+ if ( a === 0 ) relativeAttribute.setX( j, attribute.getX( j ) - baseAttribute.getX( j ) );
1953
+ if ( a === 1 ) relativeAttribute.setY( j, attribute.getY( j ) - baseAttribute.getY( j ) );
1954
+ if ( a === 2 ) relativeAttribute.setZ( j, attribute.getZ( j ) - baseAttribute.getZ( j ) );
1955
+ if ( a === 3 ) relativeAttribute.setW( j, attribute.getW( j ) - baseAttribute.getW( j ) );
1956
+
1957
+ }
1958
+
1959
+ }
1960
+
1961
+ }
1962
+
1963
+ target[ gltfAttributeName ] = this.processAccessor( relativeAttribute, geometry );
1964
+ cache.attributes.set( this.getUID( baseAttribute, true ), target[ gltfAttributeName ] );
1965
+
1966
+ }
1967
+
1968
+ targets.push( target );
1969
+
1970
+ weights.push( mesh.morphTargetInfluences[ i ] );
1971
+
1972
+ if ( mesh.morphTargetDictionary !== undefined ) targetNames.push( reverseDictionary[ i ] );
1973
+
1974
+ }
1975
+
1976
+ meshDef.weights = weights;
1977
+
1978
+ if ( targetNames.length > 0 ) {
1979
+
1980
+ meshDef.extras = {};
1981
+ meshDef.extras.targetNames = targetNames;
1982
+
1983
+ }
1984
+
1985
+ }
1986
+
1987
+ const isMultiMaterial = Array.isArray( mesh.material );
1988
+
1989
+ if ( isMultiMaterial && geometry.groups.length === 0 ) return null;
1990
+
1991
+ let didForceIndices = false;
1992
+
1993
+ if ( isMultiMaterial && geometry.index === null ) {
1994
+
1995
+ const indices = [];
1996
+
1997
+ for ( let i = 0, il = geometry.attributes.position.count; i < il; i ++ ) {
1998
+
1999
+ indices[ i ] = i;
2000
+
2001
+ }
2002
+
2003
+ geometry.setIndex( indices );
2004
+
2005
+ didForceIndices = true;
2006
+
2007
+ }
2008
+
2009
+ const materials = isMultiMaterial ? mesh.material : [ mesh.material ];
2010
+ const groups = isMultiMaterial ? geometry.groups : [ { materialIndex: 0, start: undefined, count: undefined } ];
2011
+
2012
+ for ( let i = 0, il = groups.length; i < il; i ++ ) {
2013
+
2014
+ const primitive = {
2015
+ mode: mode,
2016
+ attributes: attributes,
2017
+ };
2018
+
2019
+ this.serializeUserData( geometry, primitive );
2020
+
2021
+ if ( targets.length > 0 ) primitive.targets = targets;
2022
+
2023
+ if ( geometry.index !== null ) {
2024
+
2025
+ let cacheKey = this.getUID( geometry.index );
2026
+
2027
+ if ( groups[ i ].start !== undefined || groups[ i ].count !== undefined ) {
2028
+
2029
+ cacheKey += ':' + groups[ i ].start + ':' + groups[ i ].count;
2030
+
2031
+ }
2032
+
2033
+ if ( cache.attributes.has( cacheKey ) ) {
2034
+
2035
+ primitive.indices = cache.attributes.get( cacheKey );
2036
+
2037
+ } else {
2038
+
2039
+ primitive.indices = this.processAccessor( geometry.index, geometry, groups[ i ].start, groups[ i ].count );
2040
+ cache.attributes.set( cacheKey, primitive.indices );
2041
+
2042
+ }
2043
+
2044
+ if ( primitive.indices === null ) delete primitive.indices;
2045
+
2046
+ }
2047
+
2048
+ const material = await this.processMaterialAsync( materials[ groups[ i ].materialIndex ] );
2049
+
2050
+ if ( material !== null ) primitive.material = material;
2051
+
2052
+ primitives.push( primitive );
2053
+
2054
+ }
2055
+
2056
+ if ( didForceIndices === true ) {
2057
+
2058
+ geometry.setIndex( null );
2059
+
2060
+ }
2061
+
2062
+ meshDef.primitives = primitives;
2063
+
2064
+ if ( ! json.meshes ) json.meshes = [];
2065
+
2066
+ await this._invokeAllAsync( function ( ext ) {
2067
+
2068
+ ext.writeMesh && ext.writeMesh( mesh, meshDef );
2069
+
2070
+ } );
2071
+
2072
+ const index = json.meshes.push( meshDef ) - 1;
2073
+ cache.meshes.set( meshCacheKey, index );
2074
+ return index;
2075
+
2076
+ }
2077
+
2078
+ /**
2079
+ * If a vertex attribute with a
2080
+ * [non-standard data type](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#meshes-overview)
2081
+ * is used, it is checked whether it is a valid data type according to the
2082
+ * [KHR_mesh_quantization](https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_mesh_quantization/README.md)
2083
+ * extension.
2084
+ * In this case the extension is automatically added to the list of used extensions.
2085
+ *
2086
+ * @param {string} attributeName
2087
+ * @param {THREE.BufferAttribute} attribute
2088
+ */
2089
+ detectMeshQuantization( attributeName, attribute ) {
2090
+
2091
+ if ( this.extensionsUsed[ KHR_MESH_QUANTIZATION ] ) return;
2092
+
2093
+ let attrType = undefined;
2094
+
2095
+ switch ( attribute.array.constructor ) {
2096
+
2097
+ case Int8Array:
2098
+
2099
+ attrType = 'byte';
2100
+
2101
+ break;
2102
+
2103
+ case Uint8Array:
2104
+
2105
+ attrType = 'unsigned byte';
2106
+
2107
+ break;
2108
+
2109
+ case Int16Array:
2110
+
2111
+ attrType = 'short';
2112
+
2113
+ break;
2114
+
2115
+ case Uint16Array:
2116
+
2117
+ attrType = 'unsigned short';
2118
+
2119
+ break;
2120
+
2121
+ default:
2122
+
2123
+ return;
2124
+
2125
+ }
2126
+
2127
+ if ( attribute.normalized ) attrType += ' normalized';
2128
+
2129
+ const attrNamePrefix = attributeName.split( '_', 1 )[ 0 ];
2130
+
2131
+ if ( KHR_mesh_quantization_ExtraAttrTypes[ attrNamePrefix ] && KHR_mesh_quantization_ExtraAttrTypes[ attrNamePrefix ].includes( attrType ) ) {
2132
+
2133
+ this.extensionsUsed[ KHR_MESH_QUANTIZATION ] = true;
2134
+ this.extensionsRequired[ KHR_MESH_QUANTIZATION ] = true;
2135
+
2136
+ }
2137
+
2138
+ }
2139
+
2140
+ /**
2141
+ * Process camera
2142
+ * @param {THREE.Camera} camera Camera to process
2143
+ * @return {number} Index of the processed mesh in the "camera" array
2144
+ */
2145
+ processCamera( camera ) {
2146
+
2147
+ const json = this.json;
2148
+
2149
+ if ( ! json.cameras ) json.cameras = [];
2150
+
2151
+ const isOrtho = camera.isOrthographicCamera;
2152
+
2153
+ const cameraDef = {
2154
+ type: isOrtho ? 'orthographic' : 'perspective'
2155
+ };
2156
+
2157
+ if ( isOrtho ) {
2158
+
2159
+ cameraDef.orthographic = {
2160
+ xmag: camera.right * 2,
2161
+ ymag: camera.top * 2,
2162
+ zfar: camera.far <= 0 ? 0.001 : camera.far,
2163
+ znear: camera.near < 0 ? 0 : camera.near
2164
+ };
2165
+
2166
+ } else {
2167
+
2168
+ cameraDef.perspective = {
2169
+ aspectRatio: camera.aspect,
2170
+ yfov: MathUtils.degToRad( camera.fov ),
2171
+ zfar: camera.far <= 0 ? 0.001 : camera.far,
2172
+ znear: camera.near < 0 ? 0 : camera.near
2173
+ };
2174
+
2175
+ }
2176
+
2177
+ // Question: Is saving "type" as name intentional?
2178
+ if ( camera.name !== '' ) cameraDef.name = camera.type;
2179
+
2180
+ return json.cameras.push( cameraDef ) - 1;
2181
+
2182
+ }
2183
+
2184
+ /**
2185
+ * Creates glTF animation entry from AnimationClip object.
2186
+ *
2187
+ * Status:
2188
+ * - Only properties listed in PATH_PROPERTIES may be animated.
2189
+ *
2190
+ * @param {THREE.AnimationClip} clip
2191
+ * @param {THREE.Object3D} root
2192
+ * @return {?number}
2193
+ */
2194
+ processAnimation( clip, root ) {
2195
+
2196
+ const json = this.json;
2197
+ const nodeMap = this.nodeMap;
2198
+
2199
+ if ( ! json.animations ) json.animations = [];
2200
+
2201
+ clip = GLTFExporter.Utils.mergeMorphTargetTracks( clip.clone(), root );
2202
+
2203
+ const tracks = clip.tracks;
2204
+ const channels = [];
2205
+ const samplers = [];
2206
+
2207
+ for ( let i = 0; i < tracks.length; ++ i ) {
2208
+
2209
+ const track = tracks[ i ];
2210
+ const trackBinding = PropertyBinding.parseTrackName( track.name );
2211
+ let trackNode = PropertyBinding.findNode( root, trackBinding.nodeName );
2212
+ const trackProperty = PATH_PROPERTIES[ trackBinding.propertyName ];
2213
+
2214
+ if ( trackBinding.objectName === 'bones' ) {
2215
+
2216
+ if ( trackNode.isSkinnedMesh === true ) {
2217
+
2218
+ trackNode = trackNode.skeleton.getBoneByName( trackBinding.objectIndex );
2219
+
2220
+ } else {
2221
+
2222
+ trackNode = undefined;
2223
+
2224
+ }
2225
+
2226
+ }
2227
+
2228
+ if ( ! trackNode || ! trackProperty ) {
2229
+
2230
+ console.warn( 'THREE.GLTFExporter: Could not export animation track "%s".', track.name );
2231
+ continue;
2232
+
2233
+ }
2234
+
2235
+ const inputItemSize = 1;
2236
+ let outputItemSize = track.values.length / track.times.length;
2237
+
2238
+ if ( trackProperty === PATH_PROPERTIES.morphTargetInfluences ) {
2239
+
2240
+ outputItemSize /= trackNode.morphTargetInfluences.length;
2241
+
2242
+ }
2243
+
2244
+ let interpolation;
2245
+
2246
+ // @TODO export CubicInterpolant(InterpolateSmooth) as CUBICSPLINE
2247
+
2248
+ // Detecting glTF cubic spline interpolant by checking factory method's special property
2249
+ // GLTFCubicSplineInterpolant is a custom interpolant and track doesn't return
2250
+ // valid value from .getInterpolation().
2251
+ if ( track.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline === true ) {
2252
+
2253
+ interpolation = 'CUBICSPLINE';
2254
+
2255
+ // itemSize of CUBICSPLINE keyframe is 9
2256
+ // (VEC3 * 3: inTangent, splineVertex, and outTangent)
2257
+ // but needs to be stored as VEC3 so dividing by 3 here.
2258
+ outputItemSize /= 3;
2259
+
2260
+ } else if ( track.getInterpolation() === InterpolateDiscrete ) {
2261
+
2262
+ interpolation = 'STEP';
2263
+
2264
+ } else {
2265
+
2266
+ interpolation = 'LINEAR';
2267
+
2268
+ }
2269
+
2270
+ samplers.push( {
2271
+ input: this.processAccessor( new BufferAttribute( track.times, inputItemSize ) ),
2272
+ output: this.processAccessor( new BufferAttribute( track.values, outputItemSize ) ),
2273
+ interpolation: interpolation
2274
+ } );
2275
+
2276
+ channels.push( {
2277
+ sampler: samplers.length - 1,
2278
+ target: {
2279
+ node: nodeMap.get( trackNode ),
2280
+ path: trackProperty
2281
+ }
2282
+ } );
2283
+
2284
+ }
2285
+
2286
+ const animationDef = {
2287
+ name: clip.name || 'clip_' + json.animations.length,
2288
+ samplers: samplers,
2289
+ channels: channels
2290
+ };
2291
+
2292
+ this.serializeUserData( clip, animationDef );
2293
+
2294
+ json.animations.push( animationDef );
2295
+
2296
+ return json.animations.length - 1;
2297
+
2298
+ }
2299
+
2300
+ /**
2301
+ * @param {THREE.Object3D} object
2302
+ * @return {?number}
2303
+ */
2304
+ processSkin( object ) {
2305
+
2306
+ const json = this.json;
2307
+ const nodeMap = this.nodeMap;
2308
+
2309
+ const node = json.nodes[ nodeMap.get( object ) ];
2310
+
2311
+ const skeleton = object.skeleton;
2312
+
2313
+ if ( skeleton === undefined ) return null;
2314
+
2315
+ const rootJoint = object.skeleton.bones[ 0 ];
2316
+
2317
+ if ( rootJoint === undefined ) return null;
2318
+
2319
+ const joints = [];
2320
+ const inverseBindMatrices = new Float32Array( skeleton.bones.length * 16 );
2321
+ const temporaryBoneInverse = new Matrix4();
2322
+
2323
+ for ( let i = 0; i < skeleton.bones.length; ++ i ) {
2324
+
2325
+ joints.push( nodeMap.get( skeleton.bones[ i ] ) );
2326
+ temporaryBoneInverse.copy( skeleton.boneInverses[ i ] );
2327
+ temporaryBoneInverse.multiply( object.bindMatrix ).toArray( inverseBindMatrices, i * 16 );
2328
+
2329
+ }
2330
+
2331
+ if ( json.skins === undefined ) json.skins = [];
2332
+
2333
+ json.skins.push( {
2334
+ inverseBindMatrices: this.processAccessor( new BufferAttribute( inverseBindMatrices, 16 ) ),
2335
+ joints: joints,
2336
+ skeleton: nodeMap.get( rootJoint )
2337
+ } );
2338
+
2339
+ const skinIndex = node.skin = json.skins.length - 1;
2340
+
2341
+ return skinIndex;
2342
+
2343
+ }
2344
+
2345
+ /**
2346
+ * Process Object3D node
2347
+ * @param {THREE.Object3D} object Object3D to processNodeAsync
2348
+ * @return {Promise<number>} Index of the node in the nodes list
2349
+ */
2350
+ async processNodeAsync( object ) {
2351
+
2352
+ const json = this.json;
2353
+ const options = this.options;
2354
+ const nodeMap = this.nodeMap;
2355
+
2356
+ if ( ! json.nodes ) json.nodes = [];
2357
+
2358
+ const nodeDef = {};
2359
+
2360
+ if ( options.trs ) {
2361
+
2362
+ const rotation = object.quaternion.toArray();
2363
+ const position = object.position.toArray();
2364
+ const scale = object.scale.toArray();
2365
+
2366
+ if ( ! equalArray( rotation, [ 0, 0, 0, 1 ] ) ) {
2367
+
2368
+ nodeDef.rotation = rotation;
2369
+
2370
+ }
2371
+
2372
+ if ( ! equalArray( position, [ 0, 0, 0 ] ) ) {
2373
+
2374
+ nodeDef.translation = position;
2375
+
2376
+ }
2377
+
2378
+ if ( ! equalArray( scale, [ 1, 1, 1 ] ) ) {
2379
+
2380
+ nodeDef.scale = scale;
2381
+
2382
+ }
2383
+
2384
+ } else {
2385
+
2386
+ if ( object.matrixAutoUpdate ) {
2387
+
2388
+ object.updateMatrix();
2389
+
2390
+ }
2391
+
2392
+ if ( isIdentityMatrix( object.matrix ) === false ) {
2393
+
2394
+ nodeDef.matrix = object.matrix.elements;
2395
+
2396
+ }
2397
+
2398
+ }
2399
+
2400
+ // We don't export empty strings name because it represents no-name in Three.js.
2401
+ if ( object.name !== '' ) nodeDef.name = String( object.name );
2402
+
2403
+ this.serializeUserData( object, nodeDef );
2404
+
2405
+ if ( object.isMesh || object.isLine || object.isPoints ) {
2406
+
2407
+ const meshIndex = await this.processMeshAsync( object );
2408
+
2409
+ if ( meshIndex !== null ) nodeDef.mesh = meshIndex;
2410
+
2411
+ } else if ( object.isCamera ) {
2412
+
2413
+ nodeDef.camera = this.processCamera( object );
2414
+
2415
+ }
2416
+
2417
+ if ( object.isSkinnedMesh ) this.skins.push( object );
2418
+
2419
+ const nodeIndex = json.nodes.push( nodeDef ) - 1;
2420
+ nodeMap.set( object, nodeIndex );
2421
+
2422
+ if ( object.children.length > 0 ) {
2423
+
2424
+ const children = [];
2425
+
2426
+ for ( let i = 0, l = object.children.length; i < l; i ++ ) {
2427
+
2428
+ const child = object.children[ i ];
2429
+
2430
+ if ( child.visible || options.onlyVisible === false ) {
2431
+
2432
+ const childNodeIndex = await this.processNodeAsync( child );
2433
+
2434
+ if ( childNodeIndex !== null ) children.push( childNodeIndex );
2435
+
2436
+ }
2437
+
2438
+ }
2439
+
2440
+ if ( children.length > 0 ) nodeDef.children = children;
2441
+
2442
+ }
2443
+
2444
+ await this._invokeAllAsync( function ( ext ) {
2445
+
2446
+ ext.writeNode && ext.writeNode( object, nodeDef );
2447
+
2448
+ } );
2449
+
2450
+ return nodeIndex;
2451
+
2452
+ }
2453
+
2454
+ /**
2455
+ * Process Scene
2456
+ * @param {Scene} scene Scene to process
2457
+ */
2458
+ async processSceneAsync( scene ) {
2459
+
2460
+ const json = this.json;
2461
+ const options = this.options;
2462
+
2463
+ if ( ! json.scenes ) {
2464
+
2465
+ json.scenes = [];
2466
+ json.scene = 0;
2467
+
2468
+ }
2469
+
2470
+ const sceneDef = {};
2471
+
2472
+ if ( scene.name !== '' ) sceneDef.name = scene.name;
2473
+
2474
+ json.scenes.push( sceneDef );
2475
+
2476
+ const nodes = [];
2477
+
2478
+ for ( let i = 0, l = scene.children.length; i < l; i ++ ) {
2479
+
2480
+ const child = scene.children[ i ];
2481
+
2482
+ if ( child.visible || options.onlyVisible === false ) {
2483
+
2484
+ const nodeIndex = await this.processNodeAsync( child );
2485
+
2486
+ if ( nodeIndex !== null ) nodes.push( nodeIndex );
2487
+
2488
+ }
2489
+
2490
+ }
2491
+
2492
+ if ( nodes.length > 0 ) sceneDef.nodes = nodes;
2493
+
2494
+ this.serializeUserData( scene, sceneDef );
2495
+
2496
+ }
2497
+
2498
+ /**
2499
+ * Creates a Scene to hold a list of objects and parse it
2500
+ * @param {Array<THREE.Object3D>} objects List of objects to process
2501
+ */
2502
+ async processObjectsAsync( objects ) {
2503
+
2504
+ const scene = new Scene();
2505
+ scene.name = 'AuxScene';
2506
+
2507
+ for ( let i = 0; i < objects.length; i ++ ) {
2508
+
2509
+ // We push directly to children instead of calling `add` to prevent
2510
+ // modify the .parent and break its original scene and hierarchy
2511
+ scene.children.push( objects[ i ] );
2512
+
2513
+ }
2514
+
2515
+ await this.processSceneAsync( scene );
2516
+
2517
+ }
2518
+
2519
+ /**
2520
+ * @param {THREE.Object3D|Array<THREE.Object3D>} input
2521
+ */
2522
+ async processInputAsync( input ) {
2523
+
2524
+ const options = this.options;
2525
+
2526
+ input = input instanceof Array ? input : [ input ];
2527
+
2528
+ await this._invokeAllAsync( function ( ext ) {
2529
+
2530
+ ext.beforeParse && ext.beforeParse( input );
2531
+
2532
+ } );
2533
+
2534
+ const objectsWithoutScene = [];
2535
+
2536
+ for ( let i = 0; i < input.length; i ++ ) {
2537
+
2538
+ if ( input[ i ] instanceof Scene ) {
2539
+
2540
+ await this.processSceneAsync( input[ i ] );
2541
+
2542
+ } else {
2543
+
2544
+ objectsWithoutScene.push( input[ i ] );
2545
+
2546
+ }
2547
+
2548
+ }
2549
+
2550
+ if ( objectsWithoutScene.length > 0 ) {
2551
+
2552
+ await this.processObjectsAsync( objectsWithoutScene );
2553
+
2554
+ }
2555
+
2556
+ for ( let i = 0; i < this.skins.length; ++ i ) {
2557
+
2558
+ this.processSkin( this.skins[ i ] );
2559
+
2560
+ }
2561
+
2562
+ for ( let i = 0; i < options.animations.length; ++ i ) {
2563
+
2564
+ this.processAnimation( options.animations[ i ], input[ 0 ] );
2565
+
2566
+ }
2567
+
2568
+ await this._invokeAllAsync( function ( ext ) {
2569
+
2570
+ ext.afterParse && ext.afterParse( input );
2571
+
2572
+ } );
2573
+
2574
+ }
2575
+
2576
+ async _invokeAllAsync( func ) {
2577
+
2578
+ for ( let i = 0, il = this.plugins.length; i < il; i ++ ) {
2579
+
2580
+ await func( this.plugins[ i ] );
2581
+
2582
+ }
2583
+
2584
+ }
2585
+
2586
+ }
2587
+
2588
+ /**
2589
+ * Punctual Lights Extension
2590
+ *
2591
+ * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_lights_punctual
2592
+ *
2593
+ * @private
2594
+ */
2595
+ class GLTFLightExtension {
2596
+
2597
+ constructor( writer ) {
2598
+
2599
+ this.writer = writer;
2600
+ this.name = 'KHR_lights_punctual';
2601
+
2602
+ }
2603
+
2604
+ writeNode( light, nodeDef ) {
2605
+
2606
+ if ( ! light.isLight ) return;
2607
+
2608
+ if ( ! light.isDirectionalLight && ! light.isPointLight && ! light.isSpotLight ) {
2609
+
2610
+ console.warn( 'THREE.GLTFExporter: Only directional, point, and spot lights are supported.', light );
2611
+ return;
2612
+
2613
+ }
2614
+
2615
+ const writer = this.writer;
2616
+ const json = writer.json;
2617
+ const extensionsUsed = writer.extensionsUsed;
2618
+
2619
+ const lightDef = {};
2620
+
2621
+ if ( light.name ) lightDef.name = light.name;
2622
+
2623
+ lightDef.color = light.color.toArray();
2624
+
2625
+ lightDef.intensity = light.intensity;
2626
+
2627
+ if ( light.isDirectionalLight ) {
2628
+
2629
+ lightDef.type = 'directional';
2630
+
2631
+ } else if ( light.isPointLight ) {
2632
+
2633
+ lightDef.type = 'point';
2634
+
2635
+ if ( light.distance > 0 ) lightDef.range = light.distance;
2636
+
2637
+ } else if ( light.isSpotLight ) {
2638
+
2639
+ lightDef.type = 'spot';
2640
+
2641
+ if ( light.distance > 0 ) lightDef.range = light.distance;
2642
+
2643
+ lightDef.spot = {};
2644
+ lightDef.spot.innerConeAngle = ( 1.0 - light.penumbra ) * light.angle;
2645
+ lightDef.spot.outerConeAngle = light.angle;
2646
+
2647
+ }
2648
+
2649
+ if ( light.decay !== undefined && light.decay !== 2 ) {
2650
+
2651
+ console.warn( 'THREE.GLTFExporter: Light decay may be lost. glTF is physically-based, '
2652
+ + 'and expects light.decay=2.' );
2653
+
2654
+ }
2655
+
2656
+ if ( light.target
2657
+ && ( light.target.parent !== light
2658
+ || light.target.position.x !== 0
2659
+ || light.target.position.y !== 0
2660
+ || light.target.position.z !== - 1 ) ) {
2661
+
2662
+ console.warn( 'THREE.GLTFExporter: Light direction may be lost. For best results, '
2663
+ + 'make light.target a child of the light with position 0,0,-1.' );
2664
+
2665
+ }
2666
+
2667
+ if ( ! extensionsUsed[ this.name ] ) {
2668
+
2669
+ json.extensions = json.extensions || {};
2670
+ json.extensions[ this.name ] = { lights: [] };
2671
+ extensionsUsed[ this.name ] = true;
2672
+
2673
+ }
2674
+
2675
+ const lights = json.extensions[ this.name ].lights;
2676
+ lights.push( lightDef );
2677
+
2678
+ nodeDef.extensions = nodeDef.extensions || {};
2679
+ nodeDef.extensions[ this.name ] = { light: lights.length - 1 };
2680
+
2681
+ }
2682
+
2683
+ }
2684
+
2685
+ /**
2686
+ * Unlit Materials Extension
2687
+ *
2688
+ * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_unlit
2689
+ *
2690
+ * @private
2691
+ */
2692
+ class GLTFMaterialsUnlitExtension {
2693
+
2694
+ constructor( writer ) {
2695
+
2696
+ this.writer = writer;
2697
+ this.name = 'KHR_materials_unlit';
2698
+
2699
+ }
2700
+
2701
+ async writeMaterialAsync( material, materialDef ) {
2702
+
2703
+ if ( ! material.isMeshBasicMaterial ) return;
2704
+
2705
+ const writer = this.writer;
2706
+ const extensionsUsed = writer.extensionsUsed;
2707
+
2708
+ materialDef.extensions = materialDef.extensions || {};
2709
+ materialDef.extensions[ this.name ] = {};
2710
+
2711
+ extensionsUsed[ this.name ] = true;
2712
+
2713
+ materialDef.pbrMetallicRoughness.metallicFactor = 0.0;
2714
+ materialDef.pbrMetallicRoughness.roughnessFactor = 0.9;
2715
+
2716
+ }
2717
+
2718
+ }
2719
+
2720
+ /**
2721
+ * Clearcoat Materials Extension
2722
+ *
2723
+ * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_clearcoat
2724
+ *
2725
+ * @private
2726
+ */
2727
+ class GLTFMaterialsClearcoatExtension {
2728
+
2729
+ constructor( writer ) {
2730
+
2731
+ this.writer = writer;
2732
+ this.name = 'KHR_materials_clearcoat';
2733
+
2734
+ }
2735
+
2736
+ async writeMaterialAsync( material, materialDef ) {
2737
+
2738
+ if ( ! material.isMeshPhysicalMaterial || material.clearcoat === 0 ) return;
2739
+
2740
+ const writer = this.writer;
2741
+ const extensionsUsed = writer.extensionsUsed;
2742
+
2743
+ const extensionDef = {};
2744
+
2745
+ extensionDef.clearcoatFactor = material.clearcoat;
2746
+
2747
+ if ( material.clearcoatMap ) {
2748
+
2749
+ const clearcoatMapDef = {
2750
+ index: await writer.processTextureAsync( material.clearcoatMap ),
2751
+ texCoord: material.clearcoatMap.channel
2752
+ };
2753
+ writer.applyTextureTransform( clearcoatMapDef, material.clearcoatMap );
2754
+ extensionDef.clearcoatTexture = clearcoatMapDef;
2755
+
2756
+ }
2757
+
2758
+ extensionDef.clearcoatRoughnessFactor = material.clearcoatRoughness;
2759
+
2760
+ if ( material.clearcoatRoughnessMap ) {
2761
+
2762
+ const clearcoatRoughnessMapDef = {
2763
+ index: await writer.processTextureAsync( material.clearcoatRoughnessMap ),
2764
+ texCoord: material.clearcoatRoughnessMap.channel
2765
+ };
2766
+ writer.applyTextureTransform( clearcoatRoughnessMapDef, material.clearcoatRoughnessMap );
2767
+ extensionDef.clearcoatRoughnessTexture = clearcoatRoughnessMapDef;
2768
+
2769
+ }
2770
+
2771
+ if ( material.clearcoatNormalMap ) {
2772
+
2773
+ const clearcoatNormalMapDef = {
2774
+ index: await writer.processTextureAsync( material.clearcoatNormalMap ),
2775
+ texCoord: material.clearcoatNormalMap.channel
2776
+ };
2777
+
2778
+ if ( material.clearcoatNormalScale.x !== 1 ) clearcoatNormalMapDef.scale = material.clearcoatNormalScale.x;
2779
+
2780
+ writer.applyTextureTransform( clearcoatNormalMapDef, material.clearcoatNormalMap );
2781
+ extensionDef.clearcoatNormalTexture = clearcoatNormalMapDef;
2782
+
2783
+ }
2784
+
2785
+ materialDef.extensions = materialDef.extensions || {};
2786
+ materialDef.extensions[ this.name ] = extensionDef;
2787
+
2788
+ extensionsUsed[ this.name ] = true;
2789
+
2790
+
2791
+ }
2792
+
2793
+ }
2794
+
2795
+ /**
2796
+ * Materials dispersion Extension
2797
+ *
2798
+ * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_dispersion
2799
+ *
2800
+ * @private
2801
+ */
2802
+ class GLTFMaterialsDispersionExtension {
2803
+
2804
+ constructor( writer ) {
2805
+
2806
+ this.writer = writer;
2807
+ this.name = 'KHR_materials_dispersion';
2808
+
2809
+ }
2810
+
2811
+ async writeMaterialAsync( material, materialDef ) {
2812
+
2813
+ if ( ! material.isMeshPhysicalMaterial || material.dispersion === 0 ) return;
2814
+
2815
+ const writer = this.writer;
2816
+ const extensionsUsed = writer.extensionsUsed;
2817
+
2818
+ const extensionDef = {};
2819
+
2820
+ extensionDef.dispersion = material.dispersion;
2821
+
2822
+ materialDef.extensions = materialDef.extensions || {};
2823
+ materialDef.extensions[ this.name ] = extensionDef;
2824
+
2825
+ extensionsUsed[ this.name ] = true;
2826
+
2827
+ }
2828
+
2829
+ }
2830
+
2831
+ /**
2832
+ * Iridescence Materials Extension
2833
+ *
2834
+ * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_iridescence
2835
+ *
2836
+ * @private
2837
+ */
2838
+ class GLTFMaterialsIridescenceExtension {
2839
+
2840
+ constructor( writer ) {
2841
+
2842
+ this.writer = writer;
2843
+ this.name = 'KHR_materials_iridescence';
2844
+
2845
+ }
2846
+
2847
+ async writeMaterialAsync( material, materialDef ) {
2848
+
2849
+ if ( ! material.isMeshPhysicalMaterial || material.iridescence === 0 ) return;
2850
+
2851
+ const writer = this.writer;
2852
+ const extensionsUsed = writer.extensionsUsed;
2853
+
2854
+ const extensionDef = {};
2855
+
2856
+ extensionDef.iridescenceFactor = material.iridescence;
2857
+
2858
+ if ( material.iridescenceMap ) {
2859
+
2860
+ const iridescenceMapDef = {
2861
+ index: await writer.processTextureAsync( material.iridescenceMap ),
2862
+ texCoord: material.iridescenceMap.channel
2863
+ };
2864
+ writer.applyTextureTransform( iridescenceMapDef, material.iridescenceMap );
2865
+ extensionDef.iridescenceTexture = iridescenceMapDef;
2866
+
2867
+ }
2868
+
2869
+ extensionDef.iridescenceIor = material.iridescenceIOR;
2870
+ extensionDef.iridescenceThicknessMinimum = material.iridescenceThicknessRange[ 0 ];
2871
+ extensionDef.iridescenceThicknessMaximum = material.iridescenceThicknessRange[ 1 ];
2872
+
2873
+ if ( material.iridescenceThicknessMap ) {
2874
+
2875
+ const iridescenceThicknessMapDef = {
2876
+ index: await writer.processTextureAsync( material.iridescenceThicknessMap ),
2877
+ texCoord: material.iridescenceThicknessMap.channel
2878
+ };
2879
+ writer.applyTextureTransform( iridescenceThicknessMapDef, material.iridescenceThicknessMap );
2880
+ extensionDef.iridescenceThicknessTexture = iridescenceThicknessMapDef;
2881
+
2882
+ }
2883
+
2884
+ materialDef.extensions = materialDef.extensions || {};
2885
+ materialDef.extensions[ this.name ] = extensionDef;
2886
+
2887
+ extensionsUsed[ this.name ] = true;
2888
+
2889
+ }
2890
+
2891
+ }
2892
+
2893
+ /**
2894
+ * Transmission Materials Extension
2895
+ *
2896
+ * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_transmission
2897
+ *
2898
+ * @private
2899
+ */
2900
+ class GLTFMaterialsTransmissionExtension {
2901
+
2902
+ constructor( writer ) {
2903
+
2904
+ this.writer = writer;
2905
+ this.name = 'KHR_materials_transmission';
2906
+
2907
+ }
2908
+
2909
+ async writeMaterialAsync( material, materialDef ) {
2910
+
2911
+ if ( ! material.isMeshPhysicalMaterial || material.transmission === 0 ) return;
2912
+
2913
+ const writer = this.writer;
2914
+ const extensionsUsed = writer.extensionsUsed;
2915
+
2916
+ const extensionDef = {};
2917
+
2918
+ extensionDef.transmissionFactor = material.transmission;
2919
+
2920
+ if ( material.transmissionMap ) {
2921
+
2922
+ const transmissionMapDef = {
2923
+ index: await writer.processTextureAsync( material.transmissionMap ),
2924
+ texCoord: material.transmissionMap.channel
2925
+ };
2926
+ writer.applyTextureTransform( transmissionMapDef, material.transmissionMap );
2927
+ extensionDef.transmissionTexture = transmissionMapDef;
2928
+
2929
+ }
2930
+
2931
+ materialDef.extensions = materialDef.extensions || {};
2932
+ materialDef.extensions[ this.name ] = extensionDef;
2933
+
2934
+ extensionsUsed[ this.name ] = true;
2935
+
2936
+ }
2937
+
2938
+ }
2939
+
2940
+ /**
2941
+ * Materials Volume Extension
2942
+ *
2943
+ * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_volume
2944
+ *
2945
+ * @private
2946
+ */
2947
+ class GLTFMaterialsVolumeExtension {
2948
+
2949
+ constructor( writer ) {
2950
+
2951
+ this.writer = writer;
2952
+ this.name = 'KHR_materials_volume';
2953
+
2954
+ }
2955
+
2956
+ async writeMaterialAsync( material, materialDef ) {
2957
+
2958
+ if ( ! material.isMeshPhysicalMaterial || material.transmission === 0 ) return;
2959
+
2960
+ const writer = this.writer;
2961
+ const extensionsUsed = writer.extensionsUsed;
2962
+
2963
+ const extensionDef = {};
2964
+
2965
+ extensionDef.thicknessFactor = material.thickness;
2966
+
2967
+ if ( material.thicknessMap ) {
2968
+
2969
+ const thicknessMapDef = {
2970
+ index: await writer.processTextureAsync( material.thicknessMap ),
2971
+ texCoord: material.thicknessMap.channel
2972
+ };
2973
+ writer.applyTextureTransform( thicknessMapDef, material.thicknessMap );
2974
+ extensionDef.thicknessTexture = thicknessMapDef;
2975
+
2976
+ }
2977
+
2978
+ if ( material.attenuationDistance !== Infinity ) {
2979
+
2980
+ extensionDef.attenuationDistance = material.attenuationDistance;
2981
+
2982
+ }
2983
+
2984
+ extensionDef.attenuationColor = material.attenuationColor.toArray();
2985
+
2986
+ materialDef.extensions = materialDef.extensions || {};
2987
+ materialDef.extensions[ this.name ] = extensionDef;
2988
+
2989
+ extensionsUsed[ this.name ] = true;
2990
+
2991
+ }
2992
+
2993
+ }
2994
+
2995
+ /**
2996
+ * Materials ior Extension
2997
+ *
2998
+ * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_ior
2999
+ *
3000
+ * @private
3001
+ */
3002
+ class GLTFMaterialsIorExtension {
3003
+
3004
+ constructor( writer ) {
3005
+
3006
+ this.writer = writer;
3007
+ this.name = 'KHR_materials_ior';
3008
+
3009
+ }
3010
+
3011
+ async writeMaterialAsync( material, materialDef ) {
3012
+
3013
+ if ( ! material.isMeshPhysicalMaterial || material.ior === 1.5 ) return;
3014
+
3015
+ const writer = this.writer;
3016
+ const extensionsUsed = writer.extensionsUsed;
3017
+
3018
+ const extensionDef = {};
3019
+
3020
+ extensionDef.ior = material.ior;
3021
+
3022
+ materialDef.extensions = materialDef.extensions || {};
3023
+ materialDef.extensions[ this.name ] = extensionDef;
3024
+
3025
+ extensionsUsed[ this.name ] = true;
3026
+
3027
+ }
3028
+
3029
+ }
3030
+
3031
+ /**
3032
+ * Materials specular Extension
3033
+ *
3034
+ * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_specular
3035
+ *
3036
+ * @private
3037
+ */
3038
+ class GLTFMaterialsSpecularExtension {
3039
+
3040
+ constructor( writer ) {
3041
+
3042
+ this.writer = writer;
3043
+ this.name = 'KHR_materials_specular';
3044
+
3045
+ }
3046
+
3047
+ async writeMaterialAsync( material, materialDef ) {
3048
+
3049
+ if ( ! material.isMeshPhysicalMaterial || ( material.specularIntensity === 1.0 &&
3050
+ material.specularColor.equals( DEFAULT_SPECULAR_COLOR ) &&
3051
+ ! material.specularIntensityMap && ! material.specularColorMap ) ) return;
3052
+
3053
+ const writer = this.writer;
3054
+ const extensionsUsed = writer.extensionsUsed;
3055
+
3056
+ const extensionDef = {};
3057
+
3058
+ if ( material.specularIntensityMap ) {
3059
+
3060
+ const specularIntensityMapDef = {
3061
+ index: await writer.processTextureAsync( material.specularIntensityMap ),
3062
+ texCoord: material.specularIntensityMap.channel
3063
+ };
3064
+ writer.applyTextureTransform( specularIntensityMapDef, material.specularIntensityMap );
3065
+ extensionDef.specularTexture = specularIntensityMapDef;
3066
+
3067
+ }
3068
+
3069
+ if ( material.specularColorMap ) {
3070
+
3071
+ const specularColorMapDef = {
3072
+ index: await writer.processTextureAsync( material.specularColorMap ),
3073
+ texCoord: material.specularColorMap.channel
3074
+ };
3075
+ writer.applyTextureTransform( specularColorMapDef, material.specularColorMap );
3076
+ extensionDef.specularColorTexture = specularColorMapDef;
3077
+
3078
+ }
3079
+
3080
+ extensionDef.specularFactor = material.specularIntensity;
3081
+ extensionDef.specularColorFactor = material.specularColor.toArray();
3082
+
3083
+ materialDef.extensions = materialDef.extensions || {};
3084
+ materialDef.extensions[ this.name ] = extensionDef;
3085
+
3086
+ extensionsUsed[ this.name ] = true;
3087
+
3088
+ }
3089
+
3090
+ }
3091
+
3092
+ /**
3093
+ * Sheen Materials Extension
3094
+ *
3095
+ * Specification: https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_materials_sheen
3096
+ *
3097
+ * @private
3098
+ */
3099
+ class GLTFMaterialsSheenExtension {
3100
+
3101
+ constructor( writer ) {
3102
+
3103
+ this.writer = writer;
3104
+ this.name = 'KHR_materials_sheen';
3105
+
3106
+ }
3107
+
3108
+ async writeMaterialAsync( material, materialDef ) {
3109
+
3110
+ if ( ! material.isMeshPhysicalMaterial || material.sheen == 0.0 ) return;
3111
+
3112
+ const writer = this.writer;
3113
+ const extensionsUsed = writer.extensionsUsed;
3114
+
3115
+ const extensionDef = {};
3116
+
3117
+ if ( material.sheenRoughnessMap ) {
3118
+
3119
+ const sheenRoughnessMapDef = {
3120
+ index: await writer.processTextureAsync( material.sheenRoughnessMap ),
3121
+ texCoord: material.sheenRoughnessMap.channel
3122
+ };
3123
+ writer.applyTextureTransform( sheenRoughnessMapDef, material.sheenRoughnessMap );
3124
+ extensionDef.sheenRoughnessTexture = sheenRoughnessMapDef;
3125
+
3126
+ }
3127
+
3128
+ if ( material.sheenColorMap ) {
3129
+
3130
+ const sheenColorMapDef = {
3131
+ index: await writer.processTextureAsync( material.sheenColorMap ),
3132
+ texCoord: material.sheenColorMap.channel
3133
+ };
3134
+ writer.applyTextureTransform( sheenColorMapDef, material.sheenColorMap );
3135
+ extensionDef.sheenColorTexture = sheenColorMapDef;
3136
+
3137
+ }
3138
+
3139
+ extensionDef.sheenRoughnessFactor = material.sheenRoughness;
3140
+ extensionDef.sheenColorFactor = material.sheenColor.toArray();
3141
+
3142
+ materialDef.extensions = materialDef.extensions || {};
3143
+ materialDef.extensions[ this.name ] = extensionDef;
3144
+
3145
+ extensionsUsed[ this.name ] = true;
3146
+
3147
+ }
3148
+
3149
+ }
3150
+
3151
+ /**
3152
+ * Anisotropy Materials Extension
3153
+ *
3154
+ * Specification: https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_materials_anisotropy
3155
+ *
3156
+ * @private
3157
+ */
3158
+ class GLTFMaterialsAnisotropyExtension {
3159
+
3160
+ constructor( writer ) {
3161
+
3162
+ this.writer = writer;
3163
+ this.name = 'KHR_materials_anisotropy';
3164
+
3165
+ }
3166
+
3167
+ async writeMaterialAsync( material, materialDef ) {
3168
+
3169
+ if ( ! material.isMeshPhysicalMaterial || material.anisotropy == 0.0 ) return;
3170
+
3171
+ const writer = this.writer;
3172
+ const extensionsUsed = writer.extensionsUsed;
3173
+
3174
+ const extensionDef = {};
3175
+
3176
+ if ( material.anisotropyMap ) {
3177
+
3178
+ const anisotropyMapDef = { index: await writer.processTextureAsync( material.anisotropyMap ) };
3179
+ writer.applyTextureTransform( anisotropyMapDef, material.anisotropyMap );
3180
+ extensionDef.anisotropyTexture = anisotropyMapDef;
3181
+
3182
+ }
3183
+
3184
+ extensionDef.anisotropyStrength = material.anisotropy;
3185
+ extensionDef.anisotropyRotation = material.anisotropyRotation;
3186
+
3187
+ materialDef.extensions = materialDef.extensions || {};
3188
+ materialDef.extensions[ this.name ] = extensionDef;
3189
+
3190
+ extensionsUsed[ this.name ] = true;
3191
+
3192
+ }
3193
+
3194
+ }
3195
+
3196
+ /**
3197
+ * Materials Emissive Strength Extension
3198
+ *
3199
+ * Specification: https://github.com/KhronosGroup/glTF/blob/5768b3ce0ef32bc39cdf1bef10b948586635ead3/extensions/2.0/Khronos/KHR_materials_emissive_strength/README.md
3200
+ *
3201
+ * @private
3202
+ */
3203
+ class GLTFMaterialsEmissiveStrengthExtension {
3204
+
3205
+ constructor( writer ) {
3206
+
3207
+ this.writer = writer;
3208
+ this.name = 'KHR_materials_emissive_strength';
3209
+
3210
+ }
3211
+
3212
+ async writeMaterialAsync( material, materialDef ) {
3213
+
3214
+ if ( ! material.isMeshStandardMaterial || material.emissiveIntensity === 1.0 ) return;
3215
+
3216
+ const writer = this.writer;
3217
+ const extensionsUsed = writer.extensionsUsed;
3218
+
3219
+ const extensionDef = {};
3220
+
3221
+ extensionDef.emissiveStrength = material.emissiveIntensity;
3222
+
3223
+ materialDef.extensions = materialDef.extensions || {};
3224
+ materialDef.extensions[ this.name ] = extensionDef;
3225
+
3226
+ extensionsUsed[ this.name ] = true;
3227
+
3228
+ }
3229
+
3230
+ }
3231
+
3232
+
3233
+ /**
3234
+ * Materials bump Extension
3235
+ *
3236
+ * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/EXT_materials_bump
3237
+ *
3238
+ * @private
3239
+ */
3240
+ class GLTFMaterialsBumpExtension {
3241
+
3242
+ constructor( writer ) {
3243
+
3244
+ this.writer = writer;
3245
+ this.name = 'EXT_materials_bump';
3246
+
3247
+ }
3248
+
3249
+ async writeMaterialAsync( material, materialDef ) {
3250
+
3251
+ if ( ! material.isMeshStandardMaterial || (
3252
+ material.bumpScale === 1 &&
3253
+ ! material.bumpMap ) ) return;
3254
+
3255
+ const writer = this.writer;
3256
+ const extensionsUsed = writer.extensionsUsed;
3257
+
3258
+ const extensionDef = {};
3259
+
3260
+ if ( material.bumpMap ) {
3261
+
3262
+ const bumpMapDef = {
3263
+ index: await writer.processTextureAsync( material.bumpMap ),
3264
+ texCoord: material.bumpMap.channel
3265
+ };
3266
+ writer.applyTextureTransform( bumpMapDef, material.bumpMap );
3267
+ extensionDef.bumpTexture = bumpMapDef;
3268
+
3269
+ }
3270
+
3271
+ extensionDef.bumpFactor = material.bumpScale;
3272
+
3273
+ materialDef.extensions = materialDef.extensions || {};
3274
+ materialDef.extensions[ this.name ] = extensionDef;
3275
+
3276
+ extensionsUsed[ this.name ] = true;
3277
+
3278
+ }
3279
+
3280
+ }
3281
+
3282
+ /**
3283
+ * GPU Instancing Extension
3284
+ *
3285
+ * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/EXT_mesh_gpu_instancing
3286
+ *
3287
+ * @private
3288
+ */
3289
+ class GLTFMeshGpuInstancing {
3290
+
3291
+ constructor( writer ) {
3292
+
3293
+ this.writer = writer;
3294
+ this.name = 'EXT_mesh_gpu_instancing';
3295
+
3296
+ }
3297
+
3298
+ writeNode( object, nodeDef ) {
3299
+
3300
+ if ( ! object.isInstancedMesh ) return;
3301
+
3302
+ const writer = this.writer;
3303
+
3304
+ const mesh = object;
3305
+
3306
+ const translationAttr = new Float32Array( mesh.count * 3 );
3307
+ const rotationAttr = new Float32Array( mesh.count * 4 );
3308
+ const scaleAttr = new Float32Array( mesh.count * 3 );
3309
+
3310
+ const matrix = new Matrix4();
3311
+ const position = new Vector3();
3312
+ const quaternion = new Quaternion();
3313
+ const scale = new Vector3();
3314
+
3315
+ for ( let i = 0; i < mesh.count; i ++ ) {
3316
+
3317
+ mesh.getMatrixAt( i, matrix );
3318
+ matrix.decompose( position, quaternion, scale );
3319
+
3320
+ position.toArray( translationAttr, i * 3 );
3321
+ quaternion.toArray( rotationAttr, i * 4 );
3322
+ scale.toArray( scaleAttr, i * 3 );
3323
+
3324
+ }
3325
+
3326
+ const attributes = {
3327
+ TRANSLATION: writer.processAccessor( new BufferAttribute( translationAttr, 3 ) ),
3328
+ ROTATION: writer.processAccessor( new BufferAttribute( rotationAttr, 4 ) ),
3329
+ SCALE: writer.processAccessor( new BufferAttribute( scaleAttr, 3 ) ),
3330
+ };
3331
+
3332
+ if ( mesh.instanceColor )
3333
+ attributes._COLOR_0 = writer.processAccessor( mesh.instanceColor );
3334
+
3335
+ nodeDef.extensions = nodeDef.extensions || {};
3336
+ nodeDef.extensions[ this.name ] = { attributes };
3337
+
3338
+ writer.extensionsUsed[ this.name ] = true;
3339
+ writer.extensionsRequired[ this.name ] = true;
3340
+
3341
+ }
3342
+
3343
+ }
3344
+
3345
+ /**
3346
+ * Static utility functions
3347
+ *
3348
+ * @private
3349
+ */
3350
+ GLTFExporter.Utils = {
3351
+
3352
+ insertKeyframe: function ( track, time ) {
3353
+
3354
+ const tolerance = 0.001; // 1ms
3355
+ const valueSize = track.getValueSize();
3356
+
3357
+ const times = new track.TimeBufferType( track.times.length + 1 );
3358
+ const values = new track.ValueBufferType( track.values.length + valueSize );
3359
+ const interpolant = track.createInterpolant( new track.ValueBufferType( valueSize ) );
3360
+
3361
+ let index;
3362
+
3363
+ if ( track.times.length === 0 ) {
3364
+
3365
+ times[ 0 ] = time;
3366
+
3367
+ for ( let i = 0; i < valueSize; i ++ ) {
3368
+
3369
+ values[ i ] = 0;
3370
+
3371
+ }
3372
+
3373
+ index = 0;
3374
+
3375
+ } else if ( time < track.times[ 0 ] ) {
3376
+
3377
+ if ( Math.abs( track.times[ 0 ] - time ) < tolerance ) return 0;
3378
+
3379
+ times[ 0 ] = time;
3380
+ times.set( track.times, 1 );
3381
+
3382
+ values.set( interpolant.evaluate( time ), 0 );
3383
+ values.set( track.values, valueSize );
3384
+
3385
+ index = 0;
3386
+
3387
+ } else if ( time > track.times[ track.times.length - 1 ] ) {
3388
+
3389
+ if ( Math.abs( track.times[ track.times.length - 1 ] - time ) < tolerance ) {
3390
+
3391
+ return track.times.length - 1;
3392
+
3393
+ }
3394
+
3395
+ times[ times.length - 1 ] = time;
3396
+ times.set( track.times, 0 );
3397
+
3398
+ values.set( track.values, 0 );
3399
+ values.set( interpolant.evaluate( time ), track.values.length );
3400
+
3401
+ index = times.length - 1;
3402
+
3403
+ } else {
3404
+
3405
+ for ( let i = 0; i < track.times.length; i ++ ) {
3406
+
3407
+ if ( Math.abs( track.times[ i ] - time ) < tolerance ) return i;
3408
+
3409
+ if ( track.times[ i ] < time && track.times[ i + 1 ] > time ) {
3410
+
3411
+ times.set( track.times.slice( 0, i + 1 ), 0 );
3412
+ times[ i + 1 ] = time;
3413
+ times.set( track.times.slice( i + 1 ), i + 2 );
3414
+
3415
+ values.set( track.values.slice( 0, ( i + 1 ) * valueSize ), 0 );
3416
+ values.set( interpolant.evaluate( time ), ( i + 1 ) * valueSize );
3417
+ values.set( track.values.slice( ( i + 1 ) * valueSize ), ( i + 2 ) * valueSize );
3418
+
3419
+ index = i + 1;
3420
+
3421
+ break;
3422
+
3423
+ }
3424
+
3425
+ }
3426
+
3427
+ }
3428
+
3429
+ track.times = times;
3430
+ track.values = values;
3431
+
3432
+ return index;
3433
+
3434
+ },
3435
+
3436
+ mergeMorphTargetTracks: function ( clip, root ) {
3437
+
3438
+ const tracks = [];
3439
+ const mergedTracks = {};
3440
+ const sourceTracks = clip.tracks;
3441
+
3442
+ for ( let i = 0; i < sourceTracks.length; ++ i ) {
3443
+
3444
+ let sourceTrack = sourceTracks[ i ];
3445
+ const sourceTrackBinding = PropertyBinding.parseTrackName( sourceTrack.name );
3446
+ const sourceTrackNode = PropertyBinding.findNode( root, sourceTrackBinding.nodeName );
3447
+
3448
+ if ( sourceTrackBinding.propertyName !== 'morphTargetInfluences' || sourceTrackBinding.propertyIndex === undefined ) {
3449
+
3450
+ // Tracks that don't affect morph targets, or that affect all morph targets together, can be left as-is.
3451
+ tracks.push( sourceTrack );
3452
+ continue;
3453
+
3454
+ }
3455
+
3456
+ if ( sourceTrack.createInterpolant !== sourceTrack.InterpolantFactoryMethodDiscrete
3457
+ && sourceTrack.createInterpolant !== sourceTrack.InterpolantFactoryMethodLinear ) {
3458
+
3459
+ if ( sourceTrack.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline ) {
3460
+
3461
+ // This should never happen, because glTF morph target animations
3462
+ // affect all targets already.
3463
+ throw new Error( 'THREE.GLTFExporter: Cannot merge tracks with glTF CUBICSPLINE interpolation.' );
3464
+
3465
+ }
3466
+
3467
+ console.warn( 'THREE.GLTFExporter: Morph target interpolation mode not yet supported. Using LINEAR instead.' );
3468
+
3469
+ sourceTrack = sourceTrack.clone();
3470
+ sourceTrack.setInterpolation( InterpolateLinear );
3471
+
3472
+ }
3473
+
3474
+ const targetCount = sourceTrackNode.morphTargetInfluences.length;
3475
+ const targetIndex = sourceTrackNode.morphTargetDictionary[ sourceTrackBinding.propertyIndex ];
3476
+
3477
+ if ( targetIndex === undefined ) {
3478
+
3479
+ throw new Error( 'THREE.GLTFExporter: Morph target name not found: ' + sourceTrackBinding.propertyIndex );
3480
+
3481
+ }
3482
+
3483
+ let mergedTrack;
3484
+
3485
+ // If this is the first time we've seen this object, create a new
3486
+ // track to store merged keyframe data for each morph target.
3487
+ if ( mergedTracks[ sourceTrackNode.uuid ] === undefined ) {
3488
+
3489
+ mergedTrack = sourceTrack.clone();
3490
+
3491
+ const values = new mergedTrack.ValueBufferType( targetCount * mergedTrack.times.length );
3492
+
3493
+ for ( let j = 0; j < mergedTrack.times.length; j ++ ) {
3494
+
3495
+ values[ j * targetCount + targetIndex ] = mergedTrack.values[ j ];
3496
+
3497
+ }
3498
+
3499
+ // We need to take into consideration the intended target node
3500
+ // of our original un-merged morphTarget animation.
3501
+ mergedTrack.name = ( sourceTrackBinding.nodeName || '' ) + '.morphTargetInfluences';
3502
+ mergedTrack.values = values;
3503
+
3504
+ mergedTracks[ sourceTrackNode.uuid ] = mergedTrack;
3505
+ tracks.push( mergedTrack );
3506
+
3507
+ continue;
3508
+
3509
+ }
3510
+
3511
+ const sourceInterpolant = sourceTrack.createInterpolant( new sourceTrack.ValueBufferType( 1 ) );
3512
+
3513
+ mergedTrack = mergedTracks[ sourceTrackNode.uuid ];
3514
+
3515
+ // For every existing keyframe of the merged track, write a (possibly
3516
+ // interpolated) value from the source track.
3517
+ for ( let j = 0; j < mergedTrack.times.length; j ++ ) {
3518
+
3519
+ mergedTrack.values[ j * targetCount + targetIndex ] = sourceInterpolant.evaluate( mergedTrack.times[ j ] );
3520
+
3521
+ }
3522
+
3523
+ // For every existing keyframe of the source track, write a (possibly
3524
+ // new) keyframe to the merged track. Values from the previous loop may
3525
+ // be written again, but keyframes are de-duplicated.
3526
+ for ( let j = 0; j < sourceTrack.times.length; j ++ ) {
3527
+
3528
+ const keyframeIndex = this.insertKeyframe( mergedTrack, sourceTrack.times[ j ] );
3529
+ mergedTrack.values[ keyframeIndex * targetCount + targetIndex ] = sourceTrack.values[ j ];
3530
+
3531
+ }
3532
+
3533
+ }
3534
+
3535
+ clip.tracks = tracks;
3536
+
3537
+ return clip;
3538
+
3539
+ },
3540
+
3541
+ toFloat32BufferAttribute: function ( srcAttribute ) {
3542
+
3543
+ const dstAttribute = new BufferAttribute( new Float32Array( srcAttribute.count * srcAttribute.itemSize ), srcAttribute.itemSize, false );
3544
+
3545
+ if ( ! srcAttribute.normalized && ! srcAttribute.isInterleavedBufferAttribute ) {
3546
+
3547
+ dstAttribute.array.set( srcAttribute.array );
3548
+
3549
+ return dstAttribute;
3550
+
3551
+ }
3552
+
3553
+ for ( let i = 0, il = srcAttribute.count; i < il; i ++ ) {
3554
+
3555
+ for ( let j = 0; j < srcAttribute.itemSize; j ++ ) {
3556
+
3557
+ dstAttribute.setComponent( i, j, srcAttribute.getComponent( i, j ) );
3558
+
3559
+ }
3560
+
3561
+ }
3562
+
3563
+ return dstAttribute;
3564
+
3565
+ }
3566
+
3567
+ };
3568
+
3569
+ /**
3570
+ * Export options of `GLTFExporter`.
3571
+ *
3572
+ * @typedef {Object} GLTFExporter~Options
3573
+ * @property {boolean} [trs=false] - Export position, rotation and scale instead of matrix per node.
3574
+ * @property {boolean} [onlyVisible=true] - Export only visible 3D objects.
3575
+ * @property {boolean} [binary=false] - Export in binary (.glb) format, returning an ArrayBuffer.
3576
+ * @property {number} [maxTextureSize=Infinity] - Restricts the image maximum size (both width and height) to the given value.
3577
+ * @property {Array<AnimationClip>} [animations=[]] - List of animations to be included in the export.
3578
+ * @property {boolean} [includeCustomExtensions=false] - Export custom glTF extensions defined on an object's `userData.gltfExtensions` property.
3579
+ **/
3580
+
3581
+ /**
3582
+ * onDone callback of `GLTFExporter`.
3583
+ *
3584
+ * @callback GLTFExporter~OnDone
3585
+ * @param {ArrayBuffer|string} result - The generated .gltf (JSON) or .glb (binary).
3586
+ */
3587
+
3588
+ /**
3589
+ * onError callback of `GLTFExporter`.
3590
+ *
3591
+ * @callback GLTFExporter~OnError
3592
+ * @param {Error} error - The error object.
3593
+ */
3594
+
3595
+ export { GLTFExporter };
vendor/OrbitControls.js ADDED
@@ -0,0 +1,1860 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ Controls,
3
+ MOUSE,
4
+ Quaternion,
5
+ Spherical,
6
+ TOUCH,
7
+ Vector2,
8
+ Vector3,
9
+ Plane,
10
+ Ray,
11
+ MathUtils
12
+ } from 'three';
13
+
14
+ /**
15
+ * Fires when the camera has been transformed by the controls.
16
+ *
17
+ * @event OrbitControls#change
18
+ * @type {Object}
19
+ */
20
+ const _changeEvent = { type: 'change' };
21
+
22
+ /**
23
+ * Fires when an interaction was initiated.
24
+ *
25
+ * @event OrbitControls#start
26
+ * @type {Object}
27
+ */
28
+ const _startEvent = { type: 'start' };
29
+
30
+ /**
31
+ * Fires when an interaction has finished.
32
+ *
33
+ * @event OrbitControls#end
34
+ * @type {Object}
35
+ */
36
+ const _endEvent = { type: 'end' };
37
+
38
+ const _ray = new Ray();
39
+ const _plane = new Plane();
40
+ const _TILT_LIMIT = Math.cos( 70 * MathUtils.DEG2RAD );
41
+
42
+ const _v = new Vector3();
43
+ const _twoPI = 2 * Math.PI;
44
+
45
+ const _STATE = {
46
+ NONE: - 1,
47
+ ROTATE: 0,
48
+ DOLLY: 1,
49
+ PAN: 2,
50
+ TOUCH_ROTATE: 3,
51
+ TOUCH_PAN: 4,
52
+ TOUCH_DOLLY_PAN: 5,
53
+ TOUCH_DOLLY_ROTATE: 6
54
+ };
55
+ const _EPS = 0.000001;
56
+
57
+
58
+ /**
59
+ * Orbit controls allow the camera to orbit around a target.
60
+ *
61
+ * OrbitControls performs orbiting, dollying (zooming), and panning. Unlike {@link TrackballControls},
62
+ * it maintains the "up" direction `object.up` (+Y by default).
63
+ *
64
+ * - Orbit: Left mouse / touch: one-finger move.
65
+ * - Zoom: Middle mouse, or mousewheel / touch: two-finger spread or squish.
66
+ * - Pan: Right mouse, or left mouse + ctrl/meta/shiftKey, or arrow keys / touch: two-finger move.
67
+ *
68
+ * ```js
69
+ * const controls = new OrbitControls( camera, renderer.domElement );
70
+ *
71
+ * // controls.update() must be called after any manual changes to the camera's transform
72
+ * camera.position.set( 0, 20, 100 );
73
+ * controls.update();
74
+ *
75
+ * function animate() {
76
+ *
77
+ * // required if controls.enableDamping or controls.autoRotate are set to true
78
+ * controls.update();
79
+ *
80
+ * renderer.render( scene, camera );
81
+ *
82
+ * }
83
+ * ```
84
+ *
85
+ * @augments Controls
86
+ * @three_import import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
87
+ */
88
+ class OrbitControls extends Controls {
89
+
90
+ /**
91
+ * Constructs a new controls instance.
92
+ *
93
+ * @param {Object3D} object - The object that is managed by the controls.
94
+ * @param {?HTMLDOMElement} domElement - The HTML element used for event listeners.
95
+ */
96
+ constructor( object, domElement = null ) {
97
+
98
+ super( object, domElement );
99
+
100
+ this.state = _STATE.NONE;
101
+
102
+ /**
103
+ * The focus point of the controls, the `object` orbits around this.
104
+ * It can be updated manually at any point to change the focus of the controls.
105
+ *
106
+ * @type {Vector3}
107
+ */
108
+ this.target = new Vector3();
109
+
110
+ /**
111
+ * The focus point of the `minTargetRadius` and `maxTargetRadius` limits.
112
+ * It can be updated manually at any point to change the center of interest
113
+ * for the `target`.
114
+ *
115
+ * @type {Vector3}
116
+ */
117
+ this.cursor = new Vector3();
118
+
119
+ /**
120
+ * How far you can dolly in (perspective camera only).
121
+ *
122
+ * @type {number}
123
+ * @default 0
124
+ */
125
+ this.minDistance = 0;
126
+
127
+ /**
128
+ * How far you can dolly out (perspective camera only).
129
+ *
130
+ * @type {number}
131
+ * @default Infinity
132
+ */
133
+ this.maxDistance = Infinity;
134
+
135
+ /**
136
+ * How far you can zoom in (orthographic camera only).
137
+ *
138
+ * @type {number}
139
+ * @default 0
140
+ */
141
+ this.minZoom = 0;
142
+
143
+ /**
144
+ * How far you can zoom out (orthographic camera only).
145
+ *
146
+ * @type {number}
147
+ * @default Infinity
148
+ */
149
+ this.maxZoom = Infinity;
150
+
151
+ /**
152
+ * How close you can get the target to the 3D `cursor`.
153
+ *
154
+ * @type {number}
155
+ * @default 0
156
+ */
157
+ this.minTargetRadius = 0;
158
+
159
+ /**
160
+ * How far you can move the target from the 3D `cursor`.
161
+ *
162
+ * @type {number}
163
+ * @default Infinity
164
+ */
165
+ this.maxTargetRadius = Infinity;
166
+
167
+ /**
168
+ * How far you can orbit vertically, lower limit. Range is `[0, Math.PI]` radians.
169
+ *
170
+ * @type {number}
171
+ * @default 0
172
+ */
173
+ this.minPolarAngle = 0;
174
+
175
+ /**
176
+ * How far you can orbit vertically, upper limit. Range is `[0, Math.PI]` radians.
177
+ *
178
+ * @type {number}
179
+ * @default Math.PI
180
+ */
181
+ this.maxPolarAngle = Math.PI;
182
+
183
+ /**
184
+ * How far you can orbit horizontally, lower limit. If set, the interval `[ min, max ]`
185
+ * must be a sub-interval of `[ - 2 PI, 2 PI ]`, with `( max - min < 2 PI )`.
186
+ *
187
+ * @type {number}
188
+ * @default -Infinity
189
+ */
190
+ this.minAzimuthAngle = - Infinity;
191
+
192
+ /**
193
+ * How far you can orbit horizontally, upper limit. If set, the interval `[ min, max ]`
194
+ * must be a sub-interval of `[ - 2 PI, 2 PI ]`, with `( max - min < 2 PI )`.
195
+ *
196
+ * @type {number}
197
+ * @default -Infinity
198
+ */
199
+ this.maxAzimuthAngle = Infinity;
200
+
201
+ /**
202
+ * Set to `true` to enable damping (inertia), which can be used to give a sense of weight
203
+ * to the controls. Note that if this is enabled, you must call `update()` in your animation
204
+ * loop.
205
+ *
206
+ * @type {boolean}
207
+ * @default false
208
+ */
209
+ this.enableDamping = false;
210
+
211
+ /**
212
+ * The damping inertia used if `enableDamping` is set to `true`.
213
+ *
214
+ * Note that for this to work, you must call `update()` in your animation loop.
215
+ *
216
+ * @type {number}
217
+ * @default 0.05
218
+ */
219
+ this.dampingFactor = 0.05;
220
+
221
+ /**
222
+ * Enable or disable zooming (dollying) of the camera.
223
+ *
224
+ * @type {boolean}
225
+ * @default true
226
+ */
227
+ this.enableZoom = true;
228
+
229
+ /**
230
+ * Speed of zooming / dollying.
231
+ *
232
+ * @type {number}
233
+ * @default 1
234
+ */
235
+ this.zoomSpeed = 1.0;
236
+
237
+ /**
238
+ * Enable or disable horizontal and vertical rotation of the camera.
239
+ *
240
+ * Note that it is possible to disable a single axis by setting the min and max of the
241
+ * `minPolarAngle` or `minAzimuthAngle` to the same value, which will cause the vertical
242
+ * or horizontal rotation to be fixed at that value.
243
+ *
244
+ * @type {boolean}
245
+ * @default true
246
+ */
247
+ this.enableRotate = true;
248
+
249
+ /**
250
+ * Speed of rotation.
251
+ *
252
+ * @type {number}
253
+ * @default 1
254
+ */
255
+ this.rotateSpeed = 1.0;
256
+
257
+ /**
258
+ * How fast to rotate the camera when the keyboard is used.
259
+ *
260
+ * @type {number}
261
+ * @default 1
262
+ */
263
+ this.keyRotateSpeed = 1.0;
264
+
265
+ /**
266
+ * Enable or disable camera panning.
267
+ *
268
+ * @type {boolean}
269
+ * @default true
270
+ */
271
+ this.enablePan = true;
272
+
273
+ /**
274
+ * Speed of panning.
275
+ *
276
+ * @type {number}
277
+ * @default 1
278
+ */
279
+ this.panSpeed = 1.0;
280
+
281
+ /**
282
+ * Defines how the camera's position is translated when panning. If `true`, the camera pans
283
+ * in screen space. Otherwise, the camera pans in the plane orthogonal to the camera's up
284
+ * direction.
285
+ *
286
+ * @type {boolean}
287
+ * @default true
288
+ */
289
+ this.screenSpacePanning = true;
290
+
291
+ /**
292
+ * How fast to pan the camera when the keyboard is used in
293
+ * pixels per keypress.
294
+ *
295
+ * @type {number}
296
+ * @default 7
297
+ */
298
+ this.keyPanSpeed = 7.0;
299
+
300
+ /**
301
+ * Setting this property to `true` allows to zoom to the cursor's position.
302
+ *
303
+ * @type {boolean}
304
+ * @default false
305
+ */
306
+ this.zoomToCursor = false;
307
+
308
+ /**
309
+ * Set to true to automatically rotate around the target
310
+ *
311
+ * Note that if this is enabled, you must call `update()` in your animation loop.
312
+ * If you want the auto-rotate speed to be independent of the frame rate (the refresh
313
+ * rate of the display), you must pass the time `deltaTime`, in seconds, to `update()`.
314
+ *
315
+ * @type {boolean}
316
+ * @default false
317
+ */
318
+ this.autoRotate = false;
319
+
320
+ /**
321
+ * How fast to rotate around the target if `autoRotate` is `true`. The default equates to 30 seconds
322
+ * per orbit at 60fps.
323
+ *
324
+ * Note that if `autoRotate` is enabled, you must call `update()` in your animation loop.
325
+ *
326
+ * @type {number}
327
+ * @default 2
328
+ */
329
+ this.autoRotateSpeed = 2.0;
330
+
331
+ /**
332
+ * This object contains references to the keycodes for controlling camera panning.
333
+ *
334
+ * ```js
335
+ * controls.keys = {
336
+ * LEFT: 'ArrowLeft', //left arrow
337
+ * UP: 'ArrowUp', // up arrow
338
+ * RIGHT: 'ArrowRight', // right arrow
339
+ * BOTTOM: 'ArrowDown' // down arrow
340
+ * }
341
+ * ```
342
+ * @type {Object}
343
+ */
344
+ this.keys = { LEFT: 'ArrowLeft', UP: 'ArrowUp', RIGHT: 'ArrowRight', BOTTOM: 'ArrowDown' };
345
+
346
+ /**
347
+ * This object contains references to the mouse actions used by the controls.
348
+ *
349
+ * ```js
350
+ * controls.mouseButtons = {
351
+ * LEFT: THREE.MOUSE.ROTATE,
352
+ * MIDDLE: THREE.MOUSE.DOLLY,
353
+ * RIGHT: THREE.MOUSE.PAN
354
+ * }
355
+ * ```
356
+ * @type {Object}
357
+ */
358
+ this.mouseButtons = { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.DOLLY, RIGHT: MOUSE.PAN };
359
+
360
+ /**
361
+ * This object contains references to the touch actions used by the controls.
362
+ *
363
+ * ```js
364
+ * controls.mouseButtons = {
365
+ * ONE: THREE.TOUCH.ROTATE,
366
+ * TWO: THREE.TOUCH.DOLLY_PAN
367
+ * }
368
+ * ```
369
+ * @type {Object}
370
+ */
371
+ this.touches = { ONE: TOUCH.ROTATE, TWO: TOUCH.DOLLY_PAN };
372
+
373
+ /**
374
+ * Used internally by `saveState()` and `reset()`.
375
+ *
376
+ * @type {Vector3}
377
+ */
378
+ this.target0 = this.target.clone();
379
+
380
+ /**
381
+ * Used internally by `saveState()` and `reset()`.
382
+ *
383
+ * @type {Vector3}
384
+ */
385
+ this.position0 = this.object.position.clone();
386
+
387
+ /**
388
+ * Used internally by `saveState()` and `reset()`.
389
+ *
390
+ * @type {number}
391
+ */
392
+ this.zoom0 = this.object.zoom;
393
+
394
+ // the target DOM element for key events
395
+ this._domElementKeyEvents = null;
396
+
397
+ // internals
398
+
399
+ this._lastPosition = new Vector3();
400
+ this._lastQuaternion = new Quaternion();
401
+ this._lastTargetPosition = new Vector3();
402
+
403
+ // so camera.up is the orbit axis
404
+ this._quat = new Quaternion().setFromUnitVectors( object.up, new Vector3( 0, 1, 0 ) );
405
+ this._quatInverse = this._quat.clone().invert();
406
+
407
+ // current position in spherical coordinates
408
+ this._spherical = new Spherical();
409
+ this._sphericalDelta = new Spherical();
410
+
411
+ this._scale = 1;
412
+ this._panOffset = new Vector3();
413
+
414
+ this._rotateStart = new Vector2();
415
+ this._rotateEnd = new Vector2();
416
+ this._rotateDelta = new Vector2();
417
+
418
+ this._panStart = new Vector2();
419
+ this._panEnd = new Vector2();
420
+ this._panDelta = new Vector2();
421
+
422
+ this._dollyStart = new Vector2();
423
+ this._dollyEnd = new Vector2();
424
+ this._dollyDelta = new Vector2();
425
+
426
+ this._dollyDirection = new Vector3();
427
+ this._mouse = new Vector2();
428
+ this._performCursorZoom = false;
429
+
430
+ this._pointers = [];
431
+ this._pointerPositions = {};
432
+
433
+ this._controlActive = false;
434
+
435
+ // event listeners
436
+
437
+ this._onPointerMove = onPointerMove.bind( this );
438
+ this._onPointerDown = onPointerDown.bind( this );
439
+ this._onPointerUp = onPointerUp.bind( this );
440
+ this._onContextMenu = onContextMenu.bind( this );
441
+ this._onMouseWheel = onMouseWheel.bind( this );
442
+ this._onKeyDown = onKeyDown.bind( this );
443
+
444
+ this._onTouchStart = onTouchStart.bind( this );
445
+ this._onTouchMove = onTouchMove.bind( this );
446
+
447
+ this._onMouseDown = onMouseDown.bind( this );
448
+ this._onMouseMove = onMouseMove.bind( this );
449
+
450
+ this._interceptControlDown = interceptControlDown.bind( this );
451
+ this._interceptControlUp = interceptControlUp.bind( this );
452
+
453
+ //
454
+
455
+ if ( this.domElement !== null ) {
456
+
457
+ this.connect( this.domElement );
458
+
459
+ }
460
+
461
+ this.update();
462
+
463
+ }
464
+
465
+ connect( element ) {
466
+
467
+ super.connect( element );
468
+
469
+ this.domElement.addEventListener( 'pointerdown', this._onPointerDown );
470
+ this.domElement.addEventListener( 'pointercancel', this._onPointerUp );
471
+
472
+ this.domElement.addEventListener( 'contextmenu', this._onContextMenu );
473
+ this.domElement.addEventListener( 'wheel', this._onMouseWheel, { passive: false } );
474
+
475
+ const document = this.domElement.getRootNode(); // offscreen canvas compatibility
476
+ document.addEventListener( 'keydown', this._interceptControlDown, { passive: true, capture: true } );
477
+
478
+ this.domElement.style.touchAction = 'none'; // disable touch scroll
479
+
480
+ }
481
+
482
+ disconnect() {
483
+
484
+ this.domElement.removeEventListener( 'pointerdown', this._onPointerDown );
485
+ this.domElement.removeEventListener( 'pointermove', this._onPointerMove );
486
+ this.domElement.removeEventListener( 'pointerup', this._onPointerUp );
487
+ this.domElement.removeEventListener( 'pointercancel', this._onPointerUp );
488
+
489
+ this.domElement.removeEventListener( 'wheel', this._onMouseWheel );
490
+ this.domElement.removeEventListener( 'contextmenu', this._onContextMenu );
491
+
492
+ this.stopListenToKeyEvents();
493
+
494
+ const document = this.domElement.getRootNode(); // offscreen canvas compatibility
495
+ document.removeEventListener( 'keydown', this._interceptControlDown, { capture: true } );
496
+
497
+ this.domElement.style.touchAction = 'auto';
498
+
499
+ }
500
+
501
+ dispose() {
502
+
503
+ this.disconnect();
504
+
505
+ }
506
+
507
+ /**
508
+ * Get the current vertical rotation, in radians.
509
+ *
510
+ * @return {number} The current vertical rotation, in radians.
511
+ */
512
+ getPolarAngle() {
513
+
514
+ return this._spherical.phi;
515
+
516
+ }
517
+
518
+ /**
519
+ * Get the current horizontal rotation, in radians.
520
+ *
521
+ * @return {number} The current horizontal rotation, in radians.
522
+ */
523
+ getAzimuthalAngle() {
524
+
525
+ return this._spherical.theta;
526
+
527
+ }
528
+
529
+ /**
530
+ * Returns the distance from the camera to the target.
531
+ *
532
+ * @return {number} The distance from the camera to the target.
533
+ */
534
+ getDistance() {
535
+
536
+ return this.object.position.distanceTo( this.target );
537
+
538
+ }
539
+
540
+ /**
541
+ * Adds key event listeners to the given DOM element.
542
+ * `window` is a recommended argument for using this method.
543
+ *
544
+ * @param {HTMLDOMElement} domElement - The DOM element
545
+ */
546
+ listenToKeyEvents( domElement ) {
547
+
548
+ domElement.addEventListener( 'keydown', this._onKeyDown );
549
+ this._domElementKeyEvents = domElement;
550
+
551
+ }
552
+
553
+ /**
554
+ * Removes the key event listener previously defined with `listenToKeyEvents()`.
555
+ */
556
+ stopListenToKeyEvents() {
557
+
558
+ if ( this._domElementKeyEvents !== null ) {
559
+
560
+ this._domElementKeyEvents.removeEventListener( 'keydown', this._onKeyDown );
561
+ this._domElementKeyEvents = null;
562
+
563
+ }
564
+
565
+ }
566
+
567
+ /**
568
+ * Save the current state of the controls. This can later be recovered with `reset()`.
569
+ */
570
+ saveState() {
571
+
572
+ this.target0.copy( this.target );
573
+ this.position0.copy( this.object.position );
574
+ this.zoom0 = this.object.zoom;
575
+
576
+ }
577
+
578
+ /**
579
+ * Reset the controls to their state from either the last time the `saveState()`
580
+ * was called, or the initial state.
581
+ */
582
+ reset() {
583
+
584
+ this.target.copy( this.target0 );
585
+ this.object.position.copy( this.position0 );
586
+ this.object.zoom = this.zoom0;
587
+
588
+ this.object.updateProjectionMatrix();
589
+ this.dispatchEvent( _changeEvent );
590
+
591
+ this.update();
592
+
593
+ this.state = _STATE.NONE;
594
+
595
+ }
596
+
597
+ update( deltaTime = null ) {
598
+
599
+ const position = this.object.position;
600
+
601
+ _v.copy( position ).sub( this.target );
602
+
603
+ // rotate offset to "y-axis-is-up" space
604
+ _v.applyQuaternion( this._quat );
605
+
606
+ // angle from z-axis around y-axis
607
+ this._spherical.setFromVector3( _v );
608
+
609
+ if ( this.autoRotate && this.state === _STATE.NONE ) {
610
+
611
+ this._rotateLeft( this._getAutoRotationAngle( deltaTime ) );
612
+
613
+ }
614
+
615
+ if ( this.enableDamping ) {
616
+
617
+ this._spherical.theta += this._sphericalDelta.theta * this.dampingFactor;
618
+ this._spherical.phi += this._sphericalDelta.phi * this.dampingFactor;
619
+
620
+ } else {
621
+
622
+ this._spherical.theta += this._sphericalDelta.theta;
623
+ this._spherical.phi += this._sphericalDelta.phi;
624
+
625
+ }
626
+
627
+ // restrict theta to be between desired limits
628
+
629
+ let min = this.minAzimuthAngle;
630
+ let max = this.maxAzimuthAngle;
631
+
632
+ if ( isFinite( min ) && isFinite( max ) ) {
633
+
634
+ if ( min < - Math.PI ) min += _twoPI; else if ( min > Math.PI ) min -= _twoPI;
635
+
636
+ if ( max < - Math.PI ) max += _twoPI; else if ( max > Math.PI ) max -= _twoPI;
637
+
638
+ if ( min <= max ) {
639
+
640
+ this._spherical.theta = Math.max( min, Math.min( max, this._spherical.theta ) );
641
+
642
+ } else {
643
+
644
+ this._spherical.theta = ( this._spherical.theta > ( min + max ) / 2 ) ?
645
+ Math.max( min, this._spherical.theta ) :
646
+ Math.min( max, this._spherical.theta );
647
+
648
+ }
649
+
650
+ }
651
+
652
+ // restrict phi to be between desired limits
653
+ this._spherical.phi = Math.max( this.minPolarAngle, Math.min( this.maxPolarAngle, this._spherical.phi ) );
654
+
655
+ this._spherical.makeSafe();
656
+
657
+
658
+ // move target to panned location
659
+
660
+ if ( this.enableDamping === true ) {
661
+
662
+ this.target.addScaledVector( this._panOffset, this.dampingFactor );
663
+
664
+ } else {
665
+
666
+ this.target.add( this._panOffset );
667
+
668
+ }
669
+
670
+ // Limit the target distance from the cursor to create a sphere around the center of interest
671
+ this.target.sub( this.cursor );
672
+ this.target.clampLength( this.minTargetRadius, this.maxTargetRadius );
673
+ this.target.add( this.cursor );
674
+
675
+ let zoomChanged = false;
676
+ // adjust the camera position based on zoom only if we're not zooming to the cursor or if it's an ortho camera
677
+ // we adjust zoom later in these cases
678
+ if ( this.zoomToCursor && this._performCursorZoom || this.object.isOrthographicCamera ) {
679
+
680
+ this._spherical.radius = this._clampDistance( this._spherical.radius );
681
+
682
+ } else {
683
+
684
+ const prevRadius = this._spherical.radius;
685
+ this._spherical.radius = this._clampDistance( this._spherical.radius * this._scale );
686
+ zoomChanged = prevRadius != this._spherical.radius;
687
+
688
+ }
689
+
690
+ _v.setFromSpherical( this._spherical );
691
+
692
+ // rotate offset back to "camera-up-vector-is-up" space
693
+ _v.applyQuaternion( this._quatInverse );
694
+
695
+ position.copy( this.target ).add( _v );
696
+
697
+ this.object.lookAt( this.target );
698
+
699
+ if ( this.enableDamping === true ) {
700
+
701
+ this._sphericalDelta.theta *= ( 1 - this.dampingFactor );
702
+ this._sphericalDelta.phi *= ( 1 - this.dampingFactor );
703
+
704
+ this._panOffset.multiplyScalar( 1 - this.dampingFactor );
705
+
706
+ } else {
707
+
708
+ this._sphericalDelta.set( 0, 0, 0 );
709
+
710
+ this._panOffset.set( 0, 0, 0 );
711
+
712
+ }
713
+
714
+ // adjust camera position
715
+ if ( this.zoomToCursor && this._performCursorZoom ) {
716
+
717
+ let newRadius = null;
718
+ if ( this.object.isPerspectiveCamera ) {
719
+
720
+ // move the camera down the pointer ray
721
+ // this method avoids floating point error
722
+ const prevRadius = _v.length();
723
+ newRadius = this._clampDistance( prevRadius * this._scale );
724
+
725
+ const radiusDelta = prevRadius - newRadius;
726
+ this.object.position.addScaledVector( this._dollyDirection, radiusDelta );
727
+ this.object.updateMatrixWorld();
728
+
729
+ zoomChanged = !! radiusDelta;
730
+
731
+ } else if ( this.object.isOrthographicCamera ) {
732
+
733
+ // adjust the ortho camera position based on zoom changes
734
+ const mouseBefore = new Vector3( this._mouse.x, this._mouse.y, 0 );
735
+ mouseBefore.unproject( this.object );
736
+
737
+ const prevZoom = this.object.zoom;
738
+ this.object.zoom = Math.max( this.minZoom, Math.min( this.maxZoom, this.object.zoom / this._scale ) );
739
+ this.object.updateProjectionMatrix();
740
+
741
+ zoomChanged = prevZoom !== this.object.zoom;
742
+
743
+ const mouseAfter = new Vector3( this._mouse.x, this._mouse.y, 0 );
744
+ mouseAfter.unproject( this.object );
745
+
746
+ this.object.position.sub( mouseAfter ).add( mouseBefore );
747
+ this.object.updateMatrixWorld();
748
+
749
+ newRadius = _v.length();
750
+
751
+ } else {
752
+
753
+ console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - zoom to cursor disabled.' );
754
+ this.zoomToCursor = false;
755
+
756
+ }
757
+
758
+ // handle the placement of the target
759
+ if ( newRadius !== null ) {
760
+
761
+ if ( this.screenSpacePanning ) {
762
+
763
+ // position the orbit target in front of the new camera position
764
+ this.target.set( 0, 0, - 1 )
765
+ .transformDirection( this.object.matrix )
766
+ .multiplyScalar( newRadius )
767
+ .add( this.object.position );
768
+
769
+ } else {
770
+
771
+ // get the ray and translation plane to compute target
772
+ _ray.origin.copy( this.object.position );
773
+ _ray.direction.set( 0, 0, - 1 ).transformDirection( this.object.matrix );
774
+
775
+ // if the camera is 20 degrees above the horizon then don't adjust the focus target to avoid
776
+ // extremely large values
777
+ if ( Math.abs( this.object.up.dot( _ray.direction ) ) < _TILT_LIMIT ) {
778
+
779
+ this.object.lookAt( this.target );
780
+
781
+ } else {
782
+
783
+ _plane.setFromNormalAndCoplanarPoint( this.object.up, this.target );
784
+ _ray.intersectPlane( _plane, this.target );
785
+
786
+ }
787
+
788
+ }
789
+
790
+ }
791
+
792
+ } else if ( this.object.isOrthographicCamera ) {
793
+
794
+ const prevZoom = this.object.zoom;
795
+ this.object.zoom = Math.max( this.minZoom, Math.min( this.maxZoom, this.object.zoom / this._scale ) );
796
+
797
+ if ( prevZoom !== this.object.zoom ) {
798
+
799
+ this.object.updateProjectionMatrix();
800
+ zoomChanged = true;
801
+
802
+ }
803
+
804
+ }
805
+
806
+ this._scale = 1;
807
+ this._performCursorZoom = false;
808
+
809
+ // update condition is:
810
+ // min(camera displacement, camera rotation in radians)^2 > EPS
811
+ // using small-angle approximation cos(x/2) = 1 - x^2 / 8
812
+
813
+ if ( zoomChanged ||
814
+ this._lastPosition.distanceToSquared( this.object.position ) > _EPS ||
815
+ 8 * ( 1 - this._lastQuaternion.dot( this.object.quaternion ) ) > _EPS ||
816
+ this._lastTargetPosition.distanceToSquared( this.target ) > _EPS ) {
817
+
818
+ this.dispatchEvent( _changeEvent );
819
+
820
+ this._lastPosition.copy( this.object.position );
821
+ this._lastQuaternion.copy( this.object.quaternion );
822
+ this._lastTargetPosition.copy( this.target );
823
+
824
+ return true;
825
+
826
+ }
827
+
828
+ return false;
829
+
830
+ }
831
+
832
+ _getAutoRotationAngle( deltaTime ) {
833
+
834
+ if ( deltaTime !== null ) {
835
+
836
+ return ( _twoPI / 60 * this.autoRotateSpeed ) * deltaTime;
837
+
838
+ } else {
839
+
840
+ return _twoPI / 60 / 60 * this.autoRotateSpeed;
841
+
842
+ }
843
+
844
+ }
845
+
846
+ _getZoomScale( delta ) {
847
+
848
+ const normalizedDelta = Math.abs( delta * 0.01 );
849
+ return Math.pow( 0.95, this.zoomSpeed * normalizedDelta );
850
+
851
+ }
852
+
853
+ _rotateLeft( angle ) {
854
+
855
+ this._sphericalDelta.theta -= angle;
856
+
857
+ }
858
+
859
+ _rotateUp( angle ) {
860
+
861
+ this._sphericalDelta.phi -= angle;
862
+
863
+ }
864
+
865
+ _panLeft( distance, objectMatrix ) {
866
+
867
+ _v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix
868
+ _v.multiplyScalar( - distance );
869
+
870
+ this._panOffset.add( _v );
871
+
872
+ }
873
+
874
+ _panUp( distance, objectMatrix ) {
875
+
876
+ if ( this.screenSpacePanning === true ) {
877
+
878
+ _v.setFromMatrixColumn( objectMatrix, 1 );
879
+
880
+ } else {
881
+
882
+ _v.setFromMatrixColumn( objectMatrix, 0 );
883
+ _v.crossVectors( this.object.up, _v );
884
+
885
+ }
886
+
887
+ _v.multiplyScalar( distance );
888
+
889
+ this._panOffset.add( _v );
890
+
891
+ }
892
+
893
+ // deltaX and deltaY are in pixels; right and down are positive
894
+ _pan( deltaX, deltaY ) {
895
+
896
+ const element = this.domElement;
897
+
898
+ if ( this.object.isPerspectiveCamera ) {
899
+
900
+ // perspective
901
+ const position = this.object.position;
902
+ _v.copy( position ).sub( this.target );
903
+ let targetDistance = _v.length();
904
+
905
+ // half of the fov is center to top of screen
906
+ targetDistance *= Math.tan( ( this.object.fov / 2 ) * Math.PI / 180.0 );
907
+
908
+ // we use only clientHeight here so aspect ratio does not distort speed
909
+ this._panLeft( 2 * deltaX * targetDistance / element.clientHeight, this.object.matrix );
910
+ this._panUp( 2 * deltaY * targetDistance / element.clientHeight, this.object.matrix );
911
+
912
+ } else if ( this.object.isOrthographicCamera ) {
913
+
914
+ // orthographic
915
+ this._panLeft( deltaX * ( this.object.right - this.object.left ) / this.object.zoom / element.clientWidth, this.object.matrix );
916
+ this._panUp( deltaY * ( this.object.top - this.object.bottom ) / this.object.zoom / element.clientHeight, this.object.matrix );
917
+
918
+ } else {
919
+
920
+ // camera neither orthographic nor perspective
921
+ console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' );
922
+ this.enablePan = false;
923
+
924
+ }
925
+
926
+ }
927
+
928
+ _dollyOut( dollyScale ) {
929
+
930
+ if ( this.object.isPerspectiveCamera || this.object.isOrthographicCamera ) {
931
+
932
+ this._scale /= dollyScale;
933
+
934
+ } else {
935
+
936
+ console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
937
+ this.enableZoom = false;
938
+
939
+ }
940
+
941
+ }
942
+
943
+ _dollyIn( dollyScale ) {
944
+
945
+ if ( this.object.isPerspectiveCamera || this.object.isOrthographicCamera ) {
946
+
947
+ this._scale *= dollyScale;
948
+
949
+ } else {
950
+
951
+ console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
952
+ this.enableZoom = false;
953
+
954
+ }
955
+
956
+ }
957
+
958
+ _updateZoomParameters( x, y ) {
959
+
960
+ if ( ! this.zoomToCursor ) {
961
+
962
+ return;
963
+
964
+ }
965
+
966
+ this._performCursorZoom = true;
967
+
968
+ const rect = this.domElement.getBoundingClientRect();
969
+ const dx = x - rect.left;
970
+ const dy = y - rect.top;
971
+ const w = rect.width;
972
+ const h = rect.height;
973
+
974
+ this._mouse.x = ( dx / w ) * 2 - 1;
975
+ this._mouse.y = - ( dy / h ) * 2 + 1;
976
+
977
+ this._dollyDirection.set( this._mouse.x, this._mouse.y, 1 ).unproject( this.object ).sub( this.object.position ).normalize();
978
+
979
+ }
980
+
981
+ _clampDistance( dist ) {
982
+
983
+ return Math.max( this.minDistance, Math.min( this.maxDistance, dist ) );
984
+
985
+ }
986
+
987
+ //
988
+ // event callbacks - update the object state
989
+ //
990
+
991
+ _handleMouseDownRotate( event ) {
992
+
993
+ this._rotateStart.set( event.clientX, event.clientY );
994
+
995
+ }
996
+
997
+ _handleMouseDownDolly( event ) {
998
+
999
+ this._updateZoomParameters( event.clientX, event.clientX );
1000
+ this._dollyStart.set( event.clientX, event.clientY );
1001
+
1002
+ }
1003
+
1004
+ _handleMouseDownPan( event ) {
1005
+
1006
+ this._panStart.set( event.clientX, event.clientY );
1007
+
1008
+ }
1009
+
1010
+ _handleMouseMoveRotate( event ) {
1011
+
1012
+ this._rotateEnd.set( event.clientX, event.clientY );
1013
+
1014
+ this._rotateDelta.subVectors( this._rotateEnd, this._rotateStart ).multiplyScalar( this.rotateSpeed );
1015
+
1016
+ const element = this.domElement;
1017
+
1018
+ this._rotateLeft( _twoPI * this._rotateDelta.x / element.clientHeight ); // yes, height
1019
+
1020
+ this._rotateUp( _twoPI * this._rotateDelta.y / element.clientHeight );
1021
+
1022
+ this._rotateStart.copy( this._rotateEnd );
1023
+
1024
+ this.update();
1025
+
1026
+ }
1027
+
1028
+ _handleMouseMoveDolly( event ) {
1029
+
1030
+ this._dollyEnd.set( event.clientX, event.clientY );
1031
+
1032
+ this._dollyDelta.subVectors( this._dollyEnd, this._dollyStart );
1033
+
1034
+ if ( this._dollyDelta.y > 0 ) {
1035
+
1036
+ this._dollyOut( this._getZoomScale( this._dollyDelta.y ) );
1037
+
1038
+ } else if ( this._dollyDelta.y < 0 ) {
1039
+
1040
+ this._dollyIn( this._getZoomScale( this._dollyDelta.y ) );
1041
+
1042
+ }
1043
+
1044
+ this._dollyStart.copy( this._dollyEnd );
1045
+
1046
+ this.update();
1047
+
1048
+ }
1049
+
1050
+ _handleMouseMovePan( event ) {
1051
+
1052
+ this._panEnd.set( event.clientX, event.clientY );
1053
+
1054
+ this._panDelta.subVectors( this._panEnd, this._panStart ).multiplyScalar( this.panSpeed );
1055
+
1056
+ this._pan( this._panDelta.x, this._panDelta.y );
1057
+
1058
+ this._panStart.copy( this._panEnd );
1059
+
1060
+ this.update();
1061
+
1062
+ }
1063
+
1064
+ _handleMouseWheel( event ) {
1065
+
1066
+ this._updateZoomParameters( event.clientX, event.clientY );
1067
+
1068
+ if ( event.deltaY < 0 ) {
1069
+
1070
+ this._dollyIn( this._getZoomScale( event.deltaY ) );
1071
+
1072
+ } else if ( event.deltaY > 0 ) {
1073
+
1074
+ this._dollyOut( this._getZoomScale( event.deltaY ) );
1075
+
1076
+ }
1077
+
1078
+ this.update();
1079
+
1080
+ }
1081
+
1082
+ _handleKeyDown( event ) {
1083
+
1084
+ let needsUpdate = false;
1085
+
1086
+ switch ( event.code ) {
1087
+
1088
+ case this.keys.UP:
1089
+
1090
+ if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
1091
+
1092
+ if ( this.enableRotate ) {
1093
+
1094
+ this._rotateUp( _twoPI * this.keyRotateSpeed / this.domElement.clientHeight );
1095
+
1096
+ }
1097
+
1098
+ } else {
1099
+
1100
+ if ( this.enablePan ) {
1101
+
1102
+ this._pan( 0, this.keyPanSpeed );
1103
+
1104
+ }
1105
+
1106
+ }
1107
+
1108
+ needsUpdate = true;
1109
+ break;
1110
+
1111
+ case this.keys.BOTTOM:
1112
+
1113
+ if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
1114
+
1115
+ if ( this.enableRotate ) {
1116
+
1117
+ this._rotateUp( - _twoPI * this.keyRotateSpeed / this.domElement.clientHeight );
1118
+
1119
+ }
1120
+
1121
+ } else {
1122
+
1123
+ if ( this.enablePan ) {
1124
+
1125
+ this._pan( 0, - this.keyPanSpeed );
1126
+
1127
+ }
1128
+
1129
+ }
1130
+
1131
+ needsUpdate = true;
1132
+ break;
1133
+
1134
+ case this.keys.LEFT:
1135
+
1136
+ if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
1137
+
1138
+ if ( this.enableRotate ) {
1139
+
1140
+ this._rotateLeft( _twoPI * this.keyRotateSpeed / this.domElement.clientHeight );
1141
+
1142
+ }
1143
+
1144
+ } else {
1145
+
1146
+ if ( this.enablePan ) {
1147
+
1148
+ this._pan( this.keyPanSpeed, 0 );
1149
+
1150
+ }
1151
+
1152
+ }
1153
+
1154
+ needsUpdate = true;
1155
+ break;
1156
+
1157
+ case this.keys.RIGHT:
1158
+
1159
+ if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
1160
+
1161
+ if ( this.enableRotate ) {
1162
+
1163
+ this._rotateLeft( - _twoPI * this.keyRotateSpeed / this.domElement.clientHeight );
1164
+
1165
+ }
1166
+
1167
+ } else {
1168
+
1169
+ if ( this.enablePan ) {
1170
+
1171
+ this._pan( - this.keyPanSpeed, 0 );
1172
+
1173
+ }
1174
+
1175
+ }
1176
+
1177
+ needsUpdate = true;
1178
+ break;
1179
+
1180
+ }
1181
+
1182
+ if ( needsUpdate ) {
1183
+
1184
+ // prevent the browser from scrolling on cursor keys
1185
+ event.preventDefault();
1186
+
1187
+ this.update();
1188
+
1189
+ }
1190
+
1191
+
1192
+ }
1193
+
1194
+ _handleTouchStartRotate( event ) {
1195
+
1196
+ if ( this._pointers.length === 1 ) {
1197
+
1198
+ this._rotateStart.set( event.pageX, event.pageY );
1199
+
1200
+ } else {
1201
+
1202
+ const position = this._getSecondPointerPosition( event );
1203
+
1204
+ const x = 0.5 * ( event.pageX + position.x );
1205
+ const y = 0.5 * ( event.pageY + position.y );
1206
+
1207
+ this._rotateStart.set( x, y );
1208
+
1209
+ }
1210
+
1211
+ }
1212
+
1213
+ _handleTouchStartPan( event ) {
1214
+
1215
+ if ( this._pointers.length === 1 ) {
1216
+
1217
+ this._panStart.set( event.pageX, event.pageY );
1218
+
1219
+ } else {
1220
+
1221
+ const position = this._getSecondPointerPosition( event );
1222
+
1223
+ const x = 0.5 * ( event.pageX + position.x );
1224
+ const y = 0.5 * ( event.pageY + position.y );
1225
+
1226
+ this._panStart.set( x, y );
1227
+
1228
+ }
1229
+
1230
+ }
1231
+
1232
+ _handleTouchStartDolly( event ) {
1233
+
1234
+ const position = this._getSecondPointerPosition( event );
1235
+
1236
+ const dx = event.pageX - position.x;
1237
+ const dy = event.pageY - position.y;
1238
+
1239
+ const distance = Math.sqrt( dx * dx + dy * dy );
1240
+
1241
+ this._dollyStart.set( 0, distance );
1242
+
1243
+ }
1244
+
1245
+ _handleTouchStartDollyPan( event ) {
1246
+
1247
+ if ( this.enableZoom ) this._handleTouchStartDolly( event );
1248
+
1249
+ if ( this.enablePan ) this._handleTouchStartPan( event );
1250
+
1251
+ }
1252
+
1253
+ _handleTouchStartDollyRotate( event ) {
1254
+
1255
+ if ( this.enableZoom ) this._handleTouchStartDolly( event );
1256
+
1257
+ if ( this.enableRotate ) this._handleTouchStartRotate( event );
1258
+
1259
+ }
1260
+
1261
+ _handleTouchMoveRotate( event ) {
1262
+
1263
+ if ( this._pointers.length == 1 ) {
1264
+
1265
+ this._rotateEnd.set( event.pageX, event.pageY );
1266
+
1267
+ } else {
1268
+
1269
+ const position = this._getSecondPointerPosition( event );
1270
+
1271
+ const x = 0.5 * ( event.pageX + position.x );
1272
+ const y = 0.5 * ( event.pageY + position.y );
1273
+
1274
+ this._rotateEnd.set( x, y );
1275
+
1276
+ }
1277
+
1278
+ this._rotateDelta.subVectors( this._rotateEnd, this._rotateStart ).multiplyScalar( this.rotateSpeed );
1279
+
1280
+ const element = this.domElement;
1281
+
1282
+ this._rotateLeft( _twoPI * this._rotateDelta.x / element.clientHeight ); // yes, height
1283
+
1284
+ this._rotateUp( _twoPI * this._rotateDelta.y / element.clientHeight );
1285
+
1286
+ this._rotateStart.copy( this._rotateEnd );
1287
+
1288
+ }
1289
+
1290
+ _handleTouchMovePan( event ) {
1291
+
1292
+ if ( this._pointers.length === 1 ) {
1293
+
1294
+ this._panEnd.set( event.pageX, event.pageY );
1295
+
1296
+ } else {
1297
+
1298
+ const position = this._getSecondPointerPosition( event );
1299
+
1300
+ const x = 0.5 * ( event.pageX + position.x );
1301
+ const y = 0.5 * ( event.pageY + position.y );
1302
+
1303
+ this._panEnd.set( x, y );
1304
+
1305
+ }
1306
+
1307
+ this._panDelta.subVectors( this._panEnd, this._panStart ).multiplyScalar( this.panSpeed );
1308
+
1309
+ this._pan( this._panDelta.x, this._panDelta.y );
1310
+
1311
+ this._panStart.copy( this._panEnd );
1312
+
1313
+ }
1314
+
1315
+ _handleTouchMoveDolly( event ) {
1316
+
1317
+ const position = this._getSecondPointerPosition( event );
1318
+
1319
+ const dx = event.pageX - position.x;
1320
+ const dy = event.pageY - position.y;
1321
+
1322
+ const distance = Math.sqrt( dx * dx + dy * dy );
1323
+
1324
+ this._dollyEnd.set( 0, distance );
1325
+
1326
+ this._dollyDelta.set( 0, Math.pow( this._dollyEnd.y / this._dollyStart.y, this.zoomSpeed ) );
1327
+
1328
+ this._dollyOut( this._dollyDelta.y );
1329
+
1330
+ this._dollyStart.copy( this._dollyEnd );
1331
+
1332
+ const centerX = ( event.pageX + position.x ) * 0.5;
1333
+ const centerY = ( event.pageY + position.y ) * 0.5;
1334
+
1335
+ this._updateZoomParameters( centerX, centerY );
1336
+
1337
+ }
1338
+
1339
+ _handleTouchMoveDollyPan( event ) {
1340
+
1341
+ if ( this.enableZoom ) this._handleTouchMoveDolly( event );
1342
+
1343
+ if ( this.enablePan ) this._handleTouchMovePan( event );
1344
+
1345
+ }
1346
+
1347
+ _handleTouchMoveDollyRotate( event ) {
1348
+
1349
+ if ( this.enableZoom ) this._handleTouchMoveDolly( event );
1350
+
1351
+ if ( this.enableRotate ) this._handleTouchMoveRotate( event );
1352
+
1353
+ }
1354
+
1355
+ // pointers
1356
+
1357
+ _addPointer( event ) {
1358
+
1359
+ this._pointers.push( event.pointerId );
1360
+
1361
+ }
1362
+
1363
+ _removePointer( event ) {
1364
+
1365
+ delete this._pointerPositions[ event.pointerId ];
1366
+
1367
+ for ( let i = 0; i < this._pointers.length; i ++ ) {
1368
+
1369
+ if ( this._pointers[ i ] == event.pointerId ) {
1370
+
1371
+ this._pointers.splice( i, 1 );
1372
+ return;
1373
+
1374
+ }
1375
+
1376
+ }
1377
+
1378
+ }
1379
+
1380
+ _isTrackingPointer( event ) {
1381
+
1382
+ for ( let i = 0; i < this._pointers.length; i ++ ) {
1383
+
1384
+ if ( this._pointers[ i ] == event.pointerId ) return true;
1385
+
1386
+ }
1387
+
1388
+ return false;
1389
+
1390
+ }
1391
+
1392
+ _trackPointer( event ) {
1393
+
1394
+ let position = this._pointerPositions[ event.pointerId ];
1395
+
1396
+ if ( position === undefined ) {
1397
+
1398
+ position = new Vector2();
1399
+ this._pointerPositions[ event.pointerId ] = position;
1400
+
1401
+ }
1402
+
1403
+ position.set( event.pageX, event.pageY );
1404
+
1405
+ }
1406
+
1407
+ _getSecondPointerPosition( event ) {
1408
+
1409
+ const pointerId = ( event.pointerId === this._pointers[ 0 ] ) ? this._pointers[ 1 ] : this._pointers[ 0 ];
1410
+
1411
+ return this._pointerPositions[ pointerId ];
1412
+
1413
+ }
1414
+
1415
+ //
1416
+
1417
+ _customWheelEvent( event ) {
1418
+
1419
+ const mode = event.deltaMode;
1420
+
1421
+ // minimal wheel event altered to meet delta-zoom demand
1422
+ const newEvent = {
1423
+ clientX: event.clientX,
1424
+ clientY: event.clientY,
1425
+ deltaY: event.deltaY,
1426
+ };
1427
+
1428
+ switch ( mode ) {
1429
+
1430
+ case 1: // LINE_MODE
1431
+ newEvent.deltaY *= 16;
1432
+ break;
1433
+
1434
+ case 2: // PAGE_MODE
1435
+ newEvent.deltaY *= 100;
1436
+ break;
1437
+
1438
+ }
1439
+
1440
+ // detect if event was triggered by pinching
1441
+ if ( event.ctrlKey && ! this._controlActive ) {
1442
+
1443
+ newEvent.deltaY *= 10;
1444
+
1445
+ }
1446
+
1447
+ return newEvent;
1448
+
1449
+ }
1450
+
1451
+ }
1452
+
1453
+ function onPointerDown( event ) {
1454
+
1455
+ if ( this.enabled === false ) return;
1456
+
1457
+ if ( this._pointers.length === 0 ) {
1458
+
1459
+ this.domElement.setPointerCapture( event.pointerId );
1460
+
1461
+ this.domElement.addEventListener( 'pointermove', this._onPointerMove );
1462
+ this.domElement.addEventListener( 'pointerup', this._onPointerUp );
1463
+
1464
+ }
1465
+
1466
+ //
1467
+
1468
+ if ( this._isTrackingPointer( event ) ) return;
1469
+
1470
+ //
1471
+
1472
+ this._addPointer( event );
1473
+
1474
+ if ( event.pointerType === 'touch' ) {
1475
+
1476
+ this._onTouchStart( event );
1477
+
1478
+ } else {
1479
+
1480
+ this._onMouseDown( event );
1481
+
1482
+ }
1483
+
1484
+ }
1485
+
1486
+ function onPointerMove( event ) {
1487
+
1488
+ if ( this.enabled === false ) return;
1489
+
1490
+ if ( event.pointerType === 'touch' ) {
1491
+
1492
+ this._onTouchMove( event );
1493
+
1494
+ } else {
1495
+
1496
+ this._onMouseMove( event );
1497
+
1498
+ }
1499
+
1500
+ }
1501
+
1502
+ function onPointerUp( event ) {
1503
+
1504
+ this._removePointer( event );
1505
+
1506
+ switch ( this._pointers.length ) {
1507
+
1508
+ case 0:
1509
+
1510
+ this.domElement.releasePointerCapture( event.pointerId );
1511
+
1512
+ this.domElement.removeEventListener( 'pointermove', this._onPointerMove );
1513
+ this.domElement.removeEventListener( 'pointerup', this._onPointerUp );
1514
+
1515
+ this.dispatchEvent( _endEvent );
1516
+
1517
+ this.state = _STATE.NONE;
1518
+
1519
+ break;
1520
+
1521
+ case 1:
1522
+
1523
+ const pointerId = this._pointers[ 0 ];
1524
+ const position = this._pointerPositions[ pointerId ];
1525
+
1526
+ // minimal placeholder event - allows state correction on pointer-up
1527
+ this._onTouchStart( { pointerId: pointerId, pageX: position.x, pageY: position.y } );
1528
+
1529
+ break;
1530
+
1531
+ }
1532
+
1533
+ }
1534
+
1535
+ function onMouseDown( event ) {
1536
+
1537
+ let mouseAction;
1538
+
1539
+ switch ( event.button ) {
1540
+
1541
+ case 0:
1542
+
1543
+ mouseAction = this.mouseButtons.LEFT;
1544
+ break;
1545
+
1546
+ case 1:
1547
+
1548
+ mouseAction = this.mouseButtons.MIDDLE;
1549
+ break;
1550
+
1551
+ case 2:
1552
+
1553
+ mouseAction = this.mouseButtons.RIGHT;
1554
+ break;
1555
+
1556
+ default:
1557
+
1558
+ mouseAction = - 1;
1559
+
1560
+ }
1561
+
1562
+ switch ( mouseAction ) {
1563
+
1564
+ case MOUSE.DOLLY:
1565
+
1566
+ if ( this.enableZoom === false ) return;
1567
+
1568
+ this._handleMouseDownDolly( event );
1569
+
1570
+ this.state = _STATE.DOLLY;
1571
+
1572
+ break;
1573
+
1574
+ case MOUSE.ROTATE:
1575
+
1576
+ if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
1577
+
1578
+ if ( this.enablePan === false ) return;
1579
+
1580
+ this._handleMouseDownPan( event );
1581
+
1582
+ this.state = _STATE.PAN;
1583
+
1584
+ } else {
1585
+
1586
+ if ( this.enableRotate === false ) return;
1587
+
1588
+ this._handleMouseDownRotate( event );
1589
+
1590
+ this.state = _STATE.ROTATE;
1591
+
1592
+ }
1593
+
1594
+ break;
1595
+
1596
+ case MOUSE.PAN:
1597
+
1598
+ if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
1599
+
1600
+ if ( this.enableRotate === false ) return;
1601
+
1602
+ this._handleMouseDownRotate( event );
1603
+
1604
+ this.state = _STATE.ROTATE;
1605
+
1606
+ } else {
1607
+
1608
+ if ( this.enablePan === false ) return;
1609
+
1610
+ this._handleMouseDownPan( event );
1611
+
1612
+ this.state = _STATE.PAN;
1613
+
1614
+ }
1615
+
1616
+ break;
1617
+
1618
+ default:
1619
+
1620
+ this.state = _STATE.NONE;
1621
+
1622
+ }
1623
+
1624
+ if ( this.state !== _STATE.NONE ) {
1625
+
1626
+ this.dispatchEvent( _startEvent );
1627
+
1628
+ }
1629
+
1630
+ }
1631
+
1632
+ function onMouseMove( event ) {
1633
+
1634
+ switch ( this.state ) {
1635
+
1636
+ case _STATE.ROTATE:
1637
+
1638
+ if ( this.enableRotate === false ) return;
1639
+
1640
+ this._handleMouseMoveRotate( event );
1641
+
1642
+ break;
1643
+
1644
+ case _STATE.DOLLY:
1645
+
1646
+ if ( this.enableZoom === false ) return;
1647
+
1648
+ this._handleMouseMoveDolly( event );
1649
+
1650
+ break;
1651
+
1652
+ case _STATE.PAN:
1653
+
1654
+ if ( this.enablePan === false ) return;
1655
+
1656
+ this._handleMouseMovePan( event );
1657
+
1658
+ break;
1659
+
1660
+ }
1661
+
1662
+ }
1663
+
1664
+ function onMouseWheel( event ) {
1665
+
1666
+ if ( this.enabled === false || this.enableZoom === false || this.state !== _STATE.NONE ) return;
1667
+
1668
+ event.preventDefault();
1669
+
1670
+ this.dispatchEvent( _startEvent );
1671
+
1672
+ this._handleMouseWheel( this._customWheelEvent( event ) );
1673
+
1674
+ this.dispatchEvent( _endEvent );
1675
+
1676
+ }
1677
+
1678
+ function onKeyDown( event ) {
1679
+
1680
+ if ( this.enabled === false ) return;
1681
+
1682
+ this._handleKeyDown( event );
1683
+
1684
+ }
1685
+
1686
+ function onTouchStart( event ) {
1687
+
1688
+ this._trackPointer( event );
1689
+
1690
+ switch ( this._pointers.length ) {
1691
+
1692
+ case 1:
1693
+
1694
+ switch ( this.touches.ONE ) {
1695
+
1696
+ case TOUCH.ROTATE:
1697
+
1698
+ if ( this.enableRotate === false ) return;
1699
+
1700
+ this._handleTouchStartRotate( event );
1701
+
1702
+ this.state = _STATE.TOUCH_ROTATE;
1703
+
1704
+ break;
1705
+
1706
+ case TOUCH.PAN:
1707
+
1708
+ if ( this.enablePan === false ) return;
1709
+
1710
+ this._handleTouchStartPan( event );
1711
+
1712
+ this.state = _STATE.TOUCH_PAN;
1713
+
1714
+ break;
1715
+
1716
+ default:
1717
+
1718
+ this.state = _STATE.NONE;
1719
+
1720
+ }
1721
+
1722
+ break;
1723
+
1724
+ case 2:
1725
+
1726
+ switch ( this.touches.TWO ) {
1727
+
1728
+ case TOUCH.DOLLY_PAN:
1729
+
1730
+ if ( this.enableZoom === false && this.enablePan === false ) return;
1731
+
1732
+ this._handleTouchStartDollyPan( event );
1733
+
1734
+ this.state = _STATE.TOUCH_DOLLY_PAN;
1735
+
1736
+ break;
1737
+
1738
+ case TOUCH.DOLLY_ROTATE:
1739
+
1740
+ if ( this.enableZoom === false && this.enableRotate === false ) return;
1741
+
1742
+ this._handleTouchStartDollyRotate( event );
1743
+
1744
+ this.state = _STATE.TOUCH_DOLLY_ROTATE;
1745
+
1746
+ break;
1747
+
1748
+ default:
1749
+
1750
+ this.state = _STATE.NONE;
1751
+
1752
+ }
1753
+
1754
+ break;
1755
+
1756
+ default:
1757
+
1758
+ this.state = _STATE.NONE;
1759
+
1760
+ }
1761
+
1762
+ if ( this.state !== _STATE.NONE ) {
1763
+
1764
+ this.dispatchEvent( _startEvent );
1765
+
1766
+ }
1767
+
1768
+ }
1769
+
1770
+ function onTouchMove( event ) {
1771
+
1772
+ this._trackPointer( event );
1773
+
1774
+ switch ( this.state ) {
1775
+
1776
+ case _STATE.TOUCH_ROTATE:
1777
+
1778
+ if ( this.enableRotate === false ) return;
1779
+
1780
+ this._handleTouchMoveRotate( event );
1781
+
1782
+ this.update();
1783
+
1784
+ break;
1785
+
1786
+ case _STATE.TOUCH_PAN:
1787
+
1788
+ if ( this.enablePan === false ) return;
1789
+
1790
+ this._handleTouchMovePan( event );
1791
+
1792
+ this.update();
1793
+
1794
+ break;
1795
+
1796
+ case _STATE.TOUCH_DOLLY_PAN:
1797
+
1798
+ if ( this.enableZoom === false && this.enablePan === false ) return;
1799
+
1800
+ this._handleTouchMoveDollyPan( event );
1801
+
1802
+ this.update();
1803
+
1804
+ break;
1805
+
1806
+ case _STATE.TOUCH_DOLLY_ROTATE:
1807
+
1808
+ if ( this.enableZoom === false && this.enableRotate === false ) return;
1809
+
1810
+ this._handleTouchMoveDollyRotate( event );
1811
+
1812
+ this.update();
1813
+
1814
+ break;
1815
+
1816
+ default:
1817
+
1818
+ this.state = _STATE.NONE;
1819
+
1820
+ }
1821
+
1822
+ }
1823
+
1824
+ function onContextMenu( event ) {
1825
+
1826
+ if ( this.enabled === false ) return;
1827
+
1828
+ event.preventDefault();
1829
+
1830
+ }
1831
+
1832
+ function interceptControlDown( event ) {
1833
+
1834
+ if ( event.key === 'Control' ) {
1835
+
1836
+ this._controlActive = true;
1837
+
1838
+ const document = this.domElement.getRootNode(); // offscreen canvas compatibility
1839
+
1840
+ document.addEventListener( 'keyup', this._interceptControlUp, { passive: true, capture: true } );
1841
+
1842
+ }
1843
+
1844
+ }
1845
+
1846
+ function interceptControlUp( event ) {
1847
+
1848
+ if ( event.key === 'Control' ) {
1849
+
1850
+ this._controlActive = false;
1851
+
1852
+ const document = this.domElement.getRootNode(); // offscreen canvas compatibility
1853
+
1854
+ document.removeEventListener( 'keyup', this._interceptControlUp, { passive: true, capture: true } );
1855
+
1856
+ }
1857
+
1858
+ }
1859
+
1860
+ export { OrbitControls };
vendor/three.core.js ADDED
The diff for this file is too large to render. See raw diff
 
vendor/three.module.js ADDED
The diff for this file is too large to render. See raw diff
 
world.js ADDED
@@ -0,0 +1,305 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // World synthesis: prompt -> region plan -> semantic layout -> composite height field.
2
+ //
3
+ // This is an independent implementation of the terrain foundation described in
4
+ // "WorldClaw: Agentic 3D Open-world Generation at Scale" (arXiv 2608.05248).
5
+ // No Tencent code or model weights are used โ€” that repository publishes neither.
6
+ // The structure it describes and that we follow here:
7
+ // H(x) = sum_r m_r(x) * [ h_r + sum_k w_rk N_rk(x) + sum_j a_rj G_rj(x) ]
8
+ // i.e. per-region base elevation, per-region noise bands, and per-region geomorphic
9
+ // operators, combined through smoothed region masks so borders blend rather than step.
10
+
11
+ export const GRID = 192; // height field resolution
12
+ export const WORLD = 200; // world extent in metres
13
+
14
+ // ---------------------------------------------------------------- rng + noise --
15
+ export function makeRng(seed) {
16
+ let s = seed >>> 0 || 1;
17
+ return function rng() {
18
+ s ^= s << 13; s >>>= 0;
19
+ s ^= s >> 17;
20
+ s ^= s << 5; s >>>= 0;
21
+ return s / 4294967296;
22
+ };
23
+ }
24
+
25
+ function makeValueNoise(rng) {
26
+ const size = 256;
27
+ const perm = new Uint8Array(size * 2);
28
+ const base = new Uint8Array(size);
29
+ for (let i = 0; i < size; i++) base[i] = i;
30
+ for (let i = size - 1; i > 0; i--) {
31
+ const j = Math.floor(rng() * (i + 1));
32
+ [base[i], base[j]] = [base[j], base[i]];
33
+ }
34
+ for (let i = 0; i < size * 2; i++) perm[i] = base[i & 255];
35
+ const grad = new Float32Array(size * 2);
36
+ for (let i = 0; i < size * 2; i++) grad[i] = rng() * 2 - 1;
37
+
38
+ const fade = t => t * t * t * (t * (t * 6 - 15) + 10);
39
+ const lerp = (a, b, t) => a + (b - a) * t;
40
+
41
+ return function noise2(x, y) {
42
+ const xi = Math.floor(x) & 255, yi = Math.floor(y) & 255;
43
+ const xf = x - Math.floor(x), yf = y - Math.floor(y);
44
+ const u = fade(xf), v = fade(yf);
45
+ const aa = grad[perm[perm[xi] + yi]];
46
+ const ab = grad[perm[perm[xi] + yi + 1]];
47
+ const ba = grad[perm[perm[xi + 1] + yi]];
48
+ const bb = grad[perm[perm[xi + 1] + yi + 1]];
49
+ return lerp(lerp(aa, ba, u), lerp(ab, bb, u), v);
50
+ };
51
+ }
52
+
53
+ function fbm(noise, x, y, octaves, lacunarity = 2.0, gain = 0.5) {
54
+ let sum = 0, amp = 1, freq = 1, norm = 0;
55
+ for (let o = 0; o < octaves; o++) {
56
+ sum += amp * noise(x * freq, y * freq);
57
+ norm += amp;
58
+ amp *= gain;
59
+ freq *= lacunarity;
60
+ }
61
+ return sum / norm;
62
+ }
63
+
64
+ // ------------------------------------------------------------------- biomes ----
65
+ // Each biome is one row of the height formula: a base elevation, a noise band, and
66
+ // a geomorphic operator. Colours are the semantic layout map's encoding as well as
67
+ // the terrain tint, so the map and the mesh cannot drift apart.
68
+ export const BIOMES = {
69
+ ocean: { label: 'Ocean', color: '#1d4e6b', base: -13, amp: 1.2, freq: 1.4, op: 'flat', rough: 0.1, props: [] },
70
+ lake: { label: 'Lake', color: '#2a6b8a', base: -6, amp: 0.8, freq: 1.6, op: 'flat', rough: 0.1, props: [] },
71
+ beach: { label: 'Beach', color: '#d9c89a', base: 0.6, amp: 1.0, freq: 2.2, op: 'flat', rough: 0.2, props: ['palm', 'rock'] },
72
+ plains: { label: 'Plains', color: '#7d9455', base: 4, amp: 3.0, freq: 1.8, op: 'none', rough: 0.4, props: ['tree', 'shrub', 'rock'] },
73
+ meadow: { label: 'Meadow', color: '#94a95c', base: 5, amp: 4.0, freq: 2.4, op: 'none', rough: 0.4, props: ['shrub', 'tree'] },
74
+ savanna: { label: 'Savanna', color: '#a89a5a', base: 5, amp: 3.5, freq: 1.6, op: 'none', rough: 0.4, props: ['acacia', 'rock'] },
75
+ forest: { label: 'Forest', color: '#3f6b3a', base: 7, amp: 6.0, freq: 2.2, op: 'none', rough: 0.6, props: ['pine', 'tree', 'rock'] },
76
+ jungle: { label: 'Jungle', color: '#2f6b39', base: 6, amp: 7.0, freq: 2.8, op: 'none', rough: 0.7, props: ['palm', 'tree', 'shrub'] },
77
+ swamp: { label: 'Swamp', color: '#4a5c3a', base: 1.2, amp: 1.6, freq: 2.6, op: 'flat', rough: 0.3, props: ['tree', 'shrub'] },
78
+ desert: { label: 'Desert', color: '#c9a86a', base: 4, amp: 5.0, freq: 1.5, op: 'dune', rough: 0.3, props: ['cactus', 'rock'] },
79
+ badlands: { label: 'Badlands', color: '#a8734a', base: 9, amp: 12.0, freq: 1.9, op: 'terrace', rough: 0.8, props: ['rock', 'cactus'] },
80
+ canyon: { label: 'Canyon', color: '#9c5f42', base: 12, amp: 16.0, freq: 1.2, op: 'erosion', rough: 1.0, props: ['rock'] },
81
+ mesa: { label: 'Mesa', color: '#b07048', base: 14, amp: 10.0, freq: 1.0, op: 'terrace', rough: 0.7, props: ['rock', 'cactus'] },
82
+ hills: { label: 'Hills', color: '#6f8a4e', base: 10, amp: 9.0, freq: 1.6, op: 'none', rough: 0.5, props: ['tree', 'shrub', 'rock'] },
83
+ mountain: { label: 'Mountains', color: '#7c7a72', base: 20, amp: 26.0, freq: 1.1, op: 'peak', rough: 1.0, props: ['pine', 'rock'] },
84
+ alpine: { label: 'Alpine', color: '#8c8d8a', base: 26, amp: 24.0, freq: 1.3, op: 'peak', rough: 1.0, props: ['pine', 'rock'] },
85
+ snow: { label: 'Snowfield', color: '#dfe6ea', base: 30, amp: 16.0, freq: 1.2, op: 'peak', rough: 0.8, props: ['rock'] },
86
+ tundra: { label: 'Tundra', color: '#9aa89c', base: 6, amp: 4.0, freq: 1.8, op: 'none', rough: 0.4, props: ['rock', 'shrub'] },
87
+ volcano: { label: 'Volcano', color: '#4a3f3d', base: 22, amp: 30.0, freq: 1.0, op: 'cone', rough: 1.0, props: ['rock'] },
88
+ crater: { label: 'Crater', color: '#6b665f', base: 8, amp: 14.0, freq: 1.4, op: 'crater', rough: 0.9, props: ['rock'] },
89
+ };
90
+
91
+ // Keyword -> biome. Longer phrases are matched first so "snow mountain" does not
92
+ // collapse to "snow" alone.
93
+ const KEYWORDS = [
94
+ [['ocean', 'sea', 'coastline', '๋ฐ”๋‹ค', 'ํ•ด์•ˆ'], ['ocean', 'beach']],
95
+ [['island', '์„ฌ'], ['ocean', 'beach', 'jungle', 'mountain']],
96
+ [['lake', 'pond', 'ํ˜ธ์ˆ˜'], ['lake', 'meadow']],
97
+ [['river', 'valley', '๊ณ„๊ณก', '๊ฐ•'], ['canyon', 'hills', 'forest']],
98
+ [['beach', 'shore', 'ํ•ด๋ณ€'], ['beach', 'ocean']],
99
+ [['canyon', 'gorge', 'ํ˜‘๊ณก'], ['canyon', 'mesa', 'desert']],
100
+ [['mesa', 'butte'], ['mesa', 'desert']],
101
+ [['badland', 'wasteland', 'ํ™ฉ๋ฌด์ง€'], ['badlands', 'desert']],
102
+ [['desert', 'dune', 'sand', '์‚ฌ๋ง‰'], ['desert', 'badlands']],
103
+ [['oasis'], ['desert', 'lake', 'palm']],
104
+ [['volcano', 'lava', 'ํ™”์‚ฐ'], ['volcano', 'badlands']],
105
+ [['crater', 'moon', 'lunar', '๋ถ„ํ™”๊ตฌ'], ['crater', 'tundra']],
106
+ [['jungle', 'rainforest', '์ •๊ธ€'], ['jungle', 'hills']],
107
+ [['forest', 'wood', 'pine', 'taiga', '์ˆฒ'], ['forest', 'hills']],
108
+ [['swamp', 'marsh', 'bog', '๋Šช'], ['swamp', 'forest']],
109
+ [['savanna', 'safari'], ['savanna', 'plains']],
110
+ [['tundra', 'arctic', 'frozen', 'ํˆฐ๋“œ๋ผ'], ['tundra', 'snow']],
111
+ [['glacier', 'snow', 'ice', '์„ค์›', '๋น™ํ•˜'], ['snow', 'alpine']],
112
+ [['alpine', 'alps'], ['alpine', 'snow', 'forest']],
113
+ [['mountain', 'peak', 'ridge', '์‚ฐ'], ['mountain', 'hills']],
114
+ [['hill', 'highland', '์–ธ๋•'], ['hills', 'meadow']],
115
+ [['meadow', 'grass', 'field', 'prairie', '์ดˆ์›'], ['meadow', 'plains']],
116
+ [['plain', 'steppe', 'ํ‰์›'], ['plains', 'hills']],
117
+ ];
118
+
119
+ export function planRegions(prompt, rng) {
120
+ const text = (prompt || '').toLowerCase();
121
+
122
+ // Rank by where the word appears in the prompt, not by dictionary order: in
123
+ // "a snowy alpine range above a pine forest and a lake" the subject is the range,
124
+ // and coverage below is derived from this ranking.
125
+ const hits = [];
126
+ for (const [words, biomes] of KEYWORDS) {
127
+ let at = Infinity;
128
+ for (const w of words) {
129
+ const i = text.indexOf(w);
130
+ if (i >= 0) at = Math.min(at, i);
131
+ }
132
+ if (at < Infinity) hits.push({ at, biomes });
133
+ }
134
+ hits.sort((a, b) => a.at - b.at);
135
+
136
+ const picked = [];
137
+ for (const h of hits) {
138
+ for (const b of h.biomes) if (BIOMES[b] && !picked.includes(b)) picked.push(b);
139
+ }
140
+ if (picked.length === 0) picked.push('meadow', 'forest', 'hills', 'mountain');
141
+ if (picked.length === 1) {
142
+ const companions = { ocean: 'beach', desert: 'mesa', snow: 'alpine', jungle: 'hills' };
143
+ picked.push(companions[picked[0]] || 'plains');
144
+ }
145
+ const regions = picked.slice(0, 5);
146
+
147
+ // Coverage: earlier keywords weigh more, so the leading noun dominates the map.
148
+ const weights = regions.map((_, i) => 1 / (1 + i * 0.55));
149
+ const total = weights.reduce((a, b) => a + b, 0);
150
+ return regions.map((key, i) => ({
151
+ key,
152
+ ...BIOMES[key],
153
+ coverage: weights[i] / total,
154
+ seeds: Math.max(1, Math.round(weights[i] / total * 9)),
155
+ jitter: rng(),
156
+ }));
157
+ }
158
+
159
+ // -------------------------------------------------------- semantic layout map --
160
+ // Region ownership by warped Voronoi: the domain warp is what keeps borders from
161
+ // looking like a polygon diagram.
162
+ function buildLayout(regions, rng, noise) {
163
+ const seeds = [];
164
+ regions.forEach((r, ri) => {
165
+ for (let s = 0; s < r.seeds; s++) {
166
+ seeds.push({ ri, x: rng() * GRID, y: rng() * GRID });
167
+ }
168
+ });
169
+
170
+ const owner = new Int16Array(GRID * GRID);
171
+ for (let y = 0; y < GRID; y++) {
172
+ for (let x = 0; x < GRID; x++) {
173
+ const wx = x + fbm(noise, x * 0.018, y * 0.018, 3) * 26;
174
+ const wy = y + fbm(noise, x * 0.018 + 40, y * 0.018 + 40, 3) * 26;
175
+ let best = 0, bestD = Infinity;
176
+ for (const s of seeds) {
177
+ const d = (s.x - wx) ** 2 + (s.y - wy) ** 2;
178
+ if (d < bestD) { bestD = d; best = s.ri; }
179
+ }
180
+ owner[y * GRID + x] = best;
181
+ }
182
+ }
183
+ return owner;
184
+ }
185
+
186
+ // Soft masks: one blurred 0..1 field per region, renormalised so they sum to 1.
187
+ function buildMasks(owner, count, passes = 3) {
188
+ const masks = [];
189
+ for (let r = 0; r < count; r++) {
190
+ const m = new Float32Array(GRID * GRID);
191
+ for (let i = 0; i < m.length; i++) m[i] = owner[i] === r ? 1 : 0;
192
+ masks.push(m);
193
+ }
194
+ const tmp = new Float32Array(GRID * GRID);
195
+ for (const m of masks) {
196
+ for (let p = 0; p < passes; p++) {
197
+ for (let y = 0; y < GRID; y++) {
198
+ for (let x = 0; x < GRID; x++) {
199
+ let sum = 0, n = 0;
200
+ for (let dy = -2; dy <= 2; dy++) {
201
+ const yy = y + dy;
202
+ if (yy < 0 || yy >= GRID) continue;
203
+ for (let dx = -2; dx <= 2; dx++) {
204
+ const xx = x + dx;
205
+ if (xx < 0 || xx >= GRID) continue;
206
+ sum += m[yy * GRID + xx]; n++;
207
+ }
208
+ }
209
+ tmp[y * GRID + x] = sum / n;
210
+ }
211
+ }
212
+ m.set(tmp);
213
+ }
214
+ }
215
+ for (let i = 0; i < GRID * GRID; i++) {
216
+ let sum = 0;
217
+ for (const m of masks) sum += m[i];
218
+ if (sum > 1e-6) for (const m of masks) m[i] /= sum;
219
+ else masks[0][i] = 1;
220
+ }
221
+ return masks;
222
+ }
223
+
224
+ // -------------------------------------------------------- geomorphic operators --
225
+ function operator(kind, x, y, cx, cy, noise, phase) {
226
+ const nx = (x - cx) / GRID * 2, ny = (y - cy) / GRID * 2;
227
+ const d = Math.sqrt(nx * nx + ny * ny);
228
+ switch (kind) {
229
+ case 'peak': // ridged mass falling off outward
230
+ return Math.max(0, 1 - d * 1.15) ** 1.6 * (0.55 + 0.45 * Math.abs(fbm(noise, x * 0.02, y * 0.02, 4)));
231
+ case 'cone': // volcano: cone with a summit vent
232
+ return Math.max(0, 1 - d * 1.3) ** 1.2 - Math.max(0, 1 - d * 7) ** 2 * 0.55;
233
+ case 'crater': // rim up, floor down
234
+ return Math.max(0, 1 - Math.abs(d * 3.2 - 1) * 2.2) * 0.9 - Math.max(0, 1 - d * 3.2) ** 2 * 0.7;
235
+ case 'dune': // travelling ridges
236
+ return Math.sin((x * 0.16 + y * 0.07) + phase * 6.28 + fbm(noise, x * 0.02, y * 0.02, 2) * 2.2) * 0.5;
237
+ case 'terrace': // stepped plateaus
238
+ return Math.round(fbm(noise, x * 0.012, y * 0.012, 3) * 3.2) / 3.2;
239
+ case 'erosion': { // incised channels
240
+ const v = Math.abs(fbm(noise, x * 0.014 + phase * 10, y * 0.014, 4));
241
+ return -((1 - v) ** 2) * 1.3; // parens required: `-x ** 2` is a syntax error
242
+ }
243
+ case 'flat':
244
+ return 0;
245
+ default:
246
+ return 0;
247
+ }
248
+ }
249
+
250
+ // ------------------------------------------------------------------ synthesis --
251
+ export function synthesize(prompt, seed) {
252
+ const rng = makeRng(seed);
253
+ const noise = makeValueNoise(rng);
254
+ const regions = planRegions(prompt, rng);
255
+ const owner = buildLayout(regions, rng, noise);
256
+ const masks = buildMasks(owner, regions.length);
257
+
258
+ // Region centroids feed the radial operators (peak, cone, crater).
259
+ const cent = regions.map(() => ({ x: 0, y: 0, n: 0 }));
260
+ for (let y = 0; y < GRID; y++) {
261
+ for (let x = 0; x < GRID; x++) {
262
+ const c = cent[owner[y * GRID + x]];
263
+ c.x += x; c.y += y; c.n++;
264
+ }
265
+ }
266
+ cent.forEach(c => { if (c.n) { c.x /= c.n; c.y /= c.n; } });
267
+
268
+ const height = new Float32Array(GRID * GRID);
269
+ for (let y = 0; y < GRID; y++) {
270
+ for (let x = 0; x < GRID; x++) {
271
+ const i = y * GRID + x;
272
+ let h = 0;
273
+ for (let r = 0; r < regions.length; r++) {
274
+ const m = masks[r][i];
275
+ if (m < 0.002) continue;
276
+ const reg = regions[r];
277
+ const bands =
278
+ fbm(noise, x * 0.012 * reg.freq, y * 0.012 * reg.freq, 5) * reg.amp +
279
+ fbm(noise, x * 0.05 * reg.freq, y * 0.05 * reg.freq, 3) * reg.amp * 0.22 * reg.rough;
280
+ const geo = operator(reg.op, x, y, cent[r].x, cent[r].y, noise, reg.jitter) * reg.amp;
281
+ h += m * (reg.base + bands + geo);
282
+ }
283
+ height[i] = h;
284
+ }
285
+ }
286
+
287
+ // One smoothing pass: masks blend the fields, but operator seams still benefit.
288
+ const sm = new Float32Array(height.length);
289
+ for (let y = 0; y < GRID; y++) {
290
+ for (let x = 0; x < GRID; x++) {
291
+ let sum = 0, n = 0;
292
+ for (let dy = -1; dy <= 1; dy++) {
293
+ for (let dx = -1; dx <= 1; dx++) {
294
+ const yy = y + dy, xx = x + dx;
295
+ if (yy < 0 || yy >= GRID || xx < 0 || xx >= GRID) continue;
296
+ sum += height[yy * GRID + xx]; n++;
297
+ }
298
+ }
299
+ sm[y * GRID + x] = sum / n;
300
+ }
301
+ }
302
+
303
+ const hasWater = regions.some(r => r.key === 'ocean' || r.key === 'lake' || r.key === 'swamp');
304
+ return { regions, owner, masks, height: sm, seaLevel: hasWater ? 0 : -999 };
305
+ }