@solidrt/3d 0.0.55 → 0.0.57
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 +1206 -224
- package/README.md +16 -7
- package/examples/README.md +88 -9
- package/examples/aim.tsx +2 -3
- package/examples/cascades.tsx +1 -4
- package/examples/collision.tsx +263 -0
- package/examples/first-person.tsx +129 -0
- package/examples/fleet.tsx +200 -0
- package/examples/fog.tsx +2 -3
- 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 +35 -9
- package/examples/pick.tsx +145 -37
- package/examples/probe.tsx +107 -0
- package/examples/scene-background.tsx +2 -4
- package/examples/scene-basic.tsx +2 -4
- package/examples/scene-views.tsx +68 -48
- package/examples/shadows.tsx +2 -4
- package/examples/sky-lit.tsx +92 -0
- package/examples/skybox.tsx +134 -0
- package/examples/sprites.tsx +4 -6
- package/examples/standard.tsx +138 -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 +20 -0
- package/src/color.ts +32 -0
- package/src/components/context.tsx +35 -0
- package/src/components/directional-light.tsx +48 -0
- package/src/components/first-person-camera.tsx +72 -0
- package/src/components/group.tsx +18 -0
- package/src/components/hemisphere-light.tsx +24 -0
- package/src/components/index.ts +46 -0
- package/src/components/instanced-mesh.tsx +105 -0
- package/src/components/mesh.tsx +92 -0
- package/src/components/node-props.ts +90 -0
- package/src/components/orbit-camera.tsx +75 -0
- package/src/components/perspective-camera.tsx +32 -0
- package/src/components/point-light.tsx +52 -0
- package/src/components/record-mesh.tsx +68 -0
- package/src/components/scene.tsx +281 -0
- package/src/components/spot-light.tsx +65 -0
- package/src/components/sprite.tsx +38 -0
- package/src/components/view3d.tsx +152 -0
- package/src/environment-bake.ts +492 -0
- package/src/environment.ts +325 -0
- package/src/first-person.ts +258 -0
- package/src/geometry.ts +6 -2
- package/src/glsl.ts +1343 -82
- package/src/gltf.ts +432 -112
- package/src/index.ts +61 -9
- package/src/input.ts +70 -0
- package/src/light.ts +383 -0
- package/src/material.ts +667 -232
- 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 +204 -35
- package/src/node.ts +579 -0
- package/src/orbit.ts +292 -212
- package/src/scene-pointer.ts +294 -0
- package/src/scene-shadows.ts +471 -0
- package/src/scene.ts +1435 -1576
- package/tools/environment.ts +69 -0
- package/tools/model.ts +4 -3
- package/src/components.tsx +0 -420
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,82 +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. vWorldPos is the fog distance
|
|
95
|
-
// input; a fragment that does not read it (fog: false) leaves the out
|
|
96
|
-
// unmatched, which links fine.
|
|
97
|
-
const UNLIT_VERTEX = glsl`
|
|
98
|
-
in vec3 aPos;
|
|
99
|
-
in vec2 aUV;
|
|
100
|
-
out vec2 vUv;
|
|
101
|
-
out vec3 vWorldPos;
|
|
102
|
-
uniform mat4 uModel;
|
|
103
|
-
uniform mat4 uViewProj;
|
|
104
|
-
|
|
105
|
-
void main() {
|
|
106
|
-
vec4 world = uModel * vec4(aPos, 1.0);
|
|
107
|
-
vWorldPos = world.xyz;
|
|
108
|
-
gl_Position = uViewProj * world;
|
|
109
|
-
vUv = aUV;
|
|
110
|
-
}
|
|
111
|
-
`
|
|
112
|
-
|
|
113
|
-
// The unlit fragment (sprites share it): the color, times the map when
|
|
114
|
-
// there is one, with the alphaTest discard when asked for, then the
|
|
115
|
-
// scene's fog (FOG from ./glsl, mixed at the alpha about to be written)
|
|
116
|
-
// unless the material opted out. An opaque
|
|
117
|
-
// class writes alpha 1: the scene target is composited premultiplied, so
|
|
118
|
-
// a leaked texel alpha would punch a hole through an opaque draw.
|
|
119
|
-
function unlitFragment(map: boolean, alphaTest: boolean, transparent: boolean, fog: boolean): string {
|
|
120
|
-
let alpha = transparent ? "base.a" : "1.0"
|
|
121
|
-
return glsl`
|
|
122
|
-
${map ? "in vec2 vUv;" : ""}
|
|
123
|
-
${fog ? "in vec3 vWorldPos;" : ""}
|
|
124
|
-
${map ? "uniform sampler2D uMap;" : ""}
|
|
125
|
-
uniform vec4 uColor;
|
|
126
|
-
${alphaTest ? "uniform float uAlphaTest;" : ""}
|
|
127
|
-
${fog ? "uniform vec3 uCamPos;" : ""}
|
|
128
|
-
${fog ? FOG : ""}
|
|
129
|
-
void main() {
|
|
130
|
-
vec4 base = ${map ? "texture(uMap, vUv) * uColor" : "uColor"};
|
|
131
|
-
${alphaTest ? "if (base.a < uAlphaTest) discard;" : ""}
|
|
132
|
-
${fog ? `base.rgb = fog(base.rgb, ${alpha}, vWorldPos, uCamPos);` : ""}
|
|
133
|
-
fragColor = vec4(base.rgb, ${alpha});
|
|
134
|
-
}
|
|
135
|
-
`
|
|
136
|
-
}
|
|
137
|
-
|
|
138
121
|
// One shaderMaterialClass per unlit option combination (map x transparent
|
|
139
122
|
// x cull x alphaTest), cached for the app's lifetime like lit's; one
|
|
140
123
|
// pipeline per vertex layout inside each.
|
|
141
124
|
let unlitClasses = new Map<string, ShaderMaterialClass>()
|
|
142
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
|
+
|
|
143
134
|
export type UnlitOptions = {
|
|
144
|
-
/** 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. */
|
|
145
140
|
color?: [number, number, number] | [number, number, number, number]
|
|
146
|
-
/** 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. */
|
|
147
145
|
map?: TextureId
|
|
148
146
|
/** Blend over what is behind (color alpha and map alpha both count).
|
|
149
147
|
* Without it an alpha below 1 still draws opaque. See Material.transparent. */
|
|
@@ -166,6 +164,58 @@ export type UnlitOptions = {
|
|
|
166
164
|
* the program - a sky sphere or a far backdrop that must keep its
|
|
167
165
|
* color, an emissive marker. */
|
|
168
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]]
|
|
169
219
|
}
|
|
170
220
|
|
|
171
221
|
/**
|
|
@@ -174,20 +224,26 @@ export type UnlitOptions = {
|
|
|
174
224
|
* light list); see the scene-graph research note.
|
|
175
225
|
*/
|
|
176
226
|
export function unlit(opts: UnlitOptions = {}): Material {
|
|
177
|
-
let
|
|
178
|
-
let a = color.length === 4 ? color[3] : 1
|
|
179
|
-
let uColor = [color[0] * a, color[1] * a, color[2] * a, a]
|
|
227
|
+
let uColor = colorParam(opts.color ?? [1, 1, 1])
|
|
180
228
|
let map = opts.map !== undefined
|
|
181
229
|
let transparent = opts.transparent === true
|
|
182
230
|
let cull = opts.cull ?? "back"
|
|
183
231
|
let alphaTest = opts.alphaTest !== undefined
|
|
184
232
|
let fog = opts.fog !== false
|
|
185
|
-
let
|
|
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("|")
|
|
186
239
|
let cls = unlitClasses.get(key)
|
|
187
240
|
if (cls === undefined) {
|
|
188
241
|
cls = shaderMaterialClass({
|
|
189
|
-
vertex:
|
|
190
|
-
fragment: unlitFragment(map, alphaTest, transparent, fog),
|
|
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,
|
|
191
247
|
transparent,
|
|
192
248
|
cull,
|
|
193
249
|
label: "scene-unlit-" + key,
|
|
@@ -196,10 +252,14 @@ export function unlit(opts: UnlitOptions = {}): Material {
|
|
|
196
252
|
}
|
|
197
253
|
let params: ShaderParams = { uColor }
|
|
198
254
|
if (alphaTest) params.uAlphaTest = opts.alphaTest!
|
|
255
|
+
if (mapTransform) params.uMapTransform = mapTransformParam(opts.mapTransform!)
|
|
199
256
|
return cls.instance({
|
|
200
257
|
params,
|
|
201
258
|
textures: map ? { uMap: opts.map! } : undefined,
|
|
202
|
-
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,
|
|
203
263
|
})
|
|
204
264
|
}
|
|
205
265
|
|
|
@@ -217,6 +277,58 @@ export type LitOptions = UnlitOptions & {
|
|
|
217
277
|
* each part's size or UVs; the map must be created with
|
|
218
278
|
* `wrap: "repeat"`. */
|
|
219
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
|
|
220
332
|
/**
|
|
221
333
|
* Receive the scene's directional shadows (default true, like Godot and
|
|
222
334
|
* Three): each casting light's term is multiplied by its shadow-map
|
|
@@ -232,106 +344,54 @@ export type LitOptions = UnlitOptions & {
|
|
|
232
344
|
receiveShadow?: boolean
|
|
233
345
|
}
|
|
234
346
|
|
|
235
|
-
// The lit
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
// x
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
//
|
|
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,
|
|
245
363
|
// directional light i's maps (one, or its cascades) as map slots
|
|
246
364
|
// uShadowFirst[i] .. + uShadowCount[i] with a tile rect and a matrix
|
|
247
365
|
// each, and its biases (target-level, bound by the scene); uShadowCount
|
|
248
366
|
// 0 means it does not cast; SHADOW_LOOKUP turns the index into the factor.
|
|
249
367
|
// The option combination that picks a lit class: the class-cache key is
|
|
250
|
-
// its values in this order, and the
|
|
251
|
-
//
|
|
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.
|
|
252
371
|
type LitClass = {
|
|
253
372
|
map: boolean
|
|
254
373
|
vertexColors: boolean
|
|
255
374
|
triplanar: boolean
|
|
256
375
|
transparent: boolean
|
|
257
|
-
|
|
376
|
+
receiveShadow: boolean
|
|
258
377
|
cull: CullMode
|
|
259
378
|
alphaTest: boolean
|
|
260
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
|
|
261
390
|
}
|
|
262
391
|
|
|
263
392
|
function litClassKey(c: LitClass): string {
|
|
264
393
|
return Object.values(c).join("|")
|
|
265
394
|
}
|
|
266
|
-
|
|
267
|
-
function litFragment(c: LitClass): string {
|
|
268
|
-
let { map, vertexColors, triplanar, shadow, alphaTest, fog } = c
|
|
269
|
-
let backFaces = c.cull !== "back"
|
|
270
|
-
let alpha = c.transparent ? "base.a" : "1.0"
|
|
271
|
-
return glsl`
|
|
272
|
-
in vec3 vWorldPos;
|
|
273
|
-
in vec3 vNormal;
|
|
274
|
-
in vec2 vUv;
|
|
275
|
-
${vertexColors ? "in vec4 vColor;" : ""}
|
|
276
|
-
uniform vec4 uColor;
|
|
277
|
-
${map ? "uniform sampler2D uMap;" : ""}
|
|
278
|
-
uniform float uSpecular;
|
|
279
|
-
uniform float uShininess;
|
|
280
|
-
${triplanar ? "uniform float uTriplanar;" : ""}
|
|
281
|
-
${alphaTest ? "uniform float uAlphaTest;" : ""}
|
|
282
|
-
uniform vec3 uCamPos;
|
|
283
|
-
uniform vec3 uHemiSky;
|
|
284
|
-
uniform vec3 uHemiGround;
|
|
285
|
-
uniform int uLightCount;
|
|
286
|
-
uniform vec3 uLightDir[${MAX_LIGHTS}];
|
|
287
|
-
uniform vec3 uLightColor[${MAX_LIGHTS}];
|
|
288
|
-
${
|
|
289
|
-
shadow
|
|
290
|
-
? `${SHADOW_SLOTS}
|
|
291
|
-
${SHADOW}
|
|
292
|
-
${SHADOW_LOOKUP}`
|
|
293
|
-
: ""
|
|
294
|
-
}
|
|
295
|
-
${HEMISPHERE}
|
|
296
|
-
${LAMBERT}
|
|
297
|
-
${BLINN_SPECULAR}
|
|
298
|
-
${fog ? FOG : ""}
|
|
299
|
-
|
|
300
|
-
void main() {
|
|
301
|
-
vec3 n = normalize(vNormal);
|
|
302
|
-
${backFaces ? "if (!gl_FrontFacing) n = -n;" : ""}
|
|
303
|
-
vec4 base = uColor;
|
|
304
|
-
${
|
|
305
|
-
map
|
|
306
|
-
? triplanar
|
|
307
|
-
? `vec3 w = pow(abs(n), vec3(4.0));
|
|
308
|
-
w /= w.x + w.y + w.z;
|
|
309
|
-
vec3 p = vWorldPos * uTriplanar;
|
|
310
|
-
base *= texture(uMap, p.yz) * w.x + texture(uMap, p.xz) * w.y + texture(uMap, p.xy) * w.z;`
|
|
311
|
-
: "base *= texture(uMap, vUv);"
|
|
312
|
-
: ""
|
|
313
|
-
}
|
|
314
|
-
${vertexColors ? "base *= vColor;" : ""}
|
|
315
|
-
${alphaTest ? "if (base.a < uAlphaTest) discard;" : ""}
|
|
316
|
-
vec3 v = normalize(uCamPos - vWorldPos);
|
|
317
|
-
vec3 light = hemisphere(n, uHemiSky, uHemiGround);
|
|
318
|
-
vec3 spec = vec3(0.0);
|
|
319
|
-
for (int i = 0; i < ${MAX_LIGHTS}; i++) {
|
|
320
|
-
if (i >= uLightCount) break;
|
|
321
|
-
vec3 l = uLightDir[i];
|
|
322
|
-
${
|
|
323
|
-
shadow ? "float s = lightShadow(i, vWorldPos, n);" : "float s = 1.0;"
|
|
324
|
-
}
|
|
325
|
-
light += uLightColor[i] * lambert(n, l) * s;
|
|
326
|
-
spec += uLightColor[i] * blinnSpecular(n, v, l, uShininess) * s;
|
|
327
|
-
}
|
|
328
|
-
vec3 rgb = base.rgb * light + spec * uSpecular * base.a;
|
|
329
|
-
${fog ? `rgb = fog(rgb, ${alpha}, vWorldPos, uCamPos);` : ""}
|
|
330
|
-
fragColor = vec4(rgb, ${alpha});
|
|
331
|
-
}
|
|
332
|
-
`
|
|
333
|
-
}
|
|
334
|
-
|
|
335
395
|
let litClasses = new Map<string, ShaderMaterialClass>()
|
|
336
396
|
|
|
337
397
|
/**
|
|
@@ -345,62 +405,328 @@ let litClasses = new Map<string, ShaderMaterialClass>()
|
|
|
345
405
|
* starts at zero: set at least one of the two.
|
|
346
406
|
*/
|
|
347
407
|
export function lit(opts: LitOptions = {}): Material {
|
|
348
|
-
let
|
|
349
|
-
let a = color.length === 4 ? color[3] : 1
|
|
350
|
-
let uColor = [color[0] * a, color[1] * a, color[2] * a, a]
|
|
408
|
+
let uColor = colorParam(opts.color ?? [1, 1, 1])
|
|
351
409
|
let map = opts.map !== undefined
|
|
352
410
|
let triplanar = map && opts.triplanar !== undefined
|
|
353
411
|
let alphaTest = opts.alphaTest !== undefined
|
|
354
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
|
+
}
|
|
355
428
|
let flags: LitClass = {
|
|
356
429
|
map,
|
|
357
430
|
vertexColors: opts.vertexColors === true,
|
|
358
431
|
triplanar,
|
|
359
432
|
transparent: opts.transparent === true,
|
|
360
|
-
|
|
433
|
+
receiveShadow: opts.receiveShadow !== false,
|
|
361
434
|
cull,
|
|
362
435
|
alphaTest,
|
|
363
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,
|
|
364
447
|
}
|
|
365
448
|
let key = litClassKey(flags)
|
|
366
449
|
let cls = litClasses.get(key)
|
|
367
450
|
if (cls === undefined) {
|
|
368
451
|
cls = shaderMaterialClass({
|
|
369
|
-
vertex: flags
|
|
452
|
+
vertex: litVertex(flags),
|
|
370
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,
|
|
371
457
|
transparent: flags.transparent,
|
|
372
458
|
cull,
|
|
373
459
|
label: "scene-lit-" + key,
|
|
374
460
|
})
|
|
375
461
|
litClasses.set(key, cls)
|
|
376
462
|
}
|
|
377
|
-
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
|
+
}
|
|
378
468
|
if (triplanar) params.uTriplanar = opts.triplanar!
|
|
379
469
|
if (alphaTest) params.uAlphaTest = opts.alphaTest!
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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.
|
|
383
491
|
let material = cls.instance({
|
|
384
492
|
params,
|
|
385
|
-
textures
|
|
386
|
-
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,
|
|
387
498
|
})
|
|
388
499
|
return material
|
|
389
500
|
}
|
|
390
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
|
+
|
|
391
706
|
// The shadow depth pass: position only, no color of interest (the target's
|
|
392
707
|
// depth texture is the output; the color write is the pipeline's minimum).
|
|
393
708
|
// Front faces culled, Three's shadowSide default: the map holds each
|
|
394
709
|
// caster's BACK surface, so a receiving front face at the same depth
|
|
395
710
|
// compares lit without a bias and acne needs no fighting on closed meshes.
|
|
396
|
-
|
|
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`
|
|
397
719
|
in vec3 aPos;
|
|
720
|
+
${skinned ? SKIN_DECLS : ""}
|
|
721
|
+
${instanced ? INSTANCE_MATRIX : ""}
|
|
398
722
|
uniform mat4 uModel;
|
|
399
723
|
uniform mat4 uViewProj;
|
|
400
724
|
void main() {
|
|
401
|
-
|
|
725
|
+
${skinned ? SKIN_MATRIX : ""}
|
|
726
|
+
gl_Position = uViewProj * uModel * ${instanced ? `(instanceMatrix() * ${local})` : local};
|
|
402
727
|
}
|
|
403
728
|
`
|
|
729
|
+
}
|
|
404
730
|
|
|
405
731
|
const SHADOW_DEPTH_FRAGMENT = glsl`
|
|
406
732
|
void main() {
|
|
@@ -408,22 +734,23 @@ const SHADOW_DEPTH_FRAGMENT = glsl`
|
|
|
408
734
|
}
|
|
409
735
|
`
|
|
410
736
|
|
|
411
|
-
let shadowDepth = new Map<
|
|
737
|
+
let shadowDepth = new Map<string, Material>()
|
|
412
738
|
|
|
413
739
|
/** The override material of a scene's shadow view (internal): one class
|
|
414
|
-
* per cull mode for the app, built on first use. The default,
|
|
415
|
-
* is the caster's back surface (see above); a material culling
|
|
416
|
-
* otherwise carries its own variant as Material.shadow. */
|
|
417
|
-
export function shadowDepthMaterial(cull: CullMode = "front"): Material {
|
|
418
|
-
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)
|
|
419
746
|
if (material === undefined) {
|
|
420
747
|
material = shaderMaterialClass({
|
|
421
|
-
vertex:
|
|
748
|
+
vertex: shadowDepthVertex(skinned, false),
|
|
422
749
|
fragment: SHADOW_DEPTH_FRAGMENT,
|
|
423
750
|
cull,
|
|
424
|
-
label: "scene-shadow-depth-" +
|
|
751
|
+
label: "scene-shadow-depth-" + key,
|
|
425
752
|
}).instance()
|
|
426
|
-
shadowDepth.set(
|
|
753
|
+
shadowDepth.set(key, material)
|
|
427
754
|
}
|
|
428
755
|
return material
|
|
429
756
|
}
|
|
@@ -434,56 +761,56 @@ function shadowCull(cull: CullMode): CullMode {
|
|
|
434
761
|
return cull === "none" ? "none" : cull === "back" ? "front" : "back"
|
|
435
762
|
}
|
|
436
763
|
|
|
437
|
-
/** "back" maps to the default depth material, so its variant
|
|
438
|
-
* undefined
|
|
439
|
-
|
|
440
|
-
|
|
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)
|
|
441
770
|
}
|
|
442
771
|
|
|
443
|
-
|
|
444
|
-
// against its alphaTest, the lit fragment's test minus everything else.
|
|
445
|
-
const SHADOW_CUTOUT_VERTEX = glsl`
|
|
446
|
-
in vec3 aPos;
|
|
447
|
-
in vec2 aUV;
|
|
448
|
-
out vec2 vUv;
|
|
449
|
-
uniform mat4 uModel;
|
|
450
|
-
uniform mat4 uViewProj;
|
|
451
|
-
void main() {
|
|
452
|
-
gl_Position = uViewProj * uModel * vec4(aPos, 1.0);
|
|
453
|
-
vUv = aUV;
|
|
454
|
-
}
|
|
455
|
-
`
|
|
456
|
-
|
|
457
|
-
const SHADOW_CUTOUT_FRAGMENT = glsl`
|
|
458
|
-
in vec2 vUv;
|
|
459
|
-
uniform sampler2D uMap;
|
|
460
|
-
uniform vec4 uColor;
|
|
461
|
-
uniform float uAlphaTest;
|
|
462
|
-
void main() {
|
|
463
|
-
if (texture(uMap, vUv).a * uColor.a < uAlphaTest) discard;
|
|
464
|
-
fragColor = vec4(1.0);
|
|
465
|
-
}
|
|
466
|
-
`
|
|
772
|
+
let unlitShadowClasses = new Map<string, ShaderMaterialClass>()
|
|
467
773
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
*
|
|
472
|
-
|
|
473
|
-
|
|
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)
|
|
474
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")
|
|
475
796
|
cls = shaderMaterialClass({
|
|
476
|
-
vertex:
|
|
477
|
-
fragment
|
|
797
|
+
vertex: unlitVertex({ skinned, instanced, instanceColors }),
|
|
798
|
+
fragment,
|
|
478
799
|
cull,
|
|
479
|
-
|
|
800
|
+
instanceAttributes: stockInstanceAttributes(instanced, instanceColors),
|
|
801
|
+
label: "scene-unlit-shadow-" + key,
|
|
480
802
|
})
|
|
481
|
-
|
|
803
|
+
unlitShadowClasses.set(key, cls)
|
|
482
804
|
}
|
|
483
|
-
|
|
805
|
+
let params: ShaderParams = { uColor, uAlphaTest }
|
|
806
|
+
if (uMapTransform !== undefined) params.uMapTransform = uMapTransform
|
|
807
|
+
return cls.instance({ params, textures: { uMap } })
|
|
484
808
|
}
|
|
485
809
|
|
|
486
|
-
|
|
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"> & {
|
|
487
814
|
/** Which way the quad turns to face the camera. `"full"` (default,
|
|
488
815
|
* Three's Sprite): both axes follow the view, the quad is always flat
|
|
489
816
|
* to the screen. `"fixed-y"` (Godot's BILLBOARD_FIXED_Y): only the yaw
|
|
@@ -555,9 +882,12 @@ let spriteClasses = new Map<string, ShaderMaterialClass>()
|
|
|
555
882
|
* for an opaque one. Culling is off: a camera-facing quad has no back.
|
|
556
883
|
*/
|
|
557
884
|
export function sprite(opts: SpriteOptions = {}): Material {
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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])
|
|
561
891
|
let map = opts.map !== undefined
|
|
562
892
|
let transparent = opts.transparent !== false
|
|
563
893
|
let fixedY = opts.billboard === "fixed-y"
|
|
@@ -567,7 +897,7 @@ export function sprite(opts: SpriteOptions = {}): Material {
|
|
|
567
897
|
if (cls === undefined) {
|
|
568
898
|
cls = shaderMaterialClass({
|
|
569
899
|
vertex: fixedY ? SPRITE_FIXED_Y_VERTEX_SRC : SPRITE_VERTEX_SRC,
|
|
570
|
-
fragment: unlitFragment(map,
|
|
900
|
+
fragment: unlitFragment({ map, transparent, fog }),
|
|
571
901
|
transparent,
|
|
572
902
|
cull: "none",
|
|
573
903
|
label: "scene-sprite-" + key,
|
|
@@ -598,21 +928,51 @@ function needsHeader(source: string): boolean {
|
|
|
598
928
|
// engine's own attributeless fullscreen triangle (gl_VertexID, no vertex
|
|
599
929
|
// buffer), emitting the SAME vUV the shader-target contract provides: 0..1
|
|
600
930
|
// with origin at the displayed top-left - so a backdrop fragment written
|
|
601
|
-
// 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.
|
|
602
938
|
const BACKGROUND_VERTEX = glsl`
|
|
939
|
+
uniform mat4 uInvViewProj;
|
|
603
940
|
out vec2 vUV;
|
|
941
|
+
out vec3 vRay;
|
|
604
942
|
void main() {
|
|
605
943
|
vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
|
|
606
944
|
vUV = p;
|
|
607
|
-
|
|
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);
|
|
608
950
|
}
|
|
609
951
|
`
|
|
610
952
|
|
|
611
953
|
// Pipeline fragments get no vUV from the engine preamble (a pipeline's
|
|
612
954
|
// varyings are its own), so the background slot injects the full
|
|
613
|
-
// 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.
|
|
614
958
|
const BACKGROUND_FRAGMENT_PREAMBLE =
|
|
615
|
-
"#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
|
+
`
|
|
616
976
|
|
|
617
977
|
/** The scene's background pipeline (internal - reached via
|
|
618
978
|
* scene.setBackground): depth-free, attributeless, drawn as entry zero of
|
|
@@ -664,12 +1024,40 @@ export type ShaderMaterialClassOptions = {
|
|
|
664
1024
|
* beside the layout's own, and each drawn instance gets one record from
|
|
665
1025
|
* the mesh's instance buffer (interleaved floats in this order). A class
|
|
666
1026
|
* with instance attributes makes INSTANCED materials: attach their meshes
|
|
667
|
-
* with createInstancedMesh, which
|
|
668
|
-
* is rejected at add().
|
|
669
|
-
*
|
|
670
|
-
*
|
|
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.
|
|
671
1059
|
*/
|
|
672
|
-
|
|
1060
|
+
shadowVertex?: string
|
|
673
1061
|
/** Blend over what is behind, with the scene sorting this material's
|
|
674
1062
|
* meshes back-to-front after the opaque ones (see Material.transparent).
|
|
675
1063
|
* Sets the pipeline defaults blend "alpha" and depthWrite false; the
|
|
@@ -696,8 +1084,11 @@ export type ShaderMaterialInstanceOptions = {
|
|
|
696
1084
|
params?: ShaderParams
|
|
697
1085
|
textures?: TextureBindings
|
|
698
1086
|
/** The depth variant a shadow view draws this instance with (see
|
|
699
|
-
* Material.shadow): a cutout's discard,
|
|
700
|
-
*
|
|
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. */
|
|
701
1092
|
shadow?: Material
|
|
702
1093
|
}
|
|
703
1094
|
|
|
@@ -732,10 +1123,12 @@ export function shaderMaterialClass(opts: ShaderMaterialClassOptions): ShaderMat
|
|
|
732
1123
|
// shared params skipping undeclared names the omission would otherwise
|
|
733
1124
|
// surface as a silently untransformed render, not an error.
|
|
734
1125
|
for (let name of ["uModel", "uViewProj"]) {
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
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
|
+
}
|
|
739
1132
|
}
|
|
740
1133
|
}
|
|
741
1134
|
let program: ProgramId | undefined
|
|
@@ -743,12 +1136,47 @@ export function shaderMaterialClass(opts: ShaderMaterialClassOptions): ShaderMat
|
|
|
743
1136
|
// Attributes live in the vertex stage only, so unlike the uNormal scan
|
|
744
1137
|
// there is nothing to look for in the fragment source.
|
|
745
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)
|
|
746
1142
|
let transparent = opts.transparent ?? (opts.blend !== undefined && opts.blend !== "none")
|
|
747
1143
|
let depth = opts.depth ?? true
|
|
748
1144
|
let cull = opts.cull ?? "back"
|
|
749
1145
|
// An empty list declares nothing - same as absent (the engine requires an
|
|
750
1146
|
// instance buffer exactly when attributes are declared).
|
|
751
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
|
+
}
|
|
752
1180
|
let programFor = (): ProgramId => {
|
|
753
1181
|
if (program === undefined) {
|
|
754
1182
|
let vs = compileShader("vertex", opts.vertex, { header: needsHeader(opts.vertex) })
|
|
@@ -788,16 +1216,18 @@ export function shaderMaterialClass(opts: ShaderMaterialClassOptions): ShaderMat
|
|
|
788
1216
|
instance(inst = {}) {
|
|
789
1217
|
return {
|
|
790
1218
|
normalMatrix,
|
|
1219
|
+
skinned,
|
|
791
1220
|
attributes,
|
|
792
1221
|
transparent,
|
|
793
1222
|
instanceAttributes,
|
|
1223
|
+
instanceStyle: opts.instanceStyle,
|
|
794
1224
|
pipeline: pipelineFor,
|
|
795
1225
|
params: inst.params ?? {},
|
|
796
1226
|
textures: inst.textures,
|
|
797
1227
|
// Lazy: the depth materials are shaderMaterialClass instances
|
|
798
1228
|
// themselves, so an eager variant would recurse into its own cache.
|
|
799
1229
|
get shadow() {
|
|
800
|
-
return inst.shadow ?? shadowVariant(cull)
|
|
1230
|
+
return inst.shadow ?? shadowFor() ?? shadowVariant(cull, skinned)
|
|
801
1231
|
},
|
|
802
1232
|
}
|
|
803
1233
|
},
|
|
@@ -808,6 +1238,11 @@ export function shaderMaterialClass(opts: ShaderMaterialClassOptions): ShaderMat
|
|
|
808
1238
|
destroyProgram(program)
|
|
809
1239
|
program = undefined
|
|
810
1240
|
}
|
|
1241
|
+
if (shadowClass !== undefined) {
|
|
1242
|
+
shadowClass.dispose()
|
|
1243
|
+
shadowClass = undefined
|
|
1244
|
+
classShadow = undefined
|
|
1245
|
+
}
|
|
811
1246
|
},
|
|
812
1247
|
}
|
|
813
1248
|
}
|