@solidrt/3d 0.0.53 → 0.0.55
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 +121 -28
- package/examples/README.md +7 -0
- package/examples/cascades.tsx +21 -3
- package/examples/fog.tsx +174 -0
- package/package.json +3 -3
- package/src/components.tsx +11 -1
- package/src/glsl.ts +47 -0
- package/src/gltf.ts +22 -4
- package/src/index.ts +1 -1
- package/src/material.ts +248 -99
- package/src/model.ts +17 -3
- package/src/scene.ts +91 -1
package/src/index.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// PerspectiveCamera) on top. See AGENTS.md for the model and the traps.
|
|
7
7
|
|
|
8
8
|
export { add, createDirectionalLight, createGroup, createHemisphereLight, createInstancedMesh, createMesh, createScene, createSprite, setLight, disposeInstances, getRotation, lookAt, remove, setCastShadow, setGeometry, setInstanceCount, setInstances, setMaterial, setMeshParams, setRenderOrder, setTransform, setTransition, setVisible, worldPosition, MAX_SHADOWS } from "./scene.ts"
|
|
9
|
-
export type { CameraUpdate, DirectionalLight as DirectionalLightNode, DirectionalLightOptions, HemisphereLight as HemisphereLightNode, HemisphereLightOptions, Hit, Light, InstancedMesh as InstancedMeshNode, InstancedMeshOptions, Mesh as MeshNode, MeshInstances, OrthoExtent, Scene as SceneHandle, SceneHandlers, SceneNode, SceneOptions, ScenePointerEvent, ShadowCamera, ShadowOptions, TransformUpdate, TransitionEndEvent, View, ViewOptions } from "./scene.ts"
|
|
9
|
+
export type { CameraUpdate, DirectionalLight as DirectionalLightNode, DirectionalLightOptions, FogOptions, HemisphereLight as HemisphereLightNode, HemisphereLightOptions, Hit, Light, InstancedMesh as InstancedMeshNode, InstancedMeshOptions, Mesh as MeshNode, MeshInstances, OrthoExtent, Scene as SceneHandle, SceneHandlers, SceneNode, SceneOptions, ScenePointerEvent, ShadowCamera, ShadowOptions, TransformUpdate, TransitionEndEvent, View, ViewOptions } from "./scene.ts"
|
|
10
10
|
export type { NodeTransition, NodeTransitionSpec } from "flux:spatial"
|
|
11
11
|
export { disposeGeometry } from "./geometry-gpu.ts"
|
|
12
12
|
export { box, circle, cone, cylinder, fillAttribute, fillColors, geometryBounds, layoutAttributes, layoutKey, layoutSlot, layoutStride, mergeGeometries, packGeometry, plane, ring, sphere, torus, torusKnot, transformGeometry, validateGeometry, withAttribute, withColors, STANDARD_FLOATS, VERTEX_LAYOUTS } from "./geometry.ts"
|
package/src/material.ts
CHANGED
|
@@ -35,7 +35,6 @@ import type {
|
|
|
35
35
|
ProgramId,
|
|
36
36
|
RenderPipelineId,
|
|
37
37
|
ShaderParams,
|
|
38
|
-
ShaderStageId,
|
|
39
38
|
TextureBindings,
|
|
40
39
|
TextureId,
|
|
41
40
|
Topology,
|
|
@@ -43,7 +42,7 @@ import type {
|
|
|
43
42
|
} from "@solidrt/core/gpu"
|
|
44
43
|
import { layoutAttributes, layoutKey, layoutSlot } from "./geometry.ts"
|
|
45
44
|
import type { VertexLayout } from "./geometry.ts"
|
|
46
|
-
import { BLINN_SPECULAR, HEMISPHERE, LAMBERT, LIT_VERTEX, LIT_VERTEX_COLORED, MAX_LIGHTS, SHADOW, SHADOW_LOOKUP, SHADOW_SLOTS } from "./glsl.ts"
|
|
45
|
+
import { BLINN_SPECULAR, FOG, HEMISPHERE, LAMBERT, LIT_VERTEX, LIT_VERTEX_COLORED, MAX_LIGHTS, SHADOW, SHADOW_LOOKUP, SHADOW_SLOTS } from "./glsl.ts"
|
|
47
46
|
|
|
48
47
|
export type Material = {
|
|
49
48
|
/** The pipeline this material draws with for geometry of `layout`
|
|
@@ -73,85 +72,73 @@ export type Material = {
|
|
|
73
72
|
* instanced meshes only - createInstancedMesh supplies the record buffer,
|
|
74
73
|
* and createMesh meshes are rejected at add(). */
|
|
75
74
|
instanceAttributes?: VertexAttribute[]
|
|
75
|
+
/** What a shadow view draws this material's meshes with instead of its
|
|
76
|
+
* default depth override: the depth pass culling the side this material
|
|
77
|
+
* culls (Three's shadowSide rule, Godot's shadow pass), so a `cull:
|
|
78
|
+
* "none"` caster casts from both faces, and for a cutout (lit
|
|
79
|
+
* alphaTest with a map) the same discard, so a plant casts leaves and
|
|
80
|
+
* not rectangles. Absent = the default (a back-culling material casts
|
|
81
|
+
* from its back faces). A shaderMaterial supplies its own through the
|
|
82
|
+
* instance option of the same name. */
|
|
83
|
+
shadow?: Material
|
|
76
84
|
/** Present on materials that own their pipeline (shaderMaterial). */
|
|
77
85
|
dispose?(): void
|
|
78
86
|
}
|
|
79
87
|
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
|
|
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`
|
|
88
98
|
in vec3 aPos;
|
|
89
99
|
in vec2 aUV;
|
|
90
100
|
out vec2 vUv;
|
|
101
|
+
out vec3 vWorldPos;
|
|
91
102
|
uniform mat4 uModel;
|
|
92
103
|
uniform mat4 uViewProj;
|
|
93
104
|
|
|
94
105
|
void main() {
|
|
95
|
-
|
|
106
|
+
vec4 world = uModel * vec4(aPos, 1.0);
|
|
107
|
+
vWorldPos = world.xyz;
|
|
108
|
+
gl_Position = uViewProj * world;
|
|
96
109
|
vUv = aUV;
|
|
97
110
|
}
|
|
98
111
|
`
|
|
99
112
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
type UnlitClass = "color" | "map" | "color-transparent" | "map-transparent"
|
|
124
|
-
|
|
125
|
-
function programFor(cls: UnlitClass): ProgramId {
|
|
126
|
-
let program = programs[cls]
|
|
127
|
-
if (program === undefined) {
|
|
128
|
-
if (sharedVertex === undefined) sharedVertex = compileShader("vertex", VERTEX_SRC, { header: true })
|
|
129
|
-
let fragment = compileShader("fragment", cls.startsWith("color") ? FRAGMENT_COLOR_SRC : FRAGMENT_MAP_SRC, {
|
|
130
|
-
header: true,
|
|
131
|
-
})
|
|
132
|
-
program = linkProgram(sharedVertex, fragment, { label: "scene-unlit-" + cls })
|
|
133
|
-
programs[cls] = program
|
|
134
|
-
}
|
|
135
|
-
return program
|
|
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
136
|
}
|
|
137
137
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
let program = programFor(cls)
|
|
143
|
-
let transparent = cls.endsWith("-transparent")
|
|
144
|
-
let pipeline = createRenderPipeline(program, {
|
|
145
|
-
attributes: layoutAttributes(layout),
|
|
146
|
-
depth: true,
|
|
147
|
-
depthWrite: transparent ? false : undefined,
|
|
148
|
-
blend: transparent ? "alpha" : undefined,
|
|
149
|
-
cull: "back",
|
|
150
|
-
label: "scene-unlit-" + cls,
|
|
151
|
-
})
|
|
152
|
-
pipelines.set(key, pipeline)
|
|
153
|
-
return pipeline
|
|
154
|
-
}
|
|
138
|
+
// One shaderMaterialClass per unlit option combination (map x transparent
|
|
139
|
+
// x cull x alphaTest), cached for the app's lifetime like lit's; one
|
|
140
|
+
// pipeline per vertex layout inside each.
|
|
141
|
+
let unlitClasses = new Map<string, ShaderMaterialClass>()
|
|
155
142
|
|
|
156
143
|
export type UnlitOptions = {
|
|
157
144
|
/** Straight [r, g, b] or [r, g, b, a], 0..1. Default white. */
|
|
@@ -161,6 +148,24 @@ export type UnlitOptions = {
|
|
|
161
148
|
/** Blend over what is behind (color alpha and map alpha both count).
|
|
162
149
|
* Without it an alpha below 1 still draws opaque. See Material.transparent. */
|
|
163
150
|
transparent?: boolean
|
|
151
|
+
/** Which faces to drop; default "back". "none" draws both sides of
|
|
152
|
+
* single-layer geometry (foliage cards, glass, a mirrored part), and
|
|
153
|
+
* lit materials then light a back face with its normal flipped, as
|
|
154
|
+
* Three's DoubleSide and Godot's CULL_DISABLED do. */
|
|
155
|
+
cull?: CullMode
|
|
156
|
+
/** Cutout: drop a fragment whose final alpha (color x map, and for lit
|
|
157
|
+
* the vertex color too) is below this, 0..1 (Three's alphaTest, glTF
|
|
158
|
+
* alphaMode MASK with its alphaCutoff). Opaque otherwise:
|
|
159
|
+
* depth-written, not sorted, unlike `transparent`. Foliage cards and
|
|
160
|
+
* fences want it with `cull: "none"`; a mapped cutout casts its cutout
|
|
161
|
+
* (Material.shadow). */
|
|
162
|
+
alphaTest?: number
|
|
163
|
+
/** Take the scene's fog (default true, Three's `material.fog`): the
|
|
164
|
+
* fragment fades toward the fog color with its distance from the
|
|
165
|
+
* camera once `scene.setFog` is set. `false` drops the fog code from
|
|
166
|
+
* the program - a sky sphere or a far backdrop that must keep its
|
|
167
|
+
* color, an emissive marker. */
|
|
168
|
+
fog?: boolean
|
|
164
169
|
}
|
|
165
170
|
|
|
166
171
|
/**
|
|
@@ -172,15 +177,30 @@ export function unlit(opts: UnlitOptions = {}): Material {
|
|
|
172
177
|
let color = opts.color ?? [1, 1, 1]
|
|
173
178
|
let a = color.length === 4 ? color[3] : 1
|
|
174
179
|
let uColor = [color[0] * a, color[1] * a, color[2] * a, a]
|
|
180
|
+
let map = opts.map !== undefined
|
|
175
181
|
let transparent = opts.transparent === true
|
|
176
|
-
let
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
182
|
+
let cull = opts.cull ?? "back"
|
|
183
|
+
let alphaTest = opts.alphaTest !== undefined
|
|
184
|
+
let fog = opts.fog !== false
|
|
185
|
+
let key = [map, transparent, cull, alphaTest, fog].join("|")
|
|
186
|
+
let cls = unlitClasses.get(key)
|
|
187
|
+
if (cls === undefined) {
|
|
188
|
+
cls = shaderMaterialClass({
|
|
189
|
+
vertex: UNLIT_VERTEX,
|
|
190
|
+
fragment: unlitFragment(map, alphaTest, transparent, fog),
|
|
191
|
+
transparent,
|
|
192
|
+
cull,
|
|
193
|
+
label: "scene-unlit-" + key,
|
|
194
|
+
})
|
|
195
|
+
unlitClasses.set(key, cls)
|
|
183
196
|
}
|
|
197
|
+
let params: ShaderParams = { uColor }
|
|
198
|
+
if (alphaTest) params.uAlphaTest = opts.alphaTest!
|
|
199
|
+
return cls.instance({
|
|
200
|
+
params,
|
|
201
|
+
textures: map ? { uMap: opts.map! } : undefined,
|
|
202
|
+
shadow: alphaTest && map ? shadowCutoutMaterial(shadowCull(cull), uColor, opts.alphaTest!, opts.map!) : undefined,
|
|
203
|
+
})
|
|
184
204
|
}
|
|
185
205
|
|
|
186
206
|
export type LitOptions = UnlitOptions & {
|
|
@@ -214,14 +234,40 @@ export type LitOptions = UnlitOptions & {
|
|
|
214
234
|
|
|
215
235
|
// The lit fragment is composed from the same exported pieces an app
|
|
216
236
|
// composes by hand, per flag: map x vertexColors x triplanar x shadow x
|
|
217
|
-
// transparent
|
|
218
|
-
//
|
|
237
|
+
// transparent x cull (a class that shows back faces lights them with the
|
|
238
|
+
// normal flipped, else a double-sided leaf's back is black) x alphaTest
|
|
239
|
+
// (the cutoff itself is a per-entry uniform, one class for every value)
|
|
240
|
+
// x fog (the scene's fog composed last, or left out of the program).
|
|
241
|
+
// An opaque class writes alpha 1 (see unlitFragment). Lights arrive
|
|
242
|
+
// through the scene's shared params (light nodes); the base color, map
|
|
243
|
+
// and highlight are per entry. The
|
|
219
244
|
// shadow set is shared too and indexed like the lights: one atlas sampler,
|
|
220
245
|
// directional light i's maps (one, or its cascades) as map slots
|
|
221
246
|
// uShadowFirst[i] .. + uShadowCount[i] with a tile rect and a matrix
|
|
222
247
|
// each, and its biases (target-level, bound by the scene); uShadowCount
|
|
223
248
|
// 0 means it does not cast; SHADOW_LOOKUP turns the index into the factor.
|
|
224
|
-
|
|
249
|
+
// The option combination that picks a lit class: the class-cache key is
|
|
250
|
+
// its values in this order, and the fragment builder reads the same
|
|
251
|
+
// object, so the two cannot drift apart.
|
|
252
|
+
type LitClass = {
|
|
253
|
+
map: boolean
|
|
254
|
+
vertexColors: boolean
|
|
255
|
+
triplanar: boolean
|
|
256
|
+
transparent: boolean
|
|
257
|
+
shadow: boolean
|
|
258
|
+
cull: CullMode
|
|
259
|
+
alphaTest: boolean
|
|
260
|
+
fog: boolean
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function litClassKey(c: LitClass): string {
|
|
264
|
+
return Object.values(c).join("|")
|
|
265
|
+
}
|
|
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"
|
|
225
271
|
return glsl`
|
|
226
272
|
in vec3 vWorldPos;
|
|
227
273
|
in vec3 vNormal;
|
|
@@ -232,6 +278,7 @@ function litFragment(map: boolean, vertexColors: boolean, triplanar: boolean, sh
|
|
|
232
278
|
uniform float uSpecular;
|
|
233
279
|
uniform float uShininess;
|
|
234
280
|
${triplanar ? "uniform float uTriplanar;" : ""}
|
|
281
|
+
${alphaTest ? "uniform float uAlphaTest;" : ""}
|
|
235
282
|
uniform vec3 uCamPos;
|
|
236
283
|
uniform vec3 uHemiSky;
|
|
237
284
|
uniform vec3 uHemiGround;
|
|
@@ -248,9 +295,11 @@ function litFragment(map: boolean, vertexColors: boolean, triplanar: boolean, sh
|
|
|
248
295
|
${HEMISPHERE}
|
|
249
296
|
${LAMBERT}
|
|
250
297
|
${BLINN_SPECULAR}
|
|
298
|
+
${fog ? FOG : ""}
|
|
251
299
|
|
|
252
300
|
void main() {
|
|
253
301
|
vec3 n = normalize(vNormal);
|
|
302
|
+
${backFaces ? "if (!gl_FrontFacing) n = -n;" : ""}
|
|
254
303
|
vec4 base = uColor;
|
|
255
304
|
${
|
|
256
305
|
map
|
|
@@ -263,6 +312,7 @@ function litFragment(map: boolean, vertexColors: boolean, triplanar: boolean, sh
|
|
|
263
312
|
: ""
|
|
264
313
|
}
|
|
265
314
|
${vertexColors ? "base *= vColor;" : ""}
|
|
315
|
+
${alphaTest ? "if (base.a < uAlphaTest) discard;" : ""}
|
|
266
316
|
vec3 v = normalize(uCamPos - vWorldPos);
|
|
267
317
|
vec3 light = hemisphere(n, uHemiSky, uHemiGround);
|
|
268
318
|
vec3 spec = vec3(0.0);
|
|
@@ -275,7 +325,9 @@ function litFragment(map: boolean, vertexColors: boolean, triplanar: boolean, sh
|
|
|
275
325
|
light += uLightColor[i] * lambert(n, l) * s;
|
|
276
326
|
spec += uLightColor[i] * blinnSpecular(n, v, l, uShininess) * s;
|
|
277
327
|
}
|
|
278
|
-
|
|
328
|
+
vec3 rgb = base.rgb * light + spec * uSpecular * base.a;
|
|
329
|
+
${fog ? `rgb = fog(rgb, ${alpha}, vWorldPos, uCamPos);` : ""}
|
|
330
|
+
fragColor = vec4(rgb, ${alpha});
|
|
279
331
|
}
|
|
280
332
|
`
|
|
281
333
|
}
|
|
@@ -285,8 +337,8 @@ let litClasses = new Map<string, ShaderMaterialClass>()
|
|
|
285
337
|
/**
|
|
286
338
|
* A lit material: hemisphere ambient plus the scene's directional lights
|
|
287
339
|
* (DirectionalLight nodes), Lambert diffuse, optional
|
|
288
|
-
* Blinn-Phong highlight. Same options as unlit (color, map, transparent
|
|
289
|
-
* plus vertexColors, specular/shininess and triplanar mapping. One program
|
|
340
|
+
* Blinn-Phong highlight. Same options as unlit (color, map, transparent,
|
|
341
|
+
* cull, alphaTest) plus vertexColors, specular/shininess and triplanar mapping. One program
|
|
290
342
|
* per option combination, one pipeline per vertex layout met, shared by
|
|
291
343
|
* every instance - a thousand lit meshes still share one pipeline. No
|
|
292
344
|
* lights set means black except for the hemisphere term, which also
|
|
@@ -297,26 +349,41 @@ export function lit(opts: LitOptions = {}): Material {
|
|
|
297
349
|
let a = color.length === 4 ? color[3] : 1
|
|
298
350
|
let uColor = [color[0] * a, color[1] * a, color[2] * a, a]
|
|
299
351
|
let map = opts.map !== undefined
|
|
300
|
-
let vertexColors = opts.vertexColors === true
|
|
301
352
|
let triplanar = map && opts.triplanar !== undefined
|
|
302
|
-
let
|
|
303
|
-
let
|
|
304
|
-
let
|
|
353
|
+
let alphaTest = opts.alphaTest !== undefined
|
|
354
|
+
let cull = opts.cull ?? "back"
|
|
355
|
+
let flags: LitClass = {
|
|
356
|
+
map,
|
|
357
|
+
vertexColors: opts.vertexColors === true,
|
|
358
|
+
triplanar,
|
|
359
|
+
transparent: opts.transparent === true,
|
|
360
|
+
shadow: opts.receiveShadow !== false,
|
|
361
|
+
cull,
|
|
362
|
+
alphaTest,
|
|
363
|
+
fog: opts.fog !== false,
|
|
364
|
+
}
|
|
365
|
+
let key = litClassKey(flags)
|
|
305
366
|
let cls = litClasses.get(key)
|
|
306
367
|
if (cls === undefined) {
|
|
307
368
|
cls = shaderMaterialClass({
|
|
308
|
-
vertex: vertexColors ? LIT_VERTEX_COLORED : LIT_VERTEX,
|
|
309
|
-
fragment: litFragment(
|
|
310
|
-
transparent,
|
|
369
|
+
vertex: flags.vertexColors ? LIT_VERTEX_COLORED : LIT_VERTEX,
|
|
370
|
+
fragment: litFragment(flags),
|
|
371
|
+
transparent: flags.transparent,
|
|
372
|
+
cull,
|
|
311
373
|
label: "scene-lit-" + key,
|
|
312
374
|
})
|
|
313
375
|
litClasses.set(key, cls)
|
|
314
376
|
}
|
|
377
|
+
let params: ShaderParams = { uColor, uSpecular: opts.specular ?? 0, uShininess: opts.shininess ?? 30 }
|
|
378
|
+
if (triplanar) params.uTriplanar = opts.triplanar!
|
|
379
|
+
if (alphaTest) params.uAlphaTest = opts.alphaTest!
|
|
380
|
+
// A UV-mapped cutout casts its cutout; a color-only or triplanar
|
|
381
|
+
// alphaTest keeps the plain (cull-only) variant.
|
|
382
|
+
let cutout = alphaTest && map && !triplanar
|
|
315
383
|
let material = cls.instance({
|
|
316
|
-
params
|
|
317
|
-
? { uColor, uSpecular: opts.specular ?? 0, uShininess: opts.shininess ?? 30, uTriplanar: opts.triplanar! }
|
|
318
|
-
: { uColor, uSpecular: opts.specular ?? 0, uShininess: opts.shininess ?? 30 },
|
|
384
|
+
params,
|
|
319
385
|
textures: map ? { uMap: opts.map! } : undefined,
|
|
386
|
+
shadow: cutout ? shadowCutoutMaterial(shadowCull(cull), uColor, opts.alphaTest!, opts.map!) : undefined,
|
|
320
387
|
})
|
|
321
388
|
return material
|
|
322
389
|
}
|
|
@@ -341,20 +408,79 @@ const SHADOW_DEPTH_FRAGMENT = glsl`
|
|
|
341
408
|
}
|
|
342
409
|
`
|
|
343
410
|
|
|
344
|
-
let shadowDepth
|
|
411
|
+
let shadowDepth = new Map<CullMode, Material>()
|
|
345
412
|
|
|
346
413
|
/** The override material of a scene's shadow view (internal): one class
|
|
347
|
-
* for the app, built on first use.
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
414
|
+
* per cull mode for the app, built on first use. The default, "front",
|
|
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 material = shadowDepth.get(cull)
|
|
419
|
+
if (material === undefined) {
|
|
420
|
+
material = shaderMaterialClass({
|
|
351
421
|
vertex: SHADOW_DEPTH_VERTEX,
|
|
352
422
|
fragment: SHADOW_DEPTH_FRAGMENT,
|
|
353
|
-
cull
|
|
354
|
-
label: "scene-shadow-depth",
|
|
423
|
+
cull,
|
|
424
|
+
label: "scene-shadow-depth-" + cull,
|
|
355
425
|
}).instance()
|
|
426
|
+
shadowDepth.set(cull, material)
|
|
427
|
+
}
|
|
428
|
+
return material
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/** The shadow pass's cull for a material's cull: the opposite side
|
|
432
|
+
* (Three's shadowSide default), none stays none. */
|
|
433
|
+
function shadowCull(cull: CullMode): CullMode {
|
|
434
|
+
return cull === "none" ? "none" : cull === "back" ? "front" : "back"
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/** "back" maps to the default depth material, so its variant is
|
|
438
|
+
* undefined. */
|
|
439
|
+
function shadowVariant(cull: CullMode): Material | undefined {
|
|
440
|
+
return cull === "back" ? undefined : shadowDepthMaterial(shadowCull(cull))
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// The cutout depth pass: the caster's map alpha (times its color alpha)
|
|
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);
|
|
356
465
|
}
|
|
357
|
-
|
|
466
|
+
`
|
|
467
|
+
|
|
468
|
+
let shadowCutout = new Map<CullMode, ShaderMaterialClass>()
|
|
469
|
+
|
|
470
|
+
/** The shadow variant of a UV-mapped cutout material: one class per
|
|
471
|
+
* shadow cull mode, an instance per material (its map, color, cutoff). */
|
|
472
|
+
function shadowCutoutMaterial(cull: CullMode, uColor: number[], uAlphaTest: number, uMap: TextureId): Material {
|
|
473
|
+
let cls = shadowCutout.get(cull)
|
|
474
|
+
if (cls === undefined) {
|
|
475
|
+
cls = shaderMaterialClass({
|
|
476
|
+
vertex: SHADOW_CUTOUT_VERTEX,
|
|
477
|
+
fragment: SHADOW_CUTOUT_FRAGMENT,
|
|
478
|
+
cull,
|
|
479
|
+
label: "scene-shadow-cutout-" + cull,
|
|
480
|
+
})
|
|
481
|
+
shadowCutout.set(cull, cls)
|
|
482
|
+
}
|
|
483
|
+
return cls.instance({ params: { uColor, uAlphaTest }, textures: { uMap } })
|
|
358
484
|
}
|
|
359
485
|
|
|
360
486
|
export type SpriteOptions = UnlitOptions & {
|
|
@@ -377,6 +503,7 @@ const SPRITE_VERTEX_SRC = glsl`
|
|
|
377
503
|
in vec3 aPos;
|
|
378
504
|
in vec2 aUV;
|
|
379
505
|
out vec2 vUv;
|
|
506
|
+
out vec3 vWorldPos;
|
|
380
507
|
uniform mat4 uModel;
|
|
381
508
|
uniform mat4 uViewProj;
|
|
382
509
|
uniform vec3 uCamRight;
|
|
@@ -386,6 +513,7 @@ const SPRITE_VERTEX_SRC = glsl`
|
|
|
386
513
|
vec3 center = uModel[3].xyz;
|
|
387
514
|
vec2 size = vec2(length(uModel[0].xyz), length(uModel[1].xyz));
|
|
388
515
|
vec3 world = center + uCamRight * (aPos.x * size.x) + uCamUp * (aPos.y * size.y);
|
|
516
|
+
vWorldPos = world;
|
|
389
517
|
gl_Position = uViewProj * vec4(world, 1.0);
|
|
390
518
|
vUv = aUV;
|
|
391
519
|
}
|
|
@@ -395,6 +523,7 @@ const SPRITE_FIXED_Y_VERTEX_SRC = glsl`
|
|
|
395
523
|
in vec3 aPos;
|
|
396
524
|
in vec2 aUV;
|
|
397
525
|
out vec2 vUv;
|
|
526
|
+
out vec3 vWorldPos;
|
|
398
527
|
uniform mat4 uModel;
|
|
399
528
|
uniform mat4 uViewProj;
|
|
400
529
|
uniform vec3 uCamPos;
|
|
@@ -407,6 +536,7 @@ const SPRITE_FIXED_Y_VERTEX_SRC = glsl`
|
|
|
407
536
|
float len = length(toCam);
|
|
408
537
|
vec3 right = len > 1e-6 ? vec3(toCam.z, 0.0, -toCam.x) / len : vec3(1.0, 0.0, 0.0);
|
|
409
538
|
vec3 world = center + right * (aPos.x * size.x) + vec3(0.0, aPos.y * size.y, 0.0);
|
|
539
|
+
vWorldPos = world;
|
|
410
540
|
gl_Position = uViewProj * vec4(world, 1.0);
|
|
411
541
|
vUv = aUV;
|
|
412
542
|
}
|
|
@@ -431,12 +561,13 @@ export function sprite(opts: SpriteOptions = {}): Material {
|
|
|
431
561
|
let map = opts.map !== undefined
|
|
432
562
|
let transparent = opts.transparent !== false
|
|
433
563
|
let fixedY = opts.billboard === "fixed-y"
|
|
434
|
-
let
|
|
564
|
+
let fog = opts.fog !== false
|
|
565
|
+
let key = [map, transparent, fixedY, fog].join("|")
|
|
435
566
|
let cls = spriteClasses.get(key)
|
|
436
567
|
if (cls === undefined) {
|
|
437
568
|
cls = shaderMaterialClass({
|
|
438
569
|
vertex: fixedY ? SPRITE_FIXED_Y_VERTEX_SRC : SPRITE_VERTEX_SRC,
|
|
439
|
-
fragment: map
|
|
570
|
+
fragment: unlitFragment(map, false, transparent, fog),
|
|
440
571
|
transparent,
|
|
441
572
|
cull: "none",
|
|
442
573
|
label: "scene-sprite-" + key,
|
|
@@ -564,6 +695,10 @@ export type ShaderMaterialInstanceOptions = {
|
|
|
564
695
|
* setMeshParams. */
|
|
565
696
|
params?: ShaderParams
|
|
566
697
|
textures?: TextureBindings
|
|
698
|
+
/** The depth variant a shadow view draws this instance with (see
|
|
699
|
+
* Material.shadow): a cutout's discard, an instanced class's vertex
|
|
700
|
+
* placement. Default: the depth pass with this class's cull side. */
|
|
701
|
+
shadow?: Material
|
|
567
702
|
}
|
|
568
703
|
|
|
569
704
|
export type ShaderMaterialOptions = ShaderMaterialClassOptions & ShaderMaterialInstanceOptions
|
|
@@ -610,6 +745,7 @@ export function shaderMaterialClass(opts: ShaderMaterialClassOptions): ShaderMat
|
|
|
610
745
|
let normalMatrix = /\buNormal\b/.test(opts.vertex) || /\buNormal\b/.test(opts.fragment)
|
|
611
746
|
let transparent = opts.transparent ?? (opts.blend !== undefined && opts.blend !== "none")
|
|
612
747
|
let depth = opts.depth ?? true
|
|
748
|
+
let cull = opts.cull ?? "back"
|
|
613
749
|
// An empty list declares nothing - same as absent (the engine requires an
|
|
614
750
|
// instance buffer exactly when attributes are declared).
|
|
615
751
|
let instanceAttributes = opts.instanceAttributes?.length ? opts.instanceAttributes.map(a => ({ ...a })) : undefined
|
|
@@ -640,7 +776,7 @@ export function shaderMaterialClass(opts: ShaderMaterialClassOptions): ShaderMat
|
|
|
640
776
|
// only applies when there is one.
|
|
641
777
|
depthWrite: opts.depthWrite ?? (transparent && depth ? false : undefined),
|
|
642
778
|
blend: opts.blend ?? (transparent ? "alpha" : undefined),
|
|
643
|
-
cull
|
|
779
|
+
cull,
|
|
644
780
|
topology: opts.topology,
|
|
645
781
|
label: opts.label,
|
|
646
782
|
})
|
|
@@ -650,7 +786,20 @@ export function shaderMaterialClass(opts: ShaderMaterialClassOptions): ShaderMat
|
|
|
650
786
|
}
|
|
651
787
|
return {
|
|
652
788
|
instance(inst = {}) {
|
|
653
|
-
return {
|
|
789
|
+
return {
|
|
790
|
+
normalMatrix,
|
|
791
|
+
attributes,
|
|
792
|
+
transparent,
|
|
793
|
+
instanceAttributes,
|
|
794
|
+
pipeline: pipelineFor,
|
|
795
|
+
params: inst.params ?? {},
|
|
796
|
+
textures: inst.textures,
|
|
797
|
+
// Lazy: the depth materials are shaderMaterialClass instances
|
|
798
|
+
// themselves, so an eager variant would recurse into its own cache.
|
|
799
|
+
get shadow() {
|
|
800
|
+
return inst.shadow ?? shadowVariant(cull)
|
|
801
|
+
},
|
|
802
|
+
}
|
|
654
803
|
},
|
|
655
804
|
dispose() {
|
|
656
805
|
for (let pipeline of pipelines.values()) destroyRenderPipeline(pipeline)
|
package/src/model.ts
CHANGED
|
@@ -20,6 +20,12 @@ import type { Material } from "./material.ts"
|
|
|
20
20
|
import { add, createGroup, createMesh, remove } from "./scene.ts"
|
|
21
21
|
import type { Mesh, SceneNode } from "./scene.ts"
|
|
22
22
|
|
|
23
|
+
/** Anisotropic filtering level for a model's textures: the engines' usual
|
|
24
|
+
* default (Godot ships 2x, Unity's quality presets 2-8x) - enough to keep a
|
|
25
|
+
* tiled surface legible at a grazing angle, cheap on every GPU. Clamped to
|
|
26
|
+
* the device by the runtime. */
|
|
27
|
+
const MODEL_ANISOTROPY = 4
|
|
28
|
+
|
|
23
29
|
export type ModelOptions = {
|
|
24
30
|
/** The material for each glTF material (default: `lit` with its color,
|
|
25
31
|
* map and transparency). `map` is the uploaded base color texture, or
|
|
@@ -44,8 +50,9 @@ export type Model = SceneNode & {
|
|
|
44
50
|
|
|
45
51
|
/**
|
|
46
52
|
* Build the scene object for parsed model data: upload its images (repeat
|
|
47
|
-
* wrap, mipmapped), make a material per glTF material, a
|
|
48
|
-
* all under one Group. Synchronous - the data is already in
|
|
53
|
+
* wrap, mipmapped, MODEL_ANISOTROPY), make a material per glTF material, a
|
|
54
|
+
* mesh per part, all under one Group. Synchronous - the data is already in
|
|
55
|
+
* memory.
|
|
49
56
|
*/
|
|
50
57
|
export function createModel(data: ModelData, opts: ModelOptions = {}): Model {
|
|
51
58
|
let label = opts.label
|
|
@@ -54,11 +61,18 @@ export function createModel(data: ModelData, opts: ModelOptions = {}): Model {
|
|
|
54
61
|
return createTexture(image.data, image.width, image.height, {
|
|
55
62
|
wrap: "repeat",
|
|
56
63
|
mipmap: true,
|
|
64
|
+
anisotropy: MODEL_ANISOTROPY,
|
|
57
65
|
autoFree: false,
|
|
58
66
|
label: label ? label + "-image" + i : undefined,
|
|
59
67
|
})
|
|
60
68
|
})
|
|
61
|
-
let make = opts.material ?? ((m: ModelMaterial, map: TextureId | null): Material => lit({
|
|
69
|
+
let make = opts.material ?? ((m: ModelMaterial, map: TextureId | null): Material => lit({
|
|
70
|
+
color: m.color,
|
|
71
|
+
map: map ?? undefined,
|
|
72
|
+
transparent: m.transparent,
|
|
73
|
+
cull: m.doubleSided ? "none" : "back",
|
|
74
|
+
alphaTest: m.alphaMode === "MASK" ? m.alphaCutoff : undefined,
|
|
75
|
+
}))
|
|
62
76
|
let materials = data.materials.map((m) => make(m, m.map === null ? null : textures[m.map]!))
|
|
63
77
|
|
|
64
78
|
let model = createGroup() as Model
|