@solidrt/3d 0.0.70 → 0.0.72
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 +75 -16
- package/package.json +3 -3
- package/src/components/directional-light.tsx +2 -2
- package/src/components/first-person-camera.tsx +2 -2
- package/src/components/group.tsx +2 -2
- package/src/components/hemisphere-light.tsx +2 -2
- package/src/components/instanced-mesh.tsx +3 -3
- package/src/components/lod.tsx +3 -3
- package/src/components/mesh.tsx +2 -2
- package/src/components/node-props.ts +13 -1
- package/src/components/orbit-camera.tsx +2 -2
- package/src/components/point-light.tsx +2 -2
- package/src/components/record-mesh.tsx +2 -2
- package/src/components/scene.tsx +13 -2
- package/src/components/shots.tsx +2 -2
- package/src/components/splat-mesh.tsx +2 -2
- package/src/components/spot-light.tsx +2 -2
- package/src/components/sprite.tsx +2 -2
- package/src/components/view3d.tsx +2 -2
- package/src/geometry.ts +23 -188
- package/src/glsl.ts +13 -3
- package/src/index.ts +1 -1
- package/src/material.ts +74 -50
- package/src/mesh.ts +61 -109
- package/src/mixer.ts +6 -1
- package/src/node.ts +47 -3
- package/src/scene-pointer.ts +11 -8
- package/src/scene.ts +49 -5
package/src/geometry.ts
CHANGED
|
@@ -40,7 +40,8 @@
|
|
|
40
40
|
// array math (tests/geometry.test.ts runs it headless on
|
|
41
41
|
// flux). The GPU buffer step lives in geometry-gpu.ts.
|
|
42
42
|
|
|
43
|
-
import
|
|
43
|
+
import { attributeAccess as coreAccess, checkLayout as coreCheck, isFloatFormat, isFloatLayout as coreIsFloat, layoutBytes, layoutKey as coreKey, layoutSlot as coreSlot, layoutStride as coreStride, layoutView, VERTEX_FORMATS } from "@solidrt/core/gpu"
|
|
44
|
+
import type { AttributeAccess, Topology, VertexAttribute, VertexFormat } from "@solidrt/core/gpu"
|
|
44
45
|
import { premultipliedColor } from "./color.ts"
|
|
45
46
|
import { add, compose, cross, mat4, normalize, normalMatrix, sub, updateRotation, updateScale } from "./math.ts"
|
|
46
47
|
import type { Quat, TransformUpdate, Vec2, Vec3 } from "./math.ts"
|
|
@@ -78,144 +79,15 @@ export const VERTEX_LAYOUTS: Record<"base" | "colored" | "skinned", VertexAttrib
|
|
|
78
79
|
* format before packing). */
|
|
79
80
|
export const BASE_FLOATS = 8
|
|
80
81
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
* Every format is a multiple of 4 bytes (WebGPU's alignment rule), which
|
|
91
|
-
* is what keeps every offset and stride 4-aligned with no padding
|
|
92
|
-
* arithmetic anywhere. */
|
|
93
|
-
export type FormatCodec = {
|
|
94
|
-
bytes: number
|
|
95
|
-
components: number
|
|
96
|
-
kind: FormatKind
|
|
97
|
-
get(dv: DataView, at: number, k: number): number
|
|
98
|
-
set(dv: DataView, at: number, k: number, v: number): void
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// The integer component kinds behind the packed formats: width, the
|
|
102
|
-
// value that maps to 1 (GL ES 3.0's normalization rule, -max mapping to
|
|
103
|
-
// -1 and the one value below it clamped there), and the raw accessors.
|
|
104
|
-
type IntKind = { bytes: number; max: number; get(dv: DataView, at: number): number; set(dv: DataView, at: number, v: number): void }
|
|
105
|
-
const U8: IntKind = { bytes: 1, max: 255, get: (dv, at) => dv.getUint8(at), set: (dv, at, v) => dv.setUint8(at, v) }
|
|
106
|
-
const S8: IntKind = { bytes: 1, max: 127, get: (dv, at) => dv.getInt8(at), set: (dv, at, v) => dv.setInt8(at, v) }
|
|
107
|
-
const U16: IntKind = { bytes: 2, max: 65535, get: (dv, at) => dv.getUint16(at, true), set: (dv, at, v) => dv.setUint16(at, v, true) }
|
|
108
|
-
const S16: IntKind = { bytes: 2, max: 32767, get: (dv, at) => dv.getInt16(at, true), set: (dv, at, v) => dv.setInt16(at, v, true) }
|
|
109
|
-
const U32: IntKind = { bytes: 4, max: 4294967295, get: (dv, at) => dv.getUint32(at, true), set: (dv, at, v) => dv.setUint32(at, v, true) }
|
|
110
|
-
const S32: IntKind = { bytes: 4, max: 2147483647, get: (dv, at) => dv.getInt32(at, true), set: (dv, at, v) => dv.setInt32(at, v, true) }
|
|
111
|
-
|
|
112
|
-
function clamp(v: number, lo: number, hi: number): number {
|
|
113
|
-
return v < lo ? lo : v > hi ? hi : v
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function floatCodec(components: number, bytes: number, get: (dv: DataView, at: number) => number, set: (dv: DataView, at: number, v: number) => void): FormatCodec {
|
|
117
|
-
return {
|
|
118
|
-
bytes: components * bytes,
|
|
119
|
-
components,
|
|
120
|
-
kind: "float",
|
|
121
|
-
get: (dv, at, k) => get(dv, at + k * bytes),
|
|
122
|
-
set: (dv, at, k, v) => set(dv, at + k * bytes, v),
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function unormCodec(components: number, int: IntKind): FormatCodec {
|
|
127
|
-
return {
|
|
128
|
-
bytes: components * int.bytes,
|
|
129
|
-
components,
|
|
130
|
-
kind: "float",
|
|
131
|
-
get: (dv, at, k) => int.get(dv, at + k * int.bytes) / int.max,
|
|
132
|
-
set: (dv, at, k, v) => int.set(dv, at + k * int.bytes, Math.round(clamp(v, 0, 1) * int.max)),
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function snormCodec(components: number, int: IntKind): FormatCodec {
|
|
137
|
-
return {
|
|
138
|
-
bytes: components * int.bytes,
|
|
139
|
-
components,
|
|
140
|
-
kind: "float",
|
|
141
|
-
get: (dv, at, k) => Math.max(int.get(dv, at + k * int.bytes) / int.max, -1),
|
|
142
|
-
set: (dv, at, k, v) => int.set(dv, at + k * int.bytes, Math.round(clamp(v, -1, 1) * int.max)),
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function uintCodec(components: number, int: IntKind): FormatCodec {
|
|
147
|
-
return {
|
|
148
|
-
bytes: components * int.bytes,
|
|
149
|
-
components,
|
|
150
|
-
kind: "uint",
|
|
151
|
-
get: (dv, at, k) => int.get(dv, at + k * int.bytes),
|
|
152
|
-
set: (dv, at, k, v) => int.set(dv, at + k * int.bytes, Math.round(clamp(v, 0, int.max))),
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function sintCodec(components: number, int: IntKind): FormatCodec {
|
|
157
|
-
return {
|
|
158
|
-
bytes: components * int.bytes,
|
|
159
|
-
components,
|
|
160
|
-
kind: "sint",
|
|
161
|
-
get: (dv, at, k) => int.get(dv, at + k * int.bytes),
|
|
162
|
-
set: (dv, at, k, v) => int.set(dv, at + k * int.bytes, Math.round(clamp(v, -int.max - 1, int.max))),
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
let getF32 = (dv: DataView, at: number) => dv.getFloat32(at, true)
|
|
167
|
-
let setF32 = (dv: DataView, at: number, v: number) => dv.setFloat32(at, v, true)
|
|
168
|
-
let getF16 = (dv: DataView, at: number) => dv.getFloat16(at, true)
|
|
169
|
-
let setF16 = (dv: DataView, at: number, v: number) => dv.setFloat16(at, v, true)
|
|
170
|
-
|
|
171
|
-
/** The vertex vocabulary: one codec per format (the engine's table in
|
|
172
|
-
* the same spelling), so it is also the list a declared format must be
|
|
173
|
-
* one of. A format feeds the shader `in` of its kind and component
|
|
174
|
-
* count (`formatFeeds`). */
|
|
175
|
-
export const VERTEX_FORMATS: Record<VertexFormat, FormatCodec> = {
|
|
176
|
-
float32: floatCodec(1, Float32Array.BYTES_PER_ELEMENT, getF32, setF32),
|
|
177
|
-
float32x2: floatCodec(2, Float32Array.BYTES_PER_ELEMENT, getF32, setF32),
|
|
178
|
-
float32x3: floatCodec(3, Float32Array.BYTES_PER_ELEMENT, getF32, setF32),
|
|
179
|
-
float32x4: floatCodec(4, Float32Array.BYTES_PER_ELEMENT, getF32, setF32),
|
|
180
|
-
float16x2: floatCodec(2, Float16Array.BYTES_PER_ELEMENT, getF16, setF16),
|
|
181
|
-
float16x4: floatCodec(4, Float16Array.BYTES_PER_ELEMENT, getF16, setF16),
|
|
182
|
-
unorm8x4: unormCodec(4, U8),
|
|
183
|
-
snorm8x4: snormCodec(4, S8),
|
|
184
|
-
unorm16x2: unormCodec(2, U16),
|
|
185
|
-
unorm16x4: unormCodec(4, U16),
|
|
186
|
-
snorm16x2: snormCodec(2, S16),
|
|
187
|
-
snorm16x4: snormCodec(4, S16),
|
|
188
|
-
uint8x4: uintCodec(4, U8),
|
|
189
|
-
uint16x2: uintCodec(2, U16),
|
|
190
|
-
uint16x4: uintCodec(4, U16),
|
|
191
|
-
uint32: uintCodec(1, U32),
|
|
192
|
-
uint32x2: uintCodec(2, U32),
|
|
193
|
-
uint32x3: uintCodec(3, U32),
|
|
194
|
-
uint32x4: uintCodec(4, U32),
|
|
195
|
-
sint8x4: sintCodec(4, S8),
|
|
196
|
-
sint16x2: sintCodec(2, S16),
|
|
197
|
-
sint16x4: sintCodec(4, S16),
|
|
198
|
-
sint32: sintCodec(1, S32),
|
|
199
|
-
sint32x2: sintCodec(2, S32),
|
|
200
|
-
sint32x3: sintCodec(3, S32),
|
|
201
|
-
sint32x4: sintCodec(4, S32),
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/** Whether a layout declaring `declared` may feed a program input the
|
|
205
|
-
* engine reflects as `reflected` (float32xN for `vec*`, uint32xN for
|
|
206
|
-
* `uvec*`, sint32xN for `ivec*`): same component count and same kind,
|
|
207
|
-
* the engine's own pipeline rule. */
|
|
208
|
-
export function formatFeeds(declared: VertexFormat, reflected: VertexFormat): boolean {
|
|
209
|
-
let d = VERTEX_FORMATS[declared]
|
|
210
|
-
let r = VERTEX_FORMATS[reflected]
|
|
211
|
-
return d.components === r.components && d.kind === r.kind
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/** Whether a format is one of the float32 family: what a Float32Array
|
|
215
|
-
* holds directly, and what the generators write. */
|
|
216
|
-
export function isFloatFormat(format: VertexFormat): boolean {
|
|
217
|
-
return format.startsWith("float32")
|
|
218
|
-
}
|
|
82
|
+
// The vertex vocabulary - the format codecs, the stride and key of an
|
|
83
|
+
// attribute list, the record accessors - is core's (`@solidrt/core/gpu`,
|
|
84
|
+
// vertex.ts), shared with the 2d package's material style streams;
|
|
85
|
+
// re-exported here so the package's own modules and callers keep one
|
|
86
|
+
// import for layouts. What stays here is the PRESET side: the named
|
|
87
|
+
// layouts and the functions taking a VertexLayout (a preset name or a
|
|
88
|
+
// list).
|
|
89
|
+
export { formatFeeds, isFloatFormat, VERTEX_FORMATS } from "@solidrt/core/gpu"
|
|
90
|
+
export type { AttributeAccess, FormatCodec, FormatKind } from "@solidrt/core/gpu"
|
|
219
91
|
|
|
220
92
|
/** The attribute list of a layout (a preset name resolves to its list). */
|
|
221
93
|
export function layoutAttributes(layout?: VertexLayout): VertexAttribute[] {
|
|
@@ -227,64 +99,48 @@ export function layoutAttributes(layout?: VertexLayout): VertexAttribute[] {
|
|
|
227
99
|
|
|
228
100
|
/** Bytes per vertex of a layout - its interleave stride. */
|
|
229
101
|
export function layoutStride(layout?: VertexLayout): number {
|
|
230
|
-
|
|
231
|
-
for (let attr of layoutAttributes(layout)) stride += VERTEX_FORMATS[attr.format].bytes
|
|
232
|
-
return stride
|
|
102
|
+
return coreStride(layoutAttributes(layout))
|
|
233
103
|
}
|
|
234
104
|
|
|
235
105
|
/** A layout's identity as a string (name:format per attribute, in order):
|
|
236
106
|
* two layouts with equal keys interleave identically. */
|
|
237
107
|
export function layoutKey(layout?: VertexLayout): string {
|
|
238
|
-
return layoutAttributes(layout)
|
|
239
|
-
.map(a => a.name + ":" + a.format)
|
|
240
|
-
.join(",")
|
|
108
|
+
return coreKey(layoutAttributes(layout))
|
|
241
109
|
}
|
|
242
110
|
|
|
243
111
|
/** Where an attribute sits in the interleave: its byte offset, format
|
|
244
112
|
* and component count. Null when the layout does not carry that name. */
|
|
245
113
|
export function layoutSlot(layout: VertexLayout | undefined, name: string): { offset: number; format: VertexFormat; components: number } | null {
|
|
246
|
-
|
|
247
|
-
for (let attr of layoutAttributes(layout)) {
|
|
248
|
-
let codec = VERTEX_FORMATS[attr.format]
|
|
249
|
-
if (attr.name === name) return { offset, format: attr.format, components: codec.components }
|
|
250
|
-
offset += codec.bytes
|
|
251
|
-
}
|
|
252
|
-
return null
|
|
114
|
+
return coreSlot(layoutAttributes(layout), name)
|
|
253
115
|
}
|
|
254
116
|
|
|
255
117
|
/** Whether every attribute of a layout is float32-family: such a buffer
|
|
256
118
|
* is a Float32Array of `layoutStride / 4` floats per vertex, the
|
|
257
119
|
* generators' write form. */
|
|
258
120
|
export function isFloatLayout(layout?: VertexLayout): boolean {
|
|
259
|
-
return layoutAttributes(layout)
|
|
121
|
+
return coreIsFloat(layoutAttributes(layout))
|
|
260
122
|
}
|
|
261
123
|
|
|
262
|
-
/** The check every layout must pass: aPos float32x3 first
|
|
263
|
-
* duplicate names. */
|
|
124
|
+
/** The check every layout must pass: aPos float32x3 first, known
|
|
125
|
+
* formats and no duplicate names. */
|
|
264
126
|
function checkLayout(layout: VertexAttribute[], where: string): void {
|
|
265
127
|
let first = layout[0]
|
|
266
128
|
if (first === undefined || first.name !== "aPos" || first.format !== "float32x3") {
|
|
267
129
|
throw new Error(where + ": a layout must start with aPos float32x3")
|
|
268
130
|
}
|
|
269
|
-
|
|
270
|
-
for (let attr of layout) {
|
|
271
|
-
if (seen.has(attr.name)) throw new Error(where + ": duplicate attribute '" + attr.name + "'")
|
|
272
|
-
seen.add(attr.name)
|
|
273
|
-
}
|
|
131
|
+
coreCheck(layout, where)
|
|
274
132
|
}
|
|
275
133
|
|
|
276
134
|
/** The bytes of a vertex buffer, whatever view holds them. */
|
|
277
135
|
export function vertexBytes(vertices: ArrayBufferView): Uint8Array {
|
|
278
|
-
return
|
|
136
|
+
return layoutBytes(vertices)
|
|
279
137
|
}
|
|
280
138
|
|
|
281
139
|
/** The view a vertex buffer of `layout` is handed out as: a Float32Array
|
|
282
140
|
* over an all-float layout (the generators' form, indexable as floats),
|
|
283
141
|
* a Uint8Array over a layout with a packed channel. */
|
|
284
142
|
export function vertexView(layout: VertexLayout | undefined, buffer: ArrayBuffer, byteOffset = 0, byteLength = buffer.byteLength - byteOffset): ArrayBufferView {
|
|
285
|
-
return
|
|
286
|
-
? new Float32Array(buffer, byteOffset, byteLength / Float32Array.BYTES_PER_ELEMENT)
|
|
287
|
-
: new Uint8Array(buffer, byteOffset, byteLength)
|
|
143
|
+
return layoutView(layoutAttributes(layout), buffer, byteOffset, byteLength)
|
|
288
144
|
}
|
|
289
145
|
|
|
290
146
|
/** Vertices in a buffer of `layout`. Throws naming `where` when the byte
|
|
@@ -302,32 +158,11 @@ export function vertexCount(vertices: ArrayBufferView, layout: VertexLayout | un
|
|
|
302
158
|
return vertices.byteLength / stride
|
|
303
159
|
}
|
|
304
160
|
|
|
305
|
-
/** A view on one attribute of a vertex buffer: component `k` of vertex
|
|
306
|
-
* `i`, read and written as the float the shader sees, through the
|
|
307
|
-
* format's codec. The one way anything touches vertex data, so no
|
|
308
|
-
* caller knows an offset, a stride or a format. */
|
|
309
|
-
export type AttributeAccess = {
|
|
310
|
-
format: VertexFormat
|
|
311
|
-
components: number
|
|
312
|
-
get(i: number, k: number): number
|
|
313
|
-
set(i: number, k: number, v: number): void
|
|
314
|
-
}
|
|
315
|
-
|
|
316
161
|
/** The accessor for `name` over a bare vertex buffer of `layout`; null
|
|
317
|
-
* when the layout lacks the name
|
|
162
|
+
* when the layout lacks the name (core's attributeAccess over the
|
|
163
|
+
* resolved list). */
|
|
318
164
|
export function attributeAccess(vertices: ArrayBufferView, layout: VertexLayout | undefined, name: string): AttributeAccess | null {
|
|
319
|
-
|
|
320
|
-
if (slot === null) return null
|
|
321
|
-
let stride = layoutStride(layout)
|
|
322
|
-
let codec = VERTEX_FORMATS[slot.format]
|
|
323
|
-
let offset = slot.offset
|
|
324
|
-
let dv = new DataView(vertices.buffer, vertices.byteOffset, vertices.byteLength)
|
|
325
|
-
return {
|
|
326
|
-
format: slot.format,
|
|
327
|
-
components: codec.components,
|
|
328
|
-
get: (i, k) => codec.get(dv, i * stride + offset, k),
|
|
329
|
-
set: (i, k, v) => codec.set(dv, i * stride + offset, k, v),
|
|
330
|
-
}
|
|
165
|
+
return coreAccess(vertices, layoutAttributes(layout), name)
|
|
331
166
|
}
|
|
332
167
|
|
|
333
168
|
/** One extra vertex buffer of a geometry (see `Geometry.streams`): its
|
package/src/glsl.ts
CHANGED
|
@@ -1739,6 +1739,12 @@ export type UnlitSourceOptions = {
|
|
|
1739
1739
|
/** Multiply the base by the per-instance `iColor` (implies `instanced`;
|
|
1740
1740
|
* see the lit option): the one per-copy value an unlit fleet wants. */
|
|
1741
1741
|
instanceColors?: boolean
|
|
1742
|
+
/** The sprite material's radial shape: declare `uniform float
|
|
1743
|
+
* uFalloff` and multiply the base (color and alpha, the premultiplied
|
|
1744
|
+
* rule) by `(1 - 2 * |vUv - 0.5|)^uFalloff` clamped over the quad's
|
|
1745
|
+
* inscribed disc, after the cutout and before the surface slot. Reads
|
|
1746
|
+
* vUv, so the fragment declares it with or without a map. */
|
|
1747
|
+
radial?: boolean
|
|
1742
1748
|
/** GLSL spliced at file scope, before main. */
|
|
1743
1749
|
prelude?: string
|
|
1744
1750
|
/** GLSL declaring `void surface(inout Surface s)`, called with the base
|
|
@@ -1759,6 +1765,7 @@ type UnlitSource = {
|
|
|
1759
1765
|
instanced: boolean
|
|
1760
1766
|
instanceColors: boolean
|
|
1761
1767
|
morph: boolean
|
|
1768
|
+
radial: boolean
|
|
1762
1769
|
prelude: string
|
|
1763
1770
|
surface: string
|
|
1764
1771
|
}
|
|
@@ -1775,6 +1782,7 @@ function resolveUnlit(o: UnlitSourceOptions): UnlitSource {
|
|
|
1775
1782
|
instanced: o.instanced === true || o.instanceColors === true,
|
|
1776
1783
|
instanceColors: o.instanceColors === true,
|
|
1777
1784
|
morph: o.morph === true,
|
|
1785
|
+
radial: o.radial === true,
|
|
1778
1786
|
prelude: o.prelude ?? "",
|
|
1779
1787
|
surface: o.surface ?? "",
|
|
1780
1788
|
}
|
|
@@ -1837,20 +1845,21 @@ function unlitBase(c: UnlitSource): string {
|
|
|
1837
1845
|
* does - vWorldPos is read whether or not the program fogs).
|
|
1838
1846
|
*
|
|
1839
1847
|
* Per-entry uniforms on instance(): `uColor` (linear light, premultiplied
|
|
1840
|
-
* vec4), plus `uMap`/`uAlphaTest` for the options that declare
|
|
1841
|
-
* whatever `prelude` declares.
|
|
1848
|
+
* vec4), plus `uMap`/`uAlphaTest`/`uFalloff` for the options that declare
|
|
1849
|
+
* them, plus whatever `prelude` declares.
|
|
1842
1850
|
*/
|
|
1843
1851
|
export function unlitFragment(o: UnlitSourceOptions = {}): string {
|
|
1844
1852
|
let c = resolveUnlit(o)
|
|
1845
1853
|
let alpha = c.transparent ? "base.a" : "1.0"
|
|
1846
1854
|
return glsl`
|
|
1847
|
-
${c.map ? "in vec2 vUv;" : ""}
|
|
1855
|
+
${c.map || c.radial ? "in vec2 vUv;" : ""}
|
|
1848
1856
|
in vec3 vWorldPos;
|
|
1849
1857
|
${c.vertexColors || c.instanceColors ? "in vec4 vColor;" : ""}
|
|
1850
1858
|
${c.map ? "uniform sampler2D uMap;" : ""}
|
|
1851
1859
|
uniform vec4 uColor;
|
|
1852
1860
|
${c.alphaTest ? "uniform float uAlphaTest;" : ""}
|
|
1853
1861
|
${c.mapTransform ? "uniform vec4 uMapTransform;" : ""}
|
|
1862
|
+
${c.radial ? "uniform float uFalloff;" : ""}
|
|
1854
1863
|
${sceneSource({ lights: false, fog: c.fog })}
|
|
1855
1864
|
${LOD_FADE}
|
|
1856
1865
|
${c.prelude}
|
|
@@ -1859,6 +1868,7 @@ export function unlitFragment(o: UnlitSourceOptions = {}): string {
|
|
|
1859
1868
|
lodFade();
|
|
1860
1869
|
${unlitBase(c)}
|
|
1861
1870
|
${c.alphaTest ? "if (base.a < uAlphaTest) discard;" : ""}
|
|
1871
|
+
${c.radial ? "base *= pow(clamp(1.0 - 2.0 * length(vUv - 0.5), 0.0, 1.0), uFalloff);" : ""}
|
|
1862
1872
|
${c.surface ? "Surface s = surfaceOf(base, vec3(0.0));\n surface(s);\n base = s.base;" : ""}
|
|
1863
1873
|
fragColor = sceneOutput(base.rgb, ${alpha}, vWorldPos);
|
|
1864
1874
|
}
|
package/src/index.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// without Solid components) and the component face (Scene/Mesh/Group/
|
|
6
6
|
// PerspectiveCamera) on top. See AGENTS.md for the model and the traps.
|
|
7
7
|
|
|
8
|
-
export { add, createGroup, destroy, getMorphNames, getMorphWeights, getRotation, getTransform, lookAt, remove, setMorphWeights, setTransform, setTransition, setVisible, worldPosition } from "./node.ts"
|
|
8
|
+
export { add, createGroup, destroy, getMorphNames, getMorphWeights, getRotation, getTransform, lookAt, remove, setMorphWeights, setTimeScale, setTransform, setTransition, setVisible, timeRate, worldPosition } from "./node.ts"
|
|
9
9
|
export type { HoverHandlers, LodConfig, MorphWeights, NodePointerEvent, NodeTapEvent, NodeWheelEvent, SceneEventBase, SceneNode, ScenePointerEvent, ScenePointerListener, SceneTapEvent, SceneWheelEvent, TransformUpdate, TransitionEndEvent } from "./node.ts"
|
|
10
10
|
export { addInstance, createInstancedMesh, createMesh, createRecordMesh, createSprite, disposeInstances, setCastShadow, setCulling, setDrawRange, setEnvironment, setGeometry, setInstanceStyle, setLayers, setMaterial, setMeshParams, setRecordCount, setRecords, setRenderOrder, instanceAttribute, records, updateRecords, INSTANCE_FLOATS } from "./mesh.ts"
|
|
11
11
|
export type { InstancedMesh as InstancedMeshNode, InstancedMeshOptions, InstanceNode, InstanceOrderOptions, InstanceSlots, InstanceStream, Mesh as MeshNode, MeshInstances, RecordMesh as RecordMeshNode, RecordMeshOptions, UpdateRecordsOptions } from "./mesh.ts"
|
package/src/material.ts
CHANGED
|
@@ -257,6 +257,19 @@ export type UnlitOptions = {
|
|
|
257
257
|
* MultiMesh instance color. A cutout's shadow multiplies by the same
|
|
258
258
|
* color, so alpha below `alphaTest` cuts the shadow with the pixels. */
|
|
259
259
|
instanceColors?: boolean
|
|
260
|
+
/** GLSL at file scope for `surface`: uniforms (each an ordinary
|
|
261
|
+
* per-entry param, `params` on the mesh or setMeshParams) and helper
|
|
262
|
+
* functions. See the tier-2 look in AGENTS.md "Custom looks". */
|
|
263
|
+
prelude?: string
|
|
264
|
+
/** GLSL declaring `void surface(inout Surface s)`, called once the
|
|
265
|
+
* program has resolved the base (the color, the map, the vertex and
|
|
266
|
+
* instance colors, the cutout) and before the scene tail: rewrite
|
|
267
|
+
* `s.base` or `discard`. The unlit program shades nothing, so `base`
|
|
268
|
+
* is the only field read back and `s.normal` is zero. Runs in the
|
|
269
|
+
* shadow twin too (attached whenever a surface is given), so what it
|
|
270
|
+
* discards casts no shadow, the twin seeing a `prelude` uniform at its
|
|
271
|
+
* zero value. One program per distinct source, like every option. */
|
|
272
|
+
surface?: string
|
|
260
273
|
}
|
|
261
274
|
|
|
262
275
|
// The fog an unlit program composes: none when opted out, the additive
|
|
@@ -306,13 +319,15 @@ export function unlit(opts: UnlitOptions = {}): Material {
|
|
|
306
319
|
let instanceColors = opts.instanceColors === true
|
|
307
320
|
let instanced = opts.instanced === true || instanceColors
|
|
308
321
|
let vertexColors = opts.vertexColors === true
|
|
322
|
+
let prelude = opts.prelude ?? ""
|
|
323
|
+
let surface = opts.surface ?? ""
|
|
309
324
|
if (mapTransform && !map) throw new Error("unlit: mapTransform without a map to transform")
|
|
310
|
-
let key = [map, vertexColors, transparent, blend, cull, alphaTest, fog, mapTransform, skinned, instanced, instanceColors, morph].join("|")
|
|
325
|
+
let key = [map, vertexColors, transparent, blend, cull, alphaTest, fog, mapTransform, skinned, instanced, instanceColors, morph, prelude, surface].join("|")
|
|
311
326
|
let cls = unlitClasses.get(key)
|
|
312
327
|
if (cls === undefined) {
|
|
313
328
|
cls = shaderMaterialClass({
|
|
314
329
|
vertex: unlitVertex({ vertexColors, skinned, instanced, instanceColors, morph }),
|
|
315
|
-
fragment: unlitFragment({ map, vertexColors, alphaTest, transparent, fog, mapTransform, instanceColors }),
|
|
330
|
+
fragment: unlitFragment({ map, vertexColors, alphaTest, transparent, fog, mapTransform, instanceColors, prelude, surface }),
|
|
316
331
|
shadowVertex: instanced ? shadowDepthVertex(skinned, true, morph) : undefined,
|
|
317
332
|
instanceBuffers: stockInstanceBuffers(instanced, instanceColors),
|
|
318
333
|
instanceStyle: instanceColors ? INSTANCE_COLOR_DEFAULT : undefined,
|
|
@@ -327,12 +342,21 @@ export function unlit(opts: UnlitOptions = {}): Material {
|
|
|
327
342
|
let params: ShaderParams = { uColor }
|
|
328
343
|
if (alphaTest) params.uAlphaTest = opts.alphaTest!
|
|
329
344
|
if (mapTransform) params.uMapTransform = mapTransformParam(opts.mapTransform!)
|
|
345
|
+
// A mapped cutout casts its cutout and a surface function casts its
|
|
346
|
+
// discards (lit's rule); a color-only alphaTest is all or nothing and
|
|
347
|
+
// keeps the plain depth variant.
|
|
330
348
|
return cls.instance({
|
|
331
349
|
params,
|
|
332
350
|
textures: map ? { uMap: opts.map! } : undefined,
|
|
333
351
|
shadow:
|
|
334
|
-
alphaTest && map
|
|
335
|
-
? unlitShadowMaterial(
|
|
352
|
+
(alphaTest && map) || surface !== ""
|
|
353
|
+
? unlitShadowMaterial(
|
|
354
|
+
{ cull: shadowCull(cull), map, vertexColors, alphaTest: alphaTest && map, skinned, instanced, instanceColors, morph, mapTransform, prelude, surface },
|
|
355
|
+
uColor,
|
|
356
|
+
opts.alphaTest,
|
|
357
|
+
opts.map,
|
|
358
|
+
mapTransform ? (params.uMapTransform as number[]) : undefined,
|
|
359
|
+
)
|
|
336
360
|
: undefined,
|
|
337
361
|
})
|
|
338
362
|
}
|
|
@@ -892,42 +916,59 @@ function shadowVariant(cull: CullMode, skinned: boolean, morphed: boolean): Mate
|
|
|
892
916
|
|
|
893
917
|
let unlitShadowClasses = new Map<string, ShaderMaterialClass>()
|
|
894
918
|
|
|
919
|
+
/** The flags an unlit shadow twin is keyed and built on: the shadow
|
|
920
|
+
* cull, the source options the discard depends on, and the vertex
|
|
921
|
+
* stage's own (skinning, instancing, morphing, so the cutout casts its
|
|
922
|
+
* pose and placement). */
|
|
923
|
+
type UnlitShadowFlags = {
|
|
924
|
+
cull: CullMode
|
|
925
|
+
map: boolean
|
|
926
|
+
vertexColors: boolean
|
|
927
|
+
alphaTest: boolean
|
|
928
|
+
skinned: boolean
|
|
929
|
+
instanced: boolean
|
|
930
|
+
instanceColors: boolean
|
|
931
|
+
morph: boolean
|
|
932
|
+
mapTransform: boolean
|
|
933
|
+
prelude: string
|
|
934
|
+
surface: string
|
|
935
|
+
}
|
|
936
|
+
|
|
895
937
|
/** The shadow variant of a discarding unlit material: unlitShadowFragment
|
|
896
938
|
* on the same vertex stage (skinned when the material skins, so the
|
|
897
|
-
* cutout casts its pose), litShadowMaterial's unlit twin. unlit()
|
|
898
|
-
*
|
|
899
|
-
*
|
|
900
|
-
* combination. An instance per material (its
|
|
901
|
-
* transform)
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
vertexColors
|
|
905
|
-
skinned: boolean,
|
|
906
|
-
instanced: boolean,
|
|
907
|
-
instanceColors: boolean,
|
|
908
|
-
morph: boolean,
|
|
909
|
-
uColor: number[],
|
|
910
|
-
uAlphaTest: number,
|
|
911
|
-
uMap: TextureId,
|
|
912
|
-
uMapTransform?: number[],
|
|
913
|
-
): Material {
|
|
914
|
-
let key = [cull, uMapTransform !== undefined, vertexColors, skinned, instanced, instanceColors, morph].join("|")
|
|
939
|
+
* cutout casts its pose), litShadowMaterial's unlit twin. unlit()
|
|
940
|
+
* discards through the mapped cutout and through a surface function,
|
|
941
|
+
* so what varies is the shadow cull, the source flags and the sources
|
|
942
|
+
* themselves - one class per combination. An instance per material (its
|
|
943
|
+
* map, color, cutoff, transform); a prelude uniform reaches the twin at
|
|
944
|
+
* its zero value, mesh params going to the main entry only. */
|
|
945
|
+
function unlitShadowMaterial(flags: UnlitShadowFlags, uColor: number[], uAlphaTest: number | undefined, uMap: TextureId | undefined, uMapTransform?: number[]): Material {
|
|
946
|
+
let key = [flags.cull, flags.map, flags.vertexColors, flags.alphaTest, flags.skinned, flags.instanced, flags.instanceColors, flags.morph, flags.mapTransform, flags.prelude, flags.surface].join("|")
|
|
915
947
|
let cls = unlitShadowClasses.get(key)
|
|
916
948
|
if (cls === undefined) {
|
|
917
|
-
let fragment = unlitShadowFragment({
|
|
918
|
-
|
|
949
|
+
let fragment = unlitShadowFragment({
|
|
950
|
+
map: flags.map,
|
|
951
|
+
vertexColors: flags.vertexColors,
|
|
952
|
+
alphaTest: flags.alphaTest,
|
|
953
|
+
mapTransform: flags.mapTransform,
|
|
954
|
+
instanceColors: flags.instanceColors,
|
|
955
|
+
prelude: flags.prelude,
|
|
956
|
+
surface: flags.surface,
|
|
957
|
+
})
|
|
958
|
+
if (fragment === undefined) throw new Error("unlitShadowMaterial: the options cannot discard")
|
|
919
959
|
cls = shaderMaterialClass({
|
|
920
|
-
vertex: unlitVertex({ vertexColors, skinned, instanced, instanceColors, morph }),
|
|
960
|
+
vertex: unlitVertex({ vertexColors: flags.vertexColors, skinned: flags.skinned, instanced: flags.instanced, instanceColors: flags.instanceColors, morph: flags.morph }),
|
|
921
961
|
fragment,
|
|
922
|
-
cull,
|
|
923
|
-
instanceBuffers: stockInstanceBuffers(instanced, instanceColors),
|
|
962
|
+
cull: flags.cull,
|
|
963
|
+
instanceBuffers: stockInstanceBuffers(flags.instanced, flags.instanceColors),
|
|
924
964
|
label: "scene-unlit-shadow-" + key,
|
|
925
965
|
})
|
|
926
966
|
unlitShadowClasses.set(key, cls)
|
|
927
967
|
}
|
|
928
|
-
let params: ShaderParams = { uColor
|
|
968
|
+
let params: ShaderParams = { uColor }
|
|
969
|
+
if (flags.alphaTest) params.uAlphaTest = uAlphaTest!
|
|
929
970
|
if (uMapTransform !== undefined) params.uMapTransform = uMapTransform
|
|
930
|
-
return cls.instance({ params, textures: { uMap } })
|
|
971
|
+
return cls.instance({ params, textures: flags.map ? { uMap: uMap! } : undefined })
|
|
931
972
|
}
|
|
932
973
|
|
|
933
974
|
/** A sprite's options: unlit's, minus instancing (the billboard stage
|
|
@@ -974,19 +1015,6 @@ export type SpriteOptions = Omit<UnlitOptions, "instanced" | "instanceColors" |
|
|
|
974
1015
|
maxScreenPx?: number
|
|
975
1016
|
}
|
|
976
1017
|
|
|
977
|
-
// The radial shape as a tier-2 surface on the unlit fragment: the
|
|
978
|
-
// vertex stages always write vUv, which the fragment declares only with
|
|
979
|
-
// a map, so the mapless prelude declares it itself.
|
|
980
|
-
const RADIAL_PRELUDE = glsl`
|
|
981
|
-
uniform float uFalloff;
|
|
982
|
-
`
|
|
983
|
-
const RADIAL_SURFACE = glsl`
|
|
984
|
-
void surface(inout Surface s) {
|
|
985
|
-
float disc = clamp(1.0 - 2.0 * length(vUv - 0.5), 0.0, 1.0);
|
|
986
|
-
s.base *= pow(disc, uFalloff);
|
|
987
|
-
}
|
|
988
|
-
`
|
|
989
|
-
|
|
990
1018
|
// The billboard vertex stages: the unit quad's corners placed along the
|
|
991
1019
|
// camera axes at the mesh's world position, with the quad's size read
|
|
992
1020
|
// off uModel's column lengths so `scale` sizes the sprite like any mesh,
|
|
@@ -1103,18 +1131,14 @@ export function sprite(opts: SpriteOptions = {}): Material {
|
|
|
1103
1131
|
let minScreenPx = opts.minScreenPx ?? 0
|
|
1104
1132
|
let maxScreenPx = opts.maxScreenPx ?? 0
|
|
1105
1133
|
checkScreenSize("sprite", minScreenPx, maxScreenPx)
|
|
1106
|
-
let
|
|
1134
|
+
let prelude = opts.prelude ?? ""
|
|
1135
|
+
let surface = opts.surface ?? ""
|
|
1136
|
+
let key = [map, transparent, blend, fixedY, fog, radial, prelude, surface].join("|")
|
|
1107
1137
|
let cls = spriteClasses.get(key)
|
|
1108
1138
|
if (cls === undefined) {
|
|
1109
1139
|
cls = shaderMaterialClass({
|
|
1110
1140
|
vertex: fixedY ? SPRITE_FIXED_Y_VERTEX_SRC : SPRITE_VERTEX_SRC,
|
|
1111
|
-
fragment: unlitFragment({
|
|
1112
|
-
map,
|
|
1113
|
-
transparent,
|
|
1114
|
-
fog,
|
|
1115
|
-
prelude: radial ? (map ? "" : "in vec2 vUv;\n") + RADIAL_PRELUDE : undefined,
|
|
1116
|
-
surface: radial ? RADIAL_SURFACE : undefined,
|
|
1117
|
-
}),
|
|
1141
|
+
fragment: unlitFragment({ map, transparent, fog, radial, prelude, surface }),
|
|
1118
1142
|
transparent,
|
|
1119
1143
|
blend,
|
|
1120
1144
|
cull: "none",
|