@es3d/es3d 1.0.0 → 1.0.2

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 (80) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +95 -86
  3. package/docs/SPEC.md +426 -417
  4. package/package.json +1 -1
  5. package/scripts/es3d-cli.mjs +390 -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 +132 -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-pack.js +1461 -0
  42. package/src/io/gltf.js +630 -630
  43. package/src/io/mesh-import.js +104 -104
  44. package/src/io/obj.js +352 -352
  45. package/src/io/step-brep.js +439 -439
  46. package/src/io/step-exact.js +1149 -1149
  47. package/src/io/step.js +1418 -1418
  48. package/src/io/stl.js +193 -193
  49. package/src/io/svg-import.js +787 -787
  50. package/src/material/materials.js +154 -154
  51. package/src/math/mat4.js +475 -475
  52. package/src/math/random.js +30 -30
  53. package/src/math/trig.js +65 -65
  54. package/src/math/vec3.js +81 -81
  55. package/src/optimize/optimizer.js +604 -579
  56. package/src/path/path-flatten.js +322 -322
  57. package/src/path/path-parser.js +332 -332
  58. package/src/path/shapes.js +129 -129
  59. package/src/path/triangulate.js +517 -517
  60. package/src/pipeline.js +74 -74
  61. package/src/render/camera.js +168 -168
  62. package/src/render/frame-builder.js +362 -362
  63. package/src/render/geometry-pool.js +149 -149
  64. package/src/render/lighting.js +149 -149
  65. package/src/render/orbit-controls.js +237 -237
  66. package/src/render/renderer.js +258 -249
  67. package/src/render/scroll-timeline.js +104 -104
  68. package/src/render/shaders/glsl.js +285 -285
  69. package/src/render/shaders/wgsl.js +252 -252
  70. package/src/render/stage.js +365 -352
  71. package/src/render/texture-cache.js +40 -40
  72. package/src/render/textures.js +223 -223
  73. package/src/render/webgl2/webgl2-backend.js +586 -532
  74. package/src/render/webgpu/webgpu-backend.js +482 -480
  75. package/src/scene/flatten.js +754 -754
  76. package/src/scene/picking.js +161 -161
  77. package/src/scene/scene-graph.js +320 -320
  78. package/src/transform/transform.js +211 -211
  79. package/src/wasm/kernels-binary.js +3 -3
  80. package/src/wasm/kernels.js +385 -385
@@ -1,285 +1,285 @@
1
- // GLSL ES 3.00 shaders for the WebGL2 backend. They mirror shaders/wgsl.js.
2
- //
3
- // Instancing: the per-instance world matrix arrives as four vec4 attributes.
4
- // Normals are transformed with the cofactor matrix of the instance matrix
5
- // (proportional to the inverse transpose), so no normal matrix is uploaded and
6
- // non-uniform and mirrored scales are handled correctly.
7
-
8
- import {
9
- MAT_FLAG_MAP, MAT_FLAG_NORMAL_MAP, MAT_FLAG_UNLIT, MAT_FLAG_DOUBLE_SIDED, MAT_FLAG_MR_MAP,
10
- MAT_FLAG_OCCLUSION, MAT_FLAG_EMISSIVE_MAP, MAT_FLAG_ALPHA_MASK,
11
- } from '../lighting.js';
12
-
13
- const FRAME_BLOCK = /* glsl */ `
14
- layout(std140) uniform Frame {
15
- mat4 uViewProj;
16
- mat4 uView;
17
- vec4 uCameraPos;
18
- vec4 uSky;
19
- vec4 uGround;
20
- vec4 uParams;
21
- vec4 uLightPos[16];
22
- vec4 uLightDir[16];
23
- vec4 uLightColor[16];
24
- vec4 uLightExtra[16];
25
- };
26
- `;
27
-
28
- const MATERIAL_BLOCK = /* glsl */ `
29
- layout(std140) uniform Material {
30
- vec4 mBaseColor;
31
- vec4 mEmissive;
32
- vec4 mParams;
33
- vec4 mUv;
34
- vec4 mOverlay;
35
- };
36
- `;
37
-
38
- const INSTANCE_INPUTS = /* glsl */ `
39
- layout(location = 0) in vec3 aPosition;
40
- layout(location = 1) in vec3 aNormal;
41
- layout(location = 2) in vec2 aUv;
42
- layout(location = 3) in vec4 iM0;
43
- layout(location = 4) in vec4 iM1;
44
- layout(location = 5) in vec4 iM2;
45
- layout(location = 6) in vec4 iM3;
46
- `;
47
-
48
- const COMMON_FS = /* glsl */ `
49
- const float PI = 3.14159265359;
50
- vec3 linearToSrgb(vec3 c) {
51
- c = max(c, vec3(0.0));
52
- return mix(c * 12.92, 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, step(vec3(0.0031308), c));
53
- }
54
- vec3 aces(vec3 x) {
55
- return clamp((x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14), 0.0, 1.0);
56
- }
57
- `;
58
-
59
- /** Vertex shader of the mesh programs; texture coordinates only for textured materials. */
60
- export const meshVertexShader = (textured) => /* glsl */ `#version 300 es
61
- precision highp float;
62
- ${FRAME_BLOCK}
63
- ${INSTANCE_INPUTS}
64
- out vec3 vWorld;
65
- out vec3 vNormal;
66
- ${textured ? 'out vec2 vUv;' : ''}
67
- void main() {
68
- mat4 M = mat4(iM0, iM1, iM2, iM3);
69
- vec4 wp = M * vec4(aPosition, 1.0);
70
- vec3 c0 = iM0.xyz;
71
- vec3 c1 = iM1.xyz;
72
- vec3 c2 = iM2.xyz;
73
- mat3 cof = mat3(cross(c1, c2), cross(c2, c0), cross(c0, c1));
74
- float det = dot(c0, cross(c1, c2));
75
- vNormal = cof * aNormal * (det < 0.0 ? -1.0 : 1.0);
76
- vWorld = wp.xyz;
77
- ${textured ? 'vUv = aUv;' : ''}
78
- gl_Position = uViewProj * wp;
79
- }
80
- `;
81
-
82
- /** Material flags that need texture coordinates. */
83
- export const TEXTURE_FLAGS = MAT_FLAG_MAP | MAT_FLAG_NORMAL_MAP | MAT_FLAG_MR_MAP | MAT_FLAG_OCCLUSION | MAT_FLAG_EMISSIVE_MAP;
84
-
85
- /**
86
- * Fragment shader for one material feature set (MAT_FLAG_* bits) and one
87
- * light setup (lightTypes() of the frame: its light types, in order). Everything else is
88
- * compiled out, as a renderer that specialises its programs does: textures
89
- * are sampled only when the material has them, `discard` exists only for
90
- * alpha-masked materials (its mere presence disables early depth testing on
91
- * many GPUs), and the light loop is unrolled with each light's type known.
92
- */
93
- export function meshFragmentShader(flags, lights) {
94
- const has = (f) => (flags & f) !== 0;
95
- const map = has(MAT_FLAG_MAP), normalMap = has(MAT_FLAG_NORMAL_MAP), mrMap = has(MAT_FLAG_MR_MAP);
96
- const occlusion = has(MAT_FLAG_OCCLUSION), emissiveMap = has(MAT_FLAG_EMISSIVE_MAP);
97
- const textured = map || normalMap || mrMap || occlusion || emissiveMap;
98
- // explicit gradients only where a discard could leave later lookups without them
99
- const grad = has(MAT_FLAG_ALPHA_MASK);
100
- const sample = (sampler) => (grad ? `textureGrad(${sampler}, uv, duv1, duv2)` : `texture(${sampler}, uv)`);
101
- const L = [];
102
- L.push(`#version 300 es
103
- precision highp float;
104
- precision highp int;
105
- ${FRAME_BLOCK}
106
- ${MATERIAL_BLOCK}`);
107
- if (map) L.push('uniform sampler2D uMap;');
108
- if (normalMap) L.push('uniform sampler2D uNormalMap;');
109
- if (mrMap) L.push('uniform sampler2D uMrMap;');
110
- if (occlusion) L.push('uniform sampler2D uOcclusionMap;');
111
- if (emissiveMap) L.push('uniform sampler2D uEmissiveMap;');
112
- L.push(`in vec3 vWorld;
113
- in vec3 vNormal;
114
- ${textured ? 'in vec2 vUv;' : ''}
115
- out vec4 outColor;
116
- ${COMMON_FS}`);
117
- if (!has(MAT_FLAG_UNLIT)) L.push(BRDF);
118
- L.push('void main() {');
119
- if (textured) L.push(' vec2 uv = vUv * mUv.xy;');
120
- // derivatives before any discard, so every lookup after it still has them
121
- if ((textured && grad) || normalMap) L.push(' vec2 duv1 = dFdx(uv);', ' vec2 duv2 = dFdy(uv);');
122
- L.push(' vec4 base = mBaseColor;');
123
- if (map) L.push(` base *= ${sample('uMap')};`);
124
- if (has(MAT_FLAG_ALPHA_MASK)) L.push(' if (base.a < mParams.w) discard;');
125
- L.push(' vec3 emitted = mEmissive.rgb;');
126
- if (emissiveMap) L.push(` emitted *= ${sample('uEmissiveMap')}.rgb;`);
127
- if (has(MAT_FLAG_UNLIT)) {
128
- L.push(' outColor = vec4(linearToSrgb(base.rgb + emitted), base.a);', '}');
129
- return L.join('\n');
130
- }
131
- L.push(' vec3 N = normalize(vNormal);');
132
- if (has(MAT_FLAG_DOUBLE_SIDED)) L.push(' N = gl_FrontFacing ? N : -N;');
133
- if (normalMap) {
134
- L.push(` vec3 dp1 = dFdx(vWorld);
135
- vec3 dp2 = dFdy(vWorld);
136
- vec3 dp2perp = cross(dp2, N);
137
- vec3 dp1perp = cross(N, dp1);
138
- vec3 T = dp2perp * duv1.x + dp1perp * duv2.x;
139
- vec3 B = dp2perp * duv1.y + dp1perp * duv2.y;
140
- float invmax = inversesqrt(max(max(dot(T, T), dot(B, B)), 1e-20));
141
- vec3 n = ${sample('uNormalMap')}.xyz * 2.0 - 1.0;
142
- n.xy *= mEmissive.w;
143
- N = normalize(mat3(T * invmax, B * invmax, N) * n);`);
144
- }
145
- L.push(` vec3 V = normalize(uCameraPos.xyz - vWorld);
146
- float NdotV = max(dot(N, V), 1e-4);
147
- float rough = clamp(mParams.y, 0.045, 1.0);
148
- float metal = clamp(mParams.x, 0.0, 1.0);`);
149
- if (mrMap) {
150
- L.push(` vec4 mr = ${sample('uMrMap')};
151
- rough = clamp(rough * mr.g, 0.045, 1.0);
152
- metal = clamp(metal * mr.b, 0.0, 1.0);`);
153
- }
154
- L.push(` float a = rough * rough;
155
- vec3 f0 = mix(vec3(0.04), base.rgb, metal);
156
- vec3 diffuse = base.rgb * (1.0 - metal);
157
- vec3 color = vec3(0.0);
158
- vec3 ambient = vec3(0.0);`);
159
- if (lights) lights.split(',').forEach((type, i) => L.push(lightTerm(Number(type), i)));
160
- L.push(` float envI = uSky.w;
161
- vec3 hemi = mix(uGround.rgb, uSky.rgb, N.y * 0.5 + 0.5) * envI + ambient;
162
- vec3 R = reflect(-V, N);
163
- vec3 envSpec = mix(uGround.rgb, uSky.rgb, smoothstep(-0.25 - rough, 0.25 + rough, R.y)) * envI;
164
- envSpec = mix(envSpec, hemi, rough * rough);`);
165
- if (occlusion) {
166
- L.push(` float ao = mix(1.0, ${sample('uOcclusionMap')}.r, mUv.z);
167
- hemi *= ao;
168
- envSpec *= ao;`);
169
- }
170
- L.push(` color += diffuse * hemi + envSpec * envBRDF(f0, rough, NdotV);
171
- color += emitted;
172
- color *= uGround.w;
173
- outColor = vec4(linearToSrgb(aces(color)), base.a);
174
- }`);
175
- return L.join('\n');
176
- }
177
-
178
- const BRDF = /* glsl */ `
179
- float D_GGX(float NdotH, float a) {
180
- float a2 = a * a;
181
- float d = NdotH * NdotH * (a2 - 1.0) + 1.0;
182
- return a2 / (PI * d * d);
183
- }
184
- float V_Smith(float NdotV, float NdotL, float a) {
185
- float a2 = a * a;
186
- float gv = NdotL * sqrt(NdotV * NdotV * (1.0 - a2) + a2);
187
- float gl = NdotV * sqrt(NdotL * NdotL * (1.0 - a2) + a2);
188
- return 0.5 / max(gv + gl, 1e-5);
189
- }
190
- vec3 F_Schlick(vec3 f0, float VdotH) {
191
- return f0 + (vec3(1.0) - f0) * pow(1.0 - VdotH, 5.0);
192
- }
193
- vec3 envBRDF(vec3 f0, float roughness, float NdotV) {
194
- const vec4 c0 = vec4(-1.0, -0.0275, -0.572, 0.022);
195
- const vec4 c1 = vec4(1.0, 0.0425, 1.04, -0.04);
196
- vec4 r = roughness * c0 + c1;
197
- float a004 = min(r.x * r.x, exp2(-9.28 * NdotV)) * r.x + r.y;
198
- vec2 AB = vec2(-1.04, 1.04) * a004 + r.zw;
199
- return f0 * AB.x + AB.y;
200
- }
201
- // one light's contribution, zero when it is behind the surface
202
- vec3 shade(vec3 N, vec3 V, vec3 L, vec3 radiance, float NdotV, vec3 f0, vec3 diffuse, float a) {
203
- float NdotL = dot(N, L);
204
- if (NdotL <= 0.0) return vec3(0.0);
205
- vec3 H = normalize(V + L);
206
- float NdotH = max(dot(N, H), 0.0);
207
- float VdotH = max(dot(V, H), 0.0);
208
- vec3 F = F_Schlick(f0, VdotH);
209
- vec3 spec = D_GGX(NdotH, a) * V_Smith(NdotV, NdotL, a) * F;
210
- vec3 kd = (vec3(1.0) - F) * diffuse / PI;
211
- return (kd + spec) * radiance * NdotL;
212
- }
213
- `;
214
-
215
- /** GLSL for light i of a known type (lighting.js: 0 directional, 1 point, 2 spot, 3 ambient, 4 hemisphere). */
216
- function lightTerm(type, i) {
217
- if (type === 3) return ` ambient += uLightColor[${i}].rgb;`;
218
- if (type === 4) return ` ambient += mix(uLightExtra[${i}].yzw, uLightColor[${i}].rgb, N.y * 0.5 + 0.5);`;
219
- if (type === 0) return ` color += shade(N, V, -uLightDir[${i}].xyz, uLightColor[${i}].rgb * PI, NdotV, f0, diffuse, a);`;
220
- return ` {
221
- vec3 Lv = uLightPos[${i}].xyz - vWorld;
222
- float d = length(Lv);
223
- vec3 L = Lv / max(d, 1e-5);
224
- float att = 1.0 / max(d * d, 1e-4);
225
- float range = uLightDir[${i}].w;
226
- if (range > 0.0) {
227
- float x = d / range;
228
- float w = clamp(1.0 - x * x * x * x, 0.0, 1.0);
229
- att *= w * w;
230
- }${type === 2 ? `\n att *= smoothstep(uLightColor[${i}].w, uLightExtra[${i}].x, dot(-L, uLightDir[${i}].xyz));` : ''}
231
- color += shade(N, V, L, uLightColor[${i}].rgb * PI * att, NdotV, f0, diffuse, a);
232
- }`;
233
- }
234
-
235
- /** Lines: scene polylines (unlit material color), wireframe and bounding boxes. */
236
- export const LINE_VS = /* glsl */ `#version 300 es
237
- precision highp float;
238
- ${FRAME_BLOCK}
239
- ${INSTANCE_INPUTS}
240
- void main() {
241
- mat4 M = mat4(iM0, iM1, iM2, iM3);
242
- vec4 wp = M * vec4(aPosition, 1.0);
243
- // pull lines slightly toward the camera so they win the depth test against
244
- // the surfaces they outline (a relative offset works at every distance)
245
- wp.xyz = mix(wp.xyz, uCameraPos.xyz, uParams.z);
246
- gl_Position = uViewProj * wp;
247
- }
248
- `;
249
-
250
- /** Normal visualisation: the tip vertex (uv.x = 1) is offset along the world normal. */
251
- export const NORMAL_VS = /* glsl */ `#version 300 es
252
- precision highp float;
253
- ${FRAME_BLOCK}
254
- ${INSTANCE_INPUTS}
255
- void main() {
256
- mat4 M = mat4(iM0, iM1, iM2, iM3);
257
- vec4 wp = M * vec4(aPosition, 1.0);
258
- vec3 c0 = iM0.xyz;
259
- vec3 c1 = iM1.xyz;
260
- vec3 c2 = iM2.xyz;
261
- mat3 cof = mat3(cross(c1, c2), cross(c2, c0), cross(c0, c1));
262
- float det = dot(c0, cross(c1, c2));
263
- vec3 n = normalize(cof * aNormal * (det < 0.0 ? -1.0 : 1.0));
264
- wp.xyz += n * aUv.x * uParams.y;
265
- wp.xyz = mix(wp.xyz, uCameraPos.xyz, uParams.z);
266
- gl_Position = uViewProj * wp;
267
- }
268
- `;
269
-
270
- export const LINE_FS = /* glsl */ `#version 300 es
271
- precision highp float;
272
- precision highp int;
273
- ${FRAME_BLOCK}
274
- ${MATERIAL_BLOCK}
275
- out vec4 outColor;
276
- ${COMMON_FS}
277
- void main() {
278
- int flags = int(mParams.z + 0.5);
279
- if ((flags & 16) != 0) {
280
- outColor = mOverlay;
281
- return;
282
- }
283
- outColor = vec4(linearToSrgb(mBaseColor.rgb + mEmissive.rgb), mBaseColor.a);
284
- }
285
- `;
1
+ // GLSL ES 3.00 shaders for the WebGL2 backend. They mirror shaders/wgsl.js.
2
+ //
3
+ // Instancing: the per-instance world matrix arrives as four vec4 attributes.
4
+ // Normals are transformed with the cofactor matrix of the instance matrix
5
+ // (proportional to the inverse transpose), so no normal matrix is uploaded and
6
+ // non-uniform and mirrored scales are handled correctly.
7
+
8
+ import {
9
+ MAT_FLAG_MAP, MAT_FLAG_NORMAL_MAP, MAT_FLAG_UNLIT, MAT_FLAG_DOUBLE_SIDED, MAT_FLAG_MR_MAP,
10
+ MAT_FLAG_OCCLUSION, MAT_FLAG_EMISSIVE_MAP, MAT_FLAG_ALPHA_MASK,
11
+ } from '../lighting.js';
12
+
13
+ const FRAME_BLOCK = /* glsl */ `
14
+ layout(std140) uniform Frame {
15
+ mat4 uViewProj;
16
+ mat4 uView;
17
+ vec4 uCameraPos;
18
+ vec4 uSky;
19
+ vec4 uGround;
20
+ vec4 uParams;
21
+ vec4 uLightPos[16];
22
+ vec4 uLightDir[16];
23
+ vec4 uLightColor[16];
24
+ vec4 uLightExtra[16];
25
+ };
26
+ `;
27
+
28
+ const MATERIAL_BLOCK = /* glsl */ `
29
+ layout(std140) uniform Material {
30
+ vec4 mBaseColor;
31
+ vec4 mEmissive;
32
+ vec4 mParams;
33
+ vec4 mUv;
34
+ vec4 mOverlay;
35
+ };
36
+ `;
37
+
38
+ const INSTANCE_INPUTS = /* glsl */ `
39
+ layout(location = 0) in vec3 aPosition;
40
+ layout(location = 1) in vec3 aNormal;
41
+ layout(location = 2) in vec2 aUv;
42
+ layout(location = 3) in vec4 iM0;
43
+ layout(location = 4) in vec4 iM1;
44
+ layout(location = 5) in vec4 iM2;
45
+ layout(location = 6) in vec4 iM3;
46
+ `;
47
+
48
+ const COMMON_FS = /* glsl */ `
49
+ const float PI = 3.14159265359;
50
+ vec3 linearToSrgb(vec3 c) {
51
+ c = max(c, vec3(0.0));
52
+ return mix(c * 12.92, 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, step(vec3(0.0031308), c));
53
+ }
54
+ vec3 aces(vec3 x) {
55
+ return clamp((x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14), 0.0, 1.0);
56
+ }
57
+ `;
58
+
59
+ /** Vertex shader of the mesh programs; texture coordinates only for textured materials. */
60
+ export const meshVertexShader = (textured) => /* glsl */ `#version 300 es
61
+ precision highp float;
62
+ ${FRAME_BLOCK}
63
+ ${INSTANCE_INPUTS}
64
+ out vec3 vWorld;
65
+ out vec3 vNormal;
66
+ ${textured ? 'out vec2 vUv;' : ''}
67
+ void main() {
68
+ mat4 M = mat4(iM0, iM1, iM2, iM3);
69
+ vec4 wp = M * vec4(aPosition, 1.0);
70
+ vec3 c0 = iM0.xyz;
71
+ vec3 c1 = iM1.xyz;
72
+ vec3 c2 = iM2.xyz;
73
+ mat3 cof = mat3(cross(c1, c2), cross(c2, c0), cross(c0, c1));
74
+ float det = dot(c0, cross(c1, c2));
75
+ vNormal = cof * aNormal * (det < 0.0 ? -1.0 : 1.0);
76
+ vWorld = wp.xyz;
77
+ ${textured ? 'vUv = aUv;' : ''}
78
+ gl_Position = uViewProj * wp;
79
+ }
80
+ `;
81
+
82
+ /** Material flags that need texture coordinates. */
83
+ export const TEXTURE_FLAGS = MAT_FLAG_MAP | MAT_FLAG_NORMAL_MAP | MAT_FLAG_MR_MAP | MAT_FLAG_OCCLUSION | MAT_FLAG_EMISSIVE_MAP;
84
+
85
+ /**
86
+ * Fragment shader for one material feature set (MAT_FLAG_* bits) and one
87
+ * light setup (lightTypes() of the frame: its light types, in order). Everything else is
88
+ * compiled out, as a renderer that specialises its programs does: textures
89
+ * are sampled only when the material has them, `discard` exists only for
90
+ * alpha-masked materials (its mere presence disables early depth testing on
91
+ * many GPUs), and the light loop is unrolled with each light's type known.
92
+ */
93
+ export function meshFragmentShader(flags, lights) {
94
+ const has = (f) => (flags & f) !== 0;
95
+ const map = has(MAT_FLAG_MAP), normalMap = has(MAT_FLAG_NORMAL_MAP), mrMap = has(MAT_FLAG_MR_MAP);
96
+ const occlusion = has(MAT_FLAG_OCCLUSION), emissiveMap = has(MAT_FLAG_EMISSIVE_MAP);
97
+ const textured = map || normalMap || mrMap || occlusion || emissiveMap;
98
+ // explicit gradients only where a discard could leave later lookups without them
99
+ const grad = has(MAT_FLAG_ALPHA_MASK);
100
+ const sample = (sampler) => (grad ? `textureGrad(${sampler}, uv, duv1, duv2)` : `texture(${sampler}, uv)`);
101
+ const L = [];
102
+ L.push(`#version 300 es
103
+ precision highp float;
104
+ precision highp int;
105
+ ${FRAME_BLOCK}
106
+ ${MATERIAL_BLOCK}`);
107
+ if (map) L.push('uniform sampler2D uMap;');
108
+ if (normalMap) L.push('uniform sampler2D uNormalMap;');
109
+ if (mrMap) L.push('uniform sampler2D uMrMap;');
110
+ if (occlusion) L.push('uniform sampler2D uOcclusionMap;');
111
+ if (emissiveMap) L.push('uniform sampler2D uEmissiveMap;');
112
+ L.push(`in vec3 vWorld;
113
+ in vec3 vNormal;
114
+ ${textured ? 'in vec2 vUv;' : ''}
115
+ out vec4 outColor;
116
+ ${COMMON_FS}`);
117
+ if (!has(MAT_FLAG_UNLIT)) L.push(BRDF);
118
+ L.push('void main() {');
119
+ if (textured) L.push(' vec2 uv = vUv * mUv.xy;');
120
+ // derivatives before any discard, so every lookup after it still has them
121
+ if ((textured && grad) || normalMap) L.push(' vec2 duv1 = dFdx(uv);', ' vec2 duv2 = dFdy(uv);');
122
+ L.push(' vec4 base = mBaseColor;');
123
+ if (map) L.push(` base *= ${sample('uMap')};`);
124
+ if (has(MAT_FLAG_ALPHA_MASK)) L.push(' if (base.a < mParams.w) discard;');
125
+ L.push(' vec3 emitted = mEmissive.rgb;');
126
+ if (emissiveMap) L.push(` emitted *= ${sample('uEmissiveMap')}.rgb;`);
127
+ if (has(MAT_FLAG_UNLIT)) {
128
+ L.push(' outColor = vec4(linearToSrgb(base.rgb + emitted), base.a);', '}');
129
+ return L.join('\n');
130
+ }
131
+ L.push(' vec3 N = normalize(vNormal);');
132
+ if (has(MAT_FLAG_DOUBLE_SIDED)) L.push(' N = gl_FrontFacing ? N : -N;');
133
+ if (normalMap) {
134
+ L.push(` vec3 dp1 = dFdx(vWorld);
135
+ vec3 dp2 = dFdy(vWorld);
136
+ vec3 dp2perp = cross(dp2, N);
137
+ vec3 dp1perp = cross(N, dp1);
138
+ vec3 T = dp2perp * duv1.x + dp1perp * duv2.x;
139
+ vec3 B = dp2perp * duv1.y + dp1perp * duv2.y;
140
+ float invmax = inversesqrt(max(max(dot(T, T), dot(B, B)), 1e-20));
141
+ vec3 n = ${sample('uNormalMap')}.xyz * 2.0 - 1.0;
142
+ n.xy *= mEmissive.w;
143
+ N = normalize(mat3(T * invmax, B * invmax, N) * n);`);
144
+ }
145
+ L.push(` vec3 V = normalize(uCameraPos.xyz - vWorld);
146
+ float NdotV = max(dot(N, V), 1e-4);
147
+ float rough = clamp(mParams.y, 0.045, 1.0);
148
+ float metal = clamp(mParams.x, 0.0, 1.0);`);
149
+ if (mrMap) {
150
+ L.push(` vec4 mr = ${sample('uMrMap')};
151
+ rough = clamp(rough * mr.g, 0.045, 1.0);
152
+ metal = clamp(metal * mr.b, 0.0, 1.0);`);
153
+ }
154
+ L.push(` float a = rough * rough;
155
+ vec3 f0 = mix(vec3(0.04), base.rgb, metal);
156
+ vec3 diffuse = base.rgb * (1.0 - metal);
157
+ vec3 color = vec3(0.0);
158
+ vec3 ambient = vec3(0.0);`);
159
+ if (lights) lights.split(',').forEach((type, i) => L.push(lightTerm(Number(type), i)));
160
+ L.push(` float envI = uSky.w;
161
+ vec3 hemi = mix(uGround.rgb, uSky.rgb, N.y * 0.5 + 0.5) * envI + ambient;
162
+ vec3 R = reflect(-V, N);
163
+ vec3 envSpec = mix(uGround.rgb, uSky.rgb, smoothstep(-0.25 - rough, 0.25 + rough, R.y)) * envI;
164
+ envSpec = mix(envSpec, hemi, rough * rough);`);
165
+ if (occlusion) {
166
+ L.push(` float ao = mix(1.0, ${sample('uOcclusionMap')}.r, mUv.z);
167
+ hemi *= ao;
168
+ envSpec *= ao;`);
169
+ }
170
+ L.push(` color += diffuse * hemi + envSpec * envBRDF(f0, rough, NdotV);
171
+ color += emitted;
172
+ color *= uGround.w;
173
+ outColor = vec4(linearToSrgb(aces(color)), base.a);
174
+ }`);
175
+ return L.join('\n');
176
+ }
177
+
178
+ const BRDF = /* glsl */ `
179
+ float D_GGX(float NdotH, float a) {
180
+ float a2 = a * a;
181
+ float d = NdotH * NdotH * (a2 - 1.0) + 1.0;
182
+ return a2 / (PI * d * d);
183
+ }
184
+ float V_Smith(float NdotV, float NdotL, float a) {
185
+ float a2 = a * a;
186
+ float gv = NdotL * sqrt(NdotV * NdotV * (1.0 - a2) + a2);
187
+ float gl = NdotV * sqrt(NdotL * NdotL * (1.0 - a2) + a2);
188
+ return 0.5 / max(gv + gl, 1e-5);
189
+ }
190
+ vec3 F_Schlick(vec3 f0, float VdotH) {
191
+ return f0 + (vec3(1.0) - f0) * pow(1.0 - VdotH, 5.0);
192
+ }
193
+ vec3 envBRDF(vec3 f0, float roughness, float NdotV) {
194
+ const vec4 c0 = vec4(-1.0, -0.0275, -0.572, 0.022);
195
+ const vec4 c1 = vec4(1.0, 0.0425, 1.04, -0.04);
196
+ vec4 r = roughness * c0 + c1;
197
+ float a004 = min(r.x * r.x, exp2(-9.28 * NdotV)) * r.x + r.y;
198
+ vec2 AB = vec2(-1.04, 1.04) * a004 + r.zw;
199
+ return f0 * AB.x + AB.y;
200
+ }
201
+ // one light's contribution, zero when it is behind the surface
202
+ vec3 shade(vec3 N, vec3 V, vec3 L, vec3 radiance, float NdotV, vec3 f0, vec3 diffuse, float a) {
203
+ float NdotL = dot(N, L);
204
+ if (NdotL <= 0.0) return vec3(0.0);
205
+ vec3 H = normalize(V + L);
206
+ float NdotH = max(dot(N, H), 0.0);
207
+ float VdotH = max(dot(V, H), 0.0);
208
+ vec3 F = F_Schlick(f0, VdotH);
209
+ vec3 spec = D_GGX(NdotH, a) * V_Smith(NdotV, NdotL, a) * F;
210
+ vec3 kd = (vec3(1.0) - F) * diffuse / PI;
211
+ return (kd + spec) * radiance * NdotL;
212
+ }
213
+ `;
214
+
215
+ /** GLSL for light i of a known type (lighting.js: 0 directional, 1 point, 2 spot, 3 ambient, 4 hemisphere). */
216
+ function lightTerm(type, i) {
217
+ if (type === 3) return ` ambient += uLightColor[${i}].rgb;`;
218
+ if (type === 4) return ` ambient += mix(uLightExtra[${i}].yzw, uLightColor[${i}].rgb, N.y * 0.5 + 0.5);`;
219
+ if (type === 0) return ` color += shade(N, V, -uLightDir[${i}].xyz, uLightColor[${i}].rgb * PI, NdotV, f0, diffuse, a);`;
220
+ return ` {
221
+ vec3 Lv = uLightPos[${i}].xyz - vWorld;
222
+ float d = length(Lv);
223
+ vec3 L = Lv / max(d, 1e-5);
224
+ float att = 1.0 / max(d * d, 1e-4);
225
+ float range = uLightDir[${i}].w;
226
+ if (range > 0.0) {
227
+ float x = d / range;
228
+ float w = clamp(1.0 - x * x * x * x, 0.0, 1.0);
229
+ att *= w * w;
230
+ }${type === 2 ? `\n att *= smoothstep(uLightColor[${i}].w, uLightExtra[${i}].x, dot(-L, uLightDir[${i}].xyz));` : ''}
231
+ color += shade(N, V, L, uLightColor[${i}].rgb * PI * att, NdotV, f0, diffuse, a);
232
+ }`;
233
+ }
234
+
235
+ /** Lines: scene polylines (unlit material color), wireframe and bounding boxes. */
236
+ export const LINE_VS = /* glsl */ `#version 300 es
237
+ precision highp float;
238
+ ${FRAME_BLOCK}
239
+ ${INSTANCE_INPUTS}
240
+ void main() {
241
+ mat4 M = mat4(iM0, iM1, iM2, iM3);
242
+ vec4 wp = M * vec4(aPosition, 1.0);
243
+ // pull lines slightly toward the camera so they win the depth test against
244
+ // the surfaces they outline (a relative offset works at every distance)
245
+ wp.xyz = mix(wp.xyz, uCameraPos.xyz, uParams.z);
246
+ gl_Position = uViewProj * wp;
247
+ }
248
+ `;
249
+
250
+ /** Normal visualisation: the tip vertex (uv.x = 1) is offset along the world normal. */
251
+ export const NORMAL_VS = /* glsl */ `#version 300 es
252
+ precision highp float;
253
+ ${FRAME_BLOCK}
254
+ ${INSTANCE_INPUTS}
255
+ void main() {
256
+ mat4 M = mat4(iM0, iM1, iM2, iM3);
257
+ vec4 wp = M * vec4(aPosition, 1.0);
258
+ vec3 c0 = iM0.xyz;
259
+ vec3 c1 = iM1.xyz;
260
+ vec3 c2 = iM2.xyz;
261
+ mat3 cof = mat3(cross(c1, c2), cross(c2, c0), cross(c0, c1));
262
+ float det = dot(c0, cross(c1, c2));
263
+ vec3 n = normalize(cof * aNormal * (det < 0.0 ? -1.0 : 1.0));
264
+ wp.xyz += n * aUv.x * uParams.y;
265
+ wp.xyz = mix(wp.xyz, uCameraPos.xyz, uParams.z);
266
+ gl_Position = uViewProj * wp;
267
+ }
268
+ `;
269
+
270
+ export const LINE_FS = /* glsl */ `#version 300 es
271
+ precision highp float;
272
+ precision highp int;
273
+ ${FRAME_BLOCK}
274
+ ${MATERIAL_BLOCK}
275
+ out vec4 outColor;
276
+ ${COMMON_FS}
277
+ void main() {
278
+ int flags = int(mParams.z + 0.5);
279
+ if ((flags & 16) != 0) {
280
+ outColor = mOverlay;
281
+ return;
282
+ }
283
+ outColor = vec4(linearToSrgb(mBaseColor.rgb + mEmissive.rgb), mBaseColor.a);
284
+ }
285
+ `;