@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.
- package/AGENTS.md +1479 -678
- package/README.md +4 -2
- package/examples/README.md +56 -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 +129 -0
- 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 +187 -22
- package/src/geometry.ts +1718 -203
- package/src/glsl.ts +398 -113
- package/src/gltf.ts +457 -154
- package/src/index.ts +32 -21
- package/src/input.ts +11 -2
- package/src/light.ts +7 -8
- package/src/lod.ts +148 -0
- package/src/material.ts +322 -190
- package/src/mesh.ts +492 -259
- package/src/mixer.ts +27 -14
- package/src/model-file.ts +54 -15
- package/src/model.ts +151 -54
- package/src/motion.ts +17 -0
- package/src/node.ts +423 -23
- 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 +608 -185
- package/src/skeleton.ts +211 -0
- 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,13 +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
|
-
/** The pipeline this material draws with for geometry of `layout`
|
|
67
|
-
* (lazily created, one per
|
|
68
|
-
|
|
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"
|
|
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
|
-
/**
|
|
89
|
-
* (shaderMaterialClass's `
|
|
90
|
-
* `instanced`)
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
*
|
|
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
|
|
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
|
|
155
|
-
*
|
|
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
|
|
203
|
-
//
|
|
204
|
-
|
|
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
|
-
|
|
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
|
|
223
|
-
* the
|
|
224
|
-
*
|
|
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 =
|
|
269
|
+
let uColor = premultipliedColor(opts.color ?? [1, 1, 1])
|
|
228
270
|
let map = opts.map !== undefined
|
|
229
|
-
let
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
348
|
-
|
|
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. `
|
|
370
|
-
// 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.
|
|
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
|
|
448
|
+
let phongClasses = new Map<string, ShaderMaterialClass>()
|
|
396
449
|
|
|
397
450
|
/**
|
|
398
|
-
*
|
|
399
|
-
*
|
|
400
|
-
* Blinn-Phong highlight. Same options as unlit
|
|
401
|
-
* cull, alphaTest) plus vertexColors,
|
|
402
|
-
*
|
|
403
|
-
*
|
|
404
|
-
*
|
|
405
|
-
*
|
|
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
|
|
408
|
-
let uColor =
|
|
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("
|
|
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("
|
|
424
|
-
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)")
|
|
425
479
|
if (mapTransform && !map && !normalMap && !emissiveMap && !specularMap) {
|
|
426
|
-
throw new Error("
|
|
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
|
|
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
|
|
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 =
|
|
505
|
+
let cls = phongClasses.get(key)
|
|
450
506
|
if (cls === undefined) {
|
|
451
507
|
cls = shaderMaterialClass({
|
|
452
508
|
vertex: litVertex(flags),
|
|
453
|
-
fragment:
|
|
454
|
-
shadowVertex: flags.instanced ? shadowDepthVertex(flags.skinned, true) : undefined,
|
|
455
|
-
|
|
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-
|
|
516
|
+
label: "scene-phong-" + key,
|
|
460
517
|
})
|
|
461
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
524
|
-
// 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.
|
|
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 `
|
|
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
|
|
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
|
|
626
|
+
* combination, shared by every instance, like phong.
|
|
567
627
|
*/
|
|
568
628
|
export function standard(opts: StandardOptions = {}): Material {
|
|
569
|
-
let uColor =
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
765
|
-
* is undefined; a skinned class always needs its own
|
|
766
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
887
|
-
|
|
888
|
-
|
|
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 =
|
|
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
|
|
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,
|
|
984
|
+
export function missingAttributes(material: Material, geometry: Geometry): VertexAttribute[] {
|
|
913
985
|
let missing: VertexAttribute[] = []
|
|
914
986
|
for (let attr of material.attributes()) {
|
|
915
|
-
|
|
916
|
-
|
|
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
|
|
957
|
-
//
|
|
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"
|
|
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,
|
|
965
|
-
//
|
|
966
|
-
//
|
|
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 =
|
|
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
|
-
*
|
|
1024
|
-
*
|
|
1025
|
-
*
|
|
1026
|
-
*
|
|
1027
|
-
*
|
|
1028
|
-
*
|
|
1029
|
-
*
|
|
1030
|
-
*
|
|
1031
|
-
* the
|
|
1032
|
-
*
|
|
1033
|
-
*
|
|
1034
|
-
*
|
|
1035
|
-
*
|
|
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
|
-
|
|
1038
|
-
/** The style record a fresh instance starts with (
|
|
1039
|
-
*
|
|
1040
|
-
* whatever the stage does with the
|
|
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
|
|
1146
|
-
//
|
|
1147
|
-
|
|
1148
|
-
//
|
|
1149
|
-
//
|
|
1150
|
-
//
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
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:
|
|
1158
|
-
let
|
|
1159
|
-
if (!
|
|
1160
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
1321
|
+
// instance buffers, declared on the pipeline after the layout).
|
|
1193
1322
|
let attributes = (): VertexAttribute[] =>
|
|
1194
|
-
programAttributes(programFor()).filter(a => !
|
|
1195
|
-
let pipelineFor = (
|
|
1196
|
-
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
},
|