@solidrt/3d 0.0.58 → 0.0.59

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/AGENTS.md +734 -264
  2. package/README.md +4 -2
  3. package/examples/README.md +50 -4
  4. package/examples/bloom.tsx +113 -0
  5. package/examples/cascades.tsx +4 -4
  6. package/examples/collision.tsx +6 -6
  7. package/examples/exits.tsx +96 -0
  8. package/examples/first-person.tsx +5 -5
  9. package/examples/fleet.tsx +4 -4
  10. package/examples/fog.tsx +5 -5
  11. package/examples/instanced.tsx +70 -50
  12. package/examples/lamps.tsx +4 -4
  13. package/examples/lod.tsx +125 -0
  14. package/examples/materials.tsx +9 -9
  15. package/examples/morph.tsx +174 -0
  16. package/examples/normals.tsx +118 -0
  17. package/examples/{lit.tsx → phong.tsx} +9 -9
  18. package/examples/polyhedra.tsx +61 -0
  19. package/examples/scene-atlas.tsx +70 -0
  20. package/examples/scene-views.tsx +5 -5
  21. package/examples/shadows.tsx +9 -9
  22. package/examples/sky-lit.tsx +3 -3
  23. package/examples/skybox.tsx +5 -5
  24. package/examples/standard.tsx +2 -2
  25. package/examples/streams.tsx +122 -0
  26. package/examples/sweep-paths.tsx +5 -5
  27. package/examples/wireframe.tsx +24 -2
  28. package/package.json +3 -3
  29. package/src/clip.ts +18 -5
  30. package/src/color.ts +11 -2
  31. package/src/components/context.tsx +11 -2
  32. package/src/components/directional-light.tsx +2 -2
  33. package/src/components/first-person-camera.tsx +4 -4
  34. package/src/components/group.tsx +9 -5
  35. package/src/components/hemisphere-light.tsx +2 -2
  36. package/src/components/index.ts +4 -1
  37. package/src/components/instanced-mesh.tsx +28 -10
  38. package/src/components/lod.tsx +102 -0
  39. package/src/components/mesh.tsx +24 -6
  40. package/src/components/node-props.ts +35 -5
  41. package/src/components/orbit-camera.tsx +15 -12
  42. package/src/components/point-light.tsx +2 -2
  43. package/src/components/record-mesh.tsx +12 -7
  44. package/src/components/scene.tsx +55 -2
  45. package/src/components/spot-light.tsx +2 -2
  46. package/src/components/sprite.tsx +2 -2
  47. package/src/components/view3d.tsx +18 -1
  48. package/src/environment.ts +9 -8
  49. package/src/first-person.ts +125 -38
  50. package/src/geometry-gpu.ts +172 -39
  51. package/src/geometry.ts +1573 -239
  52. package/src/glsl.ts +398 -113
  53. package/src/gltf.ts +363 -141
  54. package/src/index.ts +31 -21
  55. package/src/light.ts +7 -8
  56. package/src/lod.ts +148 -0
  57. package/src/material.ts +316 -202
  58. package/src/mesh.ts +490 -256
  59. package/src/mixer.ts +27 -14
  60. package/src/model-file.ts +54 -15
  61. package/src/model.ts +86 -21
  62. package/src/motion.ts +17 -0
  63. package/src/node.ts +421 -22
  64. package/src/orbit.ts +378 -116
  65. package/src/profile.ts +4 -25
  66. package/src/resolve.ts +181 -0
  67. package/src/scene-shadows.ts +94 -93
  68. package/src/scene.ts +605 -185
  69. package/src/sweep.ts +27 -8
  70. package/tools/model.ts +3 -1
package/src/glsl.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  // The exported lighting GLSL: string constants an app composes into its
2
2
  // own shaderMaterial sources with plain template literals - the same
3
3
  // pieces the package's lit materials are built from (sceneSource is the
4
- // whole scene in one set; the lit, standard and unlit programs are
4
+ // whole scene in one set; the phong, standard and unlit programs are
5
5
  // assembled from it), so a custom material never becomes second-class
6
6
  // (no preprocessor, no include resolver; the policy is argued in
7
7
  // okf/notes/3d-differentiators.md).
@@ -16,7 +16,7 @@
16
16
  // every function returns its raw term, weighting and color belong to the
17
17
  // caller.
18
18
 
19
- import type { CullMode, InstanceAttribute } from "@solidrt/core/gpu"
19
+ import type { CullMode, VertexAttribute } from "@solidrt/core/gpu"
20
20
 
21
21
  // core/gpu's glsl tag, aliased locally (it is String.raw) so this module
22
22
  // stays runtime-pure: no flux:gpu import, so the checks/ rigs and any
@@ -24,7 +24,7 @@ import type { CullMode, InstanceAttribute } from "@solidrt/core/gpu"
24
24
  let glsl = String.raw
25
25
 
26
26
  /** The cap of the scene's light list (directional, spot and point nodes
27
- * alike; the hemisphere ambient is not in it) and of the `lit` fragment;
27
+ * alike; the hemisphere ambient is not in it) and of the stock lit fragments;
28
28
  * a custom fragment composes LIGHT_SLOTS and LIGHT_LOOKUP and loops to
29
29
  * `uLightCount`. A shader-source constant, so it is fixed for the app
30
30
  * (see okf/backlog/app-runtime-config.md). */
@@ -44,29 +44,31 @@ export const MAX_CASCADES = 4
44
44
  * every light is a fully cascaded sun. */
45
45
  export const MAX_SHADOW_MAPS = 8
46
46
 
47
- /** The instance attributes an instanced mesh's material declares
48
- * (`shaderMaterialClass({ instanceAttributes: INSTANCE_MATRIX_ATTRIBUTES
49
- * })`): the four vec4 columns of the per-instance matrix the spatial core
50
- * writes - the instance's placement inside the mesh (createInstancedMesh,
51
- * addInstance). Read them through INSTANCE_MATRIX. Slot 0, the core's
52
- * record; an app's own per-instance floats go in slot 1 (the style
53
- * record, see INSTANCE_COLOR_ATTRIBUTES). */
54
- export const INSTANCE_MATRIX_ATTRIBUTES: InstanceAttribute[] = [
55
- { name: "iModel0", format: "vec4" },
56
- { name: "iModel1", format: "vec4" },
57
- { name: "iModel2", format: "vec4" },
58
- { name: "iModel3", format: "vec4" },
47
+ /** The record layout an instanced mesh's material declares as its first
48
+ * instance buffer (`shaderMaterialClass({ instanceBuffers: [{ attributes:
49
+ * INSTANCE_MATRIX_ATTRIBUTES }] })`): the four vec4 columns of the
50
+ * per-instance matrix the spatial core writes - the instance's placement
51
+ * inside the mesh (createInstancedMesh, addInstance). Read them through
52
+ * INSTANCE_MATRIX. An app's own per-instance values go in the second
53
+ * instance buffer (the style record, see INSTANCE_COLOR_ATTRIBUTES). */
54
+ export const INSTANCE_MATRIX_ATTRIBUTES: VertexAttribute[] = [
55
+ { name: "iModel0", format: "float32x4" },
56
+ { name: "iModel1", format: "float32x4" },
57
+ { name: "iModel2", format: "float32x4" },
58
+ { name: "iModel3", format: "float32x4" },
59
59
  ]
60
60
 
61
61
  /** The per-instance color the stock materials read with `instanceColors`
62
62
  * (`in vec4 iColor`, premultiplied linear like aColor, forwarded as
63
- * vColor): a slot-1 attribute, so it is the instance's STYLE record -
64
- * app-owned, written with setInstanceStyle(instance, [r, g, b, a]) - beside
65
- * the core's matrix in slot 0. A custom class appends it (or any slot-1
66
- * layout of its own) to INSTANCE_MATRIX_ATTRIBUTES; the stock materials
67
- * start every instance at white. Three's instanceColor, Godot's
68
- * MultiMesh instance color. */
69
- export const INSTANCE_COLOR_ATTRIBUTES: InstanceAttribute[] = [{ name: "iColor", format: "vec4", slot: 1 }]
63
+ * vColor): the second instance buffer's layout, so it is the instance's
64
+ * STYLE record - app-owned, written with setInstanceStyle(instance, [r,
65
+ * g, b, a]) - beside the core's matrix in the first. A custom class
66
+ * declares it (or any style layout of its own) as its second instance
67
+ * buffer; the stock materials start every instance at white. Stored as
68
+ * half floats: 8 bytes per instance, and a linear color at 10-bit
69
+ * mantissa precision shows none of the banding an 8-bit unorm would in
70
+ * the darks. Three's instanceColor, Godot's MultiMesh instance color. */
71
+ export const INSTANCE_COLOR_ATTRIBUTES: VertexAttribute[] = [{ name: "iColor", format: "float16x4" }]
70
72
 
71
73
  /**
72
74
  * The vertex-stage declarations over INSTANCE_MATRIX_ATTRIBUTES:
@@ -106,7 +108,9 @@ export const INSTANCE_MATRIX = glsl`
106
108
  * `normalize(uCamPos - vWorldPos)`.
107
109
  */
108
110
  /** The skinned-layout declarations a vertex stage splices in: the
109
- * aJoints/aWeights channels, the `uBones` palette and `mat4 boneAt(int)`.
111
+ * aJoints (integer input, fed by the layout's uint8x4 or uint16x4 joint
112
+ * channel) and aWeights channels, the `uBones` palette and `mat4
113
+ * boneAt(uint)`.
110
114
  * The palette is a float texture, not a uniform array, so rig size is
111
115
  * bounded only by texture height (>= 2048 everywhere), never by the
112
116
  * vertex uniform budget: an rgba32f texture 4 texels wide, one row per
@@ -115,12 +119,13 @@ export const INSTANCE_MATRIX = glsl`
115
119
  * skinned mesh). Pair with
116
120
  * SKIN_MATRIX in main(). */
117
121
  export const SKIN_DECLS = glsl`
118
- in vec4 aJoints;
122
+ in uvec4 aJoints;
119
123
  in vec4 aWeights;
120
124
  uniform sampler2D uBones;
121
- mat4 boneAt(int j) {
122
- return mat4(texelFetch(uBones, ivec2(0, j), 0), texelFetch(uBones, ivec2(1, j), 0),
123
- texelFetch(uBones, ivec2(2, j), 0), texelFetch(uBones, ivec2(3, j), 0));
125
+ mat4 boneAt(uint j) {
126
+ int row = int(j);
127
+ return mat4(texelFetch(uBones, ivec2(0, row), 0), texelFetch(uBones, ivec2(1, row), 0),
128
+ texelFetch(uBones, ivec2(2, row), 0), texelFetch(uBones, ivec2(3, row), 0));
124
129
  }
125
130
  `
126
131
  /** The linear-blend skin matrix, spliced into main() after SKIN_DECLS:
@@ -128,25 +133,81 @@ export const SKIN_DECLS = glsl`
128
133
  * through `mat3(skin)` - exact for rigid bones, the standard
129
134
  * approximation under bone scale. */
130
135
  export const SKIN_MATRIX = glsl`
131
- mat4 skin = aWeights.x * boneAt(int(aJoints.x)) + aWeights.y * boneAt(int(aJoints.y)) +
132
- aWeights.z * boneAt(int(aJoints.z)) + aWeights.w * boneAt(int(aJoints.w));
136
+ mat4 skin = aWeights.x * boneAt(aJoints.x) + aWeights.y * boneAt(aJoints.y) +
137
+ aWeights.z * boneAt(aJoints.z) + aWeights.w * boneAt(aJoints.w);
133
138
  `
134
139
 
140
+ /** The morph-target declarations a vertex stage splices in: `uMorphs`,
141
+ * the geometry's targets packed sparse by vertex (see MorphTargets in
142
+ * geometry.ts: a header texel per vertex, then two-texel entries), laid
143
+ * into rows of `uMorphWidth` texels; `uMorphWeights`, the weights texture
144
+ * (four weights per texel, one row per weights owner - the spatial core
145
+ * writes it from the node's register), read at row `uMorphRow +
146
+ * gl_InstanceID`: a plain mesh's one row, or on an instanced mesh the
147
+ * instance's own row, its record slot (the population's texture has a
148
+ * row per slot), so every copy morphs by its own weights with no
149
+ * per-record data. Pair with MORPH_APPLY in main(). The geometry's GPU
150
+ * buffers bind uMorphs, the mesh's morph owner (the population, when
151
+ * instanced) binds uMorphWeights and the two ints. */
152
+ export const MORPH_DECLS = glsl`
153
+ uniform sampler2D uMorphs;
154
+ uniform sampler2D uMorphWeights;
155
+ uniform int uMorphWidth;
156
+ uniform int uMorphRow;
157
+ vec4 morphTexel(int i) {
158
+ return texelFetch(uMorphs, ivec2(i % uMorphWidth, i / uMorphWidth), 0);
159
+ }
160
+ float morphWeight(int t) {
161
+ vec4 four = texelFetch(uMorphWeights, ivec2(t >> 2, uMorphRow + gl_InstanceID), 0);
162
+ int c = t & 3;
163
+ return c == 0 ? four.x : c == 1 ? four.y : c == 2 ? four.z : four.w;
164
+ }
165
+ `
166
+ /** The morph displacement, spliced into main() after MORPH_DECLS: walks
167
+ * this vertex's entries (gl_VertexID indexes the headers) and leaves
168
+ * `vec3 morphPos` and `vec3 morphNormal`, the base channels plus each
169
+ * target's delta times its weight - apply the skin matrix (when any) and
170
+ * uModel to THESE. Zero-weight targets still cost their fetches; a
171
+ * vertex no target moves costs one. The normal form needs `aNormal`;
172
+ * MORPH_APPLY_POSITION is the same walk for a stage without one. */
173
+ export const MORPH_APPLY = morphApply(true)
174
+ export const MORPH_APPLY_POSITION = morphApply(false)
175
+
176
+ function morphApply(normals: boolean): string {
177
+ return glsl`
178
+ vec3 morphPos = aPos;
179
+ ${normals ? "vec3 morphNormal = aNormal;" : ""}
180
+ {
181
+ vec4 morphHead = morphTexel(gl_VertexID);
182
+ int morphAt = int(morphHead.x);
183
+ int morphCount = int(morphHead.y);
184
+ for (int k = 0; k < morphCount; k++) {
185
+ vec4 entry = morphTexel(morphAt + 2 * k);
186
+ float w = morphWeight(int(entry.x));
187
+ morphPos += w * entry.yzw;
188
+ ${normals ? "morphNormal += w * morphTexel(morphAt + 2 * k + 1).xyz;" : ""}
189
+ }
190
+ }
191
+ `
192
+ }
193
+
135
194
  // The per-instance placement, spliced into a vertex stage's position and
136
- // normal math for an instanced material: the skin (when any) first, then
137
- // the instance's matrix inside the mesh, then uModel - so a skinned
138
- // instanced fleet poses every copy by the one shared palette and places
139
- // each by its own record.
140
- function instancedPosition(skinned: boolean, instanced: boolean): string {
141
- let local = skinned ? "(skin * vec4(aPos, 1.0))" : "vec4(aPos, 1.0)"
195
+ // normal math for an instanced material: the morph (when any) and then
196
+ // the skin (when any) first, then the instance's matrix inside the mesh,
197
+ // then uModel - so a skinned instanced fleet poses every copy by the one
198
+ // shared palette and places each by its own record.
199
+ function instancedPosition(skinned: boolean, instanced: boolean, morphed = false): string {
200
+ let base = morphed ? "morphPos" : "aPos"
201
+ let local = skinned ? `(skin * vec4(${base}, 1.0))` : `vec4(${base}, 1.0)`
142
202
  return instanced ? `uModel * (instanceMatrix() * ${local})` : `uModel * ${local}`
143
203
  }
144
- function instancedNormal(skinned: boolean, instanced: boolean): string {
145
- let local = skinned ? "(mat3(skin) * aNormal)" : "aNormal"
204
+ function instancedNormal(skinned: boolean, instanced: boolean, morphed = false): string {
205
+ let base = morphed ? "morphNormal" : "aNormal"
206
+ let local = skinned ? `(mat3(skin) * ${base})` : base
146
207
  return instanced ? `mat3(uNormal) * (instanceNormalMatrix() * ${local})` : `mat3(uNormal) * ${local}`
147
208
  }
148
209
 
149
- // The one lit vertex template: the standard prefix always, aColor and
210
+ // The one lit vertex template: the base prefix always, aColor and
150
211
  // aUV2 only when the fragment reads them (an `in` the source mentions
151
212
  // makes the material require that channel, so the blocks must be absent,
152
213
  // not inactive), the skin blocks only for a skinned material (they make
@@ -154,7 +215,7 @@ function instancedNormal(skinned: boolean, instanced: boolean): string {
154
215
  // and color only for an instanced one. vColor carries the vertex color,
155
216
  // the instance color, or their product. LIT_VERTEX / LIT_VERTEX_COLORED
156
217
  // are its two named forms; litVertex(o) picks per option set.
157
- function litVertexSource(colored: boolean, uv2: boolean, skinned: boolean, instanced: boolean, instanceColors: boolean): string {
218
+ function litVertexSource(colored: boolean, uv2: boolean, skinned: boolean, instanced: boolean, instanceColors: boolean, morphed = false): string {
158
219
  let color = colored && instanceColors ? "aColor * iColor" : colored ? "aColor" : instanceColors ? "iColor" : ""
159
220
  return glsl`
160
221
  in vec3 aPos;
@@ -162,6 +223,7 @@ function litVertexSource(colored: boolean, uv2: boolean, skinned: boolean, insta
162
223
  in vec2 aUV;
163
224
  ${colored ? "in vec4 aColor;" : ""}
164
225
  ${uv2 ? "in vec2 aUV2;" : ""}
226
+ ${morphed ? MORPH_DECLS : ""}
165
227
  ${skinned ? SKIN_DECLS : ""}
166
228
  ${instanced ? INSTANCE_MATRIX : ""}
167
229
  ${instanceColors ? "in vec4 iColor;" : ""}
@@ -175,11 +237,12 @@ function litVertexSource(colored: boolean, uv2: boolean, skinned: boolean, insta
175
237
  ${uv2 ? "out vec2 vUv2;" : ""}
176
238
 
177
239
  void main() {
240
+ ${morphed ? MORPH_APPLY : ""}
178
241
  ${skinned ? SKIN_MATRIX : ""}
179
- vec4 world = ${instancedPosition(skinned, instanced)};
242
+ vec4 world = ${instancedPosition(skinned, instanced, morphed)};
180
243
  gl_Position = uViewProj * world;
181
244
  vWorldPos = world.xyz;
182
- vNormal = ${instancedNormal(skinned, instanced)};
245
+ vNormal = ${instancedNormal(skinned, instanced, morphed)};
183
246
  vUv = aUV;
184
247
  ${color ? `vColor = ${color};` : ""}
185
248
  ${uv2 ? "vUv2 = aUV2;" : ""}
@@ -308,7 +371,7 @@ export const PBR = glsl`
308
371
  * folded in) and `uLightParams[i]` (cosInner, cosOuter, distance, decay
309
372
  * - the cone cosines a spot fades between, the falloff cutoff and
310
373
  * exponent a spot or point attenuates by; unused for a directional).
311
- * Compose it before LIGHT_LOOKUP; `lit` is the shape.
374
+ * Compose it before LIGHT_LOOKUP; the stock lit program is the shape.
312
375
  */
313
376
  export const LIGHT_SLOTS = glsl`
314
377
  uniform int uLightCount;
@@ -331,7 +394,7 @@ export const LIGHT_SLOTS = glsl`
331
394
  * zero at `distance` (0 = no cutoff; Three's punctual-light falloff), a
332
395
  * spot's additionally faded across its cone from cosInner to cosOuter.
333
396
  * Zero means the light cannot reach the fragment - skip its shadow
334
- * lookup and its terms (`lit` does exactly that).
397
+ * lookup and its terms (the stock lit program does exactly that).
335
398
  */
336
399
  export const LIGHT_LOOKUP = glsl`
337
400
  // Floors the falloff divisors so a fragment at the light's own
@@ -447,7 +510,7 @@ export const FOG = glsl`
447
510
  * and `vec3 linearToSrgb(vec3)`, clamped at zero. An "rgba8-srgb" texture
448
511
  * decodes in hardware and needs neither; these serve pixels a format
449
512
  * cannot tag (a render target sampled as a map) and the output stage.
450
- * OUTPUT includes this set - never compose both.
513
+ * RESOLVE includes this set - never compose both.
451
514
  */
452
515
  export const SRGB = glsl`
453
516
  vec3 srgbToLinear(vec3 c) {
@@ -461,25 +524,48 @@ export const SRGB = glsl`
461
524
  `
462
525
 
463
526
  /**
464
- * The output stage every library fragment ends with: `vec4
465
- * outputColor(vec3 rgb, float alpha)` takes the PREMULTIPLIED linear
466
- * result and its alpha, applies the scene's exposure and tone mapping
467
- * (`uniform float uExposure`; `uniform float uToneMapping`, 0 none, 1
468
- * ACES - the shared params scene.setExposure / setToneMapping write),
469
- * encodes to sRGB and returns the premultiplied pixel the target holds.
470
- * It un-premultiplies inside so the encode is exact at partial alpha.
471
- * Compose it last in a custom fragment: `fragColor = outputColor(rgb,
472
- * alpha);` - a fragment that writes fragColor directly writes final
473
- * encoded pixels and skips exposure and tone mapping. Includes SRGB.
527
+ * The LOD cross-fade: `uLodFade` is (threshold, side) - the scene writes
528
+ * the band position of the level this entry draws (see createLod's
529
+ * `fade`), `[1, 1]` when solid. Inside a band the nearer level keeps the
530
+ * fragments whose screen hash falls below the threshold (side 1) and the
531
+ * farther level the rest (side -1): a partition, so nothing draws twice
532
+ * and nothing is missed. Call `lodFade()` first thing in main. Every
533
+ * stock fragment composes it; a custom class that declares `uLodFade`
534
+ * opts in (the scene detects the name, like uNormal).
474
535
  */
475
- export const OUTPUT = glsl`
536
+ export const LOD_FADE = glsl`
537
+ uniform vec2 uLodFade;
538
+ // Interleaved gradient noise (Jimenez 2014): a screen-position hash
539
+ // whose pattern stays put as the band moves, so the dissolve reads as
540
+ // a steady grain rather than a crawl.
541
+ float lodHash(vec2 p) {
542
+ return fract(52.9829189 * fract(dot(p, vec2(0.06711056, 0.00583715))));
543
+ }
544
+ void lodFade() {
545
+ if (uLodFade.x < 1.0) {
546
+ if ((lodHash(gl_FragCoord.xy) - uLodFade.x) * uLodFade.y >= 0.0) discard;
547
+ }
548
+ }
549
+ `
550
+
551
+ /**
552
+ * The resolve set: the display stage of a target, run ONCE per pixel by
553
+ * the resolve pass that turns a scene buffer (premultiplied linear light,
554
+ * half float where the device renders it) into the displayed rgba8
555
+ * pixels. `vec4 resolveColor(vec3 rgb, float alpha)` takes a buffer
556
+ * texel, applies the scene's exposure and tone mapping (`uniform float
557
+ * uExposure`; `uniform float uToneMapping`, 0 none, 1 ACES, 2 AgX, 3
558
+ * Neutral - the shared params scene.setExposure / setToneMapping
559
+ * write), clamps, encodes to
560
+ * sRGB with an ordered dither against 8-bit banding, and returns the
561
+ * premultiplied pixel. It un-premultiplies inside so the encode is exact
562
+ * at partial alpha. A custom resolve (SceneOptions.resolve) gets this set
563
+ * declared and ends with it; a scene fragment never composes it - a
564
+ * fragment writes linear light (see sceneOutput). Includes SRGB.
565
+ */
566
+ export const RESOLVE = glsl`
476
567
  uniform float uExposure;
477
568
  uniform float uToneMapping;
478
- // 1 (the scene's default) clamps and encodes to sRGB for display; 0
479
- // writes linear light unclamped, what a reflection probe's faces hold
480
- // so the environment lookups read radiance (the scene sets it per
481
- // target; an 8-bit target clamps it on write regardless).
482
- uniform float uOutputEncode;
483
569
  ${SRGB}
484
570
  // Stephen Hill's fit of the ACES RRT and ODT (Three's ACESFilmic, Godot's
485
571
  // ACES): the matrices between sRGB and the fit's working space, and the
@@ -503,18 +589,176 @@ export const OUTPUT = glsl`
503
589
  c = acesFit(c);
504
590
  return clamp(ACES_OUTPUT * c, 0.0, 1.0);
505
591
  }
506
- vec4 outputColor(vec3 rgb, float alpha) {
592
+ // AgX (Blender's, through Filament and Three's AgXToneMapping): rec
593
+ // 2020 primaries in, an inset toward gray, log2 encoding over the
594
+ // scene's dynamic range, the sigmoid, the outset, back to sRGB
595
+ // primaries. The 6th-order fit of the sigmoid is the iolite
596
+ // approximation Three ships.
597
+ const mat3 AGX_SRGB_TO_REC2020 = mat3(
598
+ 0.6274, 0.0691, 0.0164,
599
+ 0.3293, 0.9195, 0.0880,
600
+ 0.0433, 0.0113, 0.8956);
601
+ const mat3 AGX_REC2020_TO_SRGB = mat3(
602
+ 1.6605, -0.1246, -0.0182,
603
+ -0.5876, 1.1329, -0.1006,
604
+ -0.0728, -0.0083, 1.1187);
605
+ const mat3 AGX_INSET = mat3(
606
+ 0.856627153315983, 0.137318972929847, 0.11189821299995,
607
+ 0.0951212405381588, 0.761241990602591, 0.0767994186031903,
608
+ 0.0482516061458583, 0.101439036467562, 0.811302368396859);
609
+ const mat3 AGX_OUTSET = mat3(
610
+ 1.1271005818144368, -0.1413297634984383, -0.14132976349843826,
611
+ -0.11060664309660323, 1.157823702216272, -0.11060664309660294,
612
+ -0.016493938717834573, -0.016493938717834257, 1.2519364065950405);
613
+ // log2 of middle gray (0.18) at the range's ends, 10 stops below and
614
+ // 6.5 above.
615
+ const float AGX_MIN_EV = -12.47393;
616
+ const float AGX_MAX_EV = 4.026069;
617
+ // The encoded value's display gamma before the primaries go back.
618
+ const float AGX_GAMMA = 2.2;
619
+ vec3 agxSigmoid(vec3 x) {
620
+ vec3 x2 = x * x;
621
+ vec3 x4 = x2 * x2;
622
+ return 15.5 * x4 * x2 - 40.14 * x4 * x + 31.96 * x4 - 6.868 * x2 * x + 0.4298 * x2 + 0.1191 * x - 0.00232;
623
+ }
624
+ vec3 agxToneMap(vec3 c) {
625
+ c = AGX_INSET * (AGX_SRGB_TO_REC2020 * c);
626
+ c = log2(max(c, vec3(1e-10)));
627
+ c = clamp((c - AGX_MIN_EV) / (AGX_MAX_EV - AGX_MIN_EV), 0.0, 1.0);
628
+ c = AGX_OUTSET * agxSigmoid(c);
629
+ c = pow(max(c, vec3(0.0)), vec3(AGX_GAMMA));
630
+ return clamp(AGX_REC2020_TO_SRGB * c, 0.0, 1.0);
631
+ }
632
+ // Khronos PBR Neutral (Three's NeutralToneMapping, Unity's Neutral):
633
+ // hue-preserving, a black-level lift below a knee, then a soft peak
634
+ // compression above START_COMPRESSION that desaturates toward white.
635
+ const float NEUTRAL_START_COMPRESSION = 0.8 - 0.04;
636
+ const float NEUTRAL_DESATURATION = 0.15;
637
+ const float NEUTRAL_KNEE = 0.08;
638
+ const float NEUTRAL_KNEE_CURVE = 6.25;
639
+ const float NEUTRAL_OFFSET = 0.04;
640
+ vec3 neutralToneMap(vec3 c) {
641
+ float x = min(c.r, min(c.g, c.b));
642
+ float offset = x < NEUTRAL_KNEE ? x - NEUTRAL_KNEE_CURVE * x * x : NEUTRAL_OFFSET;
643
+ c -= offset;
644
+ float peak = max(c.r, max(c.g, c.b));
645
+ if (peak < NEUTRAL_START_COMPRESSION) return max(c, vec3(0.0));
646
+ float d = 1.0 - NEUTRAL_START_COMPRESSION;
647
+ float newPeak = 1.0 - d * d / (peak + d - NEUTRAL_START_COMPRESSION);
648
+ c *= newPeak / peak;
649
+ float g = 1.0 - 1.0 / (NEUTRAL_DESATURATION * (peak - newPeak) + 1.0);
650
+ return clamp(mix(c, vec3(newPeak), g), 0.0, 1.0);
651
+ }
652
+ // Debanding: one 8-bit step of interleaved gradient noise (Jimenez
653
+ // 2014) centered on zero, added to the encoded value before the target
654
+ // quantizes it, so a slow gradient dithers instead of stepping.
655
+ const float DITHER_STEP = 1.0 / 255.0;
656
+ float resolveDither(vec2 p) {
657
+ return fract(52.9829189 * fract(dot(p, vec2(0.06711056, 0.00583715)))) - 0.5;
658
+ }
659
+ vec4 resolveColor(vec3 rgb, float alpha) {
507
660
  vec3 c = alpha > 0.0 ? rgb / alpha : rgb;
508
661
  c *= uExposure;
509
- if (uToneMapping > 0.5) c = acesToneMap(c);
510
- // Linear output keeps its range (a half-float probe holds a sun's
511
- // radiance); the display encode is 8-bit and clamps first.
512
- if (uOutputEncode > 0.5) c = linearToSrgb(clamp(c, 0.0, 1.0));
513
- else c = max(c, vec3(0.0));
662
+ if (uToneMapping > 2.5) c = neutralToneMap(c);
663
+ else if (uToneMapping > 1.5) c = agxToneMap(c);
664
+ else if (uToneMapping > 0.5) c = acesToneMap(c);
665
+ c = linearToSrgb(clamp(c, 0.0, 1.0));
666
+ c += resolveDither(gl_FragCoord.xy) * DITHER_STEP;
514
667
  return vec4(c * alpha, alpha);
515
668
  }
516
669
  `
517
670
 
671
+ /**
672
+ * The default resolve fragment (SceneOptions.resolve when absent): one
673
+ * sample of the scene buffer through resolveColor. The contract a custom
674
+ * resolve gets: `vUV` (0..1, origin top-left), `iResolution`, `fragColor`,
675
+ * `uniform sampler2D uScene` (the buffer, premultiplied linear) and the
676
+ * RESOLVE set, all declared by the preamble; any name written through
677
+ * scene.setParams (uExposure and uToneMapping among them) is readable.
678
+ * Start a grade from this source and end in resolveColor.
679
+ */
680
+ export const DEFAULT_RESOLVE = glsl`
681
+ void main() {
682
+ vec4 s = texture(uScene, vUV);
683
+ fragColor = resolveColor(s.rgb, s.a);
684
+ }
685
+ `
686
+
687
+ /**
688
+ * The default resolve while the stock bloom is on (SceneOptions.bloom):
689
+ * the buffer's texel plus the blurred excess the bloom chain rendered,
690
+ * bound on the resolve target as `uniform sampler2D uBloom` with its
691
+ * weight in `uniform float uBloomIntensity` - both declared by the
692
+ * source, not the preamble. The glow is LIGHT over the pixel whatever it
693
+ * covers: the alpha grows with the glow's brightness (a premultiplied
694
+ * light layer), so it shows over a transparent backdrop - a view with no
695
+ * clearColor composited over UI - and an opaque pixel is exactly the
696
+ * sum. A custom resolve composes the stock bloom by declaring the same
697
+ * two names and adding the term; start from this.
698
+ */
699
+ export const BLOOM_RESOLVE = glsl`
700
+ uniform sampler2D uBloom;
701
+ uniform float uBloomIntensity;
702
+ void main() {
703
+ vec4 s = texture(uScene, vUV);
704
+ vec3 glow = texture(uBloom, vUV).rgb * uBloomIntensity;
705
+ float coverage = clamp(max(glow.r, max(glow.g, glow.b)), 0.0, 1.0);
706
+ fragColor = resolveColor(s.rgb + glow, max(s.a, coverage));
707
+ }
708
+ `
709
+
710
+ /**
711
+ * The stock bloom's threshold pass (a shader texture over the buffer):
712
+ * the texel's excess over `uniform float uThreshold` by its brightest
713
+ * channel, so a colored highlight keeps its hue, black below it.
714
+ */
715
+ export const BLOOM_THRESHOLD = glsl`
716
+ uniform sampler2D uSource;
717
+ uniform float uThreshold;
718
+ void main() {
719
+ vec4 s = texture(uSource, vUV);
720
+ vec3 c = s.a > 0.0 ? s.rgb / s.a : s.rgb;
721
+ float peak = max(max(c.r, c.g), c.b);
722
+ float keep = max(peak - uThreshold, 0.0) / max(peak, 0.0001);
723
+ fragColor = vec4(c * keep, 1.0);
724
+ }
725
+ `
726
+
727
+ /**
728
+ * The stock bloom's blur pass: a 9-tap gaussian along `uniform vec2
729
+ * uDir` (one texel step on the given axis), run across then down per
730
+ * round of the chain.
731
+ */
732
+ export const BLOOM_BLUR = glsl`
733
+ uniform sampler2D uSource;
734
+ uniform vec2 uDir;
735
+ const float WEIGHT[5] = float[5](0.2270270270, 0.1945945946, 0.1216216216, 0.0540540541, 0.0162162162);
736
+ void main() {
737
+ vec2 step = uDir / iResolution;
738
+ vec3 sum = texture(uSource, vUV).rgb * WEIGHT[0];
739
+ for (int i = 1; i < 5; i++) {
740
+ vec2 o = step * float(i);
741
+ sum += texture(uSource, vUV + o).rgb * WEIGHT[i];
742
+ sum += texture(uSource, vUV - o).rgb * WEIGHT[i];
743
+ }
744
+ fragColor = vec4(sum, 1.0);
745
+ }
746
+ `
747
+
748
+ /**
749
+ * A resolve source as a shader-texture fragment of your own: `uniform
750
+ * sampler2D uScene` and the RESOLVE set declared over `source` (default
751
+ * DEFAULT_RESOLVE), the engine preamble supplying vUV, iResolution and
752
+ * fragColor. The way an app resolves a buffer the library does not - the
753
+ * atlas its tiled views (`into`) render linear light into:
754
+ * `createShaderTexture(resolveFragment(), w, h, resolveParams(opts), {
755
+ * textures: { uScene: atlas } })`. A shader target validates its params
756
+ * strictly, so write both uExposure and uToneMapping (resolveParams does).
757
+ */
758
+ export function resolveFragment(source: string = DEFAULT_RESOLVE): string {
759
+ return "uniform sampler2D uScene;\n" + RESOLVE + "\n" + source
760
+ }
761
+
518
762
  /**
519
763
  * The scene's environment as a reflecting program declares it: `uEnv`
520
764
  * (a samplerCube the scene binds on every receiving target - a 1x1 black
@@ -539,7 +783,7 @@ export const OUTPUT = glsl`
539
783
  * camera) at roughness `sqrt(2 / (shininess + 2))`, so a wide sheen
540
784
  * reflects a blurred environment and a mirror dot a sharp one. `envWeight(n, v,
541
785
  * reflectivity)` is the Schlick fresnel weight: `reflectivity` face-on,
542
- * 1 at grazing, 0 with no environment. `lit` applies them as
786
+ * 1 at grazing, 0 with no environment. `phong` applies them as
543
787
  * `rgb = mix(rgb, envReflection(n, v, uShininess), envWeight(n, v,
544
788
  * uReflectivity))` - Three's MixOperation with a fresnel weight. Every
545
789
  * lookup is a plain `texture(uEnv, dir)` in world space: a cube map here
@@ -593,7 +837,8 @@ export const ENVIRONMENT = glsl`
593
837
  * LIGHT_SLOTS, this, SHADOW, then SHADOW_LOOKUP (in that order - the
594
838
  * lookup reads the light type and position for a point light's face
595
839
  * select) and multiplies
596
- * light i's term by `lightShadow(i, worldPos, n)` - `lit` is the shape. A
840
+ * light i's term by `lightShadow(i, worldPos, n)` - the stock lit program
841
+ * is the shape. A
597
842
  * material that does not receive composes none of it, so it declares no
598
843
  * sampler for nothing.
599
844
  */
@@ -731,7 +976,7 @@ export const SHADOW_LOOKUP = glsl`
731
976
 
732
977
  /**
733
978
  * The material defaults every Surface starts from (`surfaceOf`) and the
734
- * `lit`/`standard` options fall back to: one table, so a custom fragment
979
+ * `phong`/`standard` options fall back to: one table, so a custom fragment
735
980
  * that builds its own Surface and a stock material agree on what
736
981
  * "unspecified" means.
737
982
  */
@@ -765,7 +1010,7 @@ export const SURFACE_DEFAULTS = {
765
1010
  * `Surface surfaceOf(vec4 base, vec3 normal)` fills the rest with
766
1011
  * SURFACE_DEFAULTS (no emissive, no ambient), so a fragment sets only
767
1012
  * what it means. One struct for both light models, as Godot and Unity
768
- * have it, so a `surface` function (litFragment's slot) has one
1013
+ * have it, so a `surface` function (phongFragment's slot) has one
769
1014
  * signature whatever material it sits in; the model that shades reads
770
1015
  * its own fields and ignores the other's. Included by sceneSource - never
771
1016
  * compose both.
@@ -811,19 +1056,21 @@ export type SceneSourceOptions = {
811
1056
  * diffuse and the split-sum specular. False declares no environment
812
1057
  * cube and shades with the lights alone. Needs `lights`. */
813
1058
  env?: boolean
814
- /** Fog inside sceneOutput. False leaves the fog set out. */
815
- fog?: boolean
1059
+ /** Fog inside sceneOutput. False leaves the fog set out; "additive"
1060
+ * composes fogAdditive instead of fog, the form a `blend: "add"` draw
1061
+ * needs (it fades toward black, not the fog color). */
1062
+ fog?: boolean | "additive"
816
1063
  }
817
1064
 
818
1065
  /**
819
1066
  * The scene as a program declares it, in one set: everything a fragment
820
1067
  * needs to be a citizen of the scene - lit by its lights, shadowed by its
821
- * casters, reflecting its environment, fogged, exposed, tone mapped and
822
- * encoded like the stock materials, because the stock materials are
823
- * built from this very source. It declares `uCamPos`, `uHemiSky` /
824
- * `uHemiGround`, LIGHT_SLOTS + LIGHT_LOOKUP, the SHADOW trio, the pure
825
- * light functions (HEMISPHERE, LAMBERT, BLINN_SPECULAR, PBR),
826
- * ENVIRONMENT, FOG and OUTPUT (per the flags), SURFACE, and exports:
1068
+ * casters, reflecting its environment and fogged like the stock
1069
+ * materials, because the stock materials are built from this very
1070
+ * source. It declares `uCamPos`, `uHemiSky` / `uHemiGround`, LIGHT_SLOTS
1071
+ * + LIGHT_LOOKUP, the SHADOW trio, the pure light functions (HEMISPHERE,
1072
+ * LAMBERT, BLINN_SPECULAR, PBR), ENVIRONMENT and FOG (per the flags),
1073
+ * SURFACE, and exports:
827
1074
  *
828
1075
  * struct SceneLight { vec3 dir; vec3 color; };
829
1076
  * SceneLight sceneLight(int i, vec3 position, vec3 normal);
@@ -838,7 +1085,7 @@ export type SceneSourceOptions = {
838
1085
  * `uLightCount` (URP's GetAdditionalLight, Godot's light() inputs), and
839
1086
  * the one that makes a spot light impossible to render as a directional.
840
1087
  *
841
- * The shade functions are the whole light loop of `lit` (Blinn-Phong)
1088
+ * The shade functions are the whole light loop of `phong` (Blinn-Phong)
842
1089
  * and `standard` (GGX): hemisphere plus `Surface.ambient` plus every
843
1090
  * light's diffuse and specular, the environment term, then the emissive,
844
1091
  * returning linear PREMULTIPLIED rgb. A term added by hand after them is
@@ -848,8 +1095,12 @@ export type SceneSourceOptions = {
848
1095
  * stage carries the world position in.
849
1096
  *
850
1097
  * `sceneOutput` is the tail every library fragment ends with: fog over
851
- * the premultiplied color, then OUTPUT's exposure, tone mapping and
852
- * encode - the one place a post effect or a scene buffer change lands.
1098
+ * the premultiplied color, clamped at zero, and that is the pixel the
1099
+ * scene buffer holds - LINEAR light, unclamped above (a sun disc of 40
1100
+ * stays 40 in a half-float buffer). Exposure, tone mapping and the sRGB
1101
+ * encode happen once per pixel in the target's resolve pass (RESOLVE),
1102
+ * never in a scene fragment: a fragment that writes fragColor directly
1103
+ * writes linear premultiplied light too.
853
1104
  *
854
1105
  * ${SCENE}
855
1106
  * void main() {
@@ -866,7 +1117,7 @@ export function sceneSource(o: SceneSourceOptions = {}): string {
866
1117
  let lights = o.lights !== false
867
1118
  let receiveShadow = lights && o.receiveShadow !== false
868
1119
  let env = lights && o.env !== false
869
- let fog = o.fog !== false
1120
+ let fog = o.fog ?? true
870
1121
  return glsl`
871
1122
  ${SURFACE}
872
1123
  ${lights || fog ? "uniform vec3 uCamPos;" : ""}
@@ -895,12 +1146,11 @@ export function sceneSource(o: SceneSourceOptions = {}): string {
895
1146
  }
896
1147
  ${env ? ENVIRONMENT : ""}
897
1148
  ${fog ? FOG : ""}
898
- ${OUTPUT}
899
1149
  ${lights ? sceneShadeSource(receiveShadow, env) : ""}
900
1150
 
901
1151
  vec4 sceneOutput(vec3 rgb, float alpha, vec3 position) {
902
- ${fog ? "rgb = fog(rgb, alpha, position, uCamPos);" : ""}
903
- return outputColor(rgb, alpha);
1152
+ ${fog === "additive" ? "rgb = fogAdditive(rgb, position, uCamPos);" : fog ? "rgb = fog(rgb, alpha, position, uCamPos);" : ""}
1153
+ return vec4(max(rgb, vec3(0.0)), alpha);
904
1154
  }
905
1155
  `
906
1156
  }
@@ -910,7 +1160,8 @@ export const SCENE = sceneSource()
910
1160
 
911
1161
  // The light accessor and the two shade functions, over the sets
912
1162
  // sceneSource composed before them. A light that cannot reach the point
913
- // is skipped whole (no shadow tap, no terms), the gate `lit` always had.
1163
+ // is skipped whole (no shadow tap, no terms), the gate the stock lit
1164
+ // program always had.
914
1165
  function sceneShadeSource(receiveShadow: boolean, env: boolean): string {
915
1166
  return glsl`
916
1167
  struct SceneLight {
@@ -1021,8 +1272,10 @@ export type LitSourceOptions = {
1021
1272
  * alpha falls below it (the cutoff is a per-entry uniform, so one
1022
1273
  * program serves every value). */
1023
1274
  alphaTest?: boolean
1024
- /** Compose the scene's fog over the result (default true). */
1025
- fog?: boolean
1275
+ /** Compose the scene's fog over the result (default true); "additive"
1276
+ * for a `blend: "add"` program, which fades toward black instead of the
1277
+ * fog color (see FOG). */
1278
+ fog?: boolean | "additive"
1026
1279
  /** Bend the lit normal by a tangent-space normal map (NORMAL_MAP:
1027
1280
  * `uniform sampler2D uNormalMap` scaled by `uniform float
1028
1281
  * uNormalScale`), sampled at the same uv as uMap. The frame comes from
@@ -1064,6 +1317,13 @@ export type LitSourceOptions = {
1064
1317
  * the spatial flush writes it). The vertex stage then requires that layout
1065
1318
  * and something must bind `uBones`; the fragment is unchanged. */
1066
1319
  skinned?: boolean
1320
+ /** Displace positions and normals by the geometry's morph targets
1321
+ * (MORPH_DECLS and MORPH_APPLY, before the skin): the vertex stage then
1322
+ * declares `uMorphs`, `uMorphWeights`, `uMorphWidth` and `uMorphRow`,
1323
+ * which the scene binds from the geometry's packed targets and the
1324
+ * mesh's morph owner (a mesh whose geometry carries none is rejected
1325
+ * at add()); the fragment is unchanged. */
1326
+ morph?: boolean
1067
1327
  /** Place each copy by its instance matrix (INSTANCE_MATRIX under
1068
1328
  * uModel, normals through instanceNormalMatrix): the vertex stage of a
1069
1329
  * material for createInstancedMesh. The class then declares
@@ -1104,7 +1364,7 @@ export type StandardSourceOptions = Omit<LitSourceOptions, "specularMap" | "env"
1104
1364
  }
1105
1365
 
1106
1366
  // The resolved option set: every flag concrete, every slot a string, so
1107
- // the builders below never re-apply a default and `lit`'s class key and
1367
+ // the builders below never re-apply a default and a stock material's class key and
1108
1368
  // its program cannot drift apart.
1109
1369
  type LitSource = {
1110
1370
  map: boolean
@@ -1114,7 +1374,7 @@ type LitSource = {
1114
1374
  receiveShadow: boolean
1115
1375
  cull: CullMode
1116
1376
  alphaTest: boolean
1117
- fog: boolean
1377
+ fog: boolean | "additive"
1118
1378
  normalMap: boolean
1119
1379
  emissive: boolean
1120
1380
  emissiveMap: boolean
@@ -1125,9 +1385,10 @@ type LitSource = {
1125
1385
  skinned: boolean
1126
1386
  instanced: boolean
1127
1387
  instanceColors: boolean
1388
+ morph: boolean
1128
1389
  prelude: string
1129
1390
  surface: string
1130
- // The light model: Blinn-Phong (`lit`) or GGX metalness/roughness
1391
+ // The light model: Blinn-Phong (`phong`) or GGX metalness/roughness
1131
1392
  // (`standard`, which also samples the environment unconditionally).
1132
1393
  brdf: "blinn" | "ggx"
1133
1394
  metalnessMap: boolean
@@ -1143,7 +1404,7 @@ function resolveLit(o: LitSourceOptions): LitSource {
1143
1404
  receiveShadow: o.receiveShadow !== false,
1144
1405
  cull: o.cull ?? "back",
1145
1406
  alphaTest: o.alphaTest === true,
1146
- fog: o.fog !== false,
1407
+ fog: o.fog ?? true,
1147
1408
  normalMap: o.normalMap === true,
1148
1409
  emissive: o.emissive === true || o.emissiveMap === true,
1149
1410
  emissiveMap: o.emissiveMap === true,
@@ -1154,6 +1415,7 @@ function resolveLit(o: LitSourceOptions): LitSource {
1154
1415
  skinned: o.skinned === true,
1155
1416
  instanced: o.instanced === true || o.instanceColors === true,
1156
1417
  instanceColors: o.instanceColors === true,
1418
+ morph: o.morph === true,
1157
1419
  prelude: o.prelude ?? "",
1158
1420
  surface: o.surface ?? "",
1159
1421
  brdf: "blinn",
@@ -1221,20 +1483,20 @@ function litBase(c: LitSource, flip: boolean): string {
1221
1483
  }
1222
1484
 
1223
1485
  /**
1224
- * The vertex stage `lit` pairs with a given option set: LIT_VERTEX, or
1486
+ * The vertex stage the stock lit materials pair with a given option set: LIT_VERTEX, or
1225
1487
  * LIT_VERTEX_COLORED when the fragment reads vColor. The shadow source
1226
1488
  * takes this same stage, which is what lets a surface function read the
1227
1489
  * same varyings in both passes.
1228
1490
  */
1229
1491
  export function litVertex(o: LitSourceOptions = {}): string {
1230
1492
  let c = resolveLit(o)
1231
- return litVertexSource(c.vertexColors, c.lightMap, c.skinned, c.instanced, c.instanceColors)
1493
+ return litVertexSource(c.vertexColors, c.lightMap, c.skinned, c.instanced, c.instanceColors, c.morph)
1232
1494
  }
1233
1495
 
1234
1496
  /**
1235
- * The `lit` fragment source for an option set - the exact program `lit`
1497
+ * The `phong` fragment source for an option set - the exact program `phong`
1236
1498
  * itself builds, composed from the constants above. Pair it with
1237
- * litVertex(o) in a shaderMaterialClass to get a lit material with your
1499
+ * litVertex(o) in a shaderMaterialClass to get a Blinn-Phong material with your
1238
1500
  * own GLSL in it, and pass litShadowFragment(o) alongside when it can
1239
1501
  * discard.
1240
1502
  *
@@ -1246,7 +1508,7 @@ export function litVertex(o: LitSourceOptions = {}): string {
1246
1508
  * Everything else - the lights, the hemisphere, the camera position, the
1247
1509
  * shadow set, the fog - is written by the scene.
1248
1510
  */
1249
- export function litFragment(o: LitSourceOptions = {}): string {
1511
+ export function phongFragment(o: LitSourceOptions = {}): string {
1250
1512
  return lightingFragment(resolveLit(o))
1251
1513
  }
1252
1514
 
@@ -1257,7 +1519,7 @@ export function litFragment(o: LitSourceOptions = {}): string {
1257
1519
  * metalness/roughness model (PBR) instead of Blinn-Phong, and the
1258
1520
  * scene's ENVIRONMENT sampled unconditionally as the split sum
1259
1521
  * (envRadiance times envBrdf). Pair it with litVertex(o), and with
1260
- * litShadowFragment(o) when it can discard, exactly as litFragment.
1522
+ * litShadowFragment(o) when it can discard, exactly as phongFragment.
1261
1523
  *
1262
1524
  * Per-entry uniforms: `uColor` (premultiplied vec4), `uMetalness`,
1263
1525
  * `uRoughness`, `uMetalnessMap` (blue channel) and `uRoughnessMap`
@@ -1298,10 +1560,12 @@ function lightingFragment(c: LitSource): string {
1298
1560
  ${c.mapTransform ? "uniform vec4 uMapTransform;" : ""}
1299
1561
  ${c.normalMap ? NORMAL_MAP : ""}
1300
1562
  ${sceneSource({ receiveShadow: c.receiveShadow, env: c.env, fog: c.fog })}
1563
+ ${LOD_FADE}
1301
1564
  ${c.prelude}
1302
1565
  ${c.surface}
1303
1566
 
1304
1567
  void main() {
1568
+ lodFade();
1305
1569
  ${litBase(c, c.cull !== "back")}
1306
1570
  ${c.normalMap ? "n = perturbNormal(n, vWorldPos, uv);" : ""}
1307
1571
  Surface s = surfaceOf(base, n);
@@ -1325,7 +1589,7 @@ function lightingFragment(c: LitSource): string {
1325
1589
  /**
1326
1590
  * The depth-pass source that makes a discarding lit material cast what it
1327
1591
  * actually draws: the same base, cutout and surface function as
1328
- * litFragment(o), and nothing after them (no lighting, no fog - a shadow
1592
+ * phongFragment(o), and nothing after them (no lighting, no fog - a shadow
1329
1593
  * has neither). Undefined when the option set cannot discard (no
1330
1594
  * alphaTest, no surface), which means the scene's default depth material
1331
1595
  * is already right and the material should carry no `shadow` of its own.
@@ -1383,7 +1647,7 @@ export const UNLIT_VERTEX = glsl`
1383
1647
 
1384
1648
  /**
1385
1649
  * The option set the unlit builders take: LitSourceOptions minus
1386
- * everything lighting decides (no vertexColors, triplanar, receiveShadow)
1650
+ * everything lighting decides (no triplanar, receiveShadow)
1387
1651
  * and minus cull (the unlit fragment has no facing-dependent code - cull
1388
1652
  * is pure pipeline state, passed to shaderMaterialClass directly). The
1389
1653
  * slots follow the lit contract; the varyings here are vUv and vWorldPos
@@ -1392,13 +1656,19 @@ export const UNLIT_VERTEX = glsl`
1392
1656
  export type UnlitSourceOptions = {
1393
1657
  /** The fragment samples a `uniform sampler2D uMap`, tinted by uColor. */
1394
1658
  map?: boolean
1659
+ /** Multiply the base by the "colored" layout's per-vertex aColor (see
1660
+ * the lit option): unlitVertex(o) forwards it as vColor, times iColor
1661
+ * under instanceColors. */
1662
+ vertexColors?: boolean
1395
1663
  /** Write the base alpha through and blend; opaque (the default) writes
1396
1664
  * alpha 1. */
1397
1665
  transparent?: boolean
1398
1666
  /** Declare `uniform float uAlphaTest` and discard below it. */
1399
1667
  alphaTest?: boolean
1400
- /** Compose the scene's fog over the result (default true). */
1401
- fog?: boolean
1668
+ /** Compose the scene's fog over the result (default true); "additive"
1669
+ * for a `blend: "add"` program, which fades toward black instead of the
1670
+ * fog color (see FOG). */
1671
+ fog?: boolean | "additive"
1402
1672
  /** Declare `uniform vec4 uMapTransform` ([repeatU, repeatV, offsetU,
1403
1673
  * offsetV]) and sample the map at `vUv * repeat + offset` (see the lit
1404
1674
  * option of the same name). Needs `map`. */
@@ -1406,6 +1676,9 @@ export type UnlitSourceOptions = {
1406
1676
  /** Skin positions by the "skinned" layout against uBones (see the lit
1407
1677
  * option); unlitVertex(o) reads it, the fragment is unchanged. */
1408
1678
  skinned?: boolean
1679
+ /** Displace positions by the geometry's morph targets (see the lit
1680
+ * option); unlitVertex(o) reads it, the fragment is unchanged. */
1681
+ morph?: boolean
1409
1682
  /** Place each copy by its instance matrix (see the lit option);
1410
1683
  * unlitVertex(o) reads it, the fragment is unchanged. */
1411
1684
  instanced?: boolean
@@ -1423,13 +1696,15 @@ export type UnlitSourceOptions = {
1423
1696
 
1424
1697
  type UnlitSource = {
1425
1698
  map: boolean
1699
+ vertexColors: boolean
1426
1700
  transparent: boolean
1427
1701
  alphaTest: boolean
1428
- fog: boolean
1702
+ fog: boolean | "additive"
1429
1703
  mapTransform: boolean
1430
1704
  skinned: boolean
1431
1705
  instanced: boolean
1432
1706
  instanceColors: boolean
1707
+ morph: boolean
1433
1708
  prelude: string
1434
1709
  surface: string
1435
1710
  }
@@ -1437,13 +1712,15 @@ type UnlitSource = {
1437
1712
  function resolveUnlit(o: UnlitSourceOptions): UnlitSource {
1438
1713
  return {
1439
1714
  map: o.map === true,
1715
+ vertexColors: o.vertexColors === true,
1440
1716
  transparent: o.transparent === true,
1441
1717
  alphaTest: o.alphaTest === true,
1442
- fog: o.fog !== false,
1718
+ fog: o.fog ?? true,
1443
1719
  mapTransform: o.mapTransform === true && o.map === true,
1444
1720
  skinned: o.skinned === true,
1445
1721
  instanced: o.instanced === true || o.instanceColors === true,
1446
1722
  instanceColors: o.instanceColors === true,
1723
+ morph: o.morph === true,
1447
1724
  prelude: o.prelude ?? "",
1448
1725
  surface: o.surface ?? "",
1449
1726
  }
@@ -1451,42 +1728,48 @@ function resolveUnlit(o: UnlitSourceOptions): UnlitSource {
1451
1728
 
1452
1729
  /**
1453
1730
  * The vertex stage `unlit` pairs with an option set: UNLIT_VERTEX, or its
1731
+ * colored form (aColor forwarded as vColor) when `vertexColors`, its
1454
1732
  * skinned form (the skin matrix applied before uModel) when `skinned`,
1455
1733
  * its instanced form (the instance matrix under uModel, iColor forwarded
1456
- * as vColor under `instanceColors`) when `instanced`.
1734
+ * as vColor under `instanceColors`) when `instanced`. vColor carries the
1735
+ * vertex color, the instance color, or their product, as in lit.
1457
1736
  */
1458
1737
  export function unlitVertex(o: UnlitSourceOptions = {}): string {
1459
1738
  let c = resolveUnlit(o)
1460
- if (!c.skinned && !c.instanced) return UNLIT_VERTEX
1739
+ if (!c.vertexColors && !c.skinned && !c.instanced && !c.morph) return UNLIT_VERTEX
1740
+ let color = c.vertexColors && c.instanceColors ? "aColor * iColor" : c.vertexColors ? "aColor" : c.instanceColors ? "iColor" : ""
1461
1741
  return glsl`
1462
1742
  in vec3 aPos;
1463
1743
  in vec2 aUV;
1744
+ ${c.vertexColors ? "in vec4 aColor;" : ""}
1745
+ ${c.morph ? MORPH_DECLS : ""}
1464
1746
  ${c.skinned ? SKIN_DECLS : ""}
1465
1747
  ${c.instanced ? INSTANCE_MATRIX : ""}
1466
1748
  ${c.instanceColors ? "in vec4 iColor;" : ""}
1467
1749
  out vec2 vUv;
1468
1750
  out vec3 vWorldPos;
1469
- ${c.instanceColors ? "out vec4 vColor;" : ""}
1751
+ ${color ? "out vec4 vColor;" : ""}
1470
1752
  uniform mat4 uModel;
1471
1753
  uniform mat4 uViewProj;
1472
1754
 
1473
1755
  void main() {
1756
+ ${c.morph ? MORPH_APPLY_POSITION : ""}
1474
1757
  ${c.skinned ? SKIN_MATRIX : ""}
1475
- vec4 world = ${instancedPosition(c.skinned, c.instanced)};
1758
+ vec4 world = ${instancedPosition(c.skinned, c.instanced, c.morph)};
1476
1759
  vWorldPos = world.xyz;
1477
1760
  gl_Position = uViewProj * world;
1478
1761
  vUv = aUV;
1479
- ${c.instanceColors ? "vColor = iColor;" : ""}
1762
+ ${color ? `vColor = ${color};` : ""}
1480
1763
  }
1481
1764
  `
1482
1765
  }
1483
1766
 
1484
1767
  /** The unlit base sample: uColor times the (possibly transformed) map,
1485
- * times the instance color when the material carries one. */
1768
+ * times the vertex and instance colors when the material carries them. */
1486
1769
  function unlitBase(c: UnlitSource): string {
1487
1770
  let uv = c.mapTransform ? "vUv * uMapTransform.xy + uMapTransform.zw" : "vUv"
1488
1771
  let base = c.map ? `vec4 base = texture(uMap, ${uv}) * uColor;` : "vec4 base = uColor;"
1489
- return c.instanceColors ? base + "\n base *= vColor;" : base
1772
+ return c.vertexColors || c.instanceColors ? base + "\n base *= vColor;" : base
1490
1773
  }
1491
1774
 
1492
1775
  /**
@@ -1509,15 +1792,17 @@ export function unlitFragment(o: UnlitSourceOptions = {}): string {
1509
1792
  return glsl`
1510
1793
  ${c.map ? "in vec2 vUv;" : ""}
1511
1794
  in vec3 vWorldPos;
1512
- ${c.instanceColors ? "in vec4 vColor;" : ""}
1795
+ ${c.vertexColors || c.instanceColors ? "in vec4 vColor;" : ""}
1513
1796
  ${c.map ? "uniform sampler2D uMap;" : ""}
1514
1797
  uniform vec4 uColor;
1515
1798
  ${c.alphaTest ? "uniform float uAlphaTest;" : ""}
1516
1799
  ${c.mapTransform ? "uniform vec4 uMapTransform;" : ""}
1517
1800
  ${sceneSource({ lights: false, fog: c.fog })}
1801
+ ${LOD_FADE}
1518
1802
  ${c.prelude}
1519
1803
  ${c.surface}
1520
1804
  void main() {
1805
+ lodFade();
1521
1806
  ${unlitBase(c)}
1522
1807
  ${c.alphaTest ? "if (base.a < uAlphaTest) discard;" : ""}
1523
1808
  ${c.surface ? "Surface s = surfaceOf(base, vec3(0.0));\n surface(s);\n base = s.base;" : ""}
@@ -1537,7 +1822,7 @@ export function unlitShadowFragment(o: UnlitSourceOptions = {}): string | undefi
1537
1822
  if (!c.alphaTest && !c.surface) return undefined
1538
1823
  return glsl`
1539
1824
  ${c.map ? "in vec2 vUv;" : ""}
1540
- ${c.instanceColors ? "in vec4 vColor;" : ""}
1825
+ ${c.vertexColors || c.instanceColors ? "in vec4 vColor;" : ""}
1541
1826
  uniform vec4 uColor;
1542
1827
  ${c.map ? "uniform sampler2D uMap;" : ""}
1543
1828
  ${c.alphaTest ? "uniform float uAlphaTest;" : ""}