@solidrt/3d 0.0.57 → 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 (72) hide show
  1. package/AGENTS.md +1479 -678
  2. package/README.md +4 -2
  3. package/examples/README.md +56 -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 +129 -0
  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 +187 -22
  51. package/src/geometry.ts +1718 -203
  52. package/src/glsl.ts +398 -113
  53. package/src/gltf.ts +457 -154
  54. package/src/index.ts +32 -21
  55. package/src/input.ts +11 -2
  56. package/src/light.ts +7 -8
  57. package/src/lod.ts +148 -0
  58. package/src/material.ts +322 -190
  59. package/src/mesh.ts +492 -259
  60. package/src/mixer.ts +27 -14
  61. package/src/model-file.ts +54 -15
  62. package/src/model.ts +151 -54
  63. package/src/motion.ts +17 -0
  64. package/src/node.ts +423 -23
  65. package/src/orbit.ts +378 -116
  66. package/src/profile.ts +4 -25
  67. package/src/resolve.ts +181 -0
  68. package/src/scene-shadows.ts +94 -93
  69. package/src/scene.ts +608 -185
  70. package/src/skeleton.ts +211 -0
  71. package/src/sweep.ts +27 -8
  72. 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 { 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,13 +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
- /** The pipeline this material draws with for geometry of `layout`
67
- * (lazily created, one per layout met). */
68
- pipeline(layout: VertexLayout | undefined): RenderPipelineId
69
+ /** The pipeline this material draws with for geometry of `layout` and
70
+ * `topology` (lazily created, one per pair met): the geometry says what
71
+ * its indices are, the material draws them that way. */
72
+ pipeline(geometry: Geometry, topology: Topology): RenderPipelineId
69
73
  /** Per-entry uniform values this material contributes at addDraw. */
70
74
  params: ShaderParams
71
75
  /** Per-entry sampler bindings, when the material samples textures. */
@@ -81,20 +85,23 @@ export type Material = {
81
85
  * channels in the geometry are fine (inactive attributes keep the
82
86
  * stride). */
83
87
  attributes(): VertexAttribute[]
84
- /** 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):
85
90
  * the scene draws this material's meshes after every opaque one, sorted
86
91
  * back-to-front by mesh origin, and re-sorts them when the camera moves. */
87
92
  transparent?: boolean
88
- /** Per-instance attributes, when the material's pipeline declares them
89
- * (shaderMaterialClass's `instanceAttributes`; the stock materials'
90
- * `instanced`). Such a material draws populated meshes only - an
91
- * instanced mesh binds slot 0 to the core-written matrix records and
92
- * slot 1 to its app-written style records, a record mesh binds slot 0
93
- * to its records - and createMesh meshes are rejected at add(). */
94
- instanceAttributes?: InstanceAttribute[]
95
- /** The style record (the slot-1 attributes' floats, in order) every
96
- * fresh instance of an instanced mesh starts with; absent = zeros.
97
- * 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`. */
98
105
  instanceStyle?: ArrayLike<number>
99
106
  /** What a shadow view draws this material's meshes with instead of its
100
107
  * default depth override: the depth pass culling the side this material
@@ -114,23 +121,28 @@ export type Material = {
114
121
  * so a shadow or override entry for a skinned mesh merges the mesh's
115
122
  * own uBones binding even though this material is not the mesh's. */
116
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
117
137
  /** Present on materials that own their pipeline (shaderMaterial). */
118
138
  dispose?(): void
119
139
  }
120
140
 
121
141
  // One shaderMaterialClass per unlit option combination (map x transparent
122
- // 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
123
143
  // pipeline per vertex layout inside each.
124
144
  let unlitClasses = new Map<string, ShaderMaterialClass>()
125
145
 
126
- // The uColor a color option compiles to: sRGB decoded to linear light,
127
- // then premultiplied by its alpha (the target's contract).
128
- function colorParam(color: readonly number[]): number[] {
129
- let a = color.length === 4 ? color[3]! : 1
130
- let c = linearColor(color)
131
- return [c[0]! * a, c[1]! * a, c[2]! * a, a]
132
- }
133
-
134
146
  export type UnlitOptions = {
135
147
  /** Straight [r, g, b] or [r, g, b, a], 0..1, sRGB - a color picker's
136
148
  * values. The library decodes it to linear light for shading and the
@@ -143,16 +155,28 @@ export type UnlitOptions = {
143
155
  * linear light like `color`; a plain rgba8 map reads as linear data and
144
156
  * shows washed out. */
145
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
146
163
  /** Blend over what is behind (color alpha and map alpha both count).
147
- * 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". */
148
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
149
173
  /** Which faces to drop; default "back". "none" draws both sides of
150
174
  * single-layer geometry (foliage cards, glass, a mirrored part), and
151
175
  * lit materials then light a back face with its normal flipped, as
152
176
  * Three's DoubleSide and Godot's CULL_DISABLED do. */
153
177
  cull?: CullMode
154
- /** Cutout: drop a fragment whose final alpha (color x map, and for lit
155
- * 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
156
180
  * alphaMode MASK with its alphaCutoff). Opaque otherwise:
157
181
  * depth-written, not sorted, unlike `transparent`. Foliage cards and
158
182
  * fences want it with `cull: "none"`; a mapped cutout casts its cutout
@@ -182,6 +206,15 @@ export type UnlitOptions = {
182
206
  * the spatial flush writes it from the joints). The shadow variants
183
207
  * (depth and cutout) skin the same way, so a caster casts its pose. */
184
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
185
218
  /** Draw instanced meshes (createInstancedMesh / `<InstancedMesh>`):
186
219
  * the vertex stage places each copy by its instance matrix under the
187
220
  * mesh's uModel, and the material declares INSTANCE_MATRIX_ATTRIBUTES,
@@ -199,11 +232,20 @@ export type UnlitOptions = {
199
232
  instanceColors?: boolean
200
233
  }
201
234
 
202
- // The instance attributes a stock instanced material declares: the
203
- // matrix always, the color record beside it under instanceColors.
204
- 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 {
205
246
  if (!instanced) return undefined
206
- 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]
207
249
  }
208
250
  // A fresh instance's color: white, the multiplicative identity, so an
209
251
  // instance never written shows the material's own color (Godot's default;
@@ -219,32 +261,36 @@ function mapTransformParam(t: { offset?: [number, number]; repeat?: [number, num
219
261
  }
220
262
 
221
263
  /**
222
- * An unlit material: flat color, textured when `map` is given. Unlit is
223
- * the complete v1 set - lit materials arrive with uniform arrays (the
224
- * 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.
225
267
  */
226
268
  export function unlit(opts: UnlitOptions = {}): Material {
227
- let uColor = colorParam(opts.color ?? [1, 1, 1])
269
+ let uColor = premultipliedColor(opts.color ?? [1, 1, 1])
228
270
  let map = opts.map !== undefined
229
- let transparent = opts.transparent === true
271
+ let blend = opts.blend
272
+ let transparent = opts.transparent ?? (blend !== undefined && blend !== "none")
230
273
  let cull = opts.cull ?? "back"
231
274
  let alphaTest = opts.alphaTest !== undefined
232
- let fog = opts.fog !== false
275
+ let fog = fogForm(opts.fog, blend)
233
276
  let mapTransform = opts.mapTransform !== undefined
234
277
  let skinned = opts.skinned === true
278
+ let morph = opts.morph === true
235
279
  let instanceColors = opts.instanceColors === true
236
280
  let instanced = opts.instanced === true || instanceColors
281
+ let vertexColors = opts.vertexColors === true
237
282
  if (mapTransform && !map) throw new Error("unlit: mapTransform without a map to transform")
238
- 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("|")
239
284
  let cls = unlitClasses.get(key)
240
285
  if (cls === undefined) {
241
286
  cls = shaderMaterialClass({
242
- vertex: unlitVertex({ skinned, instanced, instanceColors }),
243
- fragment: unlitFragment({ map, alphaTest, transparent, fog, mapTransform, instanceColors }),
244
- shadowVertex: instanced ? shadowDepthVertex(skinned, true) : undefined,
245
- 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),
246
291
  instanceStyle: instanceColors ? INSTANCE_COLOR_DEFAULT : undefined,
247
292
  transparent,
293
+ blend,
248
294
  cull,
249
295
  label: "scene-unlit-" + key,
250
296
  })
@@ -258,19 +304,15 @@ export function unlit(opts: UnlitOptions = {}): Material {
258
304
  textures: map ? { uMap: opts.map! } : undefined,
259
305
  shadow:
260
306
  alphaTest && map
261
- ? 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)
262
308
  : undefined,
263
309
  })
264
310
  }
265
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. */
266
315
  export type LitOptions = UnlitOptions & {
267
- /** Multiply the base by the geometry's per-vertex aColor (withColors
268
- * geometry; add() throws without it). */
269
- vertexColors?: boolean
270
- /** Blinn-Phong highlight strength, 0..1 (default 0: pure diffuse). */
271
- specular?: number
272
- /** Highlight tightness, wide sheen (~8) to mirror dot (~150); default 30. */
273
- shininess?: number
274
316
  /** Sample `map` by WORLD position instead of UV - the value is the
275
317
  * texture repeats per world unit - blended across the three axis planes
276
318
  * by the normal. Tiles generated geometry at one density regardless of
@@ -305,21 +347,6 @@ export type LitOptions = UnlitOptions & {
305
347
  * nitro glow baked into one map. Sampled at the same uv as `map`;
306
348
  * create it with `format: "rgba8-srgb"` like `map`. */
307
349
  emissiveMap?: TextureId
308
- /** A texture whose RED channel scales `specular` per fragment (Three's
309
- * specularMap) - chrome and rubber on one mesh. With it, `specular`
310
- * defaults to 1 (the map is the strength). */
311
- specularMap?: TextureId
312
- /** Mirror the scene's environment (scene.setEnvironment), 0..1: the
313
- * face-on reflection weight, rising to 1 at grazing angles (Schlick) -
314
- * 1 is chrome, ~0.05 a glossy dielectric with rim reflections. The
315
- * reflection blurs with `shininess` (a wide sheen reflects a blurred
316
- * sky, a mirror dot a sharp one; the environment cube needs mipmaps
317
- * for the blur) and `specularMap`'s red scales it like `specular`.
318
- * Three's Phong `reflectivity` with MixOperation and a fresnel weight
319
- * (Three's default MultiplyOperation tints instead; not offered). Off
320
- * while the scene has no environment. Undefined declares no
321
- * environment sampler at all. */
322
- reflectivity?: number
323
350
  /** A baked-light texture (an offline render, an AO+GI bake), sampled by
324
351
  * the geometry's aUV2 channel and ADDED to the light sum like the
325
352
  * hemisphere term - a fully baked scene runs with no lights at all
@@ -344,10 +371,34 @@ export type LitOptions = UnlitOptions & {
344
371
  receiveShadow?: boolean
345
372
  }
346
373
 
347
- // The lit program is built by litFragment in ./glsl - the same builder an
348
- // 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
349
400
  // the same exported constants. What varies per flag: map x vertexColors x
350
- // 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
351
402
  // faces lights them with the normal flipped, else a double-sided leaf's
352
403
  // back is black) x alphaTest (the cutoff itself is a per-entry uniform,
353
404
  // one class for every value) x fog (the scene's fog composed last, or
@@ -366,17 +417,18 @@ export type LitOptions = UnlitOptions & {
366
417
  // 0 means it does not cast; SHADOW_LOOKUP turns the index into the factor.
367
418
  // The option combination that picks a lit class: the class-cache key is
368
419
  // its values in this order, and the same object builds the program, so
369
- // the two cannot drift apart. `lit` fills no slot - an app that does
370
- // 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.
371
422
  type LitClass = {
372
423
  map: boolean
373
424
  vertexColors: boolean
374
425
  triplanar: boolean
375
426
  transparent: boolean
427
+ blend: BlendMode | undefined
376
428
  receiveShadow: boolean
377
429
  cull: CullMode
378
430
  alphaTest: boolean
379
- fog: boolean
431
+ fog: boolean | "additive"
380
432
  normalMap: boolean
381
433
  emissive: boolean
382
434
  emissiveMap: boolean
@@ -387,25 +439,27 @@ type LitClass = {
387
439
  skinned: boolean
388
440
  instanced: boolean
389
441
  instanceColors: boolean
442
+ morph: boolean
390
443
  }
391
444
 
392
445
  function litClassKey(c: LitClass): string {
393
446
  return Object.values(c).join("|")
394
447
  }
395
- let litClasses = new Map<string, ShaderMaterialClass>()
448
+ let phongClasses = new Map<string, ShaderMaterialClass>()
396
449
 
397
450
  /**
398
- * A lit material: hemisphere ambient plus the scene's directional lights
399
- * (DirectionalLight nodes), Lambert diffuse, optional
400
- * Blinn-Phong highlight. Same options as unlit (color, map, transparent,
401
- * cull, alphaTest) plus vertexColors, specular/shininess and triplanar mapping. One program
402
- * per option combination, one pipeline per vertex layout met, shared by
403
- * every instance - a thousand lit meshes still share one pipeline. No
404
- * lights set means black except for the hemisphere term, which also
405
- * 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.
406
460
  */
407
- export function lit(opts: LitOptions = {}): Material {
408
- let uColor = colorParam(opts.color ?? [1, 1, 1])
461
+ export function phong(opts: PhongOptions = {}): Material {
462
+ let uColor = premultipliedColor(opts.color ?? [1, 1, 1])
409
463
  let map = opts.map !== undefined
410
464
  let triplanar = map && opts.triplanar !== undefined
411
465
  let alphaTest = opts.alphaTest !== undefined
@@ -418,22 +472,23 @@ export function lit(opts: LitOptions = {}): Material {
418
472
  let lightMap = opts.lightMap !== undefined
419
473
  let mapTransform = opts.mapTransform !== undefined
420
474
  if (env && !(Number.isFinite(opts.reflectivity) && opts.reflectivity! >= 0 && opts.reflectivity! <= 1)) {
421
- 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)
422
476
  }
423
- if (triplanar && normalMap) throw new Error("lit: normalMap cannot combine with triplanar (normal maps sample by uv)")
424
- 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)")
425
479
  if (mapTransform && !map && !normalMap && !emissiveMap && !specularMap) {
426
- throw new Error("lit: mapTransform without a map to transform")
480
+ throw new Error("phong: mapTransform without a map to transform")
427
481
  }
428
482
  let flags: LitClass = {
429
483
  map,
430
484
  vertexColors: opts.vertexColors === true,
431
485
  triplanar,
432
- transparent: opts.transparent === true,
486
+ transparent: opts.transparent ?? (opts.blend !== undefined && opts.blend !== "none"),
487
+ blend: opts.blend,
433
488
  receiveShadow: opts.receiveShadow !== false,
434
489
  cull,
435
490
  alphaTest,
436
- fog: opts.fog !== false,
491
+ fog: fogForm(opts.fog, opts.blend),
437
492
  normalMap,
438
493
  emissive,
439
494
  emissiveMap,
@@ -444,21 +499,23 @@ export function lit(opts: LitOptions = {}): Material {
444
499
  skinned: opts.skinned === true,
445
500
  instanced: opts.instanced === true || opts.instanceColors === true,
446
501
  instanceColors: opts.instanceColors === true,
502
+ morph: opts.morph === true,
447
503
  }
448
504
  let key = litClassKey(flags)
449
- let cls = litClasses.get(key)
505
+ let cls = phongClasses.get(key)
450
506
  if (cls === undefined) {
451
507
  cls = shaderMaterialClass({
452
508
  vertex: litVertex(flags),
453
- fragment: litFragment(flags),
454
- shadowVertex: flags.instanced ? shadowDepthVertex(flags.skinned, true) : undefined,
455
- 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),
456
512
  instanceStyle: flags.instanceColors ? INSTANCE_COLOR_DEFAULT : undefined,
457
513
  transparent: flags.transparent,
514
+ blend: flags.blend,
458
515
  cull,
459
- label: "scene-lit-" + key,
516
+ label: "scene-phong-" + key,
460
517
  })
461
- litClasses.set(key, cls)
518
+ phongClasses.set(key, cls)
462
519
  }
463
520
  let params: ShaderParams = {
464
521
  uColor,
@@ -499,7 +556,8 @@ export function lit(opts: LitOptions = {}): Material {
499
556
  return material
500
557
  }
501
558
 
502
- 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 & {
503
561
  /** How metallic the surface is, 0..1 (default 0; 1 when `metalnessMap`
504
562
  * is given - the map is the value): a metal has no diffuse and
505
563
  * reflects tinted by `color`, a dielectric reflects 4% face-on.
@@ -518,10 +576,10 @@ export type StandardOptions = Omit<LitOptions, "specular" | "shininess" | "specu
518
576
  roughnessMap?: TextureId
519
577
  }
520
578
 
521
- // 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
522
580
  // always on, the environment being intrinsic to the model) plus the two
523
- // packed maps. Its cutout shadow is lit's own, keyed on the shared subset
524
- // 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.
525
583
  type StandardClass = Omit<LitClass, "specularMap" | "env"> & { metalnessMap: boolean; roughnessMap: boolean }
526
584
 
527
585
  function standardClassKey(c: StandardClass): string {
@@ -535,6 +593,7 @@ function standardShadowFlags(c: StandardClass): LitClass {
535
593
  vertexColors: c.vertexColors,
536
594
  triplanar: c.triplanar,
537
595
  transparent: c.transparent,
596
+ blend: c.blend,
538
597
  receiveShadow: c.receiveShadow,
539
598
  cull: c.cull,
540
599
  alphaTest: c.alphaTest,
@@ -549,6 +608,7 @@ function standardShadowFlags(c: StandardClass): LitClass {
549
608
  skinned: c.skinned,
550
609
  instanced: c.instanced,
551
610
  instanceColors: c.instanceColors,
611
+ morph: c.morph,
552
612
  }
553
613
  }
554
614
 
@@ -556,17 +616,17 @@ function standardShadowFlags(c: StandardClass): LitClass {
556
616
  * The metalness/roughness material, the look authored assets expect
557
617
  * (Three's MeshStandardMaterial, Godot's StandardMaterial3D, Unity's
558
618
  * Standard): the same base, maps, cutout, shadows, emissive, fog and
559
- * 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`)
560
620
  * and the scene's environment sampled ALWAYS as the split sum over its
561
621
  * mip chain - no `reflectivity` switch, the environment is intrinsic to
562
- * 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
563
623
  * surface to 1). Without an environment set a metal shows only its
564
624
  * highlights - its diffuse is zero and there is nothing to reflect, as
565
625
  * in Three and Godot - so give the scene one. One program per option
566
- * combination, shared by every instance, like lit.
626
+ * combination, shared by every instance, like phong.
567
627
  */
568
628
  export function standard(opts: StandardOptions = {}): Material {
569
- let uColor = colorParam(opts.color ?? [1, 1, 1])
629
+ let uColor = premultipliedColor(opts.color ?? [1, 1, 1])
570
630
  let map = opts.map !== undefined
571
631
  let triplanar = map && opts.triplanar !== undefined
572
632
  let alphaTest = opts.alphaTest !== undefined
@@ -598,11 +658,12 @@ export function standard(opts: StandardOptions = {}): Material {
598
658
  map,
599
659
  vertexColors: opts.vertexColors === true,
600
660
  triplanar,
601
- transparent: opts.transparent === true,
661
+ transparent: opts.transparent ?? (opts.blend !== undefined && opts.blend !== "none"),
662
+ blend: opts.blend,
602
663
  receiveShadow: opts.receiveShadow !== false,
603
664
  cull,
604
665
  alphaTest,
605
- fog: opts.fog !== false,
666
+ fog: fogForm(opts.fog, opts.blend),
606
667
  normalMap,
607
668
  emissive,
608
669
  emissiveMap,
@@ -611,6 +672,7 @@ export function standard(opts: StandardOptions = {}): Material {
611
672
  skinned: opts.skinned === true,
612
673
  instanced: opts.instanced === true || opts.instanceColors === true,
613
674
  instanceColors: opts.instanceColors === true,
675
+ morph: opts.morph === true,
614
676
  metalnessMap,
615
677
  roughnessMap,
616
678
  }
@@ -620,10 +682,11 @@ export function standard(opts: StandardOptions = {}): Material {
620
682
  cls = shaderMaterialClass({
621
683
  vertex: litVertex(flags),
622
684
  fragment: standardFragment(flags),
623
- shadowVertex: flags.instanced ? shadowDepthVertex(flags.skinned, true) : undefined,
624
- instanceAttributes: stockInstanceAttributes(flags.instanced, flags.instanceColors),
685
+ shadowVertex: flags.instanced ? shadowDepthVertex(flags.skinned, true, flags.morph) : undefined,
686
+ instanceBuffers: stockInstanceBuffers(flags.instanced, flags.instanceColors),
625
687
  instanceStyle: flags.instanceColors ? INSTANCE_COLOR_DEFAULT : undefined,
626
688
  transparent: flags.transparent,
689
+ blend: flags.blend,
627
690
  cull,
628
691
  label: "scene-standard-" + key,
629
692
  })
@@ -687,7 +750,7 @@ function litShadowMaterial(
687
750
  vertex: litVertex(flags),
688
751
  fragment,
689
752
  cull: shadowCull(flags.cull),
690
- instanceAttributes: stockInstanceAttributes(flags.instanced, flags.instanceColors),
753
+ instanceBuffers: stockInstanceBuffers(flags.instanced, flags.instanceColors),
691
754
  label: "scene-lit-shadow-" + key,
692
755
  })
693
756
  litShadowClasses.set(key, cls)
@@ -713,15 +776,18 @@ function litShadowMaterial(
713
776
  // Material.skinned); the instanced form places each copy by its instance
714
777
  // matrix, so a fleet casts where its records stand - the stock instanced
715
778
  // materials pass it as their class's shadowVertex.
716
- function shadowDepthVertex(skinned: boolean, instanced: boolean): string {
717
- 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)`
718
782
  return glsl`
719
783
  in vec3 aPos;
784
+ ${morphed ? MORPH_DECLS : ""}
720
785
  ${skinned ? SKIN_DECLS : ""}
721
786
  ${instanced ? INSTANCE_MATRIX : ""}
722
787
  uniform mat4 uModel;
723
788
  uniform mat4 uViewProj;
724
789
  void main() {
790
+ ${morphed ? MORPH_APPLY_POSITION : ""}
725
791
  ${skinned ? SKIN_MATRIX : ""}
726
792
  gl_Position = uViewProj * uModel * ${instanced ? `(instanceMatrix() * ${local})` : local};
727
793
  }
@@ -740,12 +806,12 @@ let shadowDepth = new Map<string, Material>()
740
806
  * per cull mode x skinned for the app, built on first use. The default,
741
807
  * "front", is the caster's back surface (see above); a material culling
742
808
  * or skinning otherwise carries its own variant as Material.shadow. */
743
- export function shadowDepthMaterial(cull: CullMode = "front", skinned = false): Material {
744
- 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" : "")
745
811
  let material = shadowDepth.get(key)
746
812
  if (material === undefined) {
747
813
  material = shaderMaterialClass({
748
- vertex: shadowDepthVertex(skinned, false),
814
+ vertex: shadowDepthVertex(skinned, false, morphed),
749
815
  fragment: SHADOW_DEPTH_FRAGMENT,
750
816
  cull,
751
817
  label: "scene-shadow-depth-" + key,
@@ -761,12 +827,12 @@ function shadowCull(cull: CullMode): CullMode {
761
827
  return cull === "none" ? "none" : cull === "back" ? "front" : "back"
762
828
  }
763
829
 
764
- /** "back" unskinned maps to the default depth material, so its variant
765
- * is undefined; a skinned class always needs its own (the default does
766
- * not skin). */
767
- function shadowVariant(cull: CullMode, skinned: boolean): Material | undefined {
768
- if (cull === "back" && !skinned) return undefined
769
- 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)
770
836
  }
771
837
 
772
838
  let unlitShadowClasses = new Map<string, ShaderMaterialClass>()
@@ -780,24 +846,26 @@ let unlitShadowClasses = new Map<string, ShaderMaterialClass>()
780
846
  * transform). */
781
847
  function unlitShadowMaterial(
782
848
  cull: CullMode,
849
+ vertexColors: boolean,
783
850
  skinned: boolean,
784
851
  instanced: boolean,
785
852
  instanceColors: boolean,
853
+ morph: boolean,
786
854
  uColor: number[],
787
855
  uAlphaTest: number,
788
856
  uMap: TextureId,
789
857
  uMapTransform?: number[],
790
858
  ): Material {
791
- let key = [cull, uMapTransform !== undefined, skinned, instanced, instanceColors].join("|")
859
+ let key = [cull, uMapTransform !== undefined, vertexColors, skinned, instanced, instanceColors, morph].join("|")
792
860
  let cls = unlitShadowClasses.get(key)
793
861
  if (cls === undefined) {
794
- let fragment = unlitShadowFragment({ map: true, alphaTest: true, mapTransform: uMapTransform !== undefined, instanceColors })
862
+ let fragment = unlitShadowFragment({ map: true, vertexColors, alphaTest: true, mapTransform: uMapTransform !== undefined, instanceColors })
795
863
  if (fragment === undefined) throw new Error("unlitShadowMaterial: the cutout options cannot discard")
796
864
  cls = shaderMaterialClass({
797
- vertex: unlitVertex({ skinned, instanced, instanceColors }),
865
+ vertex: unlitVertex({ vertexColors, skinned, instanced, instanceColors, morph }),
798
866
  fragment,
799
867
  cull,
800
- instanceAttributes: stockInstanceAttributes(instanced, instanceColors),
868
+ instanceBuffers: stockInstanceBuffers(instanced, instanceColors),
801
869
  label: "scene-unlit-shadow-" + key,
802
870
  })
803
871
  unlitShadowClasses.set(key, cls)
@@ -809,8 +877,9 @@ function unlitShadowMaterial(
809
877
 
810
878
  /** A sprite's options: unlit's, minus instancing (the billboard stage
811
879
  * places one quad per mesh; an instanced billboard field is a custom
812
- * class's vertex stage). */
813
- 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"> & {
814
883
  /** Which way the quad turns to face the camera. `"full"` (default,
815
884
  * Three's Sprite): both axes follow the view, the quad is always flat
816
885
  * to the screen. `"fixed-y"` (Godot's BILLBOARD_FIXED_Y): only the yaw
@@ -883,22 +952,25 @@ let spriteClasses = new Map<string, ShaderMaterialClass>()
883
952
  */
884
953
  export function sprite(opts: SpriteOptions = {}): Material {
885
954
  // Excluded by the type and checked for a JS caller: the billboard stage
886
- // has no instance placement, so the flags would otherwise be ignored.
887
- if ((opts as UnlitOptions).instanced || (opts as UnlitOptions).instanceColors) {
888
- 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)")
889
959
  }
890
- let uColor = colorParam(opts.color ?? [1, 1, 1])
960
+ let uColor = premultipliedColor(opts.color ?? [1, 1, 1])
891
961
  let map = opts.map !== undefined
962
+ let blend = opts.blend
892
963
  let transparent = opts.transparent !== false
893
964
  let fixedY = opts.billboard === "fixed-y"
894
- let fog = opts.fog !== false
895
- let key = [map, transparent, fixedY, fog].join("|")
965
+ let fog = fogForm(opts.fog, blend)
966
+ let key = [map, transparent, blend, fixedY, fog].join("|")
896
967
  let cls = spriteClasses.get(key)
897
968
  if (cls === undefined) {
898
969
  cls = shaderMaterialClass({
899
970
  vertex: fixedY ? SPRITE_FIXED_Y_VERTEX_SRC : SPRITE_VERTEX_SRC,
900
971
  fragment: unlitFragment({ map, transparent, fog }),
901
972
  transparent,
973
+ blend,
902
974
  cull: "none",
903
975
  label: "scene-sprite-" + key,
904
976
  })
@@ -909,11 +981,14 @@ export function sprite(opts: SpriteOptions = {}): Material {
909
981
 
910
982
  /** The attributes `material` reads that `layout` does not carry (name and
911
983
  * format) - empty when the pair is drawable. */
912
- export function missingAttributes(material: Material, layout: VertexLayout | undefined): VertexAttribute[] {
984
+ export function missingAttributes(material: Material, geometry: Geometry): VertexAttribute[] {
913
985
  let missing: VertexAttribute[] = []
914
986
  for (let attr of material.attributes()) {
915
- let slot = layoutSlot(layout, attr.name)
916
- 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)
917
992
  }
918
993
  return missing
919
994
  }
@@ -952,25 +1027,25 @@ const BACKGROUND_VERTEX = glsl`
952
1027
 
953
1028
  // Pipeline fragments get no vUV from the engine preamble (a pipeline's
954
1029
  // varyings are its own), so the background slot injects the full
955
- // shader-target fragment contract itself: vUV, fragColor, iResolution,
956
- // its own vRay, and the OUTPUT set, so a sky can end with outputColor
957
- // 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.
958
1033
  const BACKGROUND_FRAGMENT_PREAMBLE =
959
- "#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"
960
1035
 
961
1036
  // The skybox fragment behind setBackground({ cube }): the view ray through
962
1037
  // the sky's rotation (the INVERSE turn, written by skyboxParams, so the
963
1038
  // sky itself turns by +rotation like a node would), then the cube lookup
964
- // times the intensity, through the output
965
- // stage (the preamble declares it) like every lit pixel. Opaque: the
966
- // 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.
967
1042
  export const SKYBOX_FRAGMENT = glsl`
968
1043
  uniform samplerCube uSky;
969
1044
  uniform float uSkyIntensity;
970
1045
  uniform mat4 uSkyRotation;
971
1046
  void main() {
972
1047
  vec3 dir = mat3(uSkyRotation) * normalize(vRay);
973
- fragColor = outputColor(texture(uSky, dir).rgb * uSkyIntensity, 1.0);
1048
+ fragColor = vec4(max(texture(uSky, dir).rgb * uSkyIntensity, vec3(0.0)), 1.0);
974
1049
  }
975
1050
  `
976
1051
 
@@ -991,6 +1066,35 @@ export function backgroundPipeline(fragment: string, label: string): { pipeline:
991
1066
  return { pipeline, program }
992
1067
  }
993
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
+
994
1098
  /** The class half of a shader material: sources and pipeline state, the
995
1099
  * things one compiled program fixes. */
996
1100
  export type ShaderMaterialClassOptions = {
@@ -1020,25 +1124,28 @@ export type ShaderMaterialClassOptions = {
1020
1124
  vertex: string
1021
1125
  fragment: string
1022
1126
  /**
1023
- * Per-instance attributes: the vertex stage reads these as `in` variables
1024
- * beside the layout's own, and each drawn instance gets one record from
1025
- * the mesh's instance buffer (interleaved floats in this order). A class
1026
- * with instance attributes makes INSTANCED materials: attach their meshes
1027
- * with createInstancedMesh or createRecordMesh, which carry the records
1028
- * - a createMesh mesh is rejected at add(). Each attribute's `slot`
1029
- * (default 0) picks the buffer: an instanced mesh binds the core's
1030
- * matrix records (INSTANCE_MATRIX_ATTRIBUTES, exactly) to slot 0 and
1031
- * the app's style records - any layout, written per instance with
1032
- * setInstanceStyle - to slot 1; a record mesh is slot 0 only, its
1033
- * records whatever the stage reads (a position/yaw/scale record beats
1034
- * four vec4 columns for a JS-stepped fleet, and the composed uModel
1035
- * 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.
1036
1143
  */
1037
- instanceAttributes?: InstanceAttribute[]
1038
- /** The style record a fresh instance starts with (the slot-1
1039
- * attributes' floats, in order); default zeros. The identity of
1040
- * whatever the stage does with the record - white for a multiplied
1041
- * 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. */
1042
1149
  instanceStyle?: ArrayLike<number>
1043
1150
  /**
1044
1151
  * The class's vertex stage reduced to POSITION - the instance
@@ -1072,7 +1179,6 @@ export type ShaderMaterialClassOptions = {
1072
1179
  depthWrite?: boolean
1073
1180
  blend?: BlendMode
1074
1181
  cull?: CullMode
1075
- topology?: Topology
1076
1182
  label?: string
1077
1183
  }
1078
1184
 
@@ -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,25 +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
- // An empty list declares nothing - same as absent (the engine requires an
1146
- // instance buffer exactly when attributes are declared).
1147
- let instanceAttributes = opts.instanceAttributes?.length ? opts.instanceAttributes.map(a => ({ ...a })) : undefined
1148
- // The shadowVertex depth pass binds the instance slots its stage reads
1149
- // from - the record slot always (a stage reading none of it cannot
1150
- // place the instances, an error here), a style slot only when the
1151
- // stage names one of its attributes - and declares each bound slot's
1152
- // whole layout, since a slot's stride is the record's (an unread
1153
- // attribute in a bound slot is inactive, like an extra geometry channel).
1154
- let shadowAttributes: InstanceAttribute[] | undefined
1155
- if (opts.shadowVertex !== undefined && instanceAttributes !== undefined) {
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
+ }
1271
+ }
1272
+ }
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) {
1156
1285
  let source = opts.shadowVertex
1157
- let named = (a: InstanceAttribute) => new RegExp("\\b" + a.name + "\\b").test(source)
1158
- let slots = new Set(instanceAttributes.filter(named).map(a => a.slot ?? 0))
1159
- if (!slots.has(0)) throw new Error("shaderMaterial shadowVertex must read the slot-0 instance attributes it places instances by")
1160
- 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])
1161
1290
  }
1162
1291
  // The shadowVertex depth material: one class and one shared instance,
1163
1292
  // built the first time a caster of this class meets a shadow view.
@@ -1170,7 +1299,7 @@ export function shaderMaterialClass(opts: ShaderMaterialClassOptions): ShaderMat
1170
1299
  vertex: opts.shadowVertex,
1171
1300
  fragment: SHADOW_DEPTH_FRAGMENT,
1172
1301
  cull: shadowCull(cull),
1173
- instanceAttributes: shadowAttributes,
1302
+ instanceBuffers: shadowBuffers,
1174
1303
  label: (opts.label ?? "shader-material") + "-shadow",
1175
1304
  })
1176
1305
  classShadow = shadowClass.instance()
@@ -1189,23 +1318,24 @@ export function shaderMaterialClass(opts: ShaderMaterialClassOptions): ShaderMat
1189
1318
  }
1190
1319
  // What the program reads from the GEOMETRY: the engine's reflection of
1191
1320
  // the linked program minus the per-instance names (those come from the
1192
- // record buffer, declared on the pipeline beside the layout).
1321
+ // instance buffers, declared on the pipeline after the layout).
1193
1322
  let attributes = (): VertexAttribute[] =>
1194
- programAttributes(programFor()).filter(a => !instanceAttributes?.some(i => i.name === a.name))
1195
- let pipelineFor = (layout: VertexLayout | undefined): RenderPipelineId => {
1196
- let key = layoutKey(layout)
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
1197
1326
  let pipeline = pipelines.get(key)
1198
1327
  if (pipeline === undefined) {
1328
+ // One vertex-step layout per geometry stream, the instance buffers
1329
+ // after them.
1199
1330
  pipeline = createRenderPipeline(programFor(), {
1200
- attributes: layoutAttributes(layout),
1201
- instanceAttributes,
1331
+ buffers: [...geometryLayouts(geometry).map(attributes => ({ attributes })), ...(instanceBuffers ?? [])],
1202
1332
  depth,
1203
1333
  // depthWrite needs a depth buffer, so the transparent default
1204
1334
  // only applies when there is one.
1205
1335
  depthWrite: opts.depthWrite ?? (transparent && depth ? false : undefined),
1206
1336
  blend: opts.blend ?? (transparent ? "alpha" : undefined),
1207
1337
  cull,
1208
- topology: opts.topology,
1338
+ topology,
1209
1339
  label: opts.label,
1210
1340
  })
1211
1341
  pipelines.set(key, pipeline)
@@ -1217,9 +1347,11 @@ export function shaderMaterialClass(opts: ShaderMaterialClassOptions): ShaderMat
1217
1347
  return {
1218
1348
  normalMatrix,
1219
1349
  skinned,
1350
+ morphed,
1351
+ lodFade,
1220
1352
  attributes,
1221
1353
  transparent,
1222
- instanceAttributes,
1354
+ instanceBuffers,
1223
1355
  instanceStyle: opts.instanceStyle,
1224
1356
  pipeline: pipelineFor,
1225
1357
  params: inst.params ?? {},
@@ -1227,7 +1359,7 @@ export function shaderMaterialClass(opts: ShaderMaterialClassOptions): ShaderMat
1227
1359
  // Lazy: the depth materials are shaderMaterialClass instances
1228
1360
  // themselves, so an eager variant would recurse into its own cache.
1229
1361
  get shadow() {
1230
- return inst.shadow ?? shadowFor() ?? shadowVariant(cull, skinned)
1362
+ return inst.shadow ?? shadowFor() ?? shadowVariant(cull, skinned, morphed)
1231
1363
  },
1232
1364
  }
1233
1365
  },