@solidrt/3d 0.0.58 → 0.0.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +734 -264
- package/README.md +4 -2
- package/examples/README.md +50 -4
- package/examples/bloom.tsx +113 -0
- package/examples/cascades.tsx +4 -4
- package/examples/collision.tsx +6 -6
- package/examples/exits.tsx +96 -0
- package/examples/first-person.tsx +5 -5
- package/examples/fleet.tsx +4 -4
- package/examples/fog.tsx +5 -5
- package/examples/instanced.tsx +70 -50
- package/examples/lamps.tsx +4 -4
- package/examples/lod.tsx +125 -0
- package/examples/materials.tsx +9 -9
- package/examples/morph.tsx +174 -0
- package/examples/normals.tsx +118 -0
- package/examples/{lit.tsx → phong.tsx} +9 -9
- package/examples/polyhedra.tsx +61 -0
- package/examples/scene-atlas.tsx +70 -0
- package/examples/scene-views.tsx +5 -5
- package/examples/shadows.tsx +9 -9
- package/examples/sky-lit.tsx +3 -3
- package/examples/skybox.tsx +5 -5
- package/examples/standard.tsx +2 -2
- package/examples/streams.tsx +122 -0
- package/examples/sweep-paths.tsx +5 -5
- package/examples/wireframe.tsx +24 -2
- package/package.json +3 -3
- package/src/clip.ts +18 -5
- package/src/color.ts +11 -2
- package/src/components/context.tsx +11 -2
- package/src/components/directional-light.tsx +2 -2
- package/src/components/first-person-camera.tsx +4 -4
- package/src/components/group.tsx +9 -5
- package/src/components/hemisphere-light.tsx +2 -2
- package/src/components/index.ts +4 -1
- package/src/components/instanced-mesh.tsx +28 -10
- package/src/components/lod.tsx +102 -0
- package/src/components/mesh.tsx +24 -6
- package/src/components/node-props.ts +35 -5
- package/src/components/orbit-camera.tsx +15 -12
- package/src/components/point-light.tsx +2 -2
- package/src/components/record-mesh.tsx +12 -7
- package/src/components/scene.tsx +55 -2
- package/src/components/spot-light.tsx +2 -2
- package/src/components/sprite.tsx +2 -2
- package/src/components/view3d.tsx +18 -1
- package/src/environment.ts +9 -8
- package/src/first-person.ts +125 -38
- package/src/geometry-gpu.ts +172 -39
- package/src/geometry.ts +1573 -239
- package/src/glsl.ts +398 -113
- package/src/gltf.ts +363 -141
- package/src/index.ts +31 -21
- package/src/light.ts +7 -8
- package/src/lod.ts +148 -0
- package/src/material.ts +316 -202
- package/src/mesh.ts +490 -256
- package/src/mixer.ts +27 -14
- package/src/model-file.ts +54 -15
- package/src/model.ts +86 -21
- package/src/motion.ts +17 -0
- package/src/node.ts +421 -22
- package/src/orbit.ts +378 -116
- package/src/profile.ts +4 -25
- package/src/resolve.ts +181 -0
- package/src/scene-shadows.ts +94 -93
- package/src/scene.ts +605 -185
- package/src/sweep.ts +27 -8
- 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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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"
|
|
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
|
-
/**
|
|
90
|
-
* (shaderMaterialClass's `
|
|
91
|
-
* `instanced`)
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
*
|
|
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
|
|
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
|
|
156
|
-
*
|
|
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
|
|
204
|
-
//
|
|
205
|
-
|
|
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
|
-
|
|
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
|
|
224
|
-
* the
|
|
225
|
-
*
|
|
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 =
|
|
269
|
+
let uColor = premultipliedColor(opts.color ?? [1, 1, 1])
|
|
229
270
|
let map = opts.map !== undefined
|
|
230
|
-
let
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
349
|
-
|
|
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. `
|
|
371
|
-
// reaches for
|
|
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
|
|
448
|
+
let phongClasses = new Map<string, ShaderMaterialClass>()
|
|
397
449
|
|
|
398
450
|
/**
|
|
399
|
-
*
|
|
400
|
-
*
|
|
401
|
-
* Blinn-Phong highlight. Same options as unlit
|
|
402
|
-
* cull, alphaTest) plus vertexColors,
|
|
403
|
-
*
|
|
404
|
-
*
|
|
405
|
-
*
|
|
406
|
-
*
|
|
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
|
|
409
|
-
let uColor =
|
|
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("
|
|
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("
|
|
425
|
-
if (triplanar && mapTransform) throw new Error("
|
|
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("
|
|
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
|
|
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
|
|
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 =
|
|
505
|
+
let cls = phongClasses.get(key)
|
|
451
506
|
if (cls === undefined) {
|
|
452
507
|
cls = shaderMaterialClass({
|
|
453
508
|
vertex: litVertex(flags),
|
|
454
|
-
fragment:
|
|
455
|
-
shadowVertex: flags.instanced ? shadowDepthVertex(flags.skinned, true) : undefined,
|
|
456
|
-
|
|
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-
|
|
516
|
+
label: "scene-phong-" + key,
|
|
461
517
|
})
|
|
462
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
525
|
-
// in
|
|
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 `
|
|
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
|
|
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
|
|
626
|
+
* combination, shared by every instance, like phong.
|
|
568
627
|
*/
|
|
569
628
|
export function standard(opts: StandardOptions = {}): Material {
|
|
570
|
-
let uColor =
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
766
|
-
* is undefined; a skinned class always needs its own
|
|
767
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
888
|
-
|
|
889
|
-
|
|
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 =
|
|
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
|
|
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,
|
|
984
|
+
export function missingAttributes(material: Material, geometry: Geometry): VertexAttribute[] {
|
|
914
985
|
let missing: VertexAttribute[] = []
|
|
915
986
|
for (let attr of material.attributes()) {
|
|
916
|
-
|
|
917
|
-
|
|
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
|
|
958
|
-
//
|
|
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"
|
|
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,
|
|
966
|
-
//
|
|
967
|
-
//
|
|
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 =
|
|
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
|
-
*
|
|
1025
|
-
*
|
|
1026
|
-
*
|
|
1027
|
-
*
|
|
1028
|
-
*
|
|
1029
|
-
*
|
|
1030
|
-
*
|
|
1031
|
-
*
|
|
1032
|
-
* the
|
|
1033
|
-
*
|
|
1034
|
-
*
|
|
1035
|
-
*
|
|
1036
|
-
*
|
|
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
|
-
|
|
1039
|
-
/** The style record a fresh instance starts with (
|
|
1040
|
-
*
|
|
1041
|
-
* whatever the stage does with the
|
|
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
|
-
//
|
|
1146
|
-
//
|
|
1147
|
-
//
|
|
1148
|
-
//
|
|
1149
|
-
//
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
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
|
|
1164
|
-
//
|
|
1165
|
-
let
|
|
1166
|
-
// The shadowVertex depth pass binds the instance
|
|
1167
|
-
// from - the record
|
|
1168
|
-
// place the instances, an error here),
|
|
1169
|
-
// stage names one of its attributes - and declares each bound
|
|
1170
|
-
// whole layout, since a
|
|
1171
|
-
// attribute in a bound
|
|
1172
|
-
|
|
1173
|
-
|
|
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:
|
|
1176
|
-
let
|
|
1177
|
-
if (!
|
|
1178
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
1321
|
+
// instance buffers, declared on the pipeline after the layout).
|
|
1211
1322
|
let attributes = (): VertexAttribute[] =>
|
|
1212
|
-
programAttributes(programFor()).filter(a => !
|
|
1213
|
-
let pipelineFor = (
|
|
1214
|
-
let key =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
},
|