@volter/threejs-runtime 0.5.65

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +12 -0
  3. package/package.json +62 -0
  4. package/src/adapter/body-marks.ts +69 -0
  5. package/src/adapter/constraint.ts +78 -0
  6. package/src/adapter/first-party-navigation-system.ts +48 -0
  7. package/src/adapter/hierarchy-marks.ts +156 -0
  8. package/src/adapter/ingest/scene-capture.ts +865 -0
  9. package/src/adapter/ingest/structural-ids.ts +139 -0
  10. package/src/adapter/ingest/visible-capture-window.ts +302 -0
  11. package/src/adapter/object3d-authoring-subject.ts +50 -0
  12. package/src/adapter/rapier-physics-adapter.ts +226 -0
  13. package/src/adapter/reflection-probe.ts +75 -0
  14. package/src/adapter/renderer-config.ts +116 -0
  15. package/src/adapter/trigger-volume.ts +29 -0
  16. package/src/ai/navigation.ts +627 -0
  17. package/src/animation/animation-clock.ts +479 -0
  18. package/src/animation/runtime-inspection.ts +45 -0
  19. package/src/animation/xstate-animation-binding.ts +655 -0
  20. package/src/animation/xstate-animation-meta.ts +360 -0
  21. package/src/asset-formats/camera.ts +18 -0
  22. package/src/asset-formats/collider.ts +66 -0
  23. package/src/asset-formats/index.ts +45 -0
  24. package/src/asset-formats/light.ts +50 -0
  25. package/src/asset-formats/material.ts +93 -0
  26. package/src/asset-formats/mesh.ts +78 -0
  27. package/src/asset-formats/particles.ts +426 -0
  28. package/src/asset-formats/render-env.ts +318 -0
  29. package/src/asset-formats/tuples.ts +21 -0
  30. package/src/asset-loaders.ts +242 -0
  31. package/src/asset-parse-error.ts +29 -0
  32. package/src/assets.ts +68 -0
  33. package/src/behavior/xstate-inspection.ts +51 -0
  34. package/src/defaults.ts +135 -0
  35. package/src/ecs/object-marks.ts +75 -0
  36. package/src/ecs/scene-index.ts +439 -0
  37. package/src/ecs/scene-query.ts +43 -0
  38. package/src/ecs/user-data.ts +272 -0
  39. package/src/loader.ts +134 -0
  40. package/src/physics/collider-dimensions.ts +121 -0
  41. package/src/physics/collision-system.ts +76 -0
  42. package/src/physics/physics-registry.ts +83 -0
  43. package/src/physics/transform-writer.ts +41 -0
  44. package/src/render/directional-shadow-fit.ts +158 -0
  45. package/src/render/environment-capture.ts +133 -0
  46. package/src/render/ibl-override-material.ts +202 -0
  47. package/src/render/light-camera-factory.ts +103 -0
  48. package/src/render/matcap-texture.ts +92 -0
  49. package/src/render/material-factory.ts +201 -0
  50. package/src/render/mesh-shadow.ts +18 -0
  51. package/src/render/particles-factory.ts +589 -0
  52. package/src/render/render-batch-system.ts +260 -0
  53. package/src/render/render-features.ts +128 -0
  54. package/src/render/render-settings.ts +71 -0
  55. package/src/render/soft-particle-depth.ts +360 -0
  56. package/src/render/spark-renderer-lifecycle.ts +64 -0
  57. package/src/render/structural-signature.ts +231 -0
  58. package/src/render/viewport-shading.ts +163 -0
  59. package/src/render/voxel-surface.ts +358 -0
  60. package/src/setup/setup-particles.ts +32 -0
  61. package/src/setup/setup-physics.ts +67 -0
  62. package/src/setup/setup-renderer.ts +524 -0
  63. package/src/types-n8ao.d.ts +37 -0
  64. package/src/types-realism-effects.d.ts +61 -0
  65. package/vendor/realism-effects/LICENSE.md +21 -0
  66. package/vendor/realism-effects/UPSTREAM.md +19 -0
  67. package/vendor/realism-effects/dist/index.cjs +3447 -0
  68. package/vendor/realism-effects/dist/index.d.ts +59 -0
  69. package/vendor/realism-effects/dist/index.js +3434 -0
  70. package/vendor/realism-effects/package.json +23 -0
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Shared light + camera factory — the ONE place light/camera fallbacks are
3
+ * decided, and it decides them by READING `DEFAULTS.light`/`DEFAULTS.camera`.
4
+ *
5
+ * A second copy hardcoding the same literals (color '#ffffff', fov 60, near
6
+ * 0.1, far 1000) would agree by luck rather than by construction, and any
7
+ * edit to either copy — or to `DEFAULTS` — would silently diverge.
8
+ *
9
+ * This module is now the ONE place that builds a `THREE.Light`/`THREE.Camera`
10
+ * from a `LightDescriptor`/`CameraDescriptor` descriptor, reading its fallbacks from
11
+ * `DEFAULTS` (the single source of truth for scene default values — see
12
+ * `defaults.ts`). Both the loader and the editor import it, so they
13
+ * can no longer disagree.
14
+ */
15
+
16
+ import * as THREE from 'three';
17
+ // Area lights. `RectAreaLight` requires its LTC lookup-texture uniforms
18
+ // initialized once before the first instance is constructed; three.js ships
19
+ // that setup as an addon, not part of core. Imported via the
20
+ // `three/addons/*` alias (package.json `exports`: `"./addons/*":
21
+ // "./examples/jsm/*"` — the identical file as
22
+ // `three/examples/jsm/lights/RectAreaLightUniformsLib.js`) because a literal
23
+ // `/examples/` specifier trips this repo's game/editor-example import guard —
24
+ // a same-substring false positive against our own `src/examples/` convention,
25
+ // unrelated to three.js's addon folder naming.
26
+ import { RectAreaLightUniformsLib } from 'three/addons/lights/RectAreaLightUniformsLib.js';
27
+ import type { CameraDescriptor } from '../asset-formats/camera';
28
+ import type { LightDescriptor } from '../asset-formats/light';
29
+ import { DEFAULTS } from '../defaults';
30
+
31
+ let rectAreaLightUniformsInitialized = false;
32
+
33
+ /** Build a Three.js light from a `LightDescriptor` descriptor. */
34
+ export function createLight(def: LightDescriptor): THREE.Light {
35
+ const d = DEFAULTS.light;
36
+ const color = def.color ?? d.color;
37
+ const intensity = def.intensity ?? d.intensity;
38
+
39
+ switch (def.type) {
40
+ case 'directional':
41
+ return new THREE.DirectionalLight(color, intensity);
42
+ case 'point':
43
+ return new THREE.PointLight(
44
+ color,
45
+ intensity,
46
+ def.distance ?? d.distance,
47
+ def.decay ?? d.decay,
48
+ );
49
+ case 'spot': {
50
+ const light = new THREE.SpotLight(
51
+ color,
52
+ intensity,
53
+ def.distance ?? d.distance,
54
+ def.angle ?? d.angle,
55
+ def.penumbra ?? d.penumbra,
56
+ );
57
+ light.decay = def.decay ?? d.decay;
58
+ return light;
59
+ }
60
+ case 'hemisphere':
61
+ return new THREE.HemisphereLight(color, def.groundColor ?? d.groundColor, intensity);
62
+ case 'area': {
63
+ // No shadow map is ever wired for this light (RectAreaLight does not
64
+ // support shadows in three.js — see the honesty note on the schema
65
+ // field's .describe(); degrade loudly, not silently).
66
+ if (!rectAreaLightUniformsInitialized) {
67
+ RectAreaLightUniformsLib.init();
68
+ rectAreaLightUniformsInitialized = true;
69
+ }
70
+ return new THREE.RectAreaLight(
71
+ color,
72
+ intensity,
73
+ def.width ?? d.width,
74
+ def.height ?? d.height,
75
+ );
76
+ }
77
+ default:
78
+ return new THREE.DirectionalLight(color, intensity);
79
+ }
80
+ }
81
+
82
+ /** Build a Three.js camera from a `CameraDescriptor` descriptor. */
83
+ export function createCamera(def: CameraDescriptor): THREE.Camera {
84
+ const d = DEFAULTS.camera;
85
+ const aspect = (def.width ?? d.width) / (def.height ?? d.height);
86
+ if (def.type === 'orthographic') {
87
+ const size = def.orthoSize ?? d.orthoSize;
88
+ return new THREE.OrthographicCamera(
89
+ -size * aspect,
90
+ size * aspect,
91
+ size,
92
+ -size,
93
+ def.near ?? d.near,
94
+ def.far ?? d.far,
95
+ );
96
+ }
97
+ return new THREE.PerspectiveCamera(
98
+ def.fov ?? d.fov,
99
+ aspect,
100
+ def.near ?? d.near,
101
+ def.far ?? d.far,
102
+ );
103
+ }
@@ -0,0 +1,92 @@
1
+ import * as THREE from 'three';
2
+
3
+ /**
4
+ * The neutral matcap sphere, DRAWN rather than shipped.
5
+ *
6
+ * A matcap is just a picture of a lit sphere sampled by view-space normal, so
7
+ * there is nothing to vendor: the same three numbers a shader would use
8
+ * (key, fill, rim) evaluated once per texel produce the image. Keeping it
9
+ * procedural means the diagnostic look has no binary asset, no license, no
10
+ * provenance row, and no way to go missing from a build.
11
+ *
12
+ * The look is deliberately CLAY: one warm key from the upper left, a cool
13
+ * fill from the lower right, a tight rim, and a broad soft highlight — the
14
+ * neutral sculpting material whose whole job is to let form read without
15
+ * colour, texture or authored lighting getting a vote.
16
+ */
17
+
18
+ const SIZE = 256;
19
+
20
+ /** sRGB transfer curve — the canvas holds display-referred bytes. */
21
+ function encode(value: number): number {
22
+ const c = Math.min(1, Math.max(0, value));
23
+ return c <= 0.0031308 ? c * 12.92 : 1.055 * c ** (1 / 2.4) - 0.055;
24
+ }
25
+
26
+ function normalize(x: number, y: number, z: number): [number, number, number] {
27
+ const length = Math.hypot(x, y, z) || 1;
28
+ return [x / length, y / length, z / length];
29
+ }
30
+
31
+ export function drawNeutralMatcap(size = SIZE): HTMLCanvasElement {
32
+ const canvas = document.createElement('canvas');
33
+ canvas.width = size;
34
+ canvas.height = size;
35
+ const context = canvas.getContext('2d');
36
+ if (!context) return canvas;
37
+ const image = context.createImageData(size, size);
38
+
39
+ const key = normalize(-0.45, 0.62, 0.64);
40
+ const fill = normalize(0.65, -0.35, 0.5);
41
+ // View direction is +Z for a matcap: the sphere is drawn facing the camera.
42
+ const half = normalize(key[0], key[1], key[2] + 1);
43
+ const base = [0.7, 0.69, 0.68];
44
+ const fillColor = [0.34, 0.4, 0.52];
45
+ const rimColor = [0.85, 0.88, 1];
46
+
47
+ for (let py = 0; py < size; py++) {
48
+ for (let px = 0; px < size; px++) {
49
+ const nx = (px + 0.5) / size / 0.5 - 1;
50
+ const ny = 1 - (py + 0.5) / size / 0.5;
51
+ const r2 = nx * nx + ny * ny;
52
+ // Outside the unit disc a matcap is never sampled by a facing surface,
53
+ // but bilinear filtering reaches one texel past the silhouette. Clamp
54
+ // to the rim normal so the edge stays the rim colour instead of
55
+ // bleeding whatever happened to be there.
56
+ const clamped = r2 > 1;
57
+ const scale = clamped ? 1 / Math.sqrt(r2) : 1;
58
+ const x = nx * scale;
59
+ const y = ny * scale;
60
+ const z = clamped ? 0 : Math.sqrt(Math.max(0, 1 - r2));
61
+
62
+ const diffuse = Math.max(0, x * key[0] + y * key[1] + z * key[2]);
63
+ const fillTerm = Math.max(0, x * fill[0] + y * fill[1] + z * fill[2]);
64
+ const specular = Math.max(0, x * half[0] + y * half[1] + z * half[2]) ** 42 * 0.5;
65
+ const rim = (1 - z) ** 3.2 * 0.4;
66
+
67
+ const offset = (py * size + px) * 4;
68
+ for (let channel = 0; channel < 3; channel++) {
69
+ const lit =
70
+ base[channel]! * (0.16 + 0.8 * diffuse) +
71
+ fillColor[channel]! * 0.3 * fillTerm +
72
+ rimColor[channel]! * rim +
73
+ specular;
74
+ image.data[offset + channel] = Math.round(encode(lit) * 255);
75
+ }
76
+ image.data[offset + 3] = 255;
77
+ }
78
+ }
79
+ context.putImageData(image, 0, 0);
80
+ return canvas;
81
+ }
82
+
83
+ /** The drawn sphere as a ready-to-sample texture. Callers own disposal. */
84
+ export function createNeutralMatcapTexture(size = SIZE): THREE.CanvasTexture {
85
+ const texture = new THREE.CanvasTexture(drawNeutralMatcap(size));
86
+ texture.colorSpace = THREE.SRGBColorSpace;
87
+ texture.minFilter = THREE.LinearFilter;
88
+ texture.magFilter = THREE.LinearFilter;
89
+ texture.generateMipmaps = false;
90
+ texture.needsUpdate = true;
91
+ return texture;
92
+ }
@@ -0,0 +1,201 @@
1
+ import * as THREE from 'three';
2
+ import type { MaterialDescriptor } from '../asset-formats/material';
3
+ import type { MeshDescriptor } from '../asset-formats/mesh';
4
+ import { loadTexture } from '../asset-loaders';
5
+ import { DEFAULTS } from '../defaults';
6
+
7
+ /**
8
+ * Single source of truth for turning a `MeshDescriptor` / `MaterialDescriptor` descriptor
9
+ * into Three.js geometry/material. Both the runtime scene-loader and the editor's
10
+ * entity-factory import these, so the editor preview renders exactly what the game
11
+ * renders (the WYSIWYG guarantee). Previously these were
12
+ * copy-pasted in two places and had to be hand-synced — any new material feature
13
+ * added to one and not the other silently broke that guarantee.
14
+ *
15
+ */
16
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: straightforward type-based switch with fallback args
17
+ export function createGeometry(mesh: MeshDescriptor): THREE.BufferGeometry {
18
+ const args = mesh.args ?? [];
19
+ switch (mesh.type) {
20
+ case 'box':
21
+ return new THREE.BoxGeometry(args[0] ?? 1, args[1] ?? 1, args[2] ?? 1);
22
+ case 'sphere':
23
+ return new THREE.SphereGeometry(args[0] ?? 0.5, args[1] ?? 32, args[2] ?? 16);
24
+ case 'plane':
25
+ // args[2]/args[3] (width/height segments) matter for vertex-displacing
26
+ // materials (e.g. the Water shader) — they used to be silently dropped,
27
+ // which collapsed the water demo to a 2-triangle quad.
28
+ return new THREE.PlaneGeometry(args[0] ?? 1, args[1] ?? 1, args[2] ?? 1, args[3] ?? 1);
29
+ case 'cylinder':
30
+ return new THREE.CylinderGeometry(
31
+ args[0] ?? 0.5,
32
+ args[1] ?? 0.5,
33
+ args[2] ?? 1,
34
+ args[3] ?? 32,
35
+ );
36
+ case 'capsule':
37
+ return new THREE.CapsuleGeometry(args[0] ?? 0.5, args[1] ?? 1, args[2] ?? 4, args[3] ?? 16);
38
+ default:
39
+ return new THREE.BoxGeometry(1, 1, 1);
40
+ }
41
+ }
42
+
43
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: flat type-based switch building Three materials
44
+ export function createMaterial(mat?: MaterialDescriptor): THREE.Material {
45
+ if (!mat) return new THREE.MeshStandardMaterial({ color: DEFAULTS.material.color });
46
+
47
+ // Only include texture props when actually defined (Three.js warns on undefined values)
48
+ const textures: {
49
+ map?: THREE.Texture;
50
+ normalMap?: THREE.Texture;
51
+ emissiveMap?: THREE.Texture;
52
+ aoMap?: THREE.Texture;
53
+ lightMap?: THREE.Texture;
54
+ roughnessMap?: THREE.Texture;
55
+ metalnessMap?: THREE.Texture;
56
+ } = {};
57
+ if (mat.map) textures.map = loadTexture(mat.map);
58
+ if (mat.normalMap) textures.normalMap = loadTexture(mat.normalMap);
59
+ if (mat.emissiveMap) textures.emissiveMap = loadTexture(mat.emissiveMap);
60
+ if (mat.aoMap) textures.aoMap = loadTexture(mat.aoMap);
61
+ if (mat.lightMap) textures.lightMap = loadTexture(mat.lightMap);
62
+ if (mat.roughnessMap) textures.roughnessMap = loadTexture(mat.roughnessMap);
63
+ if (mat.metalnessMap) textures.metalnessMap = loadTexture(mat.metalnessMap);
64
+
65
+ // R1c — every fallback below reads DEFAULTS.material.* instead of
66
+ // duplicating its value as a bare literal, so DEFAULTS stays the single
67
+ // source of truth for what an omitted field renders as (see
68
+ // `material-factory.test.ts`'s
69
+ // "omitted-field" locks, which fail if either copy drifts from the other).
70
+ const dMat = DEFAULTS.material;
71
+ const sideValue = mat.side ?? dMat.side;
72
+ const side =
73
+ sideValue === 'back'
74
+ ? THREE.BackSide
75
+ : sideValue === 'double'
76
+ ? THREE.DoubleSide
77
+ : THREE.FrontSide;
78
+ const opacity = mat.opacity ?? dMat.opacity;
79
+ const transparent = mat.transparent ?? dMat.transparent;
80
+
81
+ switch (mat.type) {
82
+ case 'basic':
83
+ return new THREE.MeshBasicMaterial({
84
+ color: mat.color ?? dMat.color,
85
+ ...textures,
86
+ lightMapIntensity: mat.lightMapIntensity ?? 1,
87
+ opacity,
88
+ transparent,
89
+ side,
90
+ });
91
+ case 'toon':
92
+ return new THREE.MeshToonMaterial({
93
+ color: mat.color ?? dMat.color,
94
+ ...textures,
95
+ lightMapIntensity: mat.lightMapIntensity ?? 1,
96
+ emissive: mat.emissive ?? dMat.emissive,
97
+ emissiveIntensity: mat.emissiveIntensity ?? dMat.emissiveIntensity,
98
+ opacity,
99
+ transparent,
100
+ side,
101
+ });
102
+ case 'physical':
103
+ return new THREE.MeshPhysicalMaterial({
104
+ color: mat.color ?? dMat.color,
105
+ metalness: mat.metalness ?? dMat.metalness,
106
+ roughness: mat.roughness ?? dMat.roughness,
107
+ ...textures,
108
+ lightMapIntensity: mat.lightMapIntensity ?? 1,
109
+ emissive: mat.emissive ?? dMat.emissive,
110
+ emissiveIntensity: mat.emissiveIntensity ?? dMat.emissiveIntensity,
111
+ opacity,
112
+ transparent,
113
+ side,
114
+ flatShading: mat.flatShading ?? dMat.flatShading,
115
+ // Clearcoat
116
+ ...(mat.clearcoat
117
+ ? {
118
+ clearcoat: mat.clearcoat.clearcoat,
119
+ clearcoatRoughness:
120
+ mat.clearcoat.clearcoatRoughness ?? dMat.clearcoat.clearcoatRoughness,
121
+ ...(mat.clearcoat.clearcoatMap
122
+ ? { clearcoatMap: loadTexture(mat.clearcoat.clearcoatMap) }
123
+ : {}),
124
+ ...(mat.clearcoat.clearcoatRoughnessMap
125
+ ? { clearcoatRoughnessMap: loadTexture(mat.clearcoat.clearcoatRoughnessMap) }
126
+ : {}),
127
+ }
128
+ : {}),
129
+ // Transmission — `thickness` used to hardcode a literal `0` here,
130
+ // silently diverging from DEFAULTS.material.transmission.thickness
131
+ // (0.5, per the editor's Transmission inspector default). Fixed:
132
+ // read DEFAULTS directly, like every other fallback in this file.
133
+ ...(mat.transmission
134
+ ? {
135
+ transmission: mat.transmission.transmission,
136
+ ior: mat.transmission.ior ?? dMat.transmission.ior,
137
+ thickness: mat.transmission.thickness ?? dMat.transmission.thickness,
138
+ ...(mat.transmission.attenuationColor
139
+ ? { attenuationColor: new THREE.Color(mat.transmission.attenuationColor) }
140
+ : {}),
141
+ attenuationDistance: mat.transmission.attenuationDistance ?? Infinity,
142
+ ...(mat.transmission.transmissionMap
143
+ ? { transmissionMap: loadTexture(mat.transmission.transmissionMap) }
144
+ : {}),
145
+ }
146
+ : {}),
147
+ // Sheen
148
+ ...(mat.sheen
149
+ ? {
150
+ sheen: mat.sheen.sheen,
151
+ sheenColor: new THREE.Color(mat.sheen.sheenColor ?? dMat.sheen.sheenColor),
152
+ sheenRoughness: mat.sheen.sheenRoughness ?? dMat.sheen.sheenRoughness,
153
+ ...(mat.sheen.sheenColorMap
154
+ ? { sheenColorMap: loadTexture(mat.sheen.sheenColorMap) }
155
+ : {}),
156
+ ...(mat.sheen.sheenRoughnessMap
157
+ ? { sheenRoughnessMap: loadTexture(mat.sheen.sheenRoughnessMap) }
158
+ : {}),
159
+ }
160
+ : {}),
161
+ // Iridescence
162
+ ...(mat.iridescence
163
+ ? {
164
+ iridescence: mat.iridescence.iridescence,
165
+ iridescenceIOR: mat.iridescence.iridescenceIOR ?? dMat.iridescence.iridescenceIOR,
166
+ iridescenceThicknessRange:
167
+ mat.iridescence.iridescenceThicknessRange ??
168
+ dMat.iridescence.iridescenceThicknessRange,
169
+ ...(mat.iridescence.iridescenceMap
170
+ ? { iridescenceMap: loadTexture(mat.iridescence.iridescenceMap) }
171
+ : {}),
172
+ ...(mat.iridescence.iridescenceThicknessMap
173
+ ? {
174
+ iridescenceThicknessMap: loadTexture(mat.iridescence.iridescenceThicknessMap),
175
+ }
176
+ : {}),
177
+ }
178
+ : {}),
179
+ // Displacement
180
+ ...(mat.displacementMap ? { displacementMap: loadTexture(mat.displacementMap) } : {}),
181
+ ...(mat.displacementScale !== undefined
182
+ ? { displacementScale: mat.displacementScale }
183
+ : {}),
184
+ ...(mat.displacementBias !== undefined ? { displacementBias: mat.displacementBias } : {}),
185
+ });
186
+ default:
187
+ return new THREE.MeshStandardMaterial({
188
+ color: mat.color ?? dMat.color,
189
+ metalness: mat.metalness ?? dMat.metalness,
190
+ roughness: mat.roughness ?? dMat.roughness,
191
+ ...textures,
192
+ lightMapIntensity: mat.lightMapIntensity ?? 1,
193
+ emissive: mat.emissive ?? dMat.emissive,
194
+ emissiveIntensity: mat.emissiveIntensity ?? dMat.emissiveIntensity,
195
+ opacity,
196
+ transparent,
197
+ side,
198
+ flatShading: mat.flatShading ?? dMat.flatShading,
199
+ });
200
+ }
201
+ }
@@ -0,0 +1,18 @@
1
+ import * as THREE from 'three';
2
+
3
+ /**
4
+ * Apply the authored mesh-shadow contract to every renderable in a model.
5
+ *
6
+ * Primitive scene entities contain one Mesh, while glTF entities usually
7
+ * contain a Group with many Mesh/SkinnedMesh descendants. Keeping this walk
8
+ * shared prevents editor preview and runtime Play from disagreeing by source
9
+ * format.
10
+ */
11
+ export function setMeshShadowEnabled(root: THREE.Object3D, enabled: boolean | undefined): void {
12
+ if (enabled === undefined) return;
13
+ root.traverse((object) => {
14
+ if (!(object instanceof THREE.Mesh)) return;
15
+ object.castShadow = enabled;
16
+ object.receiveShadow = enabled;
17
+ });
18
+ }