@solidrt/3d 0.0.58 → 0.0.60

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/material.ts CHANGED
@@ -33,7 +33,7 @@ import type {
33
33
  BlendMode,
34
34
  CullMode,
35
35
  ProgramId,
36
- InstanceAttribute,
36
+ VertexBufferLayout,
37
37
  RenderPipelineId,
38
38
  ShaderParams,
39
39
  TextureBindings,
@@ -41,14 +41,15 @@ import type {
41
41
  Topology,
42
42
  VertexAttribute,
43
43
  } from "@solidrt/core/gpu"
44
- import { FORMAT_FLOATS, layoutAttributes, layoutKey, layoutSlot } from "./geometry.ts"
44
+ import { formatFeeds, geometryKey, geometryLayouts, geometrySlot, VERTEX_FORMATS } from "./geometry.ts"
45
+ import type { Geometry } from "./geometry.ts"
45
46
  import type { VertexLayout } from "./geometry.ts"
46
- import { linearColor } from "./color.ts"
47
+ import { linearColor, premultipliedColor } from "./color.ts"
47
48
  import {
48
49
  INSTANCE_COLOR_ATTRIBUTES,
49
50
  INSTANCE_MATRIX,
50
51
  INSTANCE_MATRIX_ATTRIBUTES,
51
- litFragment,
52
+ phongFragment,
52
53
  litShadowFragment,
53
54
  litVertex,
54
55
  SKIN_DECLS,
@@ -59,14 +60,16 @@ import {
59
60
  unlitFragment,
60
61
  unlitShadowFragment,
61
62
  unlitVertex,
62
- OUTPUT,
63
+ resolveFragment,
64
+ MORPH_APPLY_POSITION,
65
+ MORPH_DECLS,
63
66
  } from "./glsl.ts"
64
67
 
65
68
  export type Material = {
66
69
  /** The pipeline this material draws with for geometry of `layout` and
67
70
  * `topology` (lazily created, one per pair met): the geometry says what
68
71
  * its indices are, the material draws them that way. */
69
- pipeline(layout: VertexLayout | undefined, topology: Topology): RenderPipelineId
72
+ pipeline(geometry: Geometry, topology: Topology): RenderPipelineId
70
73
  /** Per-entry uniform values this material contributes at addDraw. */
71
74
  params: ShaderParams
72
75
  /** Per-entry sampler bindings, when the material samples textures. */
@@ -82,20 +85,23 @@ export type Material = {
82
85
  * channels in the geometry are fine (inactive attributes keep the
83
86
  * stride). */
84
87
  attributes(): VertexAttribute[]
85
- /** True when the pipeline blends over (blend "alpha", depthWrite off):
88
+ /** True when the pipeline blends over (its `blend`, "alpha" by default,
89
+ * with depthWrite off):
86
90
  * the scene draws this material's meshes after every opaque one, sorted
87
91
  * back-to-front by mesh origin, and re-sorts them when the camera moves. */
88
92
  transparent?: boolean
89
- /** Per-instance attributes, when the material's pipeline declares them
90
- * (shaderMaterialClass's `instanceAttributes`; the stock materials'
91
- * `instanced`). Such a material draws populated meshes only - an
92
- * instanced mesh binds slot 0 to the core-written matrix records and
93
- * slot 1 to its app-written style records, a record mesh binds slot 0
94
- * to its records - and createMesh meshes are rejected at add(). */
95
- instanceAttributes?: InstanceAttribute[]
96
- /** The style record (the slot-1 attributes' floats, in order) every
97
- * fresh instance of an instanced mesh starts with; absent = zeros.
98
- * White for the stock materials' `instanceColors`. */
93
+ /** The instance buffers, when the material's pipeline declares them
94
+ * (shaderMaterialClass's `instanceBuffers`; the stock materials'
95
+ * `instanced`), one layout per buffer after the geometry's. Such a
96
+ * material draws populated meshes only - an instanced mesh binds the
97
+ * first to the core-written matrix records and the second to its
98
+ * app-written style records, a record mesh binds the first to its
99
+ * records - and createMesh meshes are rejected at add(). */
100
+ instanceBuffers?: VertexBufferLayout[]
101
+ /** The style record (one value per component of the second instance
102
+ * buffer's attributes, in order) every fresh instance of an instanced
103
+ * mesh starts with; absent = zeros. White for the stock materials'
104
+ * `instanceColors`. */
99
105
  instanceStyle?: ArrayLike<number>
100
106
  /** What a shadow view draws this material's meshes with instead of its
101
107
  * default depth override: the depth pass culling the side this material
@@ -115,23 +121,28 @@ export type Material = {
115
121
  * so a shadow or override entry for a skinned mesh merges the mesh's
116
122
  * own uBones binding even though this material is not the mesh's. */
117
123
  skinned?: boolean
124
+ /** True when the vertex stage declares `uMorphs` (read from the source,
125
+ * like skinned): the program morphs by the geometry's packed targets
126
+ * and the mesh's weights row, so a shadow or override entry for a
127
+ * morphed mesh merges the mesh's morph bindings and params even though
128
+ * this material is not the mesh's. */
129
+ morphed?: boolean
130
+ /** True when the fragment stage declares `uLodFade` (read from the
131
+ * source, like normalMatrix): the scene then writes the LOD cross-fade
132
+ * band to entries drawn with this material (every stock material; a
133
+ * custom class composes LOD_FADE from `@solidrt/3d/glsl`). Without it
134
+ * an entry under a fading LOD level switches hard at the band's
135
+ * midpoint. */
136
+ lodFade?: boolean
118
137
  /** Present on materials that own their pipeline (shaderMaterial). */
119
138
  dispose?(): void
120
139
  }
121
140
 
122
141
  // One shaderMaterialClass per unlit option combination (map x transparent
123
- // x cull x alphaTest), cached for the app's lifetime like lit's; one
142
+ // x cull x alphaTest), cached for the app's lifetime like phong's; one
124
143
  // pipeline per vertex layout inside each.
125
144
  let unlitClasses = new Map<string, ShaderMaterialClass>()
126
145
 
127
- // The uColor a color option compiles to: sRGB decoded to linear light,
128
- // then premultiplied by its alpha (the target's contract).
129
- function colorParam(color: readonly number[]): number[] {
130
- let a = color.length === 4 ? color[3]! : 1
131
- let c = linearColor(color)
132
- return [c[0]! * a, c[1]! * a, c[2]! * a, a]
133
- }
134
-
135
146
  export type UnlitOptions = {
136
147
  /** Straight [r, g, b] or [r, g, b, a], 0..1, sRGB - a color picker's
137
148
  * values. The library decodes it to linear light for shading and the
@@ -144,16 +155,28 @@ export type UnlitOptions = {
144
155
  * linear light like `color`; a plain rgba8 map reads as linear data and
145
156
  * shows washed out. */
146
157
  map?: TextureId
158
+ /** Multiply the base by the geometry's per-vertex aColor - the "colored"
159
+ * layout: withColors geometry, a gridHelper or axesHelper (add() throws
160
+ * without the channel). Three's vertexColors, Godot's
161
+ * vertex_color_use_as_albedo. */
162
+ vertexColors?: boolean
147
163
  /** Blend over what is behind (color alpha and map alpha both count).
148
- * Without it an alpha below 1 still draws opaque. See Material.transparent. */
164
+ * Without it an alpha below 1 still draws opaque. See Material.transparent.
165
+ * Implied by any `blend` but "none". */
149
166
  transparent?: boolean
167
+ /** The blend factors of a transparent draw, "alpha" when absent
168
+ * (Three's blending, Godot's blend_mode): "add" for glows and additive
169
+ * particles, "multiply" to darken. Any mode but "none" makes the
170
+ * material transparent - drawn after the opaques, depthWrite off -
171
+ * unless `transparent: false` says otherwise, the shaderMaterial rule. */
172
+ blend?: BlendMode
150
173
  /** Which faces to drop; default "back". "none" draws both sides of
151
174
  * single-layer geometry (foliage cards, glass, a mirrored part), and
152
175
  * lit materials then light a back face with its normal flipped, as
153
176
  * Three's DoubleSide and Godot's CULL_DISABLED do. */
154
177
  cull?: CullMode
155
- /** Cutout: drop a fragment whose final alpha (color x map, and for lit
156
- * the vertex color too) is below this, 0..1 (Three's alphaTest, glTF
178
+ /** Cutout: drop a fragment whose final alpha (color x map, and the
179
+ * vertex color under vertexColors) is below this, 0..1 (Three's alphaTest, glTF
157
180
  * alphaMode MASK with its alphaCutoff). Opaque otherwise:
158
181
  * depth-written, not sorted, unlike `transparent`. Foliage cards and
159
182
  * fences want it with `cull: "none"`; a mapped cutout casts its cutout
@@ -183,6 +206,15 @@ export type UnlitOptions = {
183
206
  * the spatial flush writes it from the joints). The shadow variants
184
207
  * (depth and cutout) skin the same way, so a caster casts its pose. */
185
208
  skinned?: boolean
209
+ /** Displace positions (and lit normals) by the geometry's morph targets
210
+ * (`geometry.morphs` - a glTF's blend shapes, or withMorphTargets)
211
+ * weighted by the mesh's morph owner (setMorphWeights, a clip's weights
212
+ * track): the vertex stage walks each vertex's packed entries before
213
+ * any skinning. The material then requires geometry carrying targets
214
+ * (a mesh without is rejected at add()); the shadow variants morph the
215
+ * same way, so a caster casts its shape. Picking and the transparent
216
+ * sort see the base shape, like skinning. */
217
+ morph?: boolean
186
218
  /** Draw instanced meshes (createInstancedMesh / `<InstancedMesh>`):
187
219
  * the vertex stage places each copy by its instance matrix under the
188
220
  * mesh's uModel, and the material declares INSTANCE_MATRIX_ATTRIBUTES,
@@ -200,11 +232,20 @@ export type UnlitOptions = {
200
232
  instanceColors?: boolean
201
233
  }
202
234
 
203
- // The instance attributes a stock instanced material declares: the
204
- // matrix always, the color record beside it under instanceColors.
205
- function stockInstanceAttributes(instanced: boolean, instanceColors: boolean): InstanceAttribute[] | undefined {
235
+ // The fog an unlit program composes: none when opted out, the additive
236
+ // form (fading toward black, see FOG) for an additive blend, else the
237
+ // scene's fog.
238
+ function fogForm(fog: boolean | undefined, blend: BlendMode | undefined): boolean | "additive" {
239
+ if (fog === false) return false
240
+ return blend === "add" ? "additive" : true
241
+ }
242
+
243
+ // The instance buffers a stock instanced material declares: the matrix
244
+ // record always, the color record beside it under instanceColors.
245
+ function stockInstanceBuffers(instanced: boolean, instanceColors: boolean): VertexBufferLayout[] | undefined {
206
246
  if (!instanced) return undefined
207
- return instanceColors ? [...INSTANCE_MATRIX_ATTRIBUTES, ...INSTANCE_COLOR_ATTRIBUTES] : INSTANCE_MATRIX_ATTRIBUTES
247
+ let matrix = { attributes: INSTANCE_MATRIX_ATTRIBUTES }
248
+ return instanceColors ? [matrix, { attributes: INSTANCE_COLOR_ATTRIBUTES }] : [matrix]
208
249
  }
209
250
  // A fresh instance's color: white, the multiplicative identity, so an
210
251
  // instance never written shows the material's own color (Godot's default;
@@ -220,32 +261,36 @@ function mapTransformParam(t: { offset?: [number, number]; repeat?: [number, num
220
261
  }
221
262
 
222
263
  /**
223
- * An unlit material: flat color, textured when `map` is given. Unlit is
224
- * the complete v1 set - lit materials arrive with uniform arrays (the
225
- * light list); see the scene-graph research note.
264
+ * An unlit material: flat color, textured when `map` is given; lines,
265
+ * the debug helpers and additive glows (`blend: "add"`) all draw with
266
+ * it.
226
267
  */
227
268
  export function unlit(opts: UnlitOptions = {}): Material {
228
- let uColor = colorParam(opts.color ?? [1, 1, 1])
269
+ let uColor = premultipliedColor(opts.color ?? [1, 1, 1])
229
270
  let map = opts.map !== undefined
230
- let transparent = opts.transparent === true
271
+ let blend = opts.blend
272
+ let transparent = opts.transparent ?? (blend !== undefined && blend !== "none")
231
273
  let cull = opts.cull ?? "back"
232
274
  let alphaTest = opts.alphaTest !== undefined
233
- let fog = opts.fog !== false
275
+ let fog = fogForm(opts.fog, blend)
234
276
  let mapTransform = opts.mapTransform !== undefined
235
277
  let skinned = opts.skinned === true
278
+ let morph = opts.morph === true
236
279
  let instanceColors = opts.instanceColors === true
237
280
  let instanced = opts.instanced === true || instanceColors
281
+ let vertexColors = opts.vertexColors === true
238
282
  if (mapTransform && !map) throw new Error("unlit: mapTransform without a map to transform")
239
- let key = [map, transparent, cull, alphaTest, fog, mapTransform, skinned, instanced, instanceColors].join("|")
283
+ let key = [map, vertexColors, transparent, blend, cull, alphaTest, fog, mapTransform, skinned, instanced, instanceColors, morph].join("|")
240
284
  let cls = unlitClasses.get(key)
241
285
  if (cls === undefined) {
242
286
  cls = shaderMaterialClass({
243
- vertex: unlitVertex({ skinned, instanced, instanceColors }),
244
- fragment: unlitFragment({ map, alphaTest, transparent, fog, mapTransform, instanceColors }),
245
- shadowVertex: instanced ? shadowDepthVertex(skinned, true) : undefined,
246
- instanceAttributes: stockInstanceAttributes(instanced, instanceColors),
287
+ vertex: unlitVertex({ vertexColors, skinned, instanced, instanceColors, morph }),
288
+ fragment: unlitFragment({ map, vertexColors, alphaTest, transparent, fog, mapTransform, instanceColors }),
289
+ shadowVertex: instanced ? shadowDepthVertex(skinned, true, morph) : undefined,
290
+ instanceBuffers: stockInstanceBuffers(instanced, instanceColors),
247
291
  instanceStyle: instanceColors ? INSTANCE_COLOR_DEFAULT : undefined,
248
292
  transparent,
293
+ blend,
249
294
  cull,
250
295
  label: "scene-unlit-" + key,
251
296
  })
@@ -259,19 +304,15 @@ export function unlit(opts: UnlitOptions = {}): Material {
259
304
  textures: map ? { uMap: opts.map! } : undefined,
260
305
  shadow:
261
306
  alphaTest && map
262
- ? unlitShadowMaterial(shadowCull(cull), skinned, instanced, instanceColors, uColor, opts.alphaTest!, opts.map!, mapTransform ? params.uMapTransform as number[] : undefined)
307
+ ? unlitShadowMaterial(shadowCull(cull), vertexColors, skinned, instanced, instanceColors, morph, uColor, opts.alphaTest!, opts.map!, mapTransform ? params.uMapTransform as number[] : undefined)
263
308
  : undefined,
264
309
  })
265
310
  }
266
311
 
312
+ /** The options every lit material takes, `phong` and `standard` alike:
313
+ * unlit's plus the surface maps, emission, baked light and shadow
314
+ * receipt the light loop reads. */
267
315
  export type LitOptions = UnlitOptions & {
268
- /** Multiply the base by the geometry's per-vertex aColor (withColors
269
- * geometry; add() throws without it). */
270
- vertexColors?: boolean
271
- /** Blinn-Phong highlight strength, 0..1 (default 0: pure diffuse). */
272
- specular?: number
273
- /** Highlight tightness, wide sheen (~8) to mirror dot (~150); default 30. */
274
- shininess?: number
275
316
  /** Sample `map` by WORLD position instead of UV - the value is the
276
317
  * texture repeats per world unit - blended across the three axis planes
277
318
  * by the normal. Tiles generated geometry at one density regardless of
@@ -306,21 +347,6 @@ export type LitOptions = UnlitOptions & {
306
347
  * nitro glow baked into one map. Sampled at the same uv as `map`;
307
348
  * create it with `format: "rgba8-srgb"` like `map`. */
308
349
  emissiveMap?: TextureId
309
- /** A texture whose RED channel scales `specular` per fragment (Three's
310
- * specularMap) - chrome and rubber on one mesh. With it, `specular`
311
- * defaults to 1 (the map is the strength). */
312
- specularMap?: TextureId
313
- /** Mirror the scene's environment (scene.setEnvironment), 0..1: the
314
- * face-on reflection weight, rising to 1 at grazing angles (Schlick) -
315
- * 1 is chrome, ~0.05 a glossy dielectric with rim reflections. The
316
- * reflection blurs with `shininess` (a wide sheen reflects a blurred
317
- * sky, a mirror dot a sharp one; the environment cube needs mipmaps
318
- * for the blur) and `specularMap`'s red scales it like `specular`.
319
- * Three's Phong `reflectivity` with MixOperation and a fresnel weight
320
- * (Three's default MultiplyOperation tints instead; not offered). Off
321
- * while the scene has no environment. Undefined declares no
322
- * environment sampler at all. */
323
- reflectivity?: number
324
350
  /** A baked-light texture (an offline render, an AO+GI bake), sampled by
325
351
  * the geometry's aUV2 channel and ADDED to the light sum like the
326
352
  * hemisphere term - a fully baked scene runs with no lights at all
@@ -345,10 +371,34 @@ export type LitOptions = UnlitOptions & {
345
371
  receiveShadow?: boolean
346
372
  }
347
373
 
348
- // The lit program is built by litFragment in ./glsl - the same builder an
349
- // app calls to get a lit material with its own GLSL in it, composed from
374
+ /** `phong`'s options: the lit set plus the Blinn-Phong highlight and the
375
+ * environment mirror. */
376
+ export type PhongOptions = LitOptions & {
377
+ /** Blinn-Phong highlight strength, 0..1 (default 0: pure diffuse). */
378
+ specular?: number
379
+ /** Highlight tightness, wide sheen (~8) to mirror dot (~150); default 30. */
380
+ shininess?: number
381
+ /** A texture whose RED channel scales `specular` per fragment (Three's
382
+ * specularMap) - chrome and rubber on one mesh. With it, `specular`
383
+ * defaults to 1 (the map is the strength). */
384
+ specularMap?: TextureId
385
+ /** Mirror the scene's environment (scene.setEnvironment), 0..1: the
386
+ * face-on reflection weight, rising to 1 at grazing angles (Schlick) -
387
+ * 1 is chrome, ~0.05 a glossy dielectric with rim reflections. The
388
+ * reflection blurs with `shininess` (a wide sheen reflects a blurred
389
+ * sky, a mirror dot a sharp one; the environment cube needs mipmaps
390
+ * for the blur) and `specularMap`'s red scales it like `specular`.
391
+ * Three's Phong `reflectivity` with MixOperation and a fresnel weight
392
+ * (Three's default MultiplyOperation tints instead; not offered). Off
393
+ * while the scene has no environment. Undefined declares no
394
+ * environment sampler at all. */
395
+ reflectivity?: number
396
+ }
397
+
398
+ // The phong program is built by phongFragment in ./glsl - the same builder an
399
+ // app calls to get a Blinn-Phong material with its own GLSL in it, composed from
350
400
  // the same exported constants. What varies per flag: map x vertexColors x
351
- // triplanar x receiveShadow x transparent x cull (a class that shows back
401
+ // triplanar x receiveShadow x transparent x blend x cull (a class that shows back
352
402
  // faces lights them with the normal flipped, else a double-sided leaf's
353
403
  // back is black) x alphaTest (the cutoff itself is a per-entry uniform,
354
404
  // one class for every value) x fog (the scene's fog composed last, or
@@ -367,17 +417,18 @@ export type LitOptions = UnlitOptions & {
367
417
  // 0 means it does not cast; SHADOW_LOOKUP turns the index into the factor.
368
418
  // The option combination that picks a lit class: the class-cache key is
369
419
  // its values in this order, and the same object builds the program, so
370
- // the two cannot drift apart. `lit` fills no slot - an app that does
371
- // reaches for litFragment directly, and owns the class it builds.
420
+ // the two cannot drift apart. `phong` fills no slot - an app that does
421
+ // reaches for phongFragment directly, and owns the class it builds.
372
422
  type LitClass = {
373
423
  map: boolean
374
424
  vertexColors: boolean
375
425
  triplanar: boolean
376
426
  transparent: boolean
427
+ blend: BlendMode | undefined
377
428
  receiveShadow: boolean
378
429
  cull: CullMode
379
430
  alphaTest: boolean
380
- fog: boolean
431
+ fog: boolean | "additive"
381
432
  normalMap: boolean
382
433
  emissive: boolean
383
434
  emissiveMap: boolean
@@ -388,25 +439,27 @@ type LitClass = {
388
439
  skinned: boolean
389
440
  instanced: boolean
390
441
  instanceColors: boolean
442
+ morph: boolean
391
443
  }
392
444
 
393
445
  function litClassKey(c: LitClass): string {
394
446
  return Object.values(c).join("|")
395
447
  }
396
- let litClasses = new Map<string, ShaderMaterialClass>()
448
+ let phongClasses = new Map<string, ShaderMaterialClass>()
397
449
 
398
450
  /**
399
- * A lit material: hemisphere ambient plus the scene's directional lights
400
- * (DirectionalLight nodes), Lambert diffuse, optional
401
- * Blinn-Phong highlight. Same options as unlit (color, map, transparent,
402
- * cull, alphaTest) plus vertexColors, specular/shininess and triplanar mapping. One program
403
- * per option combination, one pipeline per vertex layout met, shared by
404
- * every instance - a thousand lit meshes still share one pipeline. No
405
- * lights set means black except for the hemisphere term, which also
406
- * starts at zero: set at least one of the two.
451
+ * The Blinn-Phong material (Three's MeshPhongMaterial, Unity URP's Simple
452
+ * Lit; Godot has no Phong): hemisphere ambient plus the scene's lights,
453
+ * Lambert diffuse, optional Blinn-Phong highlight. Same options as unlit
454
+ * (color, map, transparent, cull, alphaTest) plus vertexColors,
455
+ * specular/shininess and triplanar mapping. One program per option
456
+ * combination, one pipeline per vertex layout met, shared by every
457
+ * instance - a thousand phong meshes still share one pipeline. No lights
458
+ * set means black except for the hemisphere term, which also starts at
459
+ * zero: set at least one of the two.
407
460
  */
408
- export function lit(opts: LitOptions = {}): Material {
409
- let uColor = colorParam(opts.color ?? [1, 1, 1])
461
+ export function phong(opts: PhongOptions = {}): Material {
462
+ let uColor = premultipliedColor(opts.color ?? [1, 1, 1])
410
463
  let map = opts.map !== undefined
411
464
  let triplanar = map && opts.triplanar !== undefined
412
465
  let alphaTest = opts.alphaTest !== undefined
@@ -419,22 +472,23 @@ export function lit(opts: LitOptions = {}): Material {
419
472
  let lightMap = opts.lightMap !== undefined
420
473
  let mapTransform = opts.mapTransform !== undefined
421
474
  if (env && !(Number.isFinite(opts.reflectivity) && opts.reflectivity! >= 0 && opts.reflectivity! <= 1)) {
422
- throw new Error("lit: reflectivity must be a number in 0..1, got " + opts.reflectivity)
475
+ throw new Error("phong: reflectivity must be a number in 0..1, got " + opts.reflectivity)
423
476
  }
424
- if (triplanar && normalMap) throw new Error("lit: normalMap cannot combine with triplanar (normal maps sample by uv)")
425
- if (triplanar && mapTransform) throw new Error("lit: mapTransform cannot combine with triplanar (its repeat is the triplanar value)")
477
+ if (triplanar && normalMap) throw new Error("phong: normalMap cannot combine with triplanar (normal maps sample by uv)")
478
+ if (triplanar && mapTransform) throw new Error("phong: mapTransform cannot combine with triplanar (its repeat is the triplanar value)")
426
479
  if (mapTransform && !map && !normalMap && !emissiveMap && !specularMap) {
427
- throw new Error("lit: mapTransform without a map to transform")
480
+ throw new Error("phong: mapTransform without a map to transform")
428
481
  }
429
482
  let flags: LitClass = {
430
483
  map,
431
484
  vertexColors: opts.vertexColors === true,
432
485
  triplanar,
433
- transparent: opts.transparent === true,
486
+ transparent: opts.transparent ?? (opts.blend !== undefined && opts.blend !== "none"),
487
+ blend: opts.blend,
434
488
  receiveShadow: opts.receiveShadow !== false,
435
489
  cull,
436
490
  alphaTest,
437
- fog: opts.fog !== false,
491
+ fog: fogForm(opts.fog, opts.blend),
438
492
  normalMap,
439
493
  emissive,
440
494
  emissiveMap,
@@ -445,21 +499,23 @@ export function lit(opts: LitOptions = {}): Material {
445
499
  skinned: opts.skinned === true,
446
500
  instanced: opts.instanced === true || opts.instanceColors === true,
447
501
  instanceColors: opts.instanceColors === true,
502
+ morph: opts.morph === true,
448
503
  }
449
504
  let key = litClassKey(flags)
450
- let cls = litClasses.get(key)
505
+ let cls = phongClasses.get(key)
451
506
  if (cls === undefined) {
452
507
  cls = shaderMaterialClass({
453
508
  vertex: litVertex(flags),
454
- fragment: litFragment(flags),
455
- shadowVertex: flags.instanced ? shadowDepthVertex(flags.skinned, true) : undefined,
456
- instanceAttributes: stockInstanceAttributes(flags.instanced, flags.instanceColors),
509
+ fragment: phongFragment(flags),
510
+ shadowVertex: flags.instanced ? shadowDepthVertex(flags.skinned, true, flags.morph) : undefined,
511
+ instanceBuffers: stockInstanceBuffers(flags.instanced, flags.instanceColors),
457
512
  instanceStyle: flags.instanceColors ? INSTANCE_COLOR_DEFAULT : undefined,
458
513
  transparent: flags.transparent,
514
+ blend: flags.blend,
459
515
  cull,
460
- label: "scene-lit-" + key,
516
+ label: "scene-phong-" + key,
461
517
  })
462
- litClasses.set(key, cls)
518
+ phongClasses.set(key, cls)
463
519
  }
464
520
  let params: ShaderParams = {
465
521
  uColor,
@@ -500,7 +556,8 @@ export function lit(opts: LitOptions = {}): Material {
500
556
  return material
501
557
  }
502
558
 
503
- export type StandardOptions = Omit<LitOptions, "specular" | "shininess" | "specularMap" | "reflectivity"> & {
559
+ /** `standard`'s options: the lit set plus the metalness/roughness model. */
560
+ export type StandardOptions = LitOptions & {
504
561
  /** How metallic the surface is, 0..1 (default 0; 1 when `metalnessMap`
505
562
  * is given - the map is the value): a metal has no diffuse and
506
563
  * reflects tinted by `color`, a dielectric reflects 4% face-on.
@@ -519,10 +576,10 @@ export type StandardOptions = Omit<LitOptions, "specular" | "shininess" | "specu
519
576
  roughnessMap?: TextureId
520
577
  }
521
578
 
522
- // The `standard` class key: lit's minus the Blinn-Phong slots (env is
579
+ // The `standard` class key: phong's minus the Blinn-Phong slots (env is
523
580
  // always on, the environment being intrinsic to the model) plus the two
524
- // packed maps. Its cutout shadow is lit's own, keyed on the shared subset
525
- // in lit's order, so the two materials share shadow programs.
581
+ // packed maps. Its cutout shadow is the shared lit one, keyed on the
582
+ // subset in phong's order, so the two materials share shadow programs.
526
583
  type StandardClass = Omit<LitClass, "specularMap" | "env"> & { metalnessMap: boolean; roughnessMap: boolean }
527
584
 
528
585
  function standardClassKey(c: StandardClass): string {
@@ -536,6 +593,7 @@ function standardShadowFlags(c: StandardClass): LitClass {
536
593
  vertexColors: c.vertexColors,
537
594
  triplanar: c.triplanar,
538
595
  transparent: c.transparent,
596
+ blend: c.blend,
539
597
  receiveShadow: c.receiveShadow,
540
598
  cull: c.cull,
541
599
  alphaTest: c.alphaTest,
@@ -550,6 +608,7 @@ function standardShadowFlags(c: StandardClass): LitClass {
550
608
  skinned: c.skinned,
551
609
  instanced: c.instanced,
552
610
  instanceColors: c.instanceColors,
611
+ morph: c.morph,
553
612
  }
554
613
  }
555
614
 
@@ -557,17 +616,17 @@ function standardShadowFlags(c: StandardClass): LitClass {
557
616
  * The metalness/roughness material, the look authored assets expect
558
617
  * (Three's MeshStandardMaterial, Godot's StandardMaterial3D, Unity's
559
618
  * Standard): the same base, maps, cutout, shadows, emissive, fog and
560
- * lights as `lit`, shaded with GGX specular (PBR in `@solidrt/3d/glsl`)
619
+ * lights as `phong`, shaded with GGX specular (PBR in `@solidrt/3d/glsl`)
561
620
  * and the scene's environment sampled ALWAYS as the split sum over its
562
621
  * mip chain - no `reflectivity` switch, the environment is intrinsic to
563
- * the model. Light intensities read as lit's (1 lights a white matte
622
+ * the model. Light intensities read as phong's (1 lights a white matte
564
623
  * surface to 1). Without an environment set a metal shows only its
565
624
  * highlights - its diffuse is zero and there is nothing to reflect, as
566
625
  * in Three and Godot - so give the scene one. One program per option
567
- * combination, shared by every instance, like lit.
626
+ * combination, shared by every instance, like phong.
568
627
  */
569
628
  export function standard(opts: StandardOptions = {}): Material {
570
- let uColor = colorParam(opts.color ?? [1, 1, 1])
629
+ let uColor = premultipliedColor(opts.color ?? [1, 1, 1])
571
630
  let map = opts.map !== undefined
572
631
  let triplanar = map && opts.triplanar !== undefined
573
632
  let alphaTest = opts.alphaTest !== undefined
@@ -599,11 +658,12 @@ export function standard(opts: StandardOptions = {}): Material {
599
658
  map,
600
659
  vertexColors: opts.vertexColors === true,
601
660
  triplanar,
602
- transparent: opts.transparent === true,
661
+ transparent: opts.transparent ?? (opts.blend !== undefined && opts.blend !== "none"),
662
+ blend: opts.blend,
603
663
  receiveShadow: opts.receiveShadow !== false,
604
664
  cull,
605
665
  alphaTest,
606
- fog: opts.fog !== false,
666
+ fog: fogForm(opts.fog, opts.blend),
607
667
  normalMap,
608
668
  emissive,
609
669
  emissiveMap,
@@ -612,6 +672,7 @@ export function standard(opts: StandardOptions = {}): Material {
612
672
  skinned: opts.skinned === true,
613
673
  instanced: opts.instanced === true || opts.instanceColors === true,
614
674
  instanceColors: opts.instanceColors === true,
675
+ morph: opts.morph === true,
615
676
  metalnessMap,
616
677
  roughnessMap,
617
678
  }
@@ -621,10 +682,11 @@ export function standard(opts: StandardOptions = {}): Material {
621
682
  cls = shaderMaterialClass({
622
683
  vertex: litVertex(flags),
623
684
  fragment: standardFragment(flags),
624
- shadowVertex: flags.instanced ? shadowDepthVertex(flags.skinned, true) : undefined,
625
- instanceAttributes: stockInstanceAttributes(flags.instanced, flags.instanceColors),
685
+ shadowVertex: flags.instanced ? shadowDepthVertex(flags.skinned, true, flags.morph) : undefined,
686
+ instanceBuffers: stockInstanceBuffers(flags.instanced, flags.instanceColors),
626
687
  instanceStyle: flags.instanceColors ? INSTANCE_COLOR_DEFAULT : undefined,
627
688
  transparent: flags.transparent,
689
+ blend: flags.blend,
628
690
  cull,
629
691
  label: "scene-standard-" + key,
630
692
  })
@@ -688,7 +750,7 @@ function litShadowMaterial(
688
750
  vertex: litVertex(flags),
689
751
  fragment,
690
752
  cull: shadowCull(flags.cull),
691
- instanceAttributes: stockInstanceAttributes(flags.instanced, flags.instanceColors),
753
+ instanceBuffers: stockInstanceBuffers(flags.instanced, flags.instanceColors),
692
754
  label: "scene-lit-shadow-" + key,
693
755
  })
694
756
  litShadowClasses.set(key, cls)
@@ -714,15 +776,18 @@ function litShadowMaterial(
714
776
  // Material.skinned); the instanced form places each copy by its instance
715
777
  // matrix, so a fleet casts where its records stand - the stock instanced
716
778
  // materials pass it as their class's shadowVertex.
717
- function shadowDepthVertex(skinned: boolean, instanced: boolean): string {
718
- let local = skinned ? "(skin * vec4(aPos, 1.0))" : "vec4(aPos, 1.0)"
779
+ function shadowDepthVertex(skinned: boolean, instanced: boolean, morphed = false): string {
780
+ let base = morphed ? "morphPos" : "aPos"
781
+ let local = skinned ? `(skin * vec4(${base}, 1.0))` : `vec4(${base}, 1.0)`
719
782
  return glsl`
720
783
  in vec3 aPos;
784
+ ${morphed ? MORPH_DECLS : ""}
721
785
  ${skinned ? SKIN_DECLS : ""}
722
786
  ${instanced ? INSTANCE_MATRIX : ""}
723
787
  uniform mat4 uModel;
724
788
  uniform mat4 uViewProj;
725
789
  void main() {
790
+ ${morphed ? MORPH_APPLY_POSITION : ""}
726
791
  ${skinned ? SKIN_MATRIX : ""}
727
792
  gl_Position = uViewProj * uModel * ${instanced ? `(instanceMatrix() * ${local})` : local};
728
793
  }
@@ -741,12 +806,12 @@ let shadowDepth = new Map<string, Material>()
741
806
  * per cull mode x skinned for the app, built on first use. The default,
742
807
  * "front", is the caster's back surface (see above); a material culling
743
808
  * or skinning otherwise carries its own variant as Material.shadow. */
744
- export function shadowDepthMaterial(cull: CullMode = "front", skinned = false): Material {
745
- let key = cull + (skinned ? "|skinned" : "")
809
+ export function shadowDepthMaterial(cull: CullMode = "front", skinned = false, morphed = false): Material {
810
+ let key = cull + (skinned ? "|skinned" : "") + (morphed ? "|morphed" : "")
746
811
  let material = shadowDepth.get(key)
747
812
  if (material === undefined) {
748
813
  material = shaderMaterialClass({
749
- vertex: shadowDepthVertex(skinned, false),
814
+ vertex: shadowDepthVertex(skinned, false, morphed),
750
815
  fragment: SHADOW_DEPTH_FRAGMENT,
751
816
  cull,
752
817
  label: "scene-shadow-depth-" + key,
@@ -762,12 +827,12 @@ function shadowCull(cull: CullMode): CullMode {
762
827
  return cull === "none" ? "none" : cull === "back" ? "front" : "back"
763
828
  }
764
829
 
765
- /** "back" unskinned maps to the default depth material, so its variant
766
- * is undefined; a skinned class always needs its own (the default does
767
- * not skin). */
768
- function shadowVariant(cull: CullMode, skinned: boolean): Material | undefined {
769
- if (cull === "back" && !skinned) return undefined
770
- return shadowDepthMaterial(shadowCull(cull), skinned)
830
+ /** "back" unskinned unmorphed maps to the default depth material, so its
831
+ * variant is undefined; a skinned or morphed class always needs its own
832
+ * (the default does neither). */
833
+ function shadowVariant(cull: CullMode, skinned: boolean, morphed: boolean): Material | undefined {
834
+ if (cull === "back" && !skinned && !morphed) return undefined
835
+ return shadowDepthMaterial(shadowCull(cull), skinned, morphed)
771
836
  }
772
837
 
773
838
  let unlitShadowClasses = new Map<string, ShaderMaterialClass>()
@@ -781,24 +846,26 @@ let unlitShadowClasses = new Map<string, ShaderMaterialClass>()
781
846
  * transform). */
782
847
  function unlitShadowMaterial(
783
848
  cull: CullMode,
849
+ vertexColors: boolean,
784
850
  skinned: boolean,
785
851
  instanced: boolean,
786
852
  instanceColors: boolean,
853
+ morph: boolean,
787
854
  uColor: number[],
788
855
  uAlphaTest: number,
789
856
  uMap: TextureId,
790
857
  uMapTransform?: number[],
791
858
  ): Material {
792
- let key = [cull, uMapTransform !== undefined, skinned, instanced, instanceColors].join("|")
859
+ let key = [cull, uMapTransform !== undefined, vertexColors, skinned, instanced, instanceColors, morph].join("|")
793
860
  let cls = unlitShadowClasses.get(key)
794
861
  if (cls === undefined) {
795
- let fragment = unlitShadowFragment({ map: true, alphaTest: true, mapTransform: uMapTransform !== undefined, instanceColors })
862
+ let fragment = unlitShadowFragment({ map: true, vertexColors, alphaTest: true, mapTransform: uMapTransform !== undefined, instanceColors })
796
863
  if (fragment === undefined) throw new Error("unlitShadowMaterial: the cutout options cannot discard")
797
864
  cls = shaderMaterialClass({
798
- vertex: unlitVertex({ skinned, instanced, instanceColors }),
865
+ vertex: unlitVertex({ vertexColors, skinned, instanced, instanceColors, morph }),
799
866
  fragment,
800
867
  cull,
801
- instanceAttributes: stockInstanceAttributes(instanced, instanceColors),
868
+ instanceBuffers: stockInstanceBuffers(instanced, instanceColors),
802
869
  label: "scene-unlit-shadow-" + key,
803
870
  })
804
871
  unlitShadowClasses.set(key, cls)
@@ -810,8 +877,9 @@ function unlitShadowMaterial(
810
877
 
811
878
  /** A sprite's options: unlit's, minus instancing (the billboard stage
812
879
  * places one quad per mesh; an instanced billboard field is a custom
813
- * class's vertex stage). */
814
- export type SpriteOptions = Omit<UnlitOptions, "instanced" | "instanceColors"> & {
880
+ * class's vertex stage) and minus vertexColors (the quad is the stage's
881
+ * own, with no color channel). */
882
+ export type SpriteOptions = Omit<UnlitOptions, "instanced" | "instanceColors" | "vertexColors"> & {
815
883
  /** Which way the quad turns to face the camera. `"full"` (default,
816
884
  * Three's Sprite): both axes follow the view, the quad is always flat
817
885
  * to the screen. `"fixed-y"` (Godot's BILLBOARD_FIXED_Y): only the yaw
@@ -884,22 +952,25 @@ let spriteClasses = new Map<string, ShaderMaterialClass>()
884
952
  */
885
953
  export function sprite(opts: SpriteOptions = {}): Material {
886
954
  // Excluded by the type and checked for a JS caller: the billboard stage
887
- // has no instance placement, so the flags would otherwise be ignored.
888
- if ((opts as UnlitOptions).instanced || (opts as UnlitOptions).instanceColors) {
889
- throw new Error("sprite: instanced and instanceColors are not sprite options (one billboard per mesh)")
955
+ // has no instance placement and no vertex channel, so the flags would
956
+ // otherwise be ignored.
957
+ if ((opts as UnlitOptions).instanced || (opts as UnlitOptions).instanceColors || (opts as UnlitOptions).vertexColors) {
958
+ throw new Error("sprite: instanced, instanceColors and vertexColors are not sprite options (one billboard quad per mesh)")
890
959
  }
891
- let uColor = colorParam(opts.color ?? [1, 1, 1])
960
+ let uColor = premultipliedColor(opts.color ?? [1, 1, 1])
892
961
  let map = opts.map !== undefined
962
+ let blend = opts.blend
893
963
  let transparent = opts.transparent !== false
894
964
  let fixedY = opts.billboard === "fixed-y"
895
- let fog = opts.fog !== false
896
- let key = [map, transparent, fixedY, fog].join("|")
965
+ let fog = fogForm(opts.fog, blend)
966
+ let key = [map, transparent, blend, fixedY, fog].join("|")
897
967
  let cls = spriteClasses.get(key)
898
968
  if (cls === undefined) {
899
969
  cls = shaderMaterialClass({
900
970
  vertex: fixedY ? SPRITE_FIXED_Y_VERTEX_SRC : SPRITE_VERTEX_SRC,
901
971
  fragment: unlitFragment({ map, transparent, fog }),
902
972
  transparent,
973
+ blend,
903
974
  cull: "none",
904
975
  label: "scene-sprite-" + key,
905
976
  })
@@ -910,11 +981,14 @@ export function sprite(opts: SpriteOptions = {}): Material {
910
981
 
911
982
  /** The attributes `material` reads that `layout` does not carry (name and
912
983
  * format) - empty when the pair is drawable. */
913
- export function missingAttributes(material: Material, layout: VertexLayout | undefined): VertexAttribute[] {
984
+ export function missingAttributes(material: Material, geometry: Geometry): VertexAttribute[] {
914
985
  let missing: VertexAttribute[] = []
915
986
  for (let attr of material.attributes()) {
916
- let slot = layoutSlot(layout, attr.name)
917
- if (slot === null || slot.format !== attr.format) missing.push(attr)
987
+ // A format matches a shader `in` by component count and kind: a
988
+ // packed color feeds `in vec4 aColor` like a float one, byte joints
989
+ // feed `in uvec4 aJoints` like 32-bit ones. Any stream may carry it.
990
+ let slot = geometrySlot(geometry, attr.name)
991
+ if (slot === null || !formatFeeds(slot.format, attr.format)) missing.push(attr)
918
992
  }
919
993
  return missing
920
994
  }
@@ -953,25 +1027,25 @@ const BACKGROUND_VERTEX = glsl`
953
1027
 
954
1028
  // Pipeline fragments get no vUV from the engine preamble (a pipeline's
955
1029
  // varyings are its own), so the background slot injects the full
956
- // shader-target fragment contract itself: vUV, fragColor, iResolution,
957
- // its own vRay, and the OUTPUT set, so a sky can end with outputColor
958
- // and take the scene's exposure and tone mapping.
1030
+ // shader-target fragment contract itself: vUV, fragColor, iResolution
1031
+ // and its own vRay. A sky writes linear light like every scene fragment;
1032
+ // the target's resolve exposes, tone maps and encodes it.
959
1033
  const BACKGROUND_FRAGMENT_PREAMBLE =
960
- "#version 300 es\nprecision highp float;\nin vec2 vUV;\nin vec3 vRay;\nout vec4 fragColor;\nuniform vec2 iResolution;\n" + OUTPUT + "\n"
1034
+ "#version 300 es\nprecision highp float;\nin vec2 vUV;\nin vec3 vRay;\nout vec4 fragColor;\nuniform vec2 iResolution;\n"
961
1035
 
962
1036
  // The skybox fragment behind setBackground({ cube }): the view ray through
963
1037
  // the sky's rotation (the INVERSE turn, written by skyboxParams, so the
964
1038
  // sky itself turns by +rotation like a node would), then the cube lookup
965
- // times the intensity, through the output
966
- // stage (the preamble declares it) like every lit pixel. Opaque: the
967
- // skybox replaces the clearColor exactly as a GLSL background does.
1039
+ // times the intensity, written as linear light like every lit pixel.
1040
+ // Opaque: the skybox replaces the clearColor exactly as a GLSL
1041
+ // background does.
968
1042
  export const SKYBOX_FRAGMENT = glsl`
969
1043
  uniform samplerCube uSky;
970
1044
  uniform float uSkyIntensity;
971
1045
  uniform mat4 uSkyRotation;
972
1046
  void main() {
973
1047
  vec3 dir = mat3(uSkyRotation) * normalize(vRay);
974
- fragColor = outputColor(texture(uSky, dir).rgb * uSkyIntensity, 1.0);
1048
+ fragColor = vec4(max(texture(uSky, dir).rgb * uSkyIntensity, vec3(0.0)), 1.0);
975
1049
  }
976
1050
  `
977
1051
 
@@ -992,6 +1066,35 @@ export function backgroundPipeline(fragment: string, label: string): { pipeline:
992
1066
  return { pipeline, program }
993
1067
  }
994
1068
 
1069
+ // The resolve pass (a scene's or view's display stage): the attributeless
1070
+ // covering triangle emitting the shader-target vUV, over a fragment that
1071
+ // samples the scene buffer once and ends in resolveColor.
1072
+ const RESOLVE_VERTEX = glsl`
1073
+ out vec2 vUV;
1074
+ void main() {
1075
+ vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
1076
+ vUV = p;
1077
+ gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);
1078
+ }
1079
+ `
1080
+
1081
+ // The resolve fragment's contract, injected like the background's: the
1082
+ // shader-target names, then resolveFragment's `uScene` and RESOLVE set.
1083
+ const RESOLVE_FRAGMENT_PREAMBLE = "#version 300 es\nprecision highp float;\nin vec2 vUV;\nout vec4 fragColor;\nuniform vec2 iResolution;\n"
1084
+
1085
+ /** A target's resolve pipeline (internal - reached via SceneOptions.resolve
1086
+ * and scene.setResolve): depth-free, attributeless, the one entry of the
1087
+ * resolve target. */
1088
+ export function resolvePipeline(fragment: string, label: string): { pipeline: RenderPipelineId; program: ProgramId } {
1089
+ let vs = compileShader("vertex", RESOLVE_VERTEX, { header: true })
1090
+ let fs = compileShader("fragment", needsHeader(fragment) ? RESOLVE_FRAGMENT_PREAMBLE + resolveFragment(fragment) : fragment, { header: false })
1091
+ let program = linkProgram(vs, fs, { label })
1092
+ destroyShader(vs)
1093
+ destroyShader(fs)
1094
+ let pipeline = createRenderPipeline(program, { label })
1095
+ return { pipeline, program }
1096
+ }
1097
+
995
1098
  /** The class half of a shader material: sources and pipeline state, the
996
1099
  * things one compiled program fixes. */
997
1100
  export type ShaderMaterialClassOptions = {
@@ -1021,25 +1124,28 @@ export type ShaderMaterialClassOptions = {
1021
1124
  vertex: string
1022
1125
  fragment: string
1023
1126
  /**
1024
- * Per-instance attributes: the vertex stage reads these as `in` variables
1025
- * beside the layout's own, and each drawn instance gets one record from
1026
- * the mesh's instance buffer (interleaved floats in this order). A class
1027
- * with instance attributes makes INSTANCED materials: attach their meshes
1028
- * with createInstancedMesh or createRecordMesh, which carry the records
1029
- * - a createMesh mesh is rejected at add(). Each attribute's `slot`
1030
- * (default 0) picks the buffer: an instanced mesh binds the core's
1031
- * matrix records (INSTANCE_MATRIX_ATTRIBUTES, exactly) to slot 0 and
1032
- * the app's style records - any layout, written per instance with
1033
- * setInstanceStyle - to slot 1; a record mesh is slot 0 only, its
1034
- * records whatever the stage reads (a position/yaw/scale record beats
1035
- * four vec4 columns for a JS-stepped fleet, and the composed uModel
1036
- * still places the whole population).
1127
+ * Instance buffers: one layout per per-instance buffer (`{ attributes
1128
+ * }`: any vertex format, tightly packed in list order like a geometry
1129
+ * layout; stepMode is "instance" by definition here). The vertex stage
1130
+ * reads the attributes as `in` variables beside the geometry layout's
1131
+ * own, and each drawn instance gets one record from each buffer. A
1132
+ * class with instance buffers makes INSTANCED materials: attach their
1133
+ * meshes with createInstancedMesh or createRecordMesh, which carry the
1134
+ * records as streams - a createMesh mesh is rejected at add(). An
1135
+ * instanced mesh binds the core's matrix records (the first buffer,
1136
+ * INSTANCE_MATRIX_ATTRIBUTES exactly) and the app's style records (the
1137
+ * second: any layout, written per instance with setInstanceStyle); a
1138
+ * record mesh's buffers are all the app's, its records whatever the
1139
+ * stage reads (a position/yaw/scale record beats four vec4 columns for
1140
+ * a JS-stepped fleet, and the composed uModel still places the whole
1141
+ * population). Every stream is written through instanceAttribute and
1142
+ * published with updateRecords.
1037
1143
  */
1038
- instanceAttributes?: InstanceAttribute[]
1039
- /** The style record a fresh instance starts with (the slot-1
1040
- * attributes' floats, in order); default zeros. The identity of
1041
- * whatever the stage does with the record - white for a multiplied
1042
- * tint. */
1144
+ instanceBuffers?: VertexBufferLayout[]
1145
+ /** The style record a fresh instance starts with (one value per
1146
+ * component of the second instance buffer's attributes, in order);
1147
+ * default zeros. The identity of whatever the stage does with the
1148
+ * record - white for a multiplied tint. */
1043
1149
  instanceStyle?: ArrayLike<number>
1044
1150
  /**
1045
1151
  * The class's vertex stage reduced to POSITION - the instance
@@ -1088,7 +1194,7 @@ export type ShaderMaterialInstanceOptions = {
1088
1194
  * `shadowVertex` cannot express. Default: the class's shadowVertex
1089
1195
  * depth material when one is declared, else the depth pass with this
1090
1196
  * class's cull side, skinned like the class when its vertex skins by
1091
- * uBones. */
1197
+ * uBones and morphed like it when its vertex morphs by uMorphs. */
1092
1198
  shadow?: Material
1093
1199
  }
1094
1200
 
@@ -1139,43 +1245,48 @@ export function shaderMaterialClass(opts: ShaderMaterialClassOptions): ShaderMat
1139
1245
  // Skinning is a vertex-stage affair like the attributes; a source that
1140
1246
  // mentions uBones skins by the mesh's palette texture (Material.skinned).
1141
1247
  let skinned = /\buBones\b/.test(opts.vertex)
1248
+ // Morphing likewise: a source that mentions uMorphs walks the
1249
+ // geometry's packed targets (Material.morphed).
1250
+ let morphed = /\buMorphs\b/.test(opts.vertex)
1251
+ // The LOD cross-fade is a fragment-stage discard (Material.lodFade).
1252
+ let lodFade = /\buLodFade\b/.test(opts.fragment)
1142
1253
  let transparent = opts.transparent ?? (opts.blend !== undefined && opts.blend !== "none")
1143
1254
  let depth = opts.depth ?? true
1144
1255
  let cull = opts.cull ?? "back"
1145
- // Formats are the vertex vocabulary (f32/vec2/vec3/vec4), not WebGPU's
1146
- // spelling, and an unknown one has no float count: it would leave here
1147
- // intact, make a NaN record stride at the mesh, and fail as an arithmetic
1148
- // complaint from createRecordMesh naming neither the attribute nor the
1149
- // format. Checked where the name is written instead.
1150
- for (let attr of opts.instanceAttributes ?? []) {
1151
- if (!(attr.format in FORMAT_FLOATS)) {
1152
- throw new Error(
1153
- "shaderMaterial unknown instance attribute format '" +
1154
- String(attr.format) +
1155
- "' for " +
1156
- attr.name +
1157
- " (expected " +
1158
- Object.keys(FORMAT_FLOATS).join(", ") +
1159
- ")",
1160
- )
1256
+ // An unknown format has no record stride: it would leave here intact,
1257
+ // make a NaN stride at the mesh, and fail as an arithmetic complaint
1258
+ // from createRecordMesh naming neither the attribute nor the format.
1259
+ // Checked where the name is written instead. Records are tightly
1260
+ // packed like geometry layouts; explicit strides and offsets are the
1261
+ // engine's, reachable through core's own pipeline API.
1262
+ for (let layout of opts.instanceBuffers ?? []) {
1263
+ if (layout.stepMode === "vertex") throw new Error("shaderMaterial instanceBuffers are instance-step by definition; drop stepMode")
1264
+ if (layout.arrayStride !== undefined || layout.attributes.some(a => a.offset !== undefined)) {
1265
+ throw new Error("shaderMaterial instanceBuffers are tightly packed records; drop arrayStride and offset")
1266
+ }
1267
+ for (let attr of layout.attributes) {
1268
+ if (!(attr.format in VERTEX_FORMATS)) {
1269
+ throw new Error("shaderMaterial unknown instance attribute format '" + String(attr.format) + "' for " + attr.name + " (expected " + Object.keys(VERTEX_FORMATS).join(", ") + ")")
1270
+ }
1161
1271
  }
1162
1272
  }
1163
- // An empty list declares nothing - same as absent (the engine requires an
1164
- // instance buffer exactly when attributes are declared).
1165
- let instanceAttributes = opts.instanceAttributes?.length ? opts.instanceAttributes.map(a => ({ ...a })) : undefined
1166
- // The shadowVertex depth pass binds the instance slots its stage reads
1167
- // from - the record slot always (a stage reading none of it cannot
1168
- // place the instances, an error here), a style slot only when the
1169
- // stage names one of its attributes - and declares each bound slot's
1170
- // whole layout, since a slot's stride is the record's (an unread
1171
- // attribute in a bound slot is inactive, like an extra geometry channel).
1172
- let shadowAttributes: InstanceAttribute[] | undefined
1173
- if (opts.shadowVertex !== undefined && instanceAttributes !== undefined) {
1273
+ // An empty list declares nothing - same as absent (the engine requires a
1274
+ // buffer exactly when a layout is declared).
1275
+ let instanceBuffers = opts.instanceBuffers?.length ? opts.instanceBuffers.map(b => ({ stepMode: "instance" as const, attributes: b.attributes.map(a => ({ ...a })) })) : undefined
1276
+ // The shadowVertex depth pass binds the instance buffers its stage reads
1277
+ // from - the record buffer always (a stage reading none of it cannot
1278
+ // place the instances, an error here), the style buffer only when the
1279
+ // stage names one of its attributes - and declares each bound buffer's
1280
+ // whole layout, since a buffer's stride is the record's (an unread
1281
+ // attribute in a bound buffer is inactive, like an extra geometry
1282
+ // channel).
1283
+ let shadowBuffers: VertexBufferLayout[] | undefined
1284
+ if (opts.shadowVertex !== undefined && instanceBuffers !== undefined) {
1174
1285
  let source = opts.shadowVertex
1175
- let named = (a: InstanceAttribute) => new RegExp("\\b" + a.name + "\\b").test(source)
1176
- let slots = new Set(instanceAttributes.filter(named).map(a => a.slot ?? 0))
1177
- if (!slots.has(0)) throw new Error("shaderMaterial shadowVertex must read the slot-0 instance attributes it places instances by")
1178
- shadowAttributes = instanceAttributes.filter(a => slots.has(a.slot ?? 0))
1286
+ let named = (a: VertexAttribute) => new RegExp("\\b" + a.name + "\\b").test(source)
1287
+ let read = instanceBuffers.map(b => b.attributes.some(named))
1288
+ if (!read[0]) throw new Error("shaderMaterial shadowVertex must read the first instance buffer's attributes, which place the instances")
1289
+ shadowBuffers = instanceBuffers.filter((_, i) => read[i])
1179
1290
  }
1180
1291
  // The shadowVertex depth material: one class and one shared instance,
1181
1292
  // built the first time a caster of this class meets a shadow view.
@@ -1188,7 +1299,7 @@ export function shaderMaterialClass(opts: ShaderMaterialClassOptions): ShaderMat
1188
1299
  vertex: opts.shadowVertex,
1189
1300
  fragment: SHADOW_DEPTH_FRAGMENT,
1190
1301
  cull: shadowCull(cull),
1191
- instanceAttributes: shadowAttributes,
1302
+ instanceBuffers: shadowBuffers,
1192
1303
  label: (opts.label ?? "shader-material") + "-shadow",
1193
1304
  })
1194
1305
  classShadow = shadowClass.instance()
@@ -1207,16 +1318,17 @@ export function shaderMaterialClass(opts: ShaderMaterialClassOptions): ShaderMat
1207
1318
  }
1208
1319
  // What the program reads from the GEOMETRY: the engine's reflection of
1209
1320
  // the linked program minus the per-instance names (those come from the
1210
- // record buffer, declared on the pipeline beside the layout).
1321
+ // instance buffers, declared on the pipeline after the layout).
1211
1322
  let attributes = (): VertexAttribute[] =>
1212
- programAttributes(programFor()).filter(a => !instanceAttributes?.some(i => i.name === a.name))
1213
- let pipelineFor = (layout: VertexLayout | undefined, topology: Topology): RenderPipelineId => {
1214
- let key = layoutKey(layout) + "|" + topology
1323
+ programAttributes(programFor()).filter(a => !instanceBuffers?.some(b => b.attributes.some(i => i.name === a.name)))
1324
+ let pipelineFor = (geometry: Geometry, topology: Topology): RenderPipelineId => {
1325
+ let key = geometryKey(geometry) + "|" + topology
1215
1326
  let pipeline = pipelines.get(key)
1216
1327
  if (pipeline === undefined) {
1328
+ // One vertex-step layout per geometry stream, the instance buffers
1329
+ // after them.
1217
1330
  pipeline = createRenderPipeline(programFor(), {
1218
- attributes: layoutAttributes(layout),
1219
- instanceAttributes,
1331
+ buffers: [...geometryLayouts(geometry).map(attributes => ({ attributes })), ...(instanceBuffers ?? [])],
1220
1332
  depth,
1221
1333
  // depthWrite needs a depth buffer, so the transparent default
1222
1334
  // only applies when there is one.
@@ -1235,9 +1347,11 @@ export function shaderMaterialClass(opts: ShaderMaterialClassOptions): ShaderMat
1235
1347
  return {
1236
1348
  normalMatrix,
1237
1349
  skinned,
1350
+ morphed,
1351
+ lodFade,
1238
1352
  attributes,
1239
1353
  transparent,
1240
- instanceAttributes,
1354
+ instanceBuffers,
1241
1355
  instanceStyle: opts.instanceStyle,
1242
1356
  pipeline: pipelineFor,
1243
1357
  params: inst.params ?? {},
@@ -1245,7 +1359,7 @@ export function shaderMaterialClass(opts: ShaderMaterialClassOptions): ShaderMat
1245
1359
  // Lazy: the depth materials are shaderMaterialClass instances
1246
1360
  // themselves, so an eager variant would recurse into its own cache.
1247
1361
  get shadow() {
1248
- return inst.shadow ?? shadowFor() ?? shadowVariant(cull, skinned)
1362
+ return inst.shadow ?? shadowFor() ?? shadowVariant(cull, skinned, morphed)
1249
1363
  },
1250
1364
  }
1251
1365
  },