@solidrt/3d 0.0.54 → 0.0.56
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 +1121 -177
- package/README.md +15 -7
- package/examples/README.md +86 -5
- package/examples/aim.tsx +2 -3
- package/examples/cascades.tsx +20 -5
- package/examples/collision.tsx +250 -0
- package/examples/first-person.tsx +119 -0
- package/examples/fleet.tsx +194 -0
- package/examples/fog.tsx +173 -0
- package/examples/instanced.tsx +72 -32
- package/examples/lamps.tsx +92 -0
- package/examples/lit.tsx +3 -5
- package/examples/materials.tsx +150 -0
- package/examples/model-load.tsx +95 -0
- package/examples/model.glb +0 -0
- package/examples/model.tsx +32 -6
- package/examples/pick.tsx +2 -4
- package/examples/probe.tsx +101 -0
- package/examples/scene-background.tsx +2 -4
- package/examples/scene-basic.tsx +2 -4
- package/examples/scene-views.tsx +32 -3
- package/examples/shadows.tsx +2 -4
- package/examples/sky-lit.tsx +86 -0
- package/examples/skybox.tsx +128 -0
- package/examples/sprites.tsx +4 -6
- package/examples/standard.tsx +132 -0
- package/examples/sweep-paths.tsx +2 -4
- package/package.json +3 -3
- package/src/camera.ts +148 -0
- package/src/clip.ts +96 -0
- package/src/collision.ts +199 -0
- package/src/color.ts +32 -0
- package/src/components.tsx +773 -82
- package/src/environment-bake.ts +492 -0
- package/src/environment.ts +325 -0
- package/src/first-person.ts +306 -0
- package/src/geometry.ts +6 -2
- package/src/glsl.ts +1390 -82
- package/src/gltf.ts +432 -112
- package/src/index.ts +57 -9
- package/src/light.ts +383 -0
- package/src/material.ts +682 -216
- package/src/math.ts +110 -0
- package/src/mesh.ts +728 -0
- package/src/mixer.ts +476 -0
- package/src/model-file.ts +80 -15
- package/src/model.ts +211 -34
- package/src/node.ts +506 -0
- package/src/orbit.ts +97 -48
- package/src/scene-pointer.ts +166 -0
- package/src/scene-shadows.ts +471 -0
- package/src/scene.ts +1304 -1532
- package/tools/environment.ts +69 -0
- package/tools/model.ts +4 -3
package/src/material.ts
CHANGED
|
@@ -33,6 +33,7 @@ import type {
|
|
|
33
33
|
BlendMode,
|
|
34
34
|
CullMode,
|
|
35
35
|
ProgramId,
|
|
36
|
+
InstanceAttribute,
|
|
36
37
|
RenderPipelineId,
|
|
37
38
|
ShaderParams,
|
|
38
39
|
TextureBindings,
|
|
@@ -42,7 +43,24 @@ import type {
|
|
|
42
43
|
} from "@solidrt/core/gpu"
|
|
43
44
|
import { layoutAttributes, layoutKey, layoutSlot } from "./geometry.ts"
|
|
44
45
|
import type { VertexLayout } from "./geometry.ts"
|
|
45
|
-
import {
|
|
46
|
+
import { linearColor } from "./color.ts"
|
|
47
|
+
import {
|
|
48
|
+
INSTANCE_COLOR_ATTRIBUTES,
|
|
49
|
+
INSTANCE_MATRIX,
|
|
50
|
+
INSTANCE_MATRIX_ATTRIBUTES,
|
|
51
|
+
litFragment,
|
|
52
|
+
litShadowFragment,
|
|
53
|
+
litVertex,
|
|
54
|
+
SKIN_DECLS,
|
|
55
|
+
SKIN_MATRIX,
|
|
56
|
+
standardFragment,
|
|
57
|
+
SURFACE_DEFAULTS,
|
|
58
|
+
UNLIT_VERTEX,
|
|
59
|
+
unlitFragment,
|
|
60
|
+
unlitShadowFragment,
|
|
61
|
+
unlitVertex,
|
|
62
|
+
OUTPUT,
|
|
63
|
+
} from "./glsl.ts"
|
|
46
64
|
|
|
47
65
|
export type Material = {
|
|
48
66
|
/** The pipeline this material draws with for geometry of `layout`
|
|
@@ -68,70 +86,62 @@ export type Material = {
|
|
|
68
86
|
* back-to-front by mesh origin, and re-sorts them when the camera moves. */
|
|
69
87
|
transparent?: boolean
|
|
70
88
|
/** Per-instance attributes, when the material's pipeline declares them
|
|
71
|
-
* (shaderMaterialClass's `instanceAttributes
|
|
72
|
-
* instanced
|
|
73
|
-
*
|
|
74
|
-
|
|
89
|
+
* (shaderMaterialClass's `instanceAttributes`; the stock materials'
|
|
90
|
+
* `instanced`). Such a material draws populated meshes only - an
|
|
91
|
+
* instanced mesh binds slot 0 to the core-written matrix records and
|
|
92
|
+
* slot 1 to its app-written style records, a record mesh binds slot 0
|
|
93
|
+
* to its records - and createMesh meshes are rejected at add(). */
|
|
94
|
+
instanceAttributes?: InstanceAttribute[]
|
|
95
|
+
/** The style record (the slot-1 attributes' floats, in order) every
|
|
96
|
+
* fresh instance of an instanced mesh starts with; absent = zeros.
|
|
97
|
+
* White for the stock materials' `instanceColors`. */
|
|
98
|
+
instanceStyle?: ArrayLike<number>
|
|
75
99
|
/** What a shadow view draws this material's meshes with instead of its
|
|
76
100
|
* default depth override: the depth pass culling the side this material
|
|
77
101
|
* culls (Three's shadowSide rule, Godot's shadow pass), so a `cull:
|
|
78
|
-
* "none"` caster casts from both faces,
|
|
102
|
+
* "none"` caster casts from both faces, for a cutout (lit
|
|
79
103
|
* alphaTest with a map) the same discard, so a plant casts leaves and
|
|
80
|
-
* not rectangles
|
|
81
|
-
*
|
|
82
|
-
* instance
|
|
104
|
+
* not rectangles, for a skinned material the same skinning, so a
|
|
105
|
+
* posed caster casts its pose, and for an instanced class with
|
|
106
|
+
* `shadowVertex` the same instance placement, so a fleet casts where
|
|
107
|
+
* its records stand. Absent = the default (a back-culling material
|
|
108
|
+
* casts from its back faces); an instanced material without one is
|
|
109
|
+
* SKIPPED by shadow views. A shaderMaterial supplies its own through
|
|
110
|
+
* the instance option of the same name. */
|
|
83
111
|
shadow?: Material
|
|
112
|
+
/** True when the vertex stage declares `uBones` (read from the source,
|
|
113
|
+
* like normalMatrix): the program skins by the mesh's palette texture,
|
|
114
|
+
* so a shadow or override entry for a skinned mesh merges the mesh's
|
|
115
|
+
* own uBones binding even though this material is not the mesh's. */
|
|
116
|
+
skinned?: boolean
|
|
84
117
|
/** Present on materials that own their pipeline (shaderMaterial). */
|
|
85
118
|
dispose?(): void
|
|
86
119
|
}
|
|
87
120
|
|
|
88
|
-
// The unlit vertex stage: model then view-projection, plus the UV
|
|
89
|
-
// varying. uModel is per-entry (the scene writes it when the mesh moves),
|
|
90
|
-
// uViewProj is target-shared (one write per camera move) - the split is
|
|
91
|
-
// what keeps camera motion O(1) instead of O(meshes), and the extra
|
|
92
|
-
// per-vertex mat4 multiply is free on the GPU. aNormal from the shared
|
|
93
|
-
// layout is deliberately not declared - inactive attributes are skipped
|
|
94
|
-
// and only the stride accounts for them.
|
|
95
|
-
const UNLIT_VERTEX = glsl`
|
|
96
|
-
in vec3 aPos;
|
|
97
|
-
in vec2 aUV;
|
|
98
|
-
out vec2 vUv;
|
|
99
|
-
uniform mat4 uModel;
|
|
100
|
-
uniform mat4 uViewProj;
|
|
101
|
-
|
|
102
|
-
void main() {
|
|
103
|
-
gl_Position = uViewProj * uModel * vec4(aPos, 1.0);
|
|
104
|
-
vUv = aUV;
|
|
105
|
-
}
|
|
106
|
-
`
|
|
107
|
-
|
|
108
|
-
// The unlit fragment (sprites share it): the color, times the map when
|
|
109
|
-
// there is one, with the alphaTest discard when asked for. An opaque
|
|
110
|
-
// class writes alpha 1: the scene target is composited premultiplied, so
|
|
111
|
-
// a leaked texel alpha would punch a hole through an opaque draw.
|
|
112
|
-
function unlitFragment(map: boolean, alphaTest: boolean, transparent: boolean): string {
|
|
113
|
-
return glsl`
|
|
114
|
-
${map ? "in vec2 vUv;" : ""}
|
|
115
|
-
${map ? "uniform sampler2D uMap;" : ""}
|
|
116
|
-
uniform vec4 uColor;
|
|
117
|
-
${alphaTest ? "uniform float uAlphaTest;" : ""}
|
|
118
|
-
void main() {
|
|
119
|
-
vec4 base = ${map ? "texture(uMap, vUv) * uColor" : "uColor"};
|
|
120
|
-
${alphaTest ? "if (base.a < uAlphaTest) discard;" : ""}
|
|
121
|
-
fragColor = ${transparent ? "base" : "vec4(base.rgb, 1.0)"};
|
|
122
|
-
}
|
|
123
|
-
`
|
|
124
|
-
}
|
|
125
|
-
|
|
126
121
|
// One shaderMaterialClass per unlit option combination (map x transparent
|
|
127
122
|
// x cull x alphaTest), cached for the app's lifetime like lit's; one
|
|
128
123
|
// pipeline per vertex layout inside each.
|
|
129
124
|
let unlitClasses = new Map<string, ShaderMaterialClass>()
|
|
130
125
|
|
|
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
|
+
|
|
131
134
|
export type UnlitOptions = {
|
|
132
|
-
/** Straight [r, g, b] or [r, g, b, a], 0..1
|
|
135
|
+
/** Straight [r, g, b] or [r, g, b, a], 0..1, sRGB - a color picker's
|
|
136
|
+
* values. The library decodes it to linear light for shading and the
|
|
137
|
+
* output stage encodes the result back (see the color contract in
|
|
138
|
+
* AGENTS.md); an unlit surface with no tone mapping shows the value
|
|
139
|
+
* as given. Default white. */
|
|
133
140
|
color?: [number, number, number] | [number, number, number, number]
|
|
134
|
-
/** A texture id to sample (tinted by `color` when both are given).
|
|
141
|
+
/** A texture id to sample (tinted by `color` when both are given).
|
|
142
|
+
* Create a color image with `format: "rgba8-srgb"` so it decodes to
|
|
143
|
+
* linear light like `color`; a plain rgba8 map reads as linear data and
|
|
144
|
+
* shows washed out. */
|
|
135
145
|
map?: TextureId
|
|
136
146
|
/** Blend over what is behind (color alpha and map alpha both count).
|
|
137
147
|
* Without it an alpha below 1 still draws opaque. See Material.transparent. */
|
|
@@ -148,6 +158,64 @@ export type UnlitOptions = {
|
|
|
148
158
|
* fences want it with `cull: "none"`; a mapped cutout casts its cutout
|
|
149
159
|
* (Material.shadow). */
|
|
150
160
|
alphaTest?: number
|
|
161
|
+
/** Take the scene's fog (default true, Three's `material.fog`): the
|
|
162
|
+
* fragment fades toward the fog color with its distance from the
|
|
163
|
+
* camera once `scene.setFog` is set. `false` drops the fog code from
|
|
164
|
+
* the program - a sky sphere or a far backdrop that must keep its
|
|
165
|
+
* color, an emissive marker. */
|
|
166
|
+
fog?: boolean
|
|
167
|
+
/** Offset and repeat for the uv every map of this material samples:
|
|
168
|
+
* `uv * repeat + offset` (defaults `[1, 1]` / `[0, 0]`). ONE transform
|
|
169
|
+
* per MATERIAL (Godot's uv1_offset/uv1_scale, Unity's Tiling/Offset) -
|
|
170
|
+
* deliberately not Three's per-texture transform, since a TextureId is
|
|
171
|
+
* a shared value whose sampling is creation-time state. A cutout's
|
|
172
|
+
* shadow transforms the same way; lit's lightMap (aUV2) is exempt.
|
|
173
|
+
* Scrolling surfaces (water, a conveyor) drive it per frame with
|
|
174
|
+
* `setMeshParams(mesh, { uMapTransform: [ru, rv, ou, ov] })`. Needs a
|
|
175
|
+
* map; not with triplanar (whose repeat is uTriplanar). */
|
|
176
|
+
mapTransform?: { offset?: [number, number]; repeat?: [number, number] }
|
|
177
|
+
/** Skin positions (and lit normals) by the "skinned" layout's aJoints/
|
|
178
|
+
* aWeights against the `uBones` palette texture - the rigged-model
|
|
179
|
+
* variant createModel picks for skinned parts. The material then
|
|
180
|
+
* requires "skinned" geometry, and something must bind `uBones`
|
|
181
|
+
* (createModel binds each skin's palette texture to its meshes and
|
|
182
|
+
* the spatial flush writes it from the joints). The shadow variants
|
|
183
|
+
* (depth and cutout) skin the same way, so a caster casts its pose. */
|
|
184
|
+
skinned?: boolean
|
|
185
|
+
/** Draw instanced meshes (createInstancedMesh / `<InstancedMesh>`):
|
|
186
|
+
* the vertex stage places each copy by its instance matrix under the
|
|
187
|
+
* mesh's uModel, and the material declares INSTANCE_MATRIX_ATTRIBUTES,
|
|
188
|
+
* so it draws populated meshes ONLY (a createMesh mesh is rejected at
|
|
189
|
+
* add()). The shadow variants place the same way, so a fleet casts.
|
|
190
|
+
* Unity's per-material "Enable GPU Instancing"; Three's built-ins do
|
|
191
|
+
* it implicitly on an InstancedMesh. */
|
|
192
|
+
instanced?: boolean
|
|
193
|
+
/** Multiply the base by a per-instance color (implies `instanced`):
|
|
194
|
+
* the instance's style record is one premultiplied linear [r, g, b, a]
|
|
195
|
+
* (INSTANCE_COLOR_ATTRIBUTES), starting white, written with
|
|
196
|
+
* setInstanceStyle or `<Instance style>`. Three's setColorAt, Godot's
|
|
197
|
+
* MultiMesh instance color. A cutout's shadow multiplies by the same
|
|
198
|
+
* color, so alpha below `alphaTest` cuts the shadow with the pixels. */
|
|
199
|
+
instanceColors?: boolean
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// The instance attributes a stock instanced material declares: the
|
|
203
|
+
// matrix always, the color record beside it under instanceColors.
|
|
204
|
+
function stockInstanceAttributes(instanced: boolean, instanceColors: boolean): InstanceAttribute[] | undefined {
|
|
205
|
+
if (!instanced) return undefined
|
|
206
|
+
return instanceColors ? [...INSTANCE_MATRIX_ATTRIBUTES, ...INSTANCE_COLOR_ATTRIBUTES] : INSTANCE_MATRIX_ATTRIBUTES
|
|
207
|
+
}
|
|
208
|
+
// A fresh instance's color: white, the multiplicative identity, so an
|
|
209
|
+
// instance never written shows the material's own color (Godot's default;
|
|
210
|
+
// Three's zero-filled instanceColor is the trap avoided here).
|
|
211
|
+
const INSTANCE_COLOR_DEFAULT = [1, 1, 1, 1]
|
|
212
|
+
|
|
213
|
+
/** The uMapTransform vec4 for a mapTransform option: [repeatU, repeatV,
|
|
214
|
+
* offsetU, offsetV]. */
|
|
215
|
+
function mapTransformParam(t: { offset?: [number, number]; repeat?: [number, number] }): number[] {
|
|
216
|
+
let repeat = t.repeat ?? [1, 1]
|
|
217
|
+
let offset = t.offset ?? [0, 0]
|
|
218
|
+
return [repeat[0], repeat[1], offset[0], offset[1]]
|
|
151
219
|
}
|
|
152
220
|
|
|
153
221
|
/**
|
|
@@ -156,19 +224,26 @@ export type UnlitOptions = {
|
|
|
156
224
|
* light list); see the scene-graph research note.
|
|
157
225
|
*/
|
|
158
226
|
export function unlit(opts: UnlitOptions = {}): Material {
|
|
159
|
-
let
|
|
160
|
-
let a = color.length === 4 ? color[3] : 1
|
|
161
|
-
let uColor = [color[0] * a, color[1] * a, color[2] * a, a]
|
|
227
|
+
let uColor = colorParam(opts.color ?? [1, 1, 1])
|
|
162
228
|
let map = opts.map !== undefined
|
|
163
229
|
let transparent = opts.transparent === true
|
|
164
230
|
let cull = opts.cull ?? "back"
|
|
165
231
|
let alphaTest = opts.alphaTest !== undefined
|
|
166
|
-
let
|
|
232
|
+
let fog = opts.fog !== false
|
|
233
|
+
let mapTransform = opts.mapTransform !== undefined
|
|
234
|
+
let skinned = opts.skinned === true
|
|
235
|
+
let instanceColors = opts.instanceColors === true
|
|
236
|
+
let instanced = opts.instanced === true || instanceColors
|
|
237
|
+
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("|")
|
|
167
239
|
let cls = unlitClasses.get(key)
|
|
168
240
|
if (cls === undefined) {
|
|
169
241
|
cls = shaderMaterialClass({
|
|
170
|
-
vertex:
|
|
171
|
-
fragment: unlitFragment(map, alphaTest, transparent),
|
|
242
|
+
vertex: unlitVertex({ skinned, instanced, instanceColors }),
|
|
243
|
+
fragment: unlitFragment({ map, alphaTest, transparent, fog, mapTransform, instanceColors }),
|
|
244
|
+
shadowVertex: instanced ? shadowDepthVertex(skinned, true) : undefined,
|
|
245
|
+
instanceAttributes: stockInstanceAttributes(instanced, instanceColors),
|
|
246
|
+
instanceStyle: instanceColors ? INSTANCE_COLOR_DEFAULT : undefined,
|
|
172
247
|
transparent,
|
|
173
248
|
cull,
|
|
174
249
|
label: "scene-unlit-" + key,
|
|
@@ -177,10 +252,14 @@ export function unlit(opts: UnlitOptions = {}): Material {
|
|
|
177
252
|
}
|
|
178
253
|
let params: ShaderParams = { uColor }
|
|
179
254
|
if (alphaTest) params.uAlphaTest = opts.alphaTest!
|
|
255
|
+
if (mapTransform) params.uMapTransform = mapTransformParam(opts.mapTransform!)
|
|
180
256
|
return cls.instance({
|
|
181
257
|
params,
|
|
182
258
|
textures: map ? { uMap: opts.map! } : undefined,
|
|
183
|
-
shadow:
|
|
259
|
+
shadow:
|
|
260
|
+
alphaTest && map
|
|
261
|
+
? unlitShadowMaterial(shadowCull(cull), skinned, instanced, instanceColors, uColor, opts.alphaTest!, opts.map!, mapTransform ? params.uMapTransform as number[] : undefined)
|
|
262
|
+
: undefined,
|
|
184
263
|
})
|
|
185
264
|
}
|
|
186
265
|
|
|
@@ -198,6 +277,58 @@ export type LitOptions = UnlitOptions & {
|
|
|
198
277
|
* each part's size or UVs; the map must be created with
|
|
199
278
|
* `wrap: "repeat"`. */
|
|
200
279
|
triplanar?: number
|
|
280
|
+
/** A tangent-space normal map (OpenGL-style +Y, as glTF mandates),
|
|
281
|
+
* sampled at the same uv as `map` and bending the lit normal - track
|
|
282
|
+
* relief, kart panel lines, without triangles. The tangent frame is
|
|
283
|
+
* built per fragment from screen-space derivatives (NORMAL_MAP in
|
|
284
|
+
* `@solidrt/3d/glsl` - Three's untangented path), so ANY UV-mapped
|
|
285
|
+
* geometry works with no tangent channel; the trade is mild seams on
|
|
286
|
+
* mirrored UVs. Not with `triplanar` (which samples by world position).
|
|
287
|
+
* Wants `mipmap: true` at creation like any map seen at distance. */
|
|
288
|
+
normalMap?: TextureId
|
|
289
|
+
/** How strongly the normal map bends the surface, default 1 (0 flattens
|
|
290
|
+
* it). One float, as in Unity (_BumpScale) and Godot (normal_scale) -
|
|
291
|
+
* not Three's Vector2, whose second component exists to flip
|
|
292
|
+
* DirectX-style green channels. */
|
|
293
|
+
normalScale?: number
|
|
294
|
+
/** Light the surface emits, [r, g, b] 0..1, sRGB like `color` - added
|
|
295
|
+
* after the lighting terms, unaffected by lights and shadows, fogged
|
|
296
|
+
* like everything else. Defaults to WHITE when `emissiveMap` is given
|
|
297
|
+
* (the map is the emission - fixing Three's black-default gotcha, where
|
|
298
|
+
* an emissiveMap alone shows nothing) and to off otherwise. */
|
|
299
|
+
emissive?: [number, number, number]
|
|
300
|
+
/** Scales `emissive` in linear light, default 1: values above 1 for a
|
|
301
|
+
* glow that tone mapping can bloom (Three's emissiveIntensity, Godot's
|
|
302
|
+
* emission_energy_multiplier, glTF's KHR_materials_emissive_strength). */
|
|
303
|
+
emissiveIntensity?: number
|
|
304
|
+
/** A texture multiplying `emissive` per fragment - lamps, screens,
|
|
305
|
+
* nitro glow baked into one map. Sampled at the same uv as `map`;
|
|
306
|
+
* create it with `format: "rgba8-srgb"` like `map`. */
|
|
307
|
+
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
|
+
/** A baked-light texture (an offline render, an AO+GI bake), sampled by
|
|
324
|
+
* the geometry's aUV2 channel and ADDED to the light sum like the
|
|
325
|
+
* hemisphere term - a fully baked scene runs with no lights at all
|
|
326
|
+
* (Three's lightMap; Unity and Godot bake at scene level, but here the
|
|
327
|
+
* material picks the program). The geometry must carry aUV2
|
|
328
|
+
* (withAttribute) or add() throws. */
|
|
329
|
+
lightMap?: TextureId
|
|
330
|
+
/** Scales the lightMap, default 1. */
|
|
331
|
+
lightMapIntensity?: number
|
|
201
332
|
/**
|
|
202
333
|
* Receive the scene's directional shadows (default true, like Godot and
|
|
203
334
|
* Three): each casting light's term is multiplied by its shadow-map
|
|
@@ -213,100 +344,54 @@ export type LitOptions = UnlitOptions & {
|
|
|
213
344
|
receiveShadow?: boolean
|
|
214
345
|
}
|
|
215
346
|
|
|
216
|
-
// The lit
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
//
|
|
347
|
+
// The lit program is built by litFragment in ./glsl - the same builder an
|
|
348
|
+
// app calls to get a lit material with its own GLSL in it, composed from
|
|
349
|
+
// 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
|
|
351
|
+
// faces lights them with the normal flipped, else a double-sided leaf's
|
|
352
|
+
// back is black) x alphaTest (the cutoff itself is a per-entry uniform,
|
|
353
|
+
// one class for every value) x fog (the scene's fog composed last, or
|
|
354
|
+
// left out of the program) x the surface maps (normalMap, emissive /
|
|
355
|
+
// emissiveMap, specularMap, lightMap) x mapTransform. The key's
|
|
356
|
+
// dimensionality costs nothing by itself: classes are created lazily per
|
|
357
|
+
// combination USED, so the program count is the app's distinct material
|
|
358
|
+
// configurations, bounded by its material count, never by this tuple's
|
|
359
|
+
// width. An opaque class writes alpha 1 (see
|
|
360
|
+
// unlitFragment). Lights arrive through the scene's shared params (light
|
|
361
|
+
// nodes); the base color, map and highlight are per entry. The shadow set
|
|
362
|
+
// is shared too and indexed like the lights: one atlas sampler,
|
|
225
363
|
// directional light i's maps (one, or its cascades) as map slots
|
|
226
364
|
// uShadowFirst[i] .. + uShadowCount[i] with a tile rect and a matrix
|
|
227
365
|
// each, and its biases (target-level, bound by the scene); uShadowCount
|
|
228
366
|
// 0 means it does not cast; SHADOW_LOOKUP turns the index into the factor.
|
|
229
367
|
// The option combination that picks a lit class: the class-cache key is
|
|
230
|
-
// its values in this order, and the
|
|
231
|
-
//
|
|
368
|
+
// its values in this order, and the same object builds the program, so
|
|
369
|
+
// the two cannot drift apart. `lit` fills no slot - an app that does
|
|
370
|
+
// reaches for litFragment directly, and owns the class it builds.
|
|
232
371
|
type LitClass = {
|
|
233
372
|
map: boolean
|
|
234
373
|
vertexColors: boolean
|
|
235
374
|
triplanar: boolean
|
|
236
375
|
transparent: boolean
|
|
237
|
-
|
|
376
|
+
receiveShadow: boolean
|
|
238
377
|
cull: CullMode
|
|
239
378
|
alphaTest: boolean
|
|
379
|
+
fog: boolean
|
|
380
|
+
normalMap: boolean
|
|
381
|
+
emissive: boolean
|
|
382
|
+
emissiveMap: boolean
|
|
383
|
+
specularMap: boolean
|
|
384
|
+
env: boolean
|
|
385
|
+
lightMap: boolean
|
|
386
|
+
mapTransform: boolean
|
|
387
|
+
skinned: boolean
|
|
388
|
+
instanced: boolean
|
|
389
|
+
instanceColors: boolean
|
|
240
390
|
}
|
|
241
391
|
|
|
242
392
|
function litClassKey(c: LitClass): string {
|
|
243
393
|
return Object.values(c).join("|")
|
|
244
394
|
}
|
|
245
|
-
|
|
246
|
-
function litFragment(c: LitClass): string {
|
|
247
|
-
let { map, vertexColors, triplanar, shadow, alphaTest } = c
|
|
248
|
-
let backFaces = c.cull !== "back"
|
|
249
|
-
return glsl`
|
|
250
|
-
in vec3 vWorldPos;
|
|
251
|
-
in vec3 vNormal;
|
|
252
|
-
in vec2 vUv;
|
|
253
|
-
${vertexColors ? "in vec4 vColor;" : ""}
|
|
254
|
-
uniform vec4 uColor;
|
|
255
|
-
${map ? "uniform sampler2D uMap;" : ""}
|
|
256
|
-
uniform float uSpecular;
|
|
257
|
-
uniform float uShininess;
|
|
258
|
-
${triplanar ? "uniform float uTriplanar;" : ""}
|
|
259
|
-
${alphaTest ? "uniform float uAlphaTest;" : ""}
|
|
260
|
-
uniform vec3 uCamPos;
|
|
261
|
-
uniform vec3 uHemiSky;
|
|
262
|
-
uniform vec3 uHemiGround;
|
|
263
|
-
uniform int uLightCount;
|
|
264
|
-
uniform vec3 uLightDir[${MAX_LIGHTS}];
|
|
265
|
-
uniform vec3 uLightColor[${MAX_LIGHTS}];
|
|
266
|
-
${
|
|
267
|
-
shadow
|
|
268
|
-
? `${SHADOW_SLOTS}
|
|
269
|
-
${SHADOW}
|
|
270
|
-
${SHADOW_LOOKUP}`
|
|
271
|
-
: ""
|
|
272
|
-
}
|
|
273
|
-
${HEMISPHERE}
|
|
274
|
-
${LAMBERT}
|
|
275
|
-
${BLINN_SPECULAR}
|
|
276
|
-
|
|
277
|
-
void main() {
|
|
278
|
-
vec3 n = normalize(vNormal);
|
|
279
|
-
${backFaces ? "if (!gl_FrontFacing) n = -n;" : ""}
|
|
280
|
-
vec4 base = uColor;
|
|
281
|
-
${
|
|
282
|
-
map
|
|
283
|
-
? triplanar
|
|
284
|
-
? `vec3 w = pow(abs(n), vec3(4.0));
|
|
285
|
-
w /= w.x + w.y + w.z;
|
|
286
|
-
vec3 p = vWorldPos * uTriplanar;
|
|
287
|
-
base *= texture(uMap, p.yz) * w.x + texture(uMap, p.xz) * w.y + texture(uMap, p.xy) * w.z;`
|
|
288
|
-
: "base *= texture(uMap, vUv);"
|
|
289
|
-
: ""
|
|
290
|
-
}
|
|
291
|
-
${vertexColors ? "base *= vColor;" : ""}
|
|
292
|
-
${alphaTest ? "if (base.a < uAlphaTest) discard;" : ""}
|
|
293
|
-
vec3 v = normalize(uCamPos - vWorldPos);
|
|
294
|
-
vec3 light = hemisphere(n, uHemiSky, uHemiGround);
|
|
295
|
-
vec3 spec = vec3(0.0);
|
|
296
|
-
for (int i = 0; i < ${MAX_LIGHTS}; i++) {
|
|
297
|
-
if (i >= uLightCount) break;
|
|
298
|
-
vec3 l = uLightDir[i];
|
|
299
|
-
${
|
|
300
|
-
shadow ? "float s = lightShadow(i, vWorldPos, n);" : "float s = 1.0;"
|
|
301
|
-
}
|
|
302
|
-
light += uLightColor[i] * lambert(n, l) * s;
|
|
303
|
-
spec += uLightColor[i] * blinnSpecular(n, v, l, uShininess) * s;
|
|
304
|
-
}
|
|
305
|
-
fragColor = vec4(base.rgb * light + spec * uSpecular * base.a, ${c.transparent ? "base.a" : "1.0"});
|
|
306
|
-
}
|
|
307
|
-
`
|
|
308
|
-
}
|
|
309
|
-
|
|
310
395
|
let litClasses = new Map<string, ShaderMaterialClass>()
|
|
311
396
|
|
|
312
397
|
/**
|
|
@@ -320,61 +405,328 @@ let litClasses = new Map<string, ShaderMaterialClass>()
|
|
|
320
405
|
* starts at zero: set at least one of the two.
|
|
321
406
|
*/
|
|
322
407
|
export function lit(opts: LitOptions = {}): Material {
|
|
323
|
-
let
|
|
324
|
-
let a = color.length === 4 ? color[3] : 1
|
|
325
|
-
let uColor = [color[0] * a, color[1] * a, color[2] * a, a]
|
|
408
|
+
let uColor = colorParam(opts.color ?? [1, 1, 1])
|
|
326
409
|
let map = opts.map !== undefined
|
|
327
410
|
let triplanar = map && opts.triplanar !== undefined
|
|
328
411
|
let alphaTest = opts.alphaTest !== undefined
|
|
329
412
|
let cull = opts.cull ?? "back"
|
|
413
|
+
let normalMap = opts.normalMap !== undefined
|
|
414
|
+
let emissiveMap = opts.emissiveMap !== undefined
|
|
415
|
+
let emissive = opts.emissive !== undefined || emissiveMap
|
|
416
|
+
let specularMap = opts.specularMap !== undefined
|
|
417
|
+
let env = opts.reflectivity !== undefined
|
|
418
|
+
let lightMap = opts.lightMap !== undefined
|
|
419
|
+
let mapTransform = opts.mapTransform !== undefined
|
|
420
|
+
if (env && !(Number.isFinite(opts.reflectivity) && opts.reflectivity! >= 0 && opts.reflectivity! <= 1)) {
|
|
421
|
+
throw new Error("lit: reflectivity must be a number in 0..1, got " + opts.reflectivity)
|
|
422
|
+
}
|
|
423
|
+
if (triplanar && normalMap) throw new Error("lit: normalMap cannot combine with triplanar (normal maps sample by uv)")
|
|
424
|
+
if (triplanar && mapTransform) throw new Error("lit: mapTransform cannot combine with triplanar (its repeat is the triplanar value)")
|
|
425
|
+
if (mapTransform && !map && !normalMap && !emissiveMap && !specularMap) {
|
|
426
|
+
throw new Error("lit: mapTransform without a map to transform")
|
|
427
|
+
}
|
|
330
428
|
let flags: LitClass = {
|
|
331
429
|
map,
|
|
332
430
|
vertexColors: opts.vertexColors === true,
|
|
333
431
|
triplanar,
|
|
334
432
|
transparent: opts.transparent === true,
|
|
335
|
-
|
|
433
|
+
receiveShadow: opts.receiveShadow !== false,
|
|
336
434
|
cull,
|
|
337
435
|
alphaTest,
|
|
436
|
+
fog: opts.fog !== false,
|
|
437
|
+
normalMap,
|
|
438
|
+
emissive,
|
|
439
|
+
emissiveMap,
|
|
440
|
+
specularMap,
|
|
441
|
+
env,
|
|
442
|
+
lightMap,
|
|
443
|
+
mapTransform,
|
|
444
|
+
skinned: opts.skinned === true,
|
|
445
|
+
instanced: opts.instanced === true || opts.instanceColors === true,
|
|
446
|
+
instanceColors: opts.instanceColors === true,
|
|
338
447
|
}
|
|
339
448
|
let key = litClassKey(flags)
|
|
340
449
|
let cls = litClasses.get(key)
|
|
341
450
|
if (cls === undefined) {
|
|
342
451
|
cls = shaderMaterialClass({
|
|
343
|
-
vertex: flags
|
|
452
|
+
vertex: litVertex(flags),
|
|
344
453
|
fragment: litFragment(flags),
|
|
454
|
+
shadowVertex: flags.instanced ? shadowDepthVertex(flags.skinned, true) : undefined,
|
|
455
|
+
instanceAttributes: stockInstanceAttributes(flags.instanced, flags.instanceColors),
|
|
456
|
+
instanceStyle: flags.instanceColors ? INSTANCE_COLOR_DEFAULT : undefined,
|
|
345
457
|
transparent: flags.transparent,
|
|
346
458
|
cull,
|
|
347
459
|
label: "scene-lit-" + key,
|
|
348
460
|
})
|
|
349
461
|
litClasses.set(key, cls)
|
|
350
462
|
}
|
|
351
|
-
let params: ShaderParams = {
|
|
463
|
+
let params: ShaderParams = {
|
|
464
|
+
uColor,
|
|
465
|
+
uSpecular: opts.specular ?? (specularMap ? 1 : SURFACE_DEFAULTS.specular),
|
|
466
|
+
uShininess: opts.shininess ?? SURFACE_DEFAULTS.shininess,
|
|
467
|
+
}
|
|
352
468
|
if (triplanar) params.uTriplanar = opts.triplanar!
|
|
353
469
|
if (alphaTest) params.uAlphaTest = opts.alphaTest!
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
470
|
+
if (normalMap) params.uNormalScale = opts.normalScale ?? 1
|
|
471
|
+
if (env) params.uReflectivity = opts.reflectivity!
|
|
472
|
+
if (emissive) {
|
|
473
|
+
let k = opts.emissiveIntensity ?? 1
|
|
474
|
+
params.uEmissive = linearColor(opts.emissive ?? [1, 1, 1]).map(c => c * k)
|
|
475
|
+
}
|
|
476
|
+
if (lightMap) params.uLightMapIntensity = opts.lightMapIntensity ?? 1
|
|
477
|
+
if (mapTransform) params.uMapTransform = mapTransformParam(opts.mapTransform!)
|
|
478
|
+
let textures: TextureBindings | undefined
|
|
479
|
+
if (map || normalMap || emissiveMap || specularMap || lightMap) {
|
|
480
|
+
textures = {}
|
|
481
|
+
if (map) textures.uMap = opts.map!
|
|
482
|
+
if (normalMap) textures.uNormalMap = opts.normalMap!
|
|
483
|
+
if (emissiveMap) textures.uEmissiveMap = opts.emissiveMap!
|
|
484
|
+
if (specularMap) textures.uSpecularMap = opts.specularMap!
|
|
485
|
+
if (lightMap) textures.uLightMap = opts.lightMap!
|
|
486
|
+
}
|
|
487
|
+
// A mapped cutout casts its cutout, triplanar included (the shadow
|
|
488
|
+
// source resolves the base exactly as the main one does). A color-only
|
|
489
|
+
// alphaTest is a constant over the mesh - all or nothing, and nothing
|
|
490
|
+
// needs no program - so it keeps the plain cull-only variant.
|
|
357
491
|
let material = cls.instance({
|
|
358
492
|
params,
|
|
359
|
-
textures
|
|
360
|
-
shadow:
|
|
493
|
+
textures,
|
|
494
|
+
shadow:
|
|
495
|
+
alphaTest && map
|
|
496
|
+
? litShadowMaterial(flags, uColor, opts.alphaTest!, opts.triplanar, opts.map!, mapTransform ? params.uMapTransform as number[] : undefined)
|
|
497
|
+
: undefined,
|
|
361
498
|
})
|
|
362
499
|
return material
|
|
363
500
|
}
|
|
364
501
|
|
|
502
|
+
export type StandardOptions = Omit<LitOptions, "specular" | "shininess" | "specularMap" | "reflectivity"> & {
|
|
503
|
+
/** How metallic the surface is, 0..1 (default 0; 1 when `metalnessMap`
|
|
504
|
+
* is given - the map is the value): a metal has no diffuse and
|
|
505
|
+
* reflects tinted by `color`, a dielectric reflects 4% face-on.
|
|
506
|
+
* Three's metalness; metallic in Godot, Unity and glTF. */
|
|
507
|
+
metalness?: number
|
|
508
|
+
/** Perceptual roughness, 0..1 (default 1): 0 a mirror, 1 fully matte;
|
|
509
|
+
* it widens the highlight and blurs the environment reflection alike.
|
|
510
|
+
* Unity's smoothness is 1 - roughness. */
|
|
511
|
+
roughness?: number
|
|
512
|
+
/** A texture whose BLUE channel scales `metalness` per fragment, and
|
|
513
|
+
* `roughnessMap` one whose GREEN channel scales `roughness`: Three's
|
|
514
|
+
* two options over glTF's ONE packed metallicRoughnessTexture, so
|
|
515
|
+
* pass the same texture to both (createModel's `maps` does). Data
|
|
516
|
+
* maps, created as plain rgba8; sampled at the same uv as `map`. */
|
|
517
|
+
metalnessMap?: TextureId
|
|
518
|
+
roughnessMap?: TextureId
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// The `standard` class key: lit's minus the Blinn-Phong slots (env is
|
|
522
|
+
// always on, the environment being intrinsic to the model) plus the two
|
|
523
|
+
// packed maps. Its cutout shadow is lit's own, keyed on the shared subset
|
|
524
|
+
// in lit's order, so the two materials share shadow programs.
|
|
525
|
+
type StandardClass = Omit<LitClass, "specularMap" | "env"> & { metalnessMap: boolean; roughnessMap: boolean }
|
|
526
|
+
|
|
527
|
+
function standardClassKey(c: StandardClass): string {
|
|
528
|
+
return Object.values(c).join("|")
|
|
529
|
+
}
|
|
530
|
+
let standardClasses = new Map<string, ShaderMaterialClass>()
|
|
531
|
+
|
|
532
|
+
function standardShadowFlags(c: StandardClass): LitClass {
|
|
533
|
+
return {
|
|
534
|
+
map: c.map,
|
|
535
|
+
vertexColors: c.vertexColors,
|
|
536
|
+
triplanar: c.triplanar,
|
|
537
|
+
transparent: c.transparent,
|
|
538
|
+
receiveShadow: c.receiveShadow,
|
|
539
|
+
cull: c.cull,
|
|
540
|
+
alphaTest: c.alphaTest,
|
|
541
|
+
fog: c.fog,
|
|
542
|
+
normalMap: c.normalMap,
|
|
543
|
+
emissive: c.emissive,
|
|
544
|
+
emissiveMap: c.emissiveMap,
|
|
545
|
+
specularMap: false,
|
|
546
|
+
env: false,
|
|
547
|
+
lightMap: c.lightMap,
|
|
548
|
+
mapTransform: c.mapTransform,
|
|
549
|
+
skinned: c.skinned,
|
|
550
|
+
instanced: c.instanced,
|
|
551
|
+
instanceColors: c.instanceColors,
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* The metalness/roughness material, the look authored assets expect
|
|
557
|
+
* (Three's MeshStandardMaterial, Godot's StandardMaterial3D, Unity's
|
|
558
|
+
* Standard): the same base, maps, cutout, shadows, emissive, fog and
|
|
559
|
+
* lights as `lit`, shaded with GGX specular (PBR in `@solidrt/3d/glsl`)
|
|
560
|
+
* and the scene's environment sampled ALWAYS as the split sum over its
|
|
561
|
+
* mip chain - no `reflectivity` switch, the environment is intrinsic to
|
|
562
|
+
* the model. Light intensities read as lit's (1 lights a white matte
|
|
563
|
+
* surface to 1). Without an environment set a metal shows only its
|
|
564
|
+
* highlights - its diffuse is zero and there is nothing to reflect, as
|
|
565
|
+
* in Three and Godot - so give the scene one. One program per option
|
|
566
|
+
* combination, shared by every instance, like lit.
|
|
567
|
+
*/
|
|
568
|
+
export function standard(opts: StandardOptions = {}): Material {
|
|
569
|
+
let uColor = colorParam(opts.color ?? [1, 1, 1])
|
|
570
|
+
let map = opts.map !== undefined
|
|
571
|
+
let triplanar = map && opts.triplanar !== undefined
|
|
572
|
+
let alphaTest = opts.alphaTest !== undefined
|
|
573
|
+
let cull = opts.cull ?? "back"
|
|
574
|
+
let normalMap = opts.normalMap !== undefined
|
|
575
|
+
let emissiveMap = opts.emissiveMap !== undefined
|
|
576
|
+
let emissive = opts.emissive !== undefined || emissiveMap
|
|
577
|
+
let metalnessMap = opts.metalnessMap !== undefined
|
|
578
|
+
let roughnessMap = opts.roughnessMap !== undefined
|
|
579
|
+
let lightMap = opts.lightMap !== undefined
|
|
580
|
+
let mapTransform = opts.mapTransform !== undefined
|
|
581
|
+
let metalness = opts.metalness ?? (metalnessMap ? 1 : SURFACE_DEFAULTS.metalness)
|
|
582
|
+
let roughness = opts.roughness ?? SURFACE_DEFAULTS.roughness
|
|
583
|
+
if (!(Number.isFinite(metalness) && metalness >= 0 && metalness <= 1)) {
|
|
584
|
+
throw new Error("standard: metalness must be a number in 0..1, got " + opts.metalness)
|
|
585
|
+
}
|
|
586
|
+
if (!(Number.isFinite(roughness) && roughness >= 0 && roughness <= 1)) {
|
|
587
|
+
throw new Error("standard: roughness must be a number in 0..1, got " + opts.roughness)
|
|
588
|
+
}
|
|
589
|
+
if (triplanar && normalMap) throw new Error("standard: normalMap cannot combine with triplanar (normal maps sample by uv)")
|
|
590
|
+
if (triplanar && (metalnessMap || roughnessMap)) {
|
|
591
|
+
throw new Error("standard: metalnessMap and roughnessMap cannot combine with triplanar (they sample by uv)")
|
|
592
|
+
}
|
|
593
|
+
if (triplanar && mapTransform) throw new Error("standard: mapTransform cannot combine with triplanar (its repeat is the triplanar value)")
|
|
594
|
+
if (mapTransform && !map && !normalMap && !emissiveMap && !metalnessMap && !roughnessMap) {
|
|
595
|
+
throw new Error("standard: mapTransform without a map to transform")
|
|
596
|
+
}
|
|
597
|
+
let flags: StandardClass = {
|
|
598
|
+
map,
|
|
599
|
+
vertexColors: opts.vertexColors === true,
|
|
600
|
+
triplanar,
|
|
601
|
+
transparent: opts.transparent === true,
|
|
602
|
+
receiveShadow: opts.receiveShadow !== false,
|
|
603
|
+
cull,
|
|
604
|
+
alphaTest,
|
|
605
|
+
fog: opts.fog !== false,
|
|
606
|
+
normalMap,
|
|
607
|
+
emissive,
|
|
608
|
+
emissiveMap,
|
|
609
|
+
lightMap,
|
|
610
|
+
mapTransform,
|
|
611
|
+
skinned: opts.skinned === true,
|
|
612
|
+
instanced: opts.instanced === true || opts.instanceColors === true,
|
|
613
|
+
instanceColors: opts.instanceColors === true,
|
|
614
|
+
metalnessMap,
|
|
615
|
+
roughnessMap,
|
|
616
|
+
}
|
|
617
|
+
let key = standardClassKey(flags)
|
|
618
|
+
let cls = standardClasses.get(key)
|
|
619
|
+
if (cls === undefined) {
|
|
620
|
+
cls = shaderMaterialClass({
|
|
621
|
+
vertex: litVertex(flags),
|
|
622
|
+
fragment: standardFragment(flags),
|
|
623
|
+
shadowVertex: flags.instanced ? shadowDepthVertex(flags.skinned, true) : undefined,
|
|
624
|
+
instanceAttributes: stockInstanceAttributes(flags.instanced, flags.instanceColors),
|
|
625
|
+
instanceStyle: flags.instanceColors ? INSTANCE_COLOR_DEFAULT : undefined,
|
|
626
|
+
transparent: flags.transparent,
|
|
627
|
+
cull,
|
|
628
|
+
label: "scene-standard-" + key,
|
|
629
|
+
})
|
|
630
|
+
standardClasses.set(key, cls)
|
|
631
|
+
}
|
|
632
|
+
let params: ShaderParams = { uColor, uMetalness: metalness, uRoughness: roughness }
|
|
633
|
+
if (triplanar) params.uTriplanar = opts.triplanar!
|
|
634
|
+
if (alphaTest) params.uAlphaTest = opts.alphaTest!
|
|
635
|
+
if (normalMap) params.uNormalScale = opts.normalScale ?? 1
|
|
636
|
+
if (emissive) {
|
|
637
|
+
let k = opts.emissiveIntensity ?? 1
|
|
638
|
+
params.uEmissive = linearColor(opts.emissive ?? [1, 1, 1]).map(c => c * k)
|
|
639
|
+
}
|
|
640
|
+
if (lightMap) params.uLightMapIntensity = opts.lightMapIntensity ?? 1
|
|
641
|
+
if (mapTransform) params.uMapTransform = mapTransformParam(opts.mapTransform!)
|
|
642
|
+
let textures: TextureBindings | undefined
|
|
643
|
+
if (map || normalMap || emissiveMap || metalnessMap || roughnessMap || lightMap) {
|
|
644
|
+
textures = {}
|
|
645
|
+
if (map) textures.uMap = opts.map!
|
|
646
|
+
if (normalMap) textures.uNormalMap = opts.normalMap!
|
|
647
|
+
if (emissiveMap) textures.uEmissiveMap = opts.emissiveMap!
|
|
648
|
+
if (metalnessMap) textures.uMetalnessMap = opts.metalnessMap!
|
|
649
|
+
if (roughnessMap) textures.uRoughnessMap = opts.roughnessMap!
|
|
650
|
+
if (lightMap) textures.uLightMap = opts.lightMap!
|
|
651
|
+
}
|
|
652
|
+
return cls.instance({
|
|
653
|
+
params,
|
|
654
|
+
textures,
|
|
655
|
+
shadow:
|
|
656
|
+
alphaTest && map
|
|
657
|
+
? litShadowMaterial(standardShadowFlags(flags), uColor, opts.alphaTest!, opts.triplanar, opts.map!, mapTransform ? params.uMapTransform as number[] : undefined)
|
|
658
|
+
: undefined,
|
|
659
|
+
})
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
let litShadowClasses = new Map<string, ShaderMaterialClass>()
|
|
663
|
+
|
|
664
|
+
/** The shadow variant of a discarding lit material: litShadowFragment on
|
|
665
|
+
* the same vertex stage, culling the side the material's own pipeline
|
|
666
|
+
* keeps (Three's shadowSide rule), instanced with the same values so the
|
|
667
|
+
* cutout it casts is the one it draws. Its params carry only the uniforms
|
|
668
|
+
* its program declares - per-entry params reject unknown names. One class
|
|
669
|
+
* per lit class, an instance per material. */
|
|
670
|
+
function litShadowMaterial(
|
|
671
|
+
flags: LitClass,
|
|
672
|
+
uColor: number[],
|
|
673
|
+
uAlphaTest: number,
|
|
674
|
+
triplanar: number | undefined,
|
|
675
|
+
uMap: TextureId,
|
|
676
|
+
uMapTransform?: number[],
|
|
677
|
+
): Material {
|
|
678
|
+
// A skinned cutout keeps its skinning (flags.skinned rides into the
|
|
679
|
+
// key and the vertex stage), so the shadow it casts is posed: the
|
|
680
|
+
// shadow entry merges the mesh's uBones binding (Material.skinned).
|
|
681
|
+
let key = litClassKey(flags)
|
|
682
|
+
let cls = litShadowClasses.get(key)
|
|
683
|
+
if (cls === undefined) {
|
|
684
|
+
let fragment = litShadowFragment(flags)
|
|
685
|
+
if (fragment === undefined) throw new Error("litShadowMaterial: " + key + " cannot discard")
|
|
686
|
+
cls = shaderMaterialClass({
|
|
687
|
+
vertex: litVertex(flags),
|
|
688
|
+
fragment,
|
|
689
|
+
cull: shadowCull(flags.cull),
|
|
690
|
+
instanceAttributes: stockInstanceAttributes(flags.instanced, flags.instanceColors),
|
|
691
|
+
label: "scene-lit-shadow-" + key,
|
|
692
|
+
})
|
|
693
|
+
litShadowClasses.set(key, cls)
|
|
694
|
+
}
|
|
695
|
+
let params: ShaderParams = { uColor, uAlphaTest }
|
|
696
|
+
if (flags.triplanar) params.uTriplanar = triplanar!
|
|
697
|
+
if (uMapTransform !== undefined) params.uMapTransform = uMapTransform
|
|
698
|
+
let material = cls.instance({ params, textures: { uMap } })
|
|
699
|
+
// Only triplanar sampling reads the normal here: everywhere else the
|
|
700
|
+
// linker drops vNormal and uNormal reflects inactive, so writing it per
|
|
701
|
+
// move would warn every time. The value cannot matter, skip the write.
|
|
702
|
+
if (!flags.triplanar) material.normalMatrix = false
|
|
703
|
+
return material
|
|
704
|
+
}
|
|
705
|
+
|
|
365
706
|
// The shadow depth pass: position only, no color of interest (the target's
|
|
366
707
|
// depth texture is the output; the color write is the pipeline's minimum).
|
|
367
708
|
// Front faces culled, Three's shadowSide default: the map holds each
|
|
368
709
|
// caster's BACK surface, so a receiving front face at the same depth
|
|
369
710
|
// compares lit without a bias and acne needs no fighting on closed meshes.
|
|
370
|
-
|
|
711
|
+
// The skinned form applies the skin matrix first, so a posed caster's
|
|
712
|
+
// depth is its pose (the entry's uBones binding comes from the mesh, see
|
|
713
|
+
// Material.skinned); the instanced form places each copy by its instance
|
|
714
|
+
// matrix, so a fleet casts where its records stand - the stock instanced
|
|
715
|
+
// materials pass it as their class's shadowVertex.
|
|
716
|
+
function shadowDepthVertex(skinned: boolean, instanced: boolean): string {
|
|
717
|
+
let local = skinned ? "(skin * vec4(aPos, 1.0))" : "vec4(aPos, 1.0)"
|
|
718
|
+
return glsl`
|
|
371
719
|
in vec3 aPos;
|
|
720
|
+
${skinned ? SKIN_DECLS : ""}
|
|
721
|
+
${instanced ? INSTANCE_MATRIX : ""}
|
|
372
722
|
uniform mat4 uModel;
|
|
373
723
|
uniform mat4 uViewProj;
|
|
374
724
|
void main() {
|
|
375
|
-
|
|
725
|
+
${skinned ? SKIN_MATRIX : ""}
|
|
726
|
+
gl_Position = uViewProj * uModel * ${instanced ? `(instanceMatrix() * ${local})` : local};
|
|
376
727
|
}
|
|
377
728
|
`
|
|
729
|
+
}
|
|
378
730
|
|
|
379
731
|
const SHADOW_DEPTH_FRAGMENT = glsl`
|
|
380
732
|
void main() {
|
|
@@ -382,22 +734,23 @@ const SHADOW_DEPTH_FRAGMENT = glsl`
|
|
|
382
734
|
}
|
|
383
735
|
`
|
|
384
736
|
|
|
385
|
-
let shadowDepth = new Map<
|
|
737
|
+
let shadowDepth = new Map<string, Material>()
|
|
386
738
|
|
|
387
739
|
/** The override material of a scene's shadow view (internal): one class
|
|
388
|
-
* per cull mode for the app, built on first use. The default,
|
|
389
|
-
* is the caster's back surface (see above); a material culling
|
|
390
|
-
* otherwise carries its own variant as Material.shadow. */
|
|
391
|
-
export function shadowDepthMaterial(cull: CullMode = "front"): Material {
|
|
392
|
-
let
|
|
740
|
+
* per cull mode x skinned for the app, built on first use. The default,
|
|
741
|
+
* "front", is the caster's back surface (see above); a material culling
|
|
742
|
+
* 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" : "")
|
|
745
|
+
let material = shadowDepth.get(key)
|
|
393
746
|
if (material === undefined) {
|
|
394
747
|
material = shaderMaterialClass({
|
|
395
|
-
vertex:
|
|
748
|
+
vertex: shadowDepthVertex(skinned, false),
|
|
396
749
|
fragment: SHADOW_DEPTH_FRAGMENT,
|
|
397
750
|
cull,
|
|
398
|
-
label: "scene-shadow-depth-" +
|
|
751
|
+
label: "scene-shadow-depth-" + key,
|
|
399
752
|
}).instance()
|
|
400
|
-
shadowDepth.set(
|
|
753
|
+
shadowDepth.set(key, material)
|
|
401
754
|
}
|
|
402
755
|
return material
|
|
403
756
|
}
|
|
@@ -408,56 +761,56 @@ function shadowCull(cull: CullMode): CullMode {
|
|
|
408
761
|
return cull === "none" ? "none" : cull === "back" ? "front" : "back"
|
|
409
762
|
}
|
|
410
763
|
|
|
411
|
-
/** "back" maps to the default depth material, so its variant
|
|
412
|
-
* undefined
|
|
413
|
-
|
|
414
|
-
|
|
764
|
+
/** "back" unskinned maps to the default depth material, so its variant
|
|
765
|
+
* is undefined; a skinned class always needs its own (the default does
|
|
766
|
+
* not skin). */
|
|
767
|
+
function shadowVariant(cull: CullMode, skinned: boolean): Material | undefined {
|
|
768
|
+
if (cull === "back" && !skinned) return undefined
|
|
769
|
+
return shadowDepthMaterial(shadowCull(cull), skinned)
|
|
415
770
|
}
|
|
416
771
|
|
|
417
|
-
|
|
418
|
-
// against its alphaTest, the lit fragment's test minus everything else.
|
|
419
|
-
const SHADOW_CUTOUT_VERTEX = glsl`
|
|
420
|
-
in vec3 aPos;
|
|
421
|
-
in vec2 aUV;
|
|
422
|
-
out vec2 vUv;
|
|
423
|
-
uniform mat4 uModel;
|
|
424
|
-
uniform mat4 uViewProj;
|
|
425
|
-
void main() {
|
|
426
|
-
gl_Position = uViewProj * uModel * vec4(aPos, 1.0);
|
|
427
|
-
vUv = aUV;
|
|
428
|
-
}
|
|
429
|
-
`
|
|
430
|
-
|
|
431
|
-
const SHADOW_CUTOUT_FRAGMENT = glsl`
|
|
432
|
-
in vec2 vUv;
|
|
433
|
-
uniform sampler2D uMap;
|
|
434
|
-
uniform vec4 uColor;
|
|
435
|
-
uniform float uAlphaTest;
|
|
436
|
-
void main() {
|
|
437
|
-
if (texture(uMap, vUv).a * uColor.a < uAlphaTest) discard;
|
|
438
|
-
fragColor = vec4(1.0);
|
|
439
|
-
}
|
|
440
|
-
`
|
|
772
|
+
let unlitShadowClasses = new Map<string, ShaderMaterialClass>()
|
|
441
773
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
*
|
|
446
|
-
|
|
447
|
-
|
|
774
|
+
/** The shadow variant of a discarding unlit material: unlitShadowFragment
|
|
775
|
+
* on the same vertex stage (skinned when the material skins, so the
|
|
776
|
+
* cutout casts its pose), litShadowMaterial's unlit twin. unlit()'s
|
|
777
|
+
* only discard is the mapped cutout, so what varies is the shadow cull,
|
|
778
|
+
* skinning and whether the cutout's uv is transformed - one class per
|
|
779
|
+
* combination. An instance per material (its map, color, cutoff,
|
|
780
|
+
* transform). */
|
|
781
|
+
function unlitShadowMaterial(
|
|
782
|
+
cull: CullMode,
|
|
783
|
+
skinned: boolean,
|
|
784
|
+
instanced: boolean,
|
|
785
|
+
instanceColors: boolean,
|
|
786
|
+
uColor: number[],
|
|
787
|
+
uAlphaTest: number,
|
|
788
|
+
uMap: TextureId,
|
|
789
|
+
uMapTransform?: number[],
|
|
790
|
+
): Material {
|
|
791
|
+
let key = [cull, uMapTransform !== undefined, skinned, instanced, instanceColors].join("|")
|
|
792
|
+
let cls = unlitShadowClasses.get(key)
|
|
448
793
|
if (cls === undefined) {
|
|
794
|
+
let fragment = unlitShadowFragment({ map: true, alphaTest: true, mapTransform: uMapTransform !== undefined, instanceColors })
|
|
795
|
+
if (fragment === undefined) throw new Error("unlitShadowMaterial: the cutout options cannot discard")
|
|
449
796
|
cls = shaderMaterialClass({
|
|
450
|
-
vertex:
|
|
451
|
-
fragment
|
|
797
|
+
vertex: unlitVertex({ skinned, instanced, instanceColors }),
|
|
798
|
+
fragment,
|
|
452
799
|
cull,
|
|
453
|
-
|
|
800
|
+
instanceAttributes: stockInstanceAttributes(instanced, instanceColors),
|
|
801
|
+
label: "scene-unlit-shadow-" + key,
|
|
454
802
|
})
|
|
455
|
-
|
|
803
|
+
unlitShadowClasses.set(key, cls)
|
|
456
804
|
}
|
|
457
|
-
|
|
805
|
+
let params: ShaderParams = { uColor, uAlphaTest }
|
|
806
|
+
if (uMapTransform !== undefined) params.uMapTransform = uMapTransform
|
|
807
|
+
return cls.instance({ params, textures: { uMap } })
|
|
458
808
|
}
|
|
459
809
|
|
|
460
|
-
|
|
810
|
+
/** A sprite's options: unlit's, minus instancing (the billboard stage
|
|
811
|
+
* places one quad per mesh; an instanced billboard field is a custom
|
|
812
|
+
* class's vertex stage). */
|
|
813
|
+
export type SpriteOptions = Omit<UnlitOptions, "instanced" | "instanceColors"> & {
|
|
461
814
|
/** Which way the quad turns to face the camera. `"full"` (default,
|
|
462
815
|
* Three's Sprite): both axes follow the view, the quad is always flat
|
|
463
816
|
* to the screen. `"fixed-y"` (Godot's BILLBOARD_FIXED_Y): only the yaw
|
|
@@ -477,6 +830,7 @@ const SPRITE_VERTEX_SRC = glsl`
|
|
|
477
830
|
in vec3 aPos;
|
|
478
831
|
in vec2 aUV;
|
|
479
832
|
out vec2 vUv;
|
|
833
|
+
out vec3 vWorldPos;
|
|
480
834
|
uniform mat4 uModel;
|
|
481
835
|
uniform mat4 uViewProj;
|
|
482
836
|
uniform vec3 uCamRight;
|
|
@@ -486,6 +840,7 @@ const SPRITE_VERTEX_SRC = glsl`
|
|
|
486
840
|
vec3 center = uModel[3].xyz;
|
|
487
841
|
vec2 size = vec2(length(uModel[0].xyz), length(uModel[1].xyz));
|
|
488
842
|
vec3 world = center + uCamRight * (aPos.x * size.x) + uCamUp * (aPos.y * size.y);
|
|
843
|
+
vWorldPos = world;
|
|
489
844
|
gl_Position = uViewProj * vec4(world, 1.0);
|
|
490
845
|
vUv = aUV;
|
|
491
846
|
}
|
|
@@ -495,6 +850,7 @@ const SPRITE_FIXED_Y_VERTEX_SRC = glsl`
|
|
|
495
850
|
in vec3 aPos;
|
|
496
851
|
in vec2 aUV;
|
|
497
852
|
out vec2 vUv;
|
|
853
|
+
out vec3 vWorldPos;
|
|
498
854
|
uniform mat4 uModel;
|
|
499
855
|
uniform mat4 uViewProj;
|
|
500
856
|
uniform vec3 uCamPos;
|
|
@@ -507,6 +863,7 @@ const SPRITE_FIXED_Y_VERTEX_SRC = glsl`
|
|
|
507
863
|
float len = length(toCam);
|
|
508
864
|
vec3 right = len > 1e-6 ? vec3(toCam.z, 0.0, -toCam.x) / len : vec3(1.0, 0.0, 0.0);
|
|
509
865
|
vec3 world = center + right * (aPos.x * size.x) + vec3(0.0, aPos.y * size.y, 0.0);
|
|
866
|
+
vWorldPos = world;
|
|
510
867
|
gl_Position = uViewProj * vec4(world, 1.0);
|
|
511
868
|
vUv = aUV;
|
|
512
869
|
}
|
|
@@ -525,18 +882,22 @@ let spriteClasses = new Map<string, ShaderMaterialClass>()
|
|
|
525
882
|
* for an opaque one. Culling is off: a camera-facing quad has no back.
|
|
526
883
|
*/
|
|
527
884
|
export function sprite(opts: SpriteOptions = {}): Material {
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
885
|
+
// Excluded by the type and checked for a JS caller: the billboard stage
|
|
886
|
+
// has no instance placement, so the flags would otherwise be ignored.
|
|
887
|
+
if ((opts as UnlitOptions).instanced || (opts as UnlitOptions).instanceColors) {
|
|
888
|
+
throw new Error("sprite: instanced and instanceColors are not sprite options (one billboard per mesh)")
|
|
889
|
+
}
|
|
890
|
+
let uColor = colorParam(opts.color ?? [1, 1, 1])
|
|
531
891
|
let map = opts.map !== undefined
|
|
532
892
|
let transparent = opts.transparent !== false
|
|
533
893
|
let fixedY = opts.billboard === "fixed-y"
|
|
534
|
-
let
|
|
894
|
+
let fog = opts.fog !== false
|
|
895
|
+
let key = [map, transparent, fixedY, fog].join("|")
|
|
535
896
|
let cls = spriteClasses.get(key)
|
|
536
897
|
if (cls === undefined) {
|
|
537
898
|
cls = shaderMaterialClass({
|
|
538
899
|
vertex: fixedY ? SPRITE_FIXED_Y_VERTEX_SRC : SPRITE_VERTEX_SRC,
|
|
539
|
-
fragment: unlitFragment(map,
|
|
900
|
+
fragment: unlitFragment({ map, transparent, fog }),
|
|
540
901
|
transparent,
|
|
541
902
|
cull: "none",
|
|
542
903
|
label: "scene-sprite-" + key,
|
|
@@ -567,21 +928,51 @@ function needsHeader(source: string): boolean {
|
|
|
567
928
|
// engine's own attributeless fullscreen triangle (gl_VertexID, no vertex
|
|
568
929
|
// buffer), emitting the SAME vUV the shader-target contract provides: 0..1
|
|
569
930
|
// with origin at the displayed top-left - so a backdrop fragment written
|
|
570
|
-
// for createShaderTexture ports verbatim
|
|
931
|
+
// for createShaderTexture ports verbatim - plus vRay, the world-space
|
|
932
|
+
// view ray through the pixel: the vertex's own clip position carried back
|
|
933
|
+
// to world at the near and far planes through the camera's shared
|
|
934
|
+
// uInvViewProj, far minus near. Linear interpolation is exact for it
|
|
935
|
+
// (at a fixed clip depth the unprojected point is affine in clip x, y
|
|
936
|
+
// under either projection), and under an orthographic camera every ray is
|
|
937
|
+
// the camera's forward. Unnormalized: a sky fragment normalizes it.
|
|
571
938
|
const BACKGROUND_VERTEX = glsl`
|
|
939
|
+
uniform mat4 uInvViewProj;
|
|
572
940
|
out vec2 vUV;
|
|
941
|
+
out vec3 vRay;
|
|
573
942
|
void main() {
|
|
574
943
|
vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
|
|
575
944
|
vUV = p;
|
|
576
|
-
|
|
945
|
+
vec2 ndc = p * 2.0 - 1.0;
|
|
946
|
+
vec4 near = uInvViewProj * vec4(ndc, -1.0, 1.0);
|
|
947
|
+
vec4 far = uInvViewProj * vec4(ndc, 1.0, 1.0);
|
|
948
|
+
vRay = far.xyz / far.w - near.xyz / near.w;
|
|
949
|
+
gl_Position = vec4(ndc, 0.0, 1.0);
|
|
577
950
|
}
|
|
578
951
|
`
|
|
579
952
|
|
|
580
953
|
// Pipeline fragments get no vUV from the engine preamble (a pipeline's
|
|
581
954
|
// varyings are its own), so the background slot injects the full
|
|
582
|
-
// shader-target fragment contract itself: vUV, fragColor, iResolution
|
|
955
|
+
// shader-target fragment contract itself: vUV, fragColor, iResolution,
|
|
956
|
+
// its own vRay, and the OUTPUT set, so a sky can end with outputColor
|
|
957
|
+
// and take the scene's exposure and tone mapping.
|
|
583
958
|
const BACKGROUND_FRAGMENT_PREAMBLE =
|
|
584
|
-
"#version 300 es\nprecision highp float;\nin vec2 vUV;\nout vec4 fragColor;\nuniform vec2 iResolution;\n"
|
|
959
|
+
"#version 300 es\nprecision highp float;\nin vec2 vUV;\nin vec3 vRay;\nout vec4 fragColor;\nuniform vec2 iResolution;\n" + OUTPUT + "\n"
|
|
960
|
+
|
|
961
|
+
// The skybox fragment behind setBackground({ cube }): the view ray through
|
|
962
|
+
// the sky's rotation (the INVERSE turn, written by skyboxParams, so the
|
|
963
|
+
// sky itself turns by +rotation like a node would), then the cube lookup
|
|
964
|
+
// times the intensity, through the output
|
|
965
|
+
// stage (the preamble declares it) like every lit pixel. Opaque: the
|
|
966
|
+
// skybox replaces the clearColor exactly as a GLSL background does.
|
|
967
|
+
export const SKYBOX_FRAGMENT = glsl`
|
|
968
|
+
uniform samplerCube uSky;
|
|
969
|
+
uniform float uSkyIntensity;
|
|
970
|
+
uniform mat4 uSkyRotation;
|
|
971
|
+
void main() {
|
|
972
|
+
vec3 dir = mat3(uSkyRotation) * normalize(vRay);
|
|
973
|
+
fragColor = outputColor(texture(uSky, dir).rgb * uSkyIntensity, 1.0);
|
|
974
|
+
}
|
|
975
|
+
`
|
|
585
976
|
|
|
586
977
|
/** The scene's background pipeline (internal - reached via
|
|
587
978
|
* scene.setBackground): depth-free, attributeless, drawn as entry zero of
|
|
@@ -633,12 +1024,40 @@ export type ShaderMaterialClassOptions = {
|
|
|
633
1024
|
* beside the layout's own, and each drawn instance gets one record from
|
|
634
1025
|
* the mesh's instance buffer (interleaved floats in this order). A class
|
|
635
1026
|
* with instance attributes makes INSTANCED materials: attach their meshes
|
|
636
|
-
* with createInstancedMesh, which
|
|
637
|
-
* is rejected at add().
|
|
638
|
-
*
|
|
639
|
-
*
|
|
1027
|
+
* with createInstancedMesh or createRecordMesh, which carry the records
|
|
1028
|
+
* - a createMesh mesh is rejected at add(). Each attribute's `slot`
|
|
1029
|
+
* (default 0) picks the buffer: an instanced mesh binds the core's
|
|
1030
|
+
* matrix records (INSTANCE_MATRIX_ATTRIBUTES, exactly) to slot 0 and
|
|
1031
|
+
* the app's style records - any layout, written per instance with
|
|
1032
|
+
* setInstanceStyle - to slot 1; a record mesh is slot 0 only, its
|
|
1033
|
+
* records whatever the stage reads (a position/yaw/scale record beats
|
|
1034
|
+
* four vec4 columns for a JS-stepped fleet, and the composed uModel
|
|
1035
|
+
* still places the whole population).
|
|
1036
|
+
*/
|
|
1037
|
+
instanceAttributes?: InstanceAttribute[]
|
|
1038
|
+
/** The style record a fresh instance starts with (the slot-1
|
|
1039
|
+
* attributes' floats, in order); default zeros. The identity of
|
|
1040
|
+
* whatever the stage does with the record - white for a multiplied
|
|
1041
|
+
* tint. */
|
|
1042
|
+
instanceStyle?: ArrayLike<number>
|
|
1043
|
+
/**
|
|
1044
|
+
* The class's vertex stage reduced to POSITION - the instance
|
|
1045
|
+
* placement, a displacement, whatever moves vertices, minus every
|
|
1046
|
+
* color/normal/uv output - for the shadow pass. With it the class's
|
|
1047
|
+
* instances cast: a shadow view draws their meshes with a depth
|
|
1048
|
+
* pipeline built from this stage and the shared depth fragment
|
|
1049
|
+
* (culling the side the class culls, binding the instance slots this
|
|
1050
|
+
* stage reads - the records always, a style slot only when it names
|
|
1051
|
+
* one of its attributes), so
|
|
1052
|
+
* an instanced fleet's shadows land where its records put the
|
|
1053
|
+
* vertices. Same standard-uniform contract as `vertex` (uModel and
|
|
1054
|
+
* uViewProj required); one extra program per class, built at first
|
|
1055
|
+
* cast. Without it, instanced meshes are SKIPPED by shadow views
|
|
1056
|
+
* (`castShadow` does nothing for them) - a non-instanced class needs
|
|
1057
|
+
* it only when its vertex stage moves vertices. Depth-only: a cutout's
|
|
1058
|
+
* discard still goes through the `shadow` instance option.
|
|
640
1059
|
*/
|
|
641
|
-
|
|
1060
|
+
shadowVertex?: string
|
|
642
1061
|
/** Blend over what is behind, with the scene sorting this material's
|
|
643
1062
|
* meshes back-to-front after the opaque ones (see Material.transparent).
|
|
644
1063
|
* Sets the pipeline defaults blend "alpha" and depthWrite false; the
|
|
@@ -665,8 +1084,11 @@ export type ShaderMaterialInstanceOptions = {
|
|
|
665
1084
|
params?: ShaderParams
|
|
666
1085
|
textures?: TextureBindings
|
|
667
1086
|
/** The depth variant a shadow view draws this instance with (see
|
|
668
|
-
* Material.shadow): a cutout's discard,
|
|
669
|
-
*
|
|
1087
|
+
* Material.shadow): a cutout's discard, a placement the class-level
|
|
1088
|
+
* `shadowVertex` cannot express. Default: the class's shadowVertex
|
|
1089
|
+
* depth material when one is declared, else the depth pass with this
|
|
1090
|
+
* class's cull side, skinned like the class when its vertex skins by
|
|
1091
|
+
* uBones. */
|
|
670
1092
|
shadow?: Material
|
|
671
1093
|
}
|
|
672
1094
|
|
|
@@ -701,10 +1123,12 @@ export function shaderMaterialClass(opts: ShaderMaterialClassOptions): ShaderMat
|
|
|
701
1123
|
// shared params skipping undeclared names the omission would otherwise
|
|
702
1124
|
// surface as a silently untransformed render, not an error.
|
|
703
1125
|
for (let name of ["uModel", "uViewProj"]) {
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
1126
|
+
for (let source of opts.shadowVertex === undefined ? [opts.vertex] : [opts.vertex, opts.shadowVertex]) {
|
|
1127
|
+
if (!new RegExp("\\b" + name + "\\b").test(source)) {
|
|
1128
|
+
throw new Error(
|
|
1129
|
+
"shaderMaterial vertex stage must declare and use '" + name + "' (see the standard uniform set in AGENTS.md)",
|
|
1130
|
+
)
|
|
1131
|
+
}
|
|
708
1132
|
}
|
|
709
1133
|
}
|
|
710
1134
|
let program: ProgramId | undefined
|
|
@@ -712,12 +1136,47 @@ export function shaderMaterialClass(opts: ShaderMaterialClassOptions): ShaderMat
|
|
|
712
1136
|
// Attributes live in the vertex stage only, so unlike the uNormal scan
|
|
713
1137
|
// there is nothing to look for in the fragment source.
|
|
714
1138
|
let normalMatrix = /\buNormal\b/.test(opts.vertex) || /\buNormal\b/.test(opts.fragment)
|
|
1139
|
+
// Skinning is a vertex-stage affair like the attributes; a source that
|
|
1140
|
+
// mentions uBones skins by the mesh's palette texture (Material.skinned).
|
|
1141
|
+
let skinned = /\buBones\b/.test(opts.vertex)
|
|
715
1142
|
let transparent = opts.transparent ?? (opts.blend !== undefined && opts.blend !== "none")
|
|
716
1143
|
let depth = opts.depth ?? true
|
|
717
1144
|
let cull = opts.cull ?? "back"
|
|
718
1145
|
// An empty list declares nothing - same as absent (the engine requires an
|
|
719
1146
|
// instance buffer exactly when attributes are declared).
|
|
720
1147
|
let instanceAttributes = opts.instanceAttributes?.length ? opts.instanceAttributes.map(a => ({ ...a })) : undefined
|
|
1148
|
+
// The shadowVertex depth pass binds the instance slots its stage reads
|
|
1149
|
+
// from - the record slot always (a stage reading none of it cannot
|
|
1150
|
+
// place the instances, an error here), a style slot only when the
|
|
1151
|
+
// stage names one of its attributes - and declares each bound slot's
|
|
1152
|
+
// whole layout, since a slot's stride is the record's (an unread
|
|
1153
|
+
// attribute in a bound slot is inactive, like an extra geometry channel).
|
|
1154
|
+
let shadowAttributes: InstanceAttribute[] | undefined
|
|
1155
|
+
if (opts.shadowVertex !== undefined && instanceAttributes !== undefined) {
|
|
1156
|
+
let source = opts.shadowVertex
|
|
1157
|
+
let named = (a: InstanceAttribute) => new RegExp("\\b" + a.name + "\\b").test(source)
|
|
1158
|
+
let slots = new Set(instanceAttributes.filter(named).map(a => a.slot ?? 0))
|
|
1159
|
+
if (!slots.has(0)) throw new Error("shaderMaterial shadowVertex must read the slot-0 instance attributes it places instances by")
|
|
1160
|
+
shadowAttributes = instanceAttributes.filter(a => slots.has(a.slot ?? 0))
|
|
1161
|
+
}
|
|
1162
|
+
// The shadowVertex depth material: one class and one shared instance,
|
|
1163
|
+
// built the first time a caster of this class meets a shadow view.
|
|
1164
|
+
let shadowClass: ShaderMaterialClass | undefined
|
|
1165
|
+
let classShadow: Material | undefined
|
|
1166
|
+
let shadowFor = (): Material | undefined => {
|
|
1167
|
+
if (opts.shadowVertex === undefined) return undefined
|
|
1168
|
+
if (classShadow === undefined) {
|
|
1169
|
+
shadowClass = shaderMaterialClass({
|
|
1170
|
+
vertex: opts.shadowVertex,
|
|
1171
|
+
fragment: SHADOW_DEPTH_FRAGMENT,
|
|
1172
|
+
cull: shadowCull(cull),
|
|
1173
|
+
instanceAttributes: shadowAttributes,
|
|
1174
|
+
label: (opts.label ?? "shader-material") + "-shadow",
|
|
1175
|
+
})
|
|
1176
|
+
classShadow = shadowClass.instance()
|
|
1177
|
+
}
|
|
1178
|
+
return classShadow
|
|
1179
|
+
}
|
|
721
1180
|
let programFor = (): ProgramId => {
|
|
722
1181
|
if (program === undefined) {
|
|
723
1182
|
let vs = compileShader("vertex", opts.vertex, { header: needsHeader(opts.vertex) })
|
|
@@ -757,16 +1216,18 @@ export function shaderMaterialClass(opts: ShaderMaterialClassOptions): ShaderMat
|
|
|
757
1216
|
instance(inst = {}) {
|
|
758
1217
|
return {
|
|
759
1218
|
normalMatrix,
|
|
1219
|
+
skinned,
|
|
760
1220
|
attributes,
|
|
761
1221
|
transparent,
|
|
762
1222
|
instanceAttributes,
|
|
1223
|
+
instanceStyle: opts.instanceStyle,
|
|
763
1224
|
pipeline: pipelineFor,
|
|
764
1225
|
params: inst.params ?? {},
|
|
765
1226
|
textures: inst.textures,
|
|
766
1227
|
// Lazy: the depth materials are shaderMaterialClass instances
|
|
767
1228
|
// themselves, so an eager variant would recurse into its own cache.
|
|
768
1229
|
get shadow() {
|
|
769
|
-
return inst.shadow ?? shadowVariant(cull)
|
|
1230
|
+
return inst.shadow ?? shadowFor() ?? shadowVariant(cull, skinned)
|
|
770
1231
|
},
|
|
771
1232
|
}
|
|
772
1233
|
},
|
|
@@ -777,6 +1238,11 @@ export function shaderMaterialClass(opts: ShaderMaterialClassOptions): ShaderMat
|
|
|
777
1238
|
destroyProgram(program)
|
|
778
1239
|
program = undefined
|
|
779
1240
|
}
|
|
1241
|
+
if (shadowClass !== undefined) {
|
|
1242
|
+
shadowClass.dispose()
|
|
1243
|
+
shadowClass = undefined
|
|
1244
|
+
classShadow = undefined
|
|
1245
|
+
}
|
|
780
1246
|
},
|
|
781
1247
|
}
|
|
782
1248
|
}
|