@solidrt/3d 0.0.48 → 0.0.50

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/src/material.ts CHANGED
@@ -7,15 +7,15 @@
7
7
  //
8
8
  // Colors are straight [r, g, b, a?] 0..1 at the API and premultiplied here
9
9
  // once, at the boundary (the engine's pixel contract). An alpha below 1
10
- // does NOT blend yet: v1 pipelines draw opaque (blend "none"), so a
11
- // translucent color overwrites what is behind it. Transparency arrives
12
- // with the blend-factor vocabulary and back-to-front sorting (see
13
- // okf/research/scene-graph-3d.md, staging step 4).
10
+ // blends only on a `transparent: true` material (Three's rule: the flag is
11
+ // explicit, alpha alone still draws opaque). Transparent materials build
12
+ // their pipeline with blend "alpha" and depthWrite off, and the scene draws
13
+ // their meshes after the opaque ones, sorted back-to-front per mesh.
14
14
  //
15
- // Custom looks need no material system: the raw layer (compileShader /
16
- // createRenderPipeline in @solidrt/core/gpu) is first-class, and a scene
17
- // draws into an ordinary draw target - a custom-shaded mesh is a future
18
- // material class here, or the app's own addDraw beside the scene's.
15
+ // Custom looks get the same split through shaderMaterialClass (one
16
+ // program, instance() per parameterisation); shaderMaterial is a class with
17
+ // a single instance. The raw layer (compileShader / createRenderPipeline in
18
+ // @solidrt/core/gpu) stays first-class beneath both.
19
19
 
20
20
  import {
21
21
  compileShader,
@@ -55,6 +55,10 @@ export type Material = {
55
55
  * mesh whose geometry layout differs is rejected at add() - the strides
56
56
  * disagree, so a mismatch would render garbage, not just miss a channel. */
57
57
  layout?: VertexLayout
58
+ /** True when the pipeline blends over (blend "alpha", depthWrite off):
59
+ * the scene draws this material's meshes after every opaque one, sorted
60
+ * back-to-front by mesh origin, and re-sorts them when the camera moves. */
61
+ transparent?: boolean
58
62
  /** Present on materials that own their pipeline (shaderMaterial). */
59
63
  dispose?(): void
60
64
  }
@@ -96,23 +100,30 @@ const FRAGMENT_MAP_SRC = glsl`
96
100
  `
97
101
 
98
102
  let sharedVertex: ShaderStageId | undefined
99
- let pipelines: { color?: RenderPipelineId; map?: RenderPipelineId } = {}
103
+ let pipelines: Partial<Record<UnlitClass, RenderPipelineId>> = {}
100
104
 
101
- function pipelineFor(kind: "color" | "map"): RenderPipelineId {
102
- let existing = pipelines[kind]
105
+ // One pipeline per unlit CLASS: fragment kind x transparency, since blend
106
+ // state is pipeline state.
107
+ type UnlitClass = "color" | "map" | "color-transparent" | "map-transparent"
108
+
109
+ function pipelineFor(cls: UnlitClass): RenderPipelineId {
110
+ let existing = pipelines[cls]
103
111
  if (existing !== undefined) return existing
104
112
  if (sharedVertex === undefined) sharedVertex = compileShader("vertex", VERTEX_SRC, { header: true })
105
- let fragment = compileShader("fragment", kind === "color" ? FRAGMENT_COLOR_SRC : FRAGMENT_MAP_SRC, {
113
+ let transparent = cls.endsWith("-transparent")
114
+ let fragment = compileShader("fragment", cls.startsWith("color") ? FRAGMENT_COLOR_SRC : FRAGMENT_MAP_SRC, {
106
115
  header: true,
107
116
  })
108
- let program = linkProgram(sharedVertex, fragment, { label: "scene-unlit-" + kind })
117
+ let program = linkProgram(sharedVertex, fragment, { label: "scene-unlit-" + cls })
109
118
  let pipeline = createRenderPipeline(program, {
110
119
  attributes: VERTEX_LAYOUTS.standard,
111
120
  depth: true,
121
+ depthWrite: transparent ? false : undefined,
122
+ blend: transparent ? "alpha" : undefined,
112
123
  cull: "back",
113
- label: "scene-unlit-" + kind,
124
+ label: "scene-unlit-" + cls,
114
125
  })
115
- pipelines[kind] = pipeline
126
+ pipelines[cls] = pipeline
116
127
  return pipeline
117
128
  }
118
129
 
@@ -121,6 +132,9 @@ export type UnlitOptions = {
121
132
  color?: [number, number, number] | [number, number, number, number]
122
133
  /** A texture id to sample (tinted by `color` when both are given). */
123
134
  map?: TextureId
135
+ /** Blend over what is behind (color alpha and map alpha both count).
136
+ * Without it an alpha below 1 still draws opaque. See Material.transparent. */
137
+ transparent?: boolean
124
138
  }
125
139
 
126
140
  /**
@@ -132,10 +146,16 @@ export function unlit(opts: UnlitOptions = {}): Material {
132
146
  let color = opts.color ?? [1, 1, 1]
133
147
  let a = color.length === 4 ? color[3] : 1
134
148
  let uColor = [color[0] * a, color[1] * a, color[2] * a, a]
149
+ let transparent = opts.transparent === true
135
150
  if (opts.map !== undefined) {
136
- return { pipeline: () => pipelineFor("map"), params: { uColor }, textures: { uMap: opts.map } }
151
+ return {
152
+ pipeline: () => pipelineFor(transparent ? "map-transparent" : "map"),
153
+ params: { uColor },
154
+ textures: { uMap: opts.map },
155
+ transparent,
156
+ }
137
157
  }
138
- return { pipeline: () => pipelineFor("color"), params: { uColor } }
158
+ return { pipeline: () => pipelineFor(transparent ? "color-transparent" : "color"), params: { uColor }, transparent }
139
159
  }
140
160
 
141
161
  // Mirrors the engine's own preamble rule: a source carrying its own
@@ -144,7 +164,46 @@ function needsHeader(source: string): boolean {
144
164
  return !source.trimStart().startsWith("#version")
145
165
  }
146
166
 
147
- export type ShaderMaterialOptions = {
167
+ // The scene-background pass (scene.setBackground). The vertex stage is the
168
+ // engine's own attributeless fullscreen triangle (gl_VertexID, no vertex
169
+ // buffer), emitting the SAME vUV the shader-target contract provides: 0..1
170
+ // with origin at the displayed top-left - so a backdrop fragment written
171
+ // for createShaderTexture ports verbatim.
172
+ const BACKGROUND_VERTEX = glsl`
173
+ out vec2 vUV;
174
+ void main() {
175
+ vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
176
+ vUV = p;
177
+ gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);
178
+ }
179
+ `
180
+
181
+ // Pipeline fragments get no vUV from the engine preamble (a pipeline's
182
+ // varyings are its own), so the background slot injects the full
183
+ // shader-target fragment contract itself: vUV, fragColor, iResolution.
184
+ const BACKGROUND_FRAGMENT_PREAMBLE =
185
+ "#version 300 es\nprecision highp float;\nin vec2 vUV;\nout vec4 fragColor;\nuniform vec2 iResolution;\n"
186
+
187
+ /** The scene's background pipeline (internal - reached via
188
+ * scene.setBackground): depth-free, attributeless, drawn as entry zero of
189
+ * the scene pass. */
190
+ export function backgroundPipeline(fragment: string, label: string): { pipeline: RenderPipelineId; program: ProgramId } {
191
+ let vs = compileShader("vertex", BACKGROUND_VERTEX, { header: true })
192
+ let fs = compileShader(
193
+ "fragment",
194
+ needsHeader(fragment) ? BACKGROUND_FRAGMENT_PREAMBLE + fragment : fragment,
195
+ { header: false },
196
+ )
197
+ let program = linkProgram(vs, fs, { label })
198
+ destroyShader(vs)
199
+ destroyShader(fs)
200
+ let pipeline = createRenderPipeline(program, { label })
201
+ return { pipeline, program }
202
+ }
203
+
204
+ /** The class half of a shader material: sources and pipeline state, the
205
+ * things one compiled program fixes. */
206
+ export type ShaderMaterialClassOptions = {
148
207
  /**
149
208
  * Vertex stage GLSL. MUST declare and use `uniform mat4 uModel` (the
150
209
  * mesh's world matrix, written per entry whenever the mesh moves) and
@@ -167,11 +226,16 @@ export type ShaderMaterialOptions = {
167
226
  */
168
227
  vertex: string
169
228
  fragment: string
170
- /** Uniform seeds beyond the standard set; update per mesh later with
171
- * setMeshParams. */
172
- params?: ShaderParams
173
- textures?: Record<string, TextureId>
174
- /** Pipeline state; defaults match unlit: depth: true, cull: "back". */
229
+ /** Blend over what is behind, with the scene sorting this material's
230
+ * meshes back-to-front after the opaque ones (see Material.transparent).
231
+ * Sets the pipeline defaults blend "alpha" and depthWrite false; the
232
+ * fragment must write premultiplied output (`vec4(rgb * a, a)`). Defaults
233
+ * to true whenever `blend` is set to anything but "none": every blended
234
+ * draw belongs after the opaques so it depth-tests against them, and
235
+ * back-to-front is harmless for the order-independent modes. */
236
+ transparent?: boolean
237
+ /** Pipeline state; defaults match unlit: depth: true, cull: "back",
238
+ * and for transparent materials blend "alpha", depthWrite: false. */
175
239
  depth?: boolean
176
240
  depthWrite?: boolean
177
241
  blend?: BlendMode
@@ -180,18 +244,41 @@ export type ShaderMaterialOptions = {
180
244
  label?: string
181
245
  }
182
246
 
247
+ /** The instance half of a shader material: uniform seeds and sampler
248
+ * bindings for one parameterisation of a class's program. */
249
+ export type ShaderMaterialInstanceOptions = {
250
+ /** Uniform seeds beyond the standard set; update per mesh later with
251
+ * setMeshParams. */
252
+ params?: ShaderParams
253
+ textures?: Record<string, TextureId>
254
+ }
255
+
256
+ export type ShaderMaterialOptions = ShaderMaterialClassOptions & ShaderMaterialInstanceOptions
257
+
183
258
  /**
184
- * A material from your own GLSL: the custom-look escape hatch, first-class
185
- * next to unlit. Sources without a `#version` line get the standard
186
- * pipeline preamble (`fragColor`, `iResolution`).
187
- *
188
- * The INSTANCE is the pipeline handle: two calls with identical sources
189
- * compile two pipelines - there is no dedupe by source value (a hidden
190
- * cache keyed by content is the anti-pattern the GPU layer avoids
191
- * throughout). Create one per look at app scope, share it across meshes,
192
- * and `dispose()` it if the app is done with the look for good.
259
+ * One program and pipeline, many parameterisations: the class/instance
260
+ * split unlit has internally, for your own GLSL. `instance()` returns a
261
+ * Material sharing the class's pipeline with its own params/textures - the
262
+ * class compiles once, and dispose() is on the class alone (instances hold
263
+ * nothing of their own).
193
264
  */
194
- export function shaderMaterial(opts: ShaderMaterialOptions): Material {
265
+ export type ShaderMaterialClass = {
266
+ instance(opts?: ShaderMaterialInstanceOptions): Material
267
+ /** Destroy the shared program and pipeline. Instances still in use draw
268
+ * nothing valid afterwards. */
269
+ dispose(): void
270
+ }
271
+
272
+ /**
273
+ * A material class from your own GLSL: sources without a `#version` line
274
+ * get the standard pipeline preamble (`fragColor`, `iResolution`). Two
275
+ * calls with identical sources compile two programs - there is no dedupe by
276
+ * source value (a hidden cache keyed by content is the anti-pattern the GPU
277
+ * layer avoids throughout); the class IS the app-owned split. Create one
278
+ * per program at app scope, `instance()` per look, and `dispose()` the class
279
+ * when the app is done with the look for good.
280
+ */
281
+ export function shaderMaterialClass(opts: ShaderMaterialClassOptions): ShaderMaterialClass {
195
282
  // The standard-set contract, checked where the mistake is made: a vertex
196
283
  // stage that never mentions the matrices cannot place meshes, and with
197
284
  // shared params skipping undeclared names the omission would otherwise
@@ -208,30 +295,34 @@ export function shaderMaterial(opts: ShaderMaterialOptions): Material {
208
295
  // Attributes live in the vertex stage only, so unlike the uNormal scan
209
296
  // there is nothing to look for in the fragment source.
210
297
  let layout: VertexLayout = /\baColor\b/.test(opts.vertex) ? "colored" : "standard"
298
+ let normalMatrix = /\buNormal\b/.test(opts.vertex) || /\buNormal\b/.test(opts.fragment)
299
+ let transparent = opts.transparent ?? (opts.blend !== undefined && opts.blend !== "none")
300
+ let depth = opts.depth ?? true
301
+ let pipelineFor = (): RenderPipelineId => {
302
+ if (pipeline === undefined) {
303
+ let vs = compileShader("vertex", opts.vertex, { header: needsHeader(opts.vertex) })
304
+ let fs = compileShader("fragment", opts.fragment, { header: needsHeader(opts.fragment) })
305
+ program = linkProgram(vs, fs, { label: opts.label })
306
+ destroyShader(vs)
307
+ destroyShader(fs)
308
+ pipeline = createRenderPipeline(program, {
309
+ attributes: VERTEX_LAYOUTS[layout],
310
+ depth,
311
+ // depthWrite needs a depth buffer, so the transparent default
312
+ // only applies when there is one.
313
+ depthWrite: opts.depthWrite ?? (transparent && depth ? false : undefined),
314
+ blend: opts.blend ?? (transparent ? "alpha" : undefined),
315
+ cull: opts.cull ?? "back",
316
+ topology: opts.topology,
317
+ label: opts.label,
318
+ })
319
+ }
320
+ return pipeline
321
+ }
211
322
  return {
212
- normalMatrix: /\buNormal\b/.test(opts.vertex) || /\buNormal\b/.test(opts.fragment),
213
- layout,
214
- pipeline() {
215
- if (pipeline === undefined) {
216
- let vs = compileShader("vertex", opts.vertex, { header: needsHeader(opts.vertex) })
217
- let fs = compileShader("fragment", opts.fragment, { header: needsHeader(opts.fragment) })
218
- program = linkProgram(vs, fs, { label: opts.label })
219
- destroyShader(vs)
220
- destroyShader(fs)
221
- pipeline = createRenderPipeline(program, {
222
- attributes: VERTEX_LAYOUTS[layout],
223
- depth: opts.depth ?? true,
224
- depthWrite: opts.depthWrite,
225
- blend: opts.blend,
226
- cull: opts.cull ?? "back",
227
- topology: opts.topology,
228
- label: opts.label,
229
- })
230
- }
231
- return pipeline
323
+ instance(inst = {}) {
324
+ return { normalMatrix, layout, transparent, pipeline: pipelineFor, params: inst.params ?? {}, textures: inst.textures }
232
325
  },
233
- params: opts.params ?? {},
234
- textures: opts.textures,
235
326
  dispose() {
236
327
  if (pipeline !== undefined) {
237
328
  destroyRenderPipeline(pipeline)
@@ -244,3 +335,20 @@ export function shaderMaterial(opts: ShaderMaterialOptions): Material {
244
335
  },
245
336
  }
246
337
  }
338
+
339
+ /**
340
+ * A material from your own GLSL: the custom-look escape hatch, first-class
341
+ * next to unlit. A class with a single instance - `shaderMaterialClass()`
342
+ * is the form for one program with many parameterisations.
343
+ *
344
+ * The INSTANCE is the pipeline handle: two calls with identical sources
345
+ * compile two pipelines - there is no dedupe by source value. Create one
346
+ * per look at app scope, share it across meshes, and `dispose()` it if the
347
+ * app is done with the look for good.
348
+ */
349
+ export function shaderMaterial(opts: ShaderMaterialOptions): Material {
350
+ let cls = shaderMaterialClass(opts)
351
+ let material = cls.instance(opts)
352
+ material.dispose = cls.dispose
353
+ return material
354
+ }
package/src/math.ts CHANGED
@@ -57,6 +57,48 @@ export function transformPoint(out: Vec4, m: Mat4, p: Vec3): Vec4 {
57
57
  return out
58
58
  }
59
59
 
60
+ /**
61
+ * Transform a DIRECTION by m's upper 3x3 (w = 0: rotation and scale apply,
62
+ * translation does not). The ray-direction counterpart of transformPoint.
63
+ */
64
+ export function transformVector(out: Vec3, m: Mat4, v: Vec3): Vec3 {
65
+ let x = v[0], y = v[1], z = v[2]
66
+ out[0] = m[0] * x + m[4] * y + m[8] * z
67
+ out[1] = m[1] * x + m[5] * y + m[9] * z
68
+ out[2] = m[2] * x + m[6] * y + m[10] * z
69
+ return out
70
+ }
71
+
72
+ /**
73
+ * Invert an AFFINE matrix - a world or local matrix whose bottom row is
74
+ * 0,0,0,1, NOT a projection: the upper 3x3 inverts by cofactors and the
75
+ * translation is pulled back through it. Picking's world-to-local step.
76
+ * `out` may alias `m`. A degenerate (zero-scale) matrix yields the raw
77
+ * cofactors instead of NaNs, the same policy as normalMatrix.
78
+ */
79
+ export function invertAffine(out: Mat4, m: Mat4): Mat4 {
80
+ let a = m[0], b = m[4], c = m[8]
81
+ let d = m[1], e = m[5], f = m[9]
82
+ let g = m[2], h = m[6], i = m[10]
83
+ let tx = m[12], ty = m[13], tz = m[14]
84
+ let c00 = e * i - f * h
85
+ let c01 = f * g - d * i
86
+ let c02 = d * h - e * g
87
+ let det = a * c00 + b * c01 + c * c02
88
+ let s = 1 / (det || 1)
89
+ let r00 = c00 * s, r01 = (c * h - b * i) * s, r02 = (b * f - c * e) * s
90
+ let r10 = c01 * s, r11 = (a * i - c * g) * s, r12 = (c * d - a * f) * s
91
+ let r20 = c02 * s, r21 = (b * g - a * h) * s, r22 = (a * e - b * d) * s
92
+ out[0] = r00; out[1] = r10; out[2] = r20; out[3] = 0
93
+ out[4] = r01; out[5] = r11; out[6] = r21; out[7] = 0
94
+ out[8] = r02; out[9] = r12; out[10] = r22; out[11] = 0
95
+ out[12] = -(r00 * tx + r01 * ty + r02 * tz)
96
+ out[13] = -(r10 * tx + r11 * ty + r12 * tz)
97
+ out[14] = -(r20 * tx + r21 * ty + r22 * tz)
98
+ out[15] = 1
99
+ return out
100
+ }
101
+
60
102
  /** out = a * b (column vectors: b applies first). out may alias a or b. */
61
103
  export function multiply(out: Mat4, a: Mat4, b: Mat4): Mat4 {
62
104
  let a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3]
package/src/order.ts ADDED
@@ -0,0 +1,51 @@
1
+ // Draw-list ordering for a scene: a pure function of the live meshes and the
2
+ // camera's view matrix, with no GUI import, so the check rig
3
+ // (checks/order-check.ts) runs it headless on flux against a linear oracle.
4
+ // The scene calls it whenever the order is dirty and hands the result to
5
+ // setDrawOrder.
6
+
7
+ import type { Mat4, Vec3 } from "./math.ts"
8
+
9
+ /** The slice of a Mesh the sort reads (field names match Mesh so the
10
+ * scene passes its meshes straight through). */
11
+ export type Orderable<T> = {
12
+ _entry: T | null
13
+ _transparent: boolean
14
+ renderOrder: number
15
+ _center: Vec3
16
+ }
17
+
18
+ /**
19
+ * Draw order: `first` (the background entry, if any), then opaque meshes by
20
+ * renderOrder with add order within a key, then transparent meshes by
21
+ * renderOrder then back-to-front by the view-space depth of the world-bounds
22
+ * center. The center, not the origin (Three's key), so geometry built
23
+ * off-origin sorts by where it is; and not the nearest bounds point, which
24
+ * would draw a large translucent ground plane over the small translucents
25
+ * resting on it. Per-mesh only: no per-triangle sort, no OIT.
26
+ */
27
+ export function orderEntries<T>(meshes: readonly Orderable<T>[], view: Mat4, first?: T): T[] {
28
+ let opaque: Orderable<T>[] = []
29
+ let transparent: Orderable<T>[] = []
30
+ for (let m of meshes) {
31
+ if (m._entry === null) continue
32
+ ;(m._transparent ? transparent : opaque).push(m)
33
+ }
34
+ // Array sort is stable, so equal keys keep add order.
35
+ opaque.sort((a, b) => a.renderOrder - b.renderOrder)
36
+ if (transparent.length > 1) {
37
+ // The camera looks down -z in view space, so farther is more negative
38
+ // and ascending depth is back-to-front.
39
+ let depth = new Map<Orderable<T>, number>()
40
+ for (let m of transparent) {
41
+ let c = m._center
42
+ depth.set(m, view[2] * c[0] + view[6] * c[1] + view[10] * c[2] + view[14])
43
+ }
44
+ transparent.sort((a, b) => a.renderOrder - b.renderOrder || depth.get(a)! - depth.get(b)!)
45
+ }
46
+ let order: T[] = []
47
+ if (first !== undefined) order.push(first)
48
+ for (let m of opaque) order.push(m._entry!)
49
+ for (let m of transparent) order.push(m._entry!)
50
+ return order
51
+ }