@es3d/es3d 1.0.0 → 1.0.1

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 (79) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +86 -86
  3. package/docs/SPEC.md +417 -417
  4. package/package.json +1 -1
  5. package/scripts/es3d-cli.mjs +377 -377
  6. package/src/cache/geometry-store.js +392 -392
  7. package/src/cache/mesh-bundle.js +123 -123
  8. package/src/cache/sha256.js +73 -73
  9. package/src/format/ast.js +238 -238
  10. package/src/format/colors.js +93 -93
  11. package/src/format/compact-parser.js +171 -171
  12. package/src/format/compact-tokenizer.js +165 -165
  13. package/src/format/errors.js +247 -247
  14. package/src/format/json.js +55 -55
  15. package/src/format/limits.js +14 -14
  16. package/src/format/parse.js +92 -92
  17. package/src/format/schema.js +617 -617
  18. package/src/format/serializer.js +333 -333
  19. package/src/format/validator.js +330 -330
  20. package/src/format/value-error.js +12 -12
  21. package/src/format/value-format.js +314 -314
  22. package/src/format/values.js +459 -459
  23. package/src/format/xml-parser.js +136 -136
  24. package/src/format/xml-tokenizer.js +237 -237
  25. package/src/geometry/contour.js +86 -86
  26. package/src/geometry/extrude.js +175 -175
  27. package/src/geometry/gear.js +85 -85
  28. package/src/geometry/geometry-cache.js +159 -159
  29. package/src/geometry/lod-error.js +178 -178
  30. package/src/geometry/mesh.js +458 -458
  31. package/src/geometry/primitives.js +299 -299
  32. package/src/geometry/registry.js +645 -645
  33. package/src/geometry/revolve.js +136 -136
  34. package/src/geometry/sweep.js +441 -441
  35. package/src/index.js +47 -47
  36. package/src/io/compression.js +67 -67
  37. package/src/io/convert.js +236 -236
  38. package/src/io/download.js +13 -13
  39. package/src/io/external-scene.js +127 -127
  40. package/src/io/file-loader.js +28 -28
  41. package/src/io/gltf.js +630 -630
  42. package/src/io/mesh-import.js +104 -104
  43. package/src/io/obj.js +352 -352
  44. package/src/io/step-brep.js +439 -439
  45. package/src/io/step-exact.js +1149 -1149
  46. package/src/io/step.js +1418 -1418
  47. package/src/io/stl.js +193 -193
  48. package/src/io/svg-import.js +787 -787
  49. package/src/material/materials.js +154 -154
  50. package/src/math/mat4.js +475 -475
  51. package/src/math/random.js +30 -30
  52. package/src/math/trig.js +65 -65
  53. package/src/math/vec3.js +81 -81
  54. package/src/optimize/optimizer.js +579 -579
  55. package/src/path/path-flatten.js +322 -322
  56. package/src/path/path-parser.js +332 -332
  57. package/src/path/shapes.js +129 -129
  58. package/src/path/triangulate.js +517 -517
  59. package/src/pipeline.js +74 -74
  60. package/src/render/camera.js +168 -168
  61. package/src/render/frame-builder.js +362 -362
  62. package/src/render/geometry-pool.js +149 -149
  63. package/src/render/lighting.js +149 -149
  64. package/src/render/orbit-controls.js +237 -237
  65. package/src/render/renderer.js +258 -249
  66. package/src/render/scroll-timeline.js +104 -104
  67. package/src/render/shaders/glsl.js +285 -285
  68. package/src/render/shaders/wgsl.js +252 -252
  69. package/src/render/stage.js +364 -352
  70. package/src/render/texture-cache.js +40 -40
  71. package/src/render/textures.js +223 -223
  72. package/src/render/webgl2/webgl2-backend.js +586 -532
  73. package/src/render/webgpu/webgpu-backend.js +482 -480
  74. package/src/scene/flatten.js +754 -754
  75. package/src/scene/picking.js +161 -161
  76. package/src/scene/scene-graph.js +320 -320
  77. package/src/transform/transform.js +211 -211
  78. package/src/wasm/kernels-binary.js +3 -3
  79. package/src/wasm/kernels.js +385 -385
@@ -1,149 +1,149 @@
1
- // Shared vertex / index buffers.
2
- //
3
- // All meshes (every geometry, every level of detail, wireframe edge lists and
4
- // normal-visualisation lines) are packed into ONE interleaved vertex array and
5
- // ONE 32-bit index array. Indices are stored pre-offset (absolute), so a draw
6
- // is just (firstIndex, indexCount). No per-draw buffer binding, and no base
7
- // vertex. That keeps the WebGL2 and WebGPU paths the same.
8
- //
9
- // The pool is CPU-side; backends upload the ranges appended since their last
10
- // sync (or everything after a reallocation).
11
-
12
- export const VERTEX_FLOATS = 8; // position xyz, normal xyz, uv
13
- export const VERTEX_BYTES = VERTEX_FLOATS * 4;
14
-
15
- export class GeometryPool {
16
- constructor(vertexCapacity = 1 << 16, indexCapacity = 1 << 17) {
17
- this.vertices = new Float32Array(vertexCapacity * VERTEX_FLOATS);
18
- this.indices = new Uint32Array(indexCapacity);
19
- this.vertexCount = 0;
20
- this.indexCount = 0;
21
- this.ranges = new Map(); // key object -> {firstIndex, indexCount}
22
- this.edgeRanges = new Map();
23
- this.normalRanges = new Map();
24
- // dirty tracking for incremental uploads
25
- this.generation = 0; // bumps on reallocation
26
- this.uploadedVertices = 0;
27
- this.uploadedIndices = 0;
28
- }
29
-
30
- reset() {
31
- this.vertexCount = 0;
32
- this.indexCount = 0;
33
- this.ranges.clear();
34
- this.edgeRanges.clear();
35
- this.normalRanges.clear();
36
- this.generation++;
37
- this.uploadedVertices = 0;
38
- this.uploadedIndices = 0;
39
- }
40
-
41
- get byteSize() {
42
- return this.vertexCount * VERTEX_BYTES + this.indexCount * 4;
43
- }
44
-
45
- ensure(extraV, extraI) {
46
- let grew = false;
47
- if ((this.vertexCount + extraV) * VERTEX_FLOATS > this.vertices.length) {
48
- let cap = this.vertices.length / VERTEX_FLOATS;
49
- while (cap < this.vertexCount + extraV) cap *= 2;
50
- const v = new Float32Array(cap * VERTEX_FLOATS);
51
- v.set(this.vertices.subarray(0, this.vertexCount * VERTEX_FLOATS));
52
- this.vertices = v;
53
- grew = true;
54
- }
55
- if (this.indexCount + extraI > this.indices.length) {
56
- let cap = this.indices.length;
57
- while (cap < this.indexCount + extraI) cap *= 2;
58
- const i = new Uint32Array(cap);
59
- i.set(this.indices.subarray(0, this.indexCount));
60
- this.indices = i;
61
- grew = true;
62
- }
63
- if (grew) {
64
- this.generation++;
65
- this.uploadedVertices = 0;
66
- this.uploadedIndices = 0;
67
- }
68
- }
69
-
70
- appendVertices(mesh) {
71
- const n = mesh.vertexCount;
72
- const base = this.vertexCount;
73
- const v = this.vertices;
74
- const p = mesh.positions, nr = mesh.normals, t = mesh.uvs;
75
- let o = base * VERTEX_FLOATS;
76
- for (let i = 0; i < n; i++) {
77
- v[o] = p[i * 3];
78
- v[o + 1] = p[i * 3 + 1];
79
- v[o + 2] = p[i * 3 + 2];
80
- v[o + 3] = nr[i * 3];
81
- v[o + 4] = nr[i * 3 + 1];
82
- v[o + 5] = nr[i * 3 + 2];
83
- v[o + 6] = t[i * 2];
84
- v[o + 7] = t[i * 2 + 1];
85
- o += VERTEX_FLOATS;
86
- }
87
- this.vertexCount += n;
88
- return base;
89
- }
90
-
91
- appendIndices(src, base) {
92
- const first = this.indexCount;
93
- const dst = this.indices;
94
- for (let i = 0; i < src.length; i++) dst[first + i] = src[i] + base;
95
- this.indexCount += src.length;
96
- return first;
97
- }
98
-
99
- /** Range of a mesh's primitives (triangles or lines). */
100
- meshRange(mesh) {
101
- let r = this.ranges.get(mesh);
102
- if (r) return r;
103
- this.ensure(mesh.vertexCount, mesh.indices.length);
104
- const base = this.appendVertices(mesh);
105
- const first = this.appendIndices(mesh.indices, base);
106
- r = { firstIndex: first, indexCount: mesh.indices.length, baseVertex: base, kind: mesh.kind };
107
- this.ranges.set(mesh, r);
108
- return r;
109
- }
110
-
111
- /** Line range with the unique edges of a mesh (wireframe). */
112
- edgeRange(mesh) {
113
- let r = this.edgeRanges.get(mesh);
114
- if (r) return r;
115
- const base = this.meshRange(mesh).baseVertex;
116
- const edges = mesh.edges();
117
- this.ensure(0, edges.length);
118
- const first = this.appendIndices(edges, base);
119
- r = { firstIndex: first, indexCount: edges.length, kind: 'lines' };
120
- this.edgeRanges.set(mesh, r);
121
- return r;
122
- }
123
-
124
- /** Line range visualising vertex normals (uv.x = 0 at the base, 1 at the tip). */
125
- normalRange(mesh) {
126
- let r = this.normalRanges.get(mesh);
127
- if (r) return r;
128
- const lines = mesh.normalLines();
129
- r = this.meshRange(lines);
130
- this.normalRanges.set(mesh, r);
131
- return r;
132
- }
133
-
134
- /** Pending upload ranges since the last sync. */
135
- pending() {
136
- return {
137
- full: this.uploadedVertices === 0 && this.uploadedIndices === 0,
138
- vertexStart: this.uploadedVertices,
139
- vertexEnd: this.vertexCount,
140
- indexStart: this.uploadedIndices,
141
- indexEnd: this.indexCount,
142
- };
143
- }
144
-
145
- markUploaded() {
146
- this.uploadedVertices = this.vertexCount;
147
- this.uploadedIndices = this.indexCount;
148
- }
149
- }
1
+ // Shared vertex / index buffers.
2
+ //
3
+ // All meshes (every geometry, every level of detail, wireframe edge lists and
4
+ // normal-visualisation lines) are packed into ONE interleaved vertex array and
5
+ // ONE 32-bit index array. Indices are stored pre-offset (absolute), so a draw
6
+ // is just (firstIndex, indexCount). No per-draw buffer binding, and no base
7
+ // vertex. That keeps the WebGL2 and WebGPU paths the same.
8
+ //
9
+ // The pool is CPU-side; backends upload the ranges appended since their last
10
+ // sync (or everything after a reallocation).
11
+
12
+ export const VERTEX_FLOATS = 8; // position xyz, normal xyz, uv
13
+ export const VERTEX_BYTES = VERTEX_FLOATS * 4;
14
+
15
+ export class GeometryPool {
16
+ constructor(vertexCapacity = 1 << 16, indexCapacity = 1 << 17) {
17
+ this.vertices = new Float32Array(vertexCapacity * VERTEX_FLOATS);
18
+ this.indices = new Uint32Array(indexCapacity);
19
+ this.vertexCount = 0;
20
+ this.indexCount = 0;
21
+ this.ranges = new Map(); // key object -> {firstIndex, indexCount}
22
+ this.edgeRanges = new Map();
23
+ this.normalRanges = new Map();
24
+ // dirty tracking for incremental uploads
25
+ this.generation = 0; // bumps on reallocation
26
+ this.uploadedVertices = 0;
27
+ this.uploadedIndices = 0;
28
+ }
29
+
30
+ reset() {
31
+ this.vertexCount = 0;
32
+ this.indexCount = 0;
33
+ this.ranges.clear();
34
+ this.edgeRanges.clear();
35
+ this.normalRanges.clear();
36
+ this.generation++;
37
+ this.uploadedVertices = 0;
38
+ this.uploadedIndices = 0;
39
+ }
40
+
41
+ get byteSize() {
42
+ return this.vertexCount * VERTEX_BYTES + this.indexCount * 4;
43
+ }
44
+
45
+ ensure(extraV, extraI) {
46
+ let grew = false;
47
+ if ((this.vertexCount + extraV) * VERTEX_FLOATS > this.vertices.length) {
48
+ let cap = this.vertices.length / VERTEX_FLOATS;
49
+ while (cap < this.vertexCount + extraV) cap *= 2;
50
+ const v = new Float32Array(cap * VERTEX_FLOATS);
51
+ v.set(this.vertices.subarray(0, this.vertexCount * VERTEX_FLOATS));
52
+ this.vertices = v;
53
+ grew = true;
54
+ }
55
+ if (this.indexCount + extraI > this.indices.length) {
56
+ let cap = this.indices.length;
57
+ while (cap < this.indexCount + extraI) cap *= 2;
58
+ const i = new Uint32Array(cap);
59
+ i.set(this.indices.subarray(0, this.indexCount));
60
+ this.indices = i;
61
+ grew = true;
62
+ }
63
+ if (grew) {
64
+ this.generation++;
65
+ this.uploadedVertices = 0;
66
+ this.uploadedIndices = 0;
67
+ }
68
+ }
69
+
70
+ appendVertices(mesh) {
71
+ const n = mesh.vertexCount;
72
+ const base = this.vertexCount;
73
+ const v = this.vertices;
74
+ const p = mesh.positions, nr = mesh.normals, t = mesh.uvs;
75
+ let o = base * VERTEX_FLOATS;
76
+ for (let i = 0; i < n; i++) {
77
+ v[o] = p[i * 3];
78
+ v[o + 1] = p[i * 3 + 1];
79
+ v[o + 2] = p[i * 3 + 2];
80
+ v[o + 3] = nr[i * 3];
81
+ v[o + 4] = nr[i * 3 + 1];
82
+ v[o + 5] = nr[i * 3 + 2];
83
+ v[o + 6] = t[i * 2];
84
+ v[o + 7] = t[i * 2 + 1];
85
+ o += VERTEX_FLOATS;
86
+ }
87
+ this.vertexCount += n;
88
+ return base;
89
+ }
90
+
91
+ appendIndices(src, base) {
92
+ const first = this.indexCount;
93
+ const dst = this.indices;
94
+ for (let i = 0; i < src.length; i++) dst[first + i] = src[i] + base;
95
+ this.indexCount += src.length;
96
+ return first;
97
+ }
98
+
99
+ /** Range of a mesh's primitives (triangles or lines). */
100
+ meshRange(mesh) {
101
+ let r = this.ranges.get(mesh);
102
+ if (r) return r;
103
+ this.ensure(mesh.vertexCount, mesh.indices.length);
104
+ const base = this.appendVertices(mesh);
105
+ const first = this.appendIndices(mesh.indices, base);
106
+ r = { firstIndex: first, indexCount: mesh.indices.length, baseVertex: base, kind: mesh.kind };
107
+ this.ranges.set(mesh, r);
108
+ return r;
109
+ }
110
+
111
+ /** Line range with the unique edges of a mesh (wireframe). */
112
+ edgeRange(mesh) {
113
+ let r = this.edgeRanges.get(mesh);
114
+ if (r) return r;
115
+ const base = this.meshRange(mesh).baseVertex;
116
+ const edges = mesh.edges();
117
+ this.ensure(0, edges.length);
118
+ const first = this.appendIndices(edges, base);
119
+ r = { firstIndex: first, indexCount: edges.length, kind: 'lines' };
120
+ this.edgeRanges.set(mesh, r);
121
+ return r;
122
+ }
123
+
124
+ /** Line range visualising vertex normals (uv.x = 0 at the base, 1 at the tip). */
125
+ normalRange(mesh) {
126
+ let r = this.normalRanges.get(mesh);
127
+ if (r) return r;
128
+ const lines = mesh.normalLines();
129
+ r = this.meshRange(lines);
130
+ this.normalRanges.set(mesh, r);
131
+ return r;
132
+ }
133
+
134
+ /** Pending upload ranges since the last sync. */
135
+ pending() {
136
+ return {
137
+ full: this.uploadedVertices === 0 && this.uploadedIndices === 0,
138
+ vertexStart: this.uploadedVertices,
139
+ vertexEnd: this.vertexCount,
140
+ indexStart: this.uploadedIndices,
141
+ indexEnd: this.indexCount,
142
+ };
143
+ }
144
+
145
+ markUploaded() {
146
+ this.uploadedVertices = this.vertexCount;
147
+ this.uploadedIndices = this.indexCount;
148
+ }
149
+ }
@@ -1,149 +1,149 @@
1
- // Packing of per-frame uniforms (camera, environment and lights) into the
2
- // layout shared by the GLSL uniform block and the WGSL uniform struct:
3
- //
4
- // mat4 viewProj; mat4 view; vec4 cameraPos(time); vec4 sky(envIntensity);
5
- // vec4 ground(exposure); vec4 params(lightCount, normalLength, lineBias, viewportH);
6
- // vec4 lightPos[16] xyz = position, w = type (0 dir, 1 point, 2 spot, 3 ambient, 4 hemisphere)
7
- // vec4 lightDir[16] xyz = direction, w = range
8
- // vec4 lightColor[16] rgb = color * intensity, w = cos(outer cone)
9
- // vec4 lightExtra[16] x = cos(inner cone), yzw = hemisphere ground color
10
-
11
- import { colorToLinear } from '../format/colors.js';
12
-
13
- export const MAX_LIGHTS = 16;
14
- export const FRAME_FLOATS = 48 + MAX_LIGHTS * 16;
15
- export const FRAME_BYTES = FRAME_FLOATS * 4;
16
-
17
- const TYPE_CODE = { directional: 0, point: 1, spot: 2, ambient: 3, hemisphere: 4 };
18
-
19
- /** Lights used when a document defines none: a key and a fill light. */
20
- // Light intensity convention: a white directional light of intensity 1 lights a
21
- // white diffuse surface it faces head-on to full brightness (radiance * PI in the shader).
22
- export const DEFAULT_LIGHTS = [
23
- { type: 'directional', color: [1.1, 1.08, 1.04], direction: norm([-0.45, -0.85, -0.35]), position: [0, 0, 0], range: 0, outer: 0, inner: 0, ground: [0, 0, 0] },
24
- { type: 'directional', color: [0.22, 0.25, 0.32], direction: norm([0.6, -0.2, 0.75]), position: [0, 0, 0], range: 0, outer: 0, inner: 0, ground: [0, 0, 0] },
25
- ];
26
-
27
- function norm(v) {
28
- const l = Math.hypot(v[0], v[1], v[2]) || 1;
29
- return [v[0] / l, v[1] / l, v[2] / l];
30
- }
31
-
32
- /**
33
- * @returns {{data: Float32Array, lightCount: number, truncated: number}}
34
- */
35
- export function packFrameUniforms(out, { viewProj, view, cameraPos, time, environment, lights, normalLength = 0.1, lineBias = 0.0015, viewportHeight = 1 }) {
36
- out.fill(0);
37
- out.set(viewProj, 0);
38
- out.set(view, 16);
39
- out[32] = cameraPos[0];
40
- out[33] = cameraPos[1];
41
- out[34] = cameraPos[2];
42
- out[35] = time || 0;
43
- const sky = colorToLinear(environment.sky);
44
- const ground = colorToLinear(environment.ground);
45
- out[36] = sky[0];
46
- out[37] = sky[1];
47
- out[38] = sky[2];
48
- out[39] = environment.intensity;
49
- out[40] = ground[0];
50
- out[41] = ground[1];
51
- out[42] = ground[2];
52
- out[43] = environment.exposure;
53
- const list = lights && lights.length ? lights : DEFAULT_LIGHTS;
54
- const count = Math.min(list.length, MAX_LIGHTS);
55
- out[44] = count;
56
- out[45] = normalLength;
57
- out[46] = lineBias;
58
- out[47] = viewportHeight;
59
- for (let i = 0; i < count; i++) {
60
- const l = list[i];
61
- const p = 48 + i * 4;
62
- const d = 48 + MAX_LIGHTS * 4 + i * 4;
63
- const c = 48 + MAX_LIGHTS * 8 + i * 4;
64
- const e = 48 + MAX_LIGHTS * 12 + i * 4;
65
- out[p] = l.position[0];
66
- out[p + 1] = l.position[1];
67
- out[p + 2] = l.position[2];
68
- out[p + 3] = TYPE_CODE[l.type] ?? 0;
69
- out[d] = l.direction[0];
70
- out[d + 1] = l.direction[1];
71
- out[d + 2] = l.direction[2];
72
- out[d + 3] = l.range || 0;
73
- out[c] = l.color[0];
74
- out[c + 1] = l.color[1];
75
- out[c + 2] = l.color[2];
76
- out[c + 3] = l.outer || 0;
77
- // smoothstep needs edge0 < edge1: keep the inner cone strictly inside the outer one
78
- out[e] = Math.max(l.inner || 0, (l.outer || 0) + 1e-4);
79
- out[e + 1] = l.ground ? l.ground[0] : 0;
80
- out[e + 2] = l.ground ? l.ground[1] : 0;
81
- out[e + 3] = l.ground ? l.ground[2] : 0;
82
- }
83
- return { data: out, lightCount: count, truncated: Math.max(0, list.length - MAX_LIGHTS), usedDefaults: !(lights && lights.length) };
84
- }
85
-
86
- /**
87
- * The light types of packed frame uniforms, in order, as a short key ("0,0",
88
- * "0,1,2"): the light setup that mesh shaders are specialised for.
89
- */
90
- export function lightTypes(frameData) {
91
- const n = Math.round(frameData[44]);
92
- let key = '';
93
- for (let i = 0; i < n; i++) key += (i ? ',' : '') + Math.round(frameData[48 + i * 4 + 3]);
94
- return key;
95
- }
96
-
97
- /** Material uniform block: baseColor, emissive(normalScale), params(metallic, roughness, flags), uvScale, overlay color. */
98
- export const MATERIAL_FLOATS = 20;
99
-
100
- export const MAT_FLAG_MAP = 1;
101
- export const MAT_FLAG_NORMAL_MAP = 2;
102
- export const MAT_FLAG_DOUBLE_SIDED = 4;
103
- export const MAT_FLAG_UNLIT = 8;
104
- export const MAT_FLAG_OVERLAY = 16;
105
- export const MAT_FLAG_MR_MAP = 32;
106
- export const MAT_FLAG_OCCLUSION = 64;
107
- export const MAT_FLAG_EMISSIVE_MAP = 128;
108
- export const MAT_FLAG_ALPHA_MASK = 256;
109
-
110
- export function packMaterial(out, m, { hasMap = false, hasNormalMap = false, hasMrMap = false, hasOcclusionMap = false, hasEmissiveMap = false, doubleSided = false } = {}) {
111
- out.fill(0);
112
- out[0] = m.baseColor[0];
113
- out[1] = m.baseColor[1];
114
- out[2] = m.baseColor[2];
115
- out[3] = m.alphaMode === 'opaque' ? 1 : m.opacity;
116
- out[4] = m.emissive[0];
117
- out[5] = m.emissive[1];
118
- out[6] = m.emissive[2];
119
- out[7] = m.normalScale;
120
- out[8] = m.metallic;
121
- out[9] = m.roughness;
122
- out[10] =
123
- (hasMap ? MAT_FLAG_MAP : 0) |
124
- (hasNormalMap ? MAT_FLAG_NORMAL_MAP : 0) |
125
- (doubleSided ? MAT_FLAG_DOUBLE_SIDED : 0) |
126
- (m.unlit ? MAT_FLAG_UNLIT : 0) |
127
- (hasMrMap ? MAT_FLAG_MR_MAP : 0) |
128
- (hasOcclusionMap ? MAT_FLAG_OCCLUSION : 0) |
129
- (hasEmissiveMap ? MAT_FLAG_EMISSIVE_MAP : 0) |
130
- (m.alphaMode === 'mask' ? MAT_FLAG_ALPHA_MASK : 0);
131
- out[11] = m.alphaCutoff ?? 0.5;
132
- out[12] = m.uvScale[0];
133
- out[13] = m.uvScale[1];
134
- out[14] = m.occlusionStrength ?? 1;
135
- return out;
136
- }
137
-
138
- /** Uniforms for debug line overlays (sRGB color, drawn unlit). */
139
- export function packOverlay(out, rgba) {
140
- out.fill(0);
141
- out[16] = rgba[0];
142
- out[17] = rgba[1];
143
- out[18] = rgba[2];
144
- out[19] = rgba[3];
145
- out[10] = MAT_FLAG_OVERLAY;
146
- out[12] = 1;
147
- out[13] = 1;
148
- return out;
149
- }
1
+ // Packing of per-frame uniforms (camera, environment and lights) into the
2
+ // layout shared by the GLSL uniform block and the WGSL uniform struct:
3
+ //
4
+ // mat4 viewProj; mat4 view; vec4 cameraPos(time); vec4 sky(envIntensity);
5
+ // vec4 ground(exposure); vec4 params(lightCount, normalLength, lineBias, viewportH);
6
+ // vec4 lightPos[16] xyz = position, w = type (0 dir, 1 point, 2 spot, 3 ambient, 4 hemisphere)
7
+ // vec4 lightDir[16] xyz = direction, w = range
8
+ // vec4 lightColor[16] rgb = color * intensity, w = cos(outer cone)
9
+ // vec4 lightExtra[16] x = cos(inner cone), yzw = hemisphere ground color
10
+
11
+ import { colorToLinear } from '../format/colors.js';
12
+
13
+ export const MAX_LIGHTS = 16;
14
+ export const FRAME_FLOATS = 48 + MAX_LIGHTS * 16;
15
+ export const FRAME_BYTES = FRAME_FLOATS * 4;
16
+
17
+ const TYPE_CODE = { directional: 0, point: 1, spot: 2, ambient: 3, hemisphere: 4 };
18
+
19
+ /** Lights used when a document defines none: a key and a fill light. */
20
+ // Light intensity convention: a white directional light of intensity 1 lights a
21
+ // white diffuse surface it faces head-on to full brightness (radiance * PI in the shader).
22
+ export const DEFAULT_LIGHTS = [
23
+ { type: 'directional', color: [1.1, 1.08, 1.04], direction: norm([-0.45, -0.85, -0.35]), position: [0, 0, 0], range: 0, outer: 0, inner: 0, ground: [0, 0, 0] },
24
+ { type: 'directional', color: [0.22, 0.25, 0.32], direction: norm([0.6, -0.2, 0.75]), position: [0, 0, 0], range: 0, outer: 0, inner: 0, ground: [0, 0, 0] },
25
+ ];
26
+
27
+ function norm(v) {
28
+ const l = Math.hypot(v[0], v[1], v[2]) || 1;
29
+ return [v[0] / l, v[1] / l, v[2] / l];
30
+ }
31
+
32
+ /**
33
+ * @returns {{data: Float32Array, lightCount: number, truncated: number}}
34
+ */
35
+ export function packFrameUniforms(out, { viewProj, view, cameraPos, time, environment, lights, normalLength = 0.1, lineBias = 0.0015, viewportHeight = 1 }) {
36
+ out.fill(0);
37
+ out.set(viewProj, 0);
38
+ out.set(view, 16);
39
+ out[32] = cameraPos[0];
40
+ out[33] = cameraPos[1];
41
+ out[34] = cameraPos[2];
42
+ out[35] = time || 0;
43
+ const sky = colorToLinear(environment.sky);
44
+ const ground = colorToLinear(environment.ground);
45
+ out[36] = sky[0];
46
+ out[37] = sky[1];
47
+ out[38] = sky[2];
48
+ out[39] = environment.intensity;
49
+ out[40] = ground[0];
50
+ out[41] = ground[1];
51
+ out[42] = ground[2];
52
+ out[43] = environment.exposure;
53
+ const list = lights && lights.length ? lights : DEFAULT_LIGHTS;
54
+ const count = Math.min(list.length, MAX_LIGHTS);
55
+ out[44] = count;
56
+ out[45] = normalLength;
57
+ out[46] = lineBias;
58
+ out[47] = viewportHeight;
59
+ for (let i = 0; i < count; i++) {
60
+ const l = list[i];
61
+ const p = 48 + i * 4;
62
+ const d = 48 + MAX_LIGHTS * 4 + i * 4;
63
+ const c = 48 + MAX_LIGHTS * 8 + i * 4;
64
+ const e = 48 + MAX_LIGHTS * 12 + i * 4;
65
+ out[p] = l.position[0];
66
+ out[p + 1] = l.position[1];
67
+ out[p + 2] = l.position[2];
68
+ out[p + 3] = TYPE_CODE[l.type] ?? 0;
69
+ out[d] = l.direction[0];
70
+ out[d + 1] = l.direction[1];
71
+ out[d + 2] = l.direction[2];
72
+ out[d + 3] = l.range || 0;
73
+ out[c] = l.color[0];
74
+ out[c + 1] = l.color[1];
75
+ out[c + 2] = l.color[2];
76
+ out[c + 3] = l.outer || 0;
77
+ // smoothstep needs edge0 < edge1: keep the inner cone strictly inside the outer one
78
+ out[e] = Math.max(l.inner || 0, (l.outer || 0) + 1e-4);
79
+ out[e + 1] = l.ground ? l.ground[0] : 0;
80
+ out[e + 2] = l.ground ? l.ground[1] : 0;
81
+ out[e + 3] = l.ground ? l.ground[2] : 0;
82
+ }
83
+ return { data: out, lightCount: count, truncated: Math.max(0, list.length - MAX_LIGHTS), usedDefaults: !(lights && lights.length) };
84
+ }
85
+
86
+ /**
87
+ * The light types of packed frame uniforms, in order, as a short key ("0,0",
88
+ * "0,1,2"): the light setup that mesh shaders are specialised for.
89
+ */
90
+ export function lightTypes(frameData) {
91
+ const n = Math.round(frameData[44]);
92
+ let key = '';
93
+ for (let i = 0; i < n; i++) key += (i ? ',' : '') + Math.round(frameData[48 + i * 4 + 3]);
94
+ return key;
95
+ }
96
+
97
+ /** Material uniform block: baseColor, emissive(normalScale), params(metallic, roughness, flags), uvScale, overlay color. */
98
+ export const MATERIAL_FLOATS = 20;
99
+
100
+ export const MAT_FLAG_MAP = 1;
101
+ export const MAT_FLAG_NORMAL_MAP = 2;
102
+ export const MAT_FLAG_DOUBLE_SIDED = 4;
103
+ export const MAT_FLAG_UNLIT = 8;
104
+ export const MAT_FLAG_OVERLAY = 16;
105
+ export const MAT_FLAG_MR_MAP = 32;
106
+ export const MAT_FLAG_OCCLUSION = 64;
107
+ export const MAT_FLAG_EMISSIVE_MAP = 128;
108
+ export const MAT_FLAG_ALPHA_MASK = 256;
109
+
110
+ export function packMaterial(out, m, { hasMap = false, hasNormalMap = false, hasMrMap = false, hasOcclusionMap = false, hasEmissiveMap = false, doubleSided = false } = {}) {
111
+ out.fill(0);
112
+ out[0] = m.baseColor[0];
113
+ out[1] = m.baseColor[1];
114
+ out[2] = m.baseColor[2];
115
+ out[3] = m.alphaMode === 'opaque' ? 1 : m.opacity;
116
+ out[4] = m.emissive[0];
117
+ out[5] = m.emissive[1];
118
+ out[6] = m.emissive[2];
119
+ out[7] = m.normalScale;
120
+ out[8] = m.metallic;
121
+ out[9] = m.roughness;
122
+ out[10] =
123
+ (hasMap ? MAT_FLAG_MAP : 0) |
124
+ (hasNormalMap ? MAT_FLAG_NORMAL_MAP : 0) |
125
+ (doubleSided ? MAT_FLAG_DOUBLE_SIDED : 0) |
126
+ (m.unlit ? MAT_FLAG_UNLIT : 0) |
127
+ (hasMrMap ? MAT_FLAG_MR_MAP : 0) |
128
+ (hasOcclusionMap ? MAT_FLAG_OCCLUSION : 0) |
129
+ (hasEmissiveMap ? MAT_FLAG_EMISSIVE_MAP : 0) |
130
+ (m.alphaMode === 'mask' ? MAT_FLAG_ALPHA_MASK : 0);
131
+ out[11] = m.alphaCutoff ?? 0.5;
132
+ out[12] = m.uvScale[0];
133
+ out[13] = m.uvScale[1];
134
+ out[14] = m.occlusionStrength ?? 1;
135
+ return out;
136
+ }
137
+
138
+ /** Uniforms for debug line overlays (sRGB color, drawn unlit). */
139
+ export function packOverlay(out, rgba) {
140
+ out.fill(0);
141
+ out[16] = rgba[0];
142
+ out[17] = rgba[1];
143
+ out[18] = rgba[2];
144
+ out[19] = rgba[3];
145
+ out[10] = MAT_FLAG_OVERLAY;
146
+ out[12] = 1;
147
+ out[13] = 1;
148
+ return out;
149
+ }