@solidrt/3d 0.0.49 → 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/AGENTS.md +79 -12
- package/README.md +6 -3
- package/package.json +2 -2
- package/src/bvh.ts +2 -2
- package/src/components.tsx +7 -0
- package/src/index.ts +3 -3
- package/src/material.ts +126 -55
- package/src/order.ts +51 -0
- package/src/scene.ts +133 -34
package/AGENTS.md
CHANGED
|
@@ -11,7 +11,7 @@ blendMode and pointer events like any element. Design rationale:
|
|
|
11
11
|
|
|
12
12
|
- Two layers. The imperative core is Solid-free: `createScene`,
|
|
13
13
|
`createMesh(geometry, material)`, `add`/`remove`, `setTransform`,
|
|
14
|
-
`lookAt`, `getRotation`, `setVisible` - plain objects with dirty flags, batched to a
|
|
14
|
+
`lookAt`, `getRotation`, `setVisible`, `setRenderOrder` - plain objects with dirty flags, batched to a
|
|
15
15
|
microtask,
|
|
16
16
|
one `setDrawParams` (uModel, plus uNormal for materials declaring it)
|
|
17
17
|
per changed mesh and ONE `setTargetParams` (the shared uViewProj +
|
|
@@ -42,8 +42,14 @@ blendMode and pointer events like any element. Design rationale:
|
|
|
42
42
|
lazy, shared, and app-lifetime (owner-scoped free would break sharing);
|
|
43
43
|
`disposeGeometry` frees them.
|
|
44
44
|
- Materials dedupe hard: one program + one pipeline per material CLASS
|
|
45
|
-
(unlit color, unlit map), `depth: true` +
|
|
45
|
+
(unlit color, unlit map, each opaque or transparent), `depth: true` +
|
|
46
|
+
`cull: "back"`; an instance is
|
|
46
47
|
just per-entry uniforms (`uColor`) and bindings (`uMap`).
|
|
48
|
+
- The pure pieces (`math.ts`, `bvh.ts`, `order.ts`) have check rigs in
|
|
49
|
+
`checks/`, run headless on flux from the repo root:
|
|
50
|
+
`bunx srt bundle -f --stdout packages/3d/checks/<name>-check.ts | target/release/flux - [seed]`.
|
|
51
|
+
They print PASS or FAIL lines and throw on failure (the flux binary exits
|
|
52
|
+
0 either way - read the output). Extend the rig when you change the module.
|
|
47
53
|
|
|
48
54
|
## Components
|
|
49
55
|
|
|
@@ -194,7 +200,10 @@ Materials:
|
|
|
194
200
|
at shaderMaterial() creation. The rest is opt-in by declare-and-use:
|
|
195
201
|
`uniform vec3 uCamPos` (the camera's world position, shared and written
|
|
196
202
|
with uViewProj - the specular/fresnel view vector is
|
|
197
|
-
`normalize(uCamPos - worldPos)`)
|
|
203
|
+
`normalize(uCamPos - worldPos)`), `uniform vec3 uCamRight` / `uCamUp`
|
|
204
|
+
(the camera's world-space view axes, shared likewise - a billboard is
|
|
205
|
+
`center + uCamRight * x + uCamUp * y`; do NOT rebuild them from
|
|
206
|
+
uViewProj rows, that carries the clip flip) and `uniform mat4 uNormal` (the world
|
|
198
207
|
inverse-transpose, written beside uModel for this material's meshes;
|
|
199
208
|
take `mat3(uNormal)` - correct under non-uniform scale, where
|
|
200
209
|
mat3(uModel) bends normals off the surface). Attributes come from the
|
|
@@ -208,6 +217,14 @@ Materials:
|
|
|
208
217
|
with the `Mesh` `params` prop (same merge semantics - a key that
|
|
209
218
|
disappears from the object keeps its old value; for per-frame values
|
|
210
219
|
prefer `ref` + setMeshParams from onFrame, the setTransform split).
|
|
220
|
+
Scene-wide values (a clock, a sun direction, fog) go through
|
|
221
|
+
`scene.setParams({ uTime })` instead - one write for every mesh.
|
|
222
|
+
- `shaderMaterialClass({ vertex, fragment, ...pipeline state })` - the
|
|
223
|
+
class/instance split for your own GLSL: compiles once, and
|
|
224
|
+
`cls.instance({ params?, textures? })` returns a Material sharing that
|
|
225
|
+
pipeline with its own values. `dispose()` lives on the class alone.
|
|
226
|
+
`shaderMaterial(opts)` is exactly a class with one instance (its
|
|
227
|
+
`dispose` forwards to the class).
|
|
211
228
|
|
|
212
229
|
Background: `scene.setBackground(source | null)`, the `background` option
|
|
213
230
|
on createScene, and the reactive `Scene` prop. Fragment GLSL drawn as the
|
|
@@ -250,9 +267,26 @@ system.
|
|
|
250
267
|
sync() turns it on when it writes uModel - never add one live: it has no
|
|
251
268
|
world matrix yet, and drawn before the sync microtask it flashes at the
|
|
252
269
|
world origin for a frame.
|
|
253
|
-
-
|
|
254
|
-
|
|
255
|
-
|
|
270
|
+
- Transparency is an EXPLICIT material flag, Three's rule: `unlit({ color:
|
|
271
|
+
[r, g, b, 0.5] })` still draws opaque; `unlit({ ..., transparent: true })`
|
|
272
|
+
(or `shaderMaterial({ transparent: true })`) builds the pipeline with
|
|
273
|
+
`blend: "alpha"` and `depthWrite: false` (depth test stays on, so it hides
|
|
274
|
+
behind opaques without occluding other translucents). The one inference:
|
|
275
|
+
a `shaderMaterial` with any `blend` but "none" is transparent unless told
|
|
276
|
+
`transparent: false` - every blended draw belongs after the opaques, and
|
|
277
|
+
back-to-front is harmless for add/multiply. The scene owns the
|
|
278
|
+
order: background, opaque meshes by `renderOrder` then add order,
|
|
279
|
+
transparent meshes by `renderOrder` then back-to-front by the CENTER of
|
|
280
|
+
the mesh's world bounds in view space (not the origin: off-origin geometry
|
|
281
|
+
sorts by where it is; not the nearest bounds point: a big translucent
|
|
282
|
+
ground plane would cover the small translucents on it) - one `setDrawOrder` from sync() whenever the list changed, a
|
|
283
|
+
renderOrder changed, or (with two or more transparent meshes) the camera
|
|
284
|
+
or a transparent mesh moved, and skipped when the resort lands on the
|
|
285
|
+
permutation already issued. Per-mesh sort only: one non-convex translucent
|
|
286
|
+
mesh still overlaps itself in vertex order, and two large interpenetrating
|
|
287
|
+
translucents can sort wrong (center distance, not per-pixel) - that is the
|
|
288
|
+
engine contract, no OIT. A `shaderMaterial({ transparent: true })`
|
|
289
|
+
fragment must write PREMULTIPLIED output (`vec4(rgb * a, a)`).
|
|
256
290
|
- Rotation is stored as a QUATERNION (`node.quaternion`, `[x, y, z, w]`,
|
|
257
291
|
always unit). There is exactly one rotation field: no `node.rotation`
|
|
258
292
|
shadowing it, because a second field is a second thing to go stale (an
|
|
@@ -329,9 +363,36 @@ system.
|
|
|
329
363
|
camera writes (uEye-style per-mesh params are exactly the O(scene) cost
|
|
330
364
|
the shared channel removed). Scene scale honestly: hundreds to a
|
|
331
365
|
few thousand objects, bounded by the interpreter, not the GPU.
|
|
366
|
+
- SCENE-WIDE uniforms go through that same shared channel via
|
|
367
|
+
`scene.setParams({ uTime })`, and this is the single highest-leverage
|
|
368
|
+
pattern in the library. It merges an app-owned name in beside
|
|
369
|
+
uViewProj/uCamPos/uCamRight/uCamUp - names merge, a target tolerates
|
|
370
|
+
zero coverage, neither side clobbers the other. One write per frame
|
|
371
|
+
however many meshes read it, with the motion itself in vertex shaders
|
|
372
|
+
off that one clock. `params`/`setMeshParams` is the PER-MESH answer and
|
|
373
|
+
is O(meshes) per frame; reach for it only when the value genuinely
|
|
374
|
+
differs per mesh. (`scene.texture` IS the draw target id, so
|
|
375
|
+
`setTargetParams(scene.texture, ...)` is the same write - setParams is
|
|
376
|
+
the sanctioned spelling.)
|
|
377
|
+
- Vec3/Quat arguments are COPIED IN everywhere (`setTransform`, `lookAt`,
|
|
378
|
+
`setCamera`, params), so ONE scratch array reused every frame is safe -
|
|
379
|
+
allocating three arrays per node per frame is pure waste. The node's own
|
|
380
|
+
`position`/`quaternion`/`scale` are the live arrays: read them, do not
|
|
381
|
+
hand them out and do not mutate them (that write does not sync).
|
|
382
|
+
- `setTransform` early-outs on an unchanged value (rotation compared AFTER
|
|
383
|
+
euler conversion), so driving every node unconditionally from `onFrame`
|
|
384
|
+
costs only the compare for nodes that did not move. Compares are exact,
|
|
385
|
+
like `setVisible`.
|
|
386
|
+
- Per-generator conventions - orientation, UV mapping, which axis a solid
|
|
387
|
+
stands on, what a cap looks like - live on each generator's doc comment,
|
|
388
|
+
not here. They are consistent (`plane`/`circle`/`ring` face +z, `torus`
|
|
389
|
+
lies flat with the hole on y, discs and cylinder caps get a PLANAR disc
|
|
390
|
+
map inscribed in the unit square) but the doc comment is the source.
|
|
332
391
|
- Entry rebuild order: `setGeometry`/`setMaterial` re-add the entry at the
|
|
333
|
-
list END
|
|
334
|
-
|
|
392
|
+
list END and dirty the order, so the next sync() re-sorts and the mesh
|
|
393
|
+
keeps its place. `_transparent` on the mesh is the flag AS ATTACHED
|
|
394
|
+
(setMaterial swaps `mesh.material` before the rebuild, so _detach must
|
|
395
|
+
not read the new material's flag).
|
|
335
396
|
- `lathe` takes a CLOSED profile (a cross-section with thickness, or run
|
|
336
397
|
to the axis at x = 0) - it is a solid of revolution, NOT Three's open
|
|
337
398
|
polyline shell. An "open" outline must be closed by the author;
|
|
@@ -342,7 +403,14 @@ system.
|
|
|
342
403
|
compile twice - no dedupe by source value (deliberate; hidden
|
|
343
404
|
content-keyed caches are the anti-pattern the GPU layer avoids). Create
|
|
344
405
|
one per look at app scope, share across meshes, `dispose()` when done
|
|
345
|
-
for good.
|
|
406
|
+
for good. Looks that differ only in params/textures are ONE
|
|
407
|
+
`shaderMaterialClass` and many `instance()`s - the app-owned split, not
|
|
408
|
+
a cache. A class instance has no `dispose` of its own; disposing the
|
|
409
|
+
class invalidates every instance.
|
|
410
|
+
- A parameterised class whose variants (mapped/unmapped, ...) are SEPARATE
|
|
411
|
+
classes must have every variant reference every shared uniform it is
|
|
412
|
+
seeded with: a declared-but-unused per-entry name compiles out and
|
|
413
|
+
throws at add(). Open item: `okf/backlog/gpu-inactive-uniform-two-tier.md`.
|
|
346
414
|
- The standard-set contract is checked TEXTUALLY at shaderMaterial()
|
|
347
415
|
creation (uModel and uViewProj must appear in the vertex source) and
|
|
348
416
|
strictly at add() for the per-entry names: a uModel or uNormal that is
|
|
@@ -380,9 +448,8 @@ system.
|
|
|
380
448
|
Geometry instead.
|
|
381
449
|
- The background covers the whole target with depth off, drawn first: it
|
|
382
450
|
REPLACES the clearColor visually (the clear still runs; you just never
|
|
383
|
-
see it), and a
|
|
384
|
-
is
|
|
385
|
-
two-layer composition until blend factors land).
|
|
451
|
+
see it), and a `transparent: true` mesh blends over it in-pass since the
|
|
452
|
+
background is always entry zero.
|
|
386
453
|
- The background pipeline/program are SCENE-OWNED (unlike shared
|
|
387
454
|
material pipelines): setBackground(null), replacement, and dispose()
|
|
388
455
|
destroy them. Do not hand the background's pipeline to anything else.
|
package/README.md
CHANGED
|
@@ -62,9 +62,12 @@ bounding-box accurate in v1. A scene also takes a `background` - fragment
|
|
|
62
62
|
GLSL drawn inside its own pass behind the meshes, replacing the stacked
|
|
63
63
|
backdrop-texture pattern.
|
|
64
64
|
Custom materials get a standard uniform set - per-mesh `uModel`/`uNormal`,
|
|
65
|
-
shared `uViewProj`/`uCamPos`, each written once per
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
shared `uViewProj`/`uCamPos`/`uCamRight`/`uCamUp`, each written once per
|
|
66
|
+
change - plus your own uniforms: scene-wide via `scene.setParams` (one write
|
|
67
|
+
however many meshes read it), or per mesh, declaratively via the `params`
|
|
68
|
+
prop on `<Mesh>` or imperatively via `setMeshParams`. `shaderMaterialClass`
|
|
69
|
+
compiles one program and hands out `instance()` materials that differ only
|
|
70
|
+
in params/textures. And
|
|
68
71
|
`@solidrt/3d/glsl` exports the lighting pieces (hemisphere, lambert,
|
|
69
72
|
blinn, fresnel, a standard vertex stage) to compose your own lit looks
|
|
70
73
|
from plain template literals.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidrt/3d",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.50",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Antoine van Wel",
|
|
6
6
|
"type": "module",
|
|
@@ -17,6 +17,6 @@
|
|
|
17
17
|
],
|
|
18
18
|
"peerDependencies": {
|
|
19
19
|
"@solidjs/signals": "2.0.0-rc.0",
|
|
20
|
-
"@solidrt/core": "0.0.
|
|
20
|
+
"@solidrt/core": "0.0.50"
|
|
21
21
|
}
|
|
22
22
|
}
|
package/src/bvh.ts
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
// pays nothing. Storage is flat parallel arrays indexed by node id (no
|
|
13
13
|
// per-node objects, no allocation at steady state past tree growth).
|
|
14
14
|
//
|
|
15
|
-
// Pure module by design: no
|
|
16
|
-
// (checks/pick-check.ts) runs it
|
|
15
|
+
// Pure module by design: no GUI imports, so the differential check rig
|
|
16
|
+
// (checks/pick-check.ts) runs it headless on flux against a linear oracle.
|
|
17
17
|
|
|
18
18
|
/** Fat-margin fraction of a leaf's largest extent. Bigger = fewer
|
|
19
19
|
* re-inserts while moving, worse query pruning; 5% is the usual trade. */
|
package/src/components.tsx
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
setGeometry,
|
|
18
18
|
setMaterial,
|
|
19
19
|
setMeshParams,
|
|
20
|
+
setRenderOrder,
|
|
20
21
|
setTransform,
|
|
21
22
|
setVisible,
|
|
22
23
|
} from "./scene.ts"
|
|
@@ -179,6 +180,8 @@ export type MeshProps = TransformProps & PointerEventProps & {
|
|
|
179
180
|
* changing every frame prefer `ref` + setMeshParams from onFrame, the
|
|
180
181
|
* same split as setTransform. */
|
|
181
182
|
params?: ShaderParams
|
|
183
|
+
/** Explicit draw-order key (setRenderOrder as a prop); default 0. */
|
|
184
|
+
renderOrder?: number
|
|
182
185
|
ref?: (mesh: MeshNode) => void
|
|
183
186
|
}
|
|
184
187
|
|
|
@@ -203,6 +206,10 @@ export let Mesh: VoidComponent<MeshProps> = props => {
|
|
|
203
206
|
if (p !== undefined) setMeshParams(mesh, p)
|
|
204
207
|
},
|
|
205
208
|
)
|
|
209
|
+
createEffect(
|
|
210
|
+
() => props.renderOrder,
|
|
211
|
+
o => setRenderOrder(mesh, o ?? 0),
|
|
212
|
+
)
|
|
206
213
|
syncNode(mesh, props)
|
|
207
214
|
untrack(() => props.ref)?.(mesh)
|
|
208
215
|
onCleanup(() => remove(mesh))
|
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, createMesh, createScene, getRotation, lookAt, remove, setGeometry, setMaterial, setMeshParams, setTransform, setVisible, worldPosition } from "./scene.ts"
|
|
8
|
+
export { add, createGroup, createMesh, createScene, getRotation, lookAt, remove, setGeometry, setMaterial, setMeshParams, setRenderOrder, setTransform, setVisible, worldPosition } from "./scene.ts"
|
|
9
9
|
export type { CameraUpdate, Hit, Mesh as MeshNode, Scene as SceneHandle, SceneHandlers, SceneNode, SceneOptions, ScenePointerEvent, TransformUpdate } from "./scene.ts"
|
|
10
10
|
export { box, circle, cone, cylinder, disposeGeometry, fillColors, plane, ring, sphere, torus, torusKnot, withColors, FLOATS_PER_VERTEX, VERTEX_LAYOUTS } from "./geometry.ts"
|
|
11
11
|
export type { ColorFill, Geometry, VertexLayout } from "./geometry.ts"
|
|
@@ -13,8 +13,8 @@ export { fillet, roundRect, shape, triangulate } from "./profile.ts"
|
|
|
13
13
|
export type { Profile, ProfilePoint } from "./profile.ts"
|
|
14
14
|
export { extrude, lathe, pathFrames, sweep, tube } from "./sweep.ts"
|
|
15
15
|
export type { PathFrames, PathPoint, SweepPath } from "./sweep.ts"
|
|
16
|
-
export { shaderMaterial, unlit } from "./material.ts"
|
|
17
|
-
export type { Material, ShaderMaterialOptions, UnlitOptions } from "./material.ts"
|
|
16
|
+
export { shaderMaterial, shaderMaterialClass, unlit } from "./material.ts"
|
|
17
|
+
export type { Material, ShaderMaterialClass, ShaderMaterialClassOptions, ShaderMaterialInstanceOptions, ShaderMaterialOptions, UnlitOptions } from "./material.ts"
|
|
18
18
|
export { Group, Mesh, PerspectiveCamera, Scene, useScene } from "./components.tsx"
|
|
19
19
|
export type { MeshProps, PerspectiveCameraProps, PointerEventProps, SceneProps, TransformProps } from "./components.tsx"
|
|
20
20
|
export { createOrbitCamera } from "./orbit.ts"
|
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
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// with
|
|
13
|
-
//
|
|
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
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
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:
|
|
103
|
+
let pipelines: Partial<Record<UnlitClass, RenderPipelineId>> = {}
|
|
100
104
|
|
|
101
|
-
|
|
102
|
-
|
|
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
|
|
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-" +
|
|
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-" +
|
|
124
|
+
label: "scene-unlit-" + cls,
|
|
114
125
|
})
|
|
115
|
-
pipelines[
|
|
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 {
|
|
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
|
|
@@ -181,7 +201,9 @@ export function backgroundPipeline(fragment: string, label: string): { pipeline:
|
|
|
181
201
|
return { pipeline, program }
|
|
182
202
|
}
|
|
183
203
|
|
|
184
|
-
|
|
204
|
+
/** The class half of a shader material: sources and pipeline state, the
|
|
205
|
+
* things one compiled program fixes. */
|
|
206
|
+
export type ShaderMaterialClassOptions = {
|
|
185
207
|
/**
|
|
186
208
|
* Vertex stage GLSL. MUST declare and use `uniform mat4 uModel` (the
|
|
187
209
|
* mesh's world matrix, written per entry whenever the mesh moves) and
|
|
@@ -204,11 +226,16 @@ export type ShaderMaterialOptions = {
|
|
|
204
226
|
*/
|
|
205
227
|
vertex: string
|
|
206
228
|
fragment: string
|
|
207
|
-
/**
|
|
208
|
-
*
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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. */
|
|
212
239
|
depth?: boolean
|
|
213
240
|
depthWrite?: boolean
|
|
214
241
|
blend?: BlendMode
|
|
@@ -217,18 +244,41 @@ export type ShaderMaterialOptions = {
|
|
|
217
244
|
label?: string
|
|
218
245
|
}
|
|
219
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
|
+
|
|
220
258
|
/**
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
* pipeline
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
* compile two pipelines - there is no dedupe by source value (a hidden
|
|
227
|
-
* cache keyed by content is the anti-pattern the GPU layer avoids
|
|
228
|
-
* throughout). Create one per look at app scope, share it across meshes,
|
|
229
|
-
* 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).
|
|
230
264
|
*/
|
|
231
|
-
export
|
|
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 {
|
|
232
282
|
// The standard-set contract, checked where the mistake is made: a vertex
|
|
233
283
|
// stage that never mentions the matrices cannot place meshes, and with
|
|
234
284
|
// shared params skipping undeclared names the omission would otherwise
|
|
@@ -245,30 +295,34 @@ export function shaderMaterial(opts: ShaderMaterialOptions): Material {
|
|
|
245
295
|
// Attributes live in the vertex stage only, so unlike the uNormal scan
|
|
246
296
|
// there is nothing to look for in the fragment source.
|
|
247
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
|
+
}
|
|
248
322
|
return {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
pipeline() {
|
|
252
|
-
if (pipeline === undefined) {
|
|
253
|
-
let vs = compileShader("vertex", opts.vertex, { header: needsHeader(opts.vertex) })
|
|
254
|
-
let fs = compileShader("fragment", opts.fragment, { header: needsHeader(opts.fragment) })
|
|
255
|
-
program = linkProgram(vs, fs, { label: opts.label })
|
|
256
|
-
destroyShader(vs)
|
|
257
|
-
destroyShader(fs)
|
|
258
|
-
pipeline = createRenderPipeline(program, {
|
|
259
|
-
attributes: VERTEX_LAYOUTS[layout],
|
|
260
|
-
depth: opts.depth ?? true,
|
|
261
|
-
depthWrite: opts.depthWrite,
|
|
262
|
-
blend: opts.blend,
|
|
263
|
-
cull: opts.cull ?? "back",
|
|
264
|
-
topology: opts.topology,
|
|
265
|
-
label: opts.label,
|
|
266
|
-
})
|
|
267
|
-
}
|
|
268
|
-
return pipeline
|
|
323
|
+
instance(inst = {}) {
|
|
324
|
+
return { normalMatrix, layout, transparent, pipeline: pipelineFor, params: inst.params ?? {}, textures: inst.textures }
|
|
269
325
|
},
|
|
270
|
-
params: opts.params ?? {},
|
|
271
|
-
textures: opts.textures,
|
|
272
326
|
dispose() {
|
|
273
327
|
if (pipeline !== undefined) {
|
|
274
328
|
destroyRenderPipeline(pipeline)
|
|
@@ -281,3 +335,20 @@ export function shaderMaterial(opts: ShaderMaterialOptions): Material {
|
|
|
281
335
|
},
|
|
282
336
|
}
|
|
283
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/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
|
+
}
|
package/src/scene.ts
CHANGED
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
// component boundary (components.tsx). A scene compiles to one draw
|
|
4
4
|
// target: every mesh is one draw entry whose uModel (and, for materials
|
|
5
5
|
// declaring it, uNormal) this module keeps in step with the tree, and the
|
|
6
|
-
// camera is the target's SHARED uViewProj + uCamPos
|
|
7
|
-
// per camera move, not one write per mesh.
|
|
8
|
-
// shared params tolerate zero
|
|
9
|
-
// declaring material arrives), so no
|
|
6
|
+
// camera is the target's SHARED uViewProj + uCamPos + uCamRight/uCamUp -
|
|
7
|
+
// one setTargetParams per camera move, not one write per mesh. The
|
|
8
|
+
// non-matrix names ride unconditionally: shared params tolerate zero
|
|
9
|
+
// coverage (stored and skipped until a declaring material arrives), so no
|
|
10
|
+
// bookkeeping tracks who reads them. scene.setParams merges app-owned
|
|
11
|
+
// names into the same set.
|
|
10
12
|
// Mutations batch to a microtask, so a burst of writes (a whole subtree
|
|
11
13
|
// moved, many effects in one flush) syncs once.
|
|
12
14
|
//
|
|
@@ -17,17 +19,18 @@
|
|
|
17
19
|
// each write lands here, the microtask syncs the affected uModels, and the
|
|
18
20
|
// flush renders once that frame.
|
|
19
21
|
|
|
20
|
-
import { addDraw, createDrawTarget, destroyProgram, destroyRenderPipeline, destroyTexture, removeDraw, setDrawParams, setDrawRange, setTargetParams, setTargetSize } from "@solidrt/core/gpu"
|
|
22
|
+
import { addDraw, createDrawTarget, destroyProgram, destroyRenderPipeline, destroyTexture, removeDraw, setDrawOrder, setDrawParams, setDrawRange, setTargetParams, setTargetSize } from "@solidrt/core/gpu"
|
|
21
23
|
import type { DrawId, FilterMode, ProgramId, RenderPipelineId, ShaderParams, TextureId, WrapMode } from "@solidrt/core/gpu"
|
|
22
24
|
import { getOwner, onCleanup } from "@solidrt/core"
|
|
23
25
|
import type { PointerEvent as ElementPointerEvent } from "@solidrt/core"
|
|
24
26
|
// The scene's lookAt() aims a node; math's builds a camera's view matrix -
|
|
25
27
|
// the same pairing (and the same name) as Three's Object3D/Matrix4.
|
|
26
|
-
import { compose, copy, eulerFromQuat, identity, invertAffine, lookAt as lookAtMatrix, mat4, multiply, normalMatrix, perspective, quatFromEuler, quatFromFrame, quatNormalize, transformPoint, transformVector } from "./math.ts"
|
|
28
|
+
import { compose, copy, eulerFromQuat, identity, invertAffine, lookAt as lookAtMatrix, mat4, multiply, normalMatrix, perspective, quat, quatFromEuler, quatFromFrame, quatNormalize, transformPoint, transformVector } from "./math.ts"
|
|
27
29
|
import type { Mat4, Quat, Vec3, Vec4 } from "./math.ts"
|
|
28
30
|
import { geometryBounds, geometryBuffers } from "./geometry.ts"
|
|
29
31
|
import type { Geometry } from "./geometry.ts"
|
|
30
32
|
import { backgroundPipeline } from "./material.ts"
|
|
33
|
+
import { orderEntries } from "./order.ts"
|
|
31
34
|
import type { Material } from "./material.ts"
|
|
32
35
|
import { createBvh, rayBoxDistance } from "./bvh.ts"
|
|
33
36
|
|
|
@@ -51,6 +54,9 @@ let upScratch: Vec3 = [0, 0, 0]
|
|
|
51
54
|
let pickInv = mat4()
|
|
52
55
|
let pickOrigin: Vec4 = [0, 0, 0, 0]
|
|
53
56
|
let pickDir: Vec3 = [0, 0, 0]
|
|
57
|
+
// setTransform's rotation compare happens AFTER conversion, so an euler and
|
|
58
|
+
// the quaternion it produces are the same write. Nothing outlives the call.
|
|
59
|
+
let rotScratch = quat()
|
|
54
60
|
|
|
55
61
|
// The scene half a node needs to reach: attach/detach entries and schedule
|
|
56
62
|
// a sync. Kept separate from the public Scene type so internals stay off
|
|
@@ -62,6 +68,7 @@ type SceneHooks = {
|
|
|
62
68
|
_attach(mesh: Mesh): void
|
|
63
69
|
_detach(mesh: Mesh): void
|
|
64
70
|
_setParams(mesh: Mesh, params: ShaderParams): void
|
|
71
|
+
_reorder(): void
|
|
65
72
|
}
|
|
66
73
|
|
|
67
74
|
export type SceneNode = {
|
|
@@ -97,7 +104,18 @@ export type Mesh = SceneNode & {
|
|
|
97
104
|
kind: "mesh"
|
|
98
105
|
geometry: Geometry
|
|
99
106
|
material: Material
|
|
107
|
+
/** Explicit draw-order key (default 0), Three's name: lower draws first.
|
|
108
|
+
* Sorts within the opaque group and within the transparent group; the
|
|
109
|
+
* transparent group always follows the opaque one. Set with setRenderOrder. */
|
|
110
|
+
renderOrder: number
|
|
100
111
|
_entry: DrawId | null
|
|
112
|
+
/** material.transparent as of the last attach - the entry's actual
|
|
113
|
+
* pipeline state, and what _detach counts against (setMaterial swaps
|
|
114
|
+
* mesh.material before the rebuild). */
|
|
115
|
+
_transparent: boolean
|
|
116
|
+
/** World-space center of the geometry bounds, kept by the sync walk
|
|
117
|
+
* beside the picking leaf: the transparent sort key. */
|
|
118
|
+
_center: Vec3
|
|
101
119
|
_hidden: boolean
|
|
102
120
|
_fresh: boolean
|
|
103
121
|
_params: ShaderParams | null
|
|
@@ -184,6 +202,15 @@ export type Scene = {
|
|
|
184
202
|
/** Partial camera update; absent keys keep their current value. */
|
|
185
203
|
setCamera(update: CameraUpdate): void
|
|
186
204
|
setSize(width: number, height: number): void
|
|
205
|
+
/**
|
|
206
|
+
* Scene-wide uniforms: merge app-owned names into the target's SHARED
|
|
207
|
+
* params, beside the standard uViewProj/uCamPos/uCamRight/uCamUp the
|
|
208
|
+
* camera writes. One write per frame however many meshes read the name
|
|
209
|
+
* (a clock, a sun direction, fog) - the per-mesh channel is
|
|
210
|
+
* setMeshParams. Merge semantics, no unset; a material that does not
|
|
211
|
+
* declare a name simply skips it. Frame-rate-safe like setTransform.
|
|
212
|
+
*/
|
|
213
|
+
setParams(params: ShaderParams): void
|
|
187
214
|
/**
|
|
188
215
|
* Set, replace, or remove (null) the scene's background: fragment GLSL
|
|
189
216
|
* drawn as the FIRST entry of the scene's own pass - one target, no
|
|
@@ -279,7 +306,10 @@ export function createMesh(geometry: Geometry, material: Material): Mesh {
|
|
|
279
306
|
let mesh = makeNode("mesh") as Mesh
|
|
280
307
|
mesh.geometry = geometry
|
|
281
308
|
mesh.material = material
|
|
309
|
+
mesh.renderOrder = 0
|
|
282
310
|
mesh._entry = null
|
|
311
|
+
mesh._transparent = false
|
|
312
|
+
mesh._center = [0, 0, 0]
|
|
283
313
|
mesh._hidden = false
|
|
284
314
|
mesh._fresh = false
|
|
285
315
|
mesh._params = null
|
|
@@ -340,33 +370,55 @@ export type TransformUpdate = {
|
|
|
340
370
|
* Values are copied in; absent keys keep their current value. This is also
|
|
341
371
|
* the frame-rate escape hatch: call it from onFrame on a node grabbed via
|
|
342
372
|
* `ref`, bypassing signals entirely.
|
|
373
|
+
*
|
|
374
|
+
* A write that changes nothing schedules nothing, so driving every node
|
|
375
|
+
* unconditionally from onFrame costs only the compare for the nodes that
|
|
376
|
+
* did not move. Rotation is compared after conversion, so passing an euler
|
|
377
|
+
* equal to the node's current quaternion is also a no-op.
|
|
343
378
|
*/
|
|
344
379
|
export function setTransform(node: SceneNode, update: TransformUpdate): void {
|
|
380
|
+
let r = update.rotation
|
|
381
|
+
let q = update.quaternion
|
|
382
|
+
if (r !== undefined && q !== undefined) {
|
|
383
|
+
throw new Error("Pass rotation or quaternion to setTransform, not both")
|
|
384
|
+
}
|
|
385
|
+
// A no-op write costs nothing: driving every node from onFrame is the
|
|
386
|
+
// intended shape, and most nodes did not move. Exact compares, like
|
|
387
|
+
// setVisible - a value that survives a float round trip unchanged is the
|
|
388
|
+
// same value, and an epsilon would need a scale-dependent one anyway.
|
|
389
|
+
let changed = false
|
|
345
390
|
let p = update.position
|
|
346
|
-
if (p) {
|
|
391
|
+
if (p && (p[0] !== node.position[0] || p[1] !== node.position[1] || p[2] !== node.position[2])) {
|
|
347
392
|
node.position[0] = p[0]
|
|
348
393
|
node.position[1] = p[1]
|
|
349
394
|
node.position[2] = p[2]
|
|
395
|
+
changed = true
|
|
350
396
|
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
if (r !== undefined
|
|
354
|
-
|
|
397
|
+
if (r !== undefined) quatFromEuler(rotScratch, r)
|
|
398
|
+
else if (q !== undefined) quatNormalize(rotScratch, q)
|
|
399
|
+
if (r !== undefined || q !== undefined) {
|
|
400
|
+
let n = node.quaternion
|
|
401
|
+
if (rotScratch[0] !== n[0] || rotScratch[1] !== n[1] || rotScratch[2] !== n[2] || rotScratch[3] !== n[3]) {
|
|
402
|
+
n[0] = rotScratch[0]
|
|
403
|
+
n[1] = rotScratch[1]
|
|
404
|
+
n[2] = rotScratch[2]
|
|
405
|
+
n[3] = rotScratch[3]
|
|
406
|
+
changed = true
|
|
407
|
+
}
|
|
355
408
|
}
|
|
356
|
-
if (r !== undefined) quatFromEuler(node.quaternion, r)
|
|
357
|
-
else if (q !== undefined) quatNormalize(node.quaternion, q)
|
|
358
409
|
let s = update.scale
|
|
359
410
|
if (s !== undefined) {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
node.scale[
|
|
366
|
-
node.scale[
|
|
367
|
-
|
|
411
|
+
let sx = typeof s === "number" ? s : s[0]
|
|
412
|
+
let sy = typeof s === "number" ? s : s[1]
|
|
413
|
+
let sz = typeof s === "number" ? s : s[2]
|
|
414
|
+
if (sx !== node.scale[0] || sy !== node.scale[1] || sz !== node.scale[2]) {
|
|
415
|
+
node.scale[0] = sx
|
|
416
|
+
node.scale[1] = sy
|
|
417
|
+
node.scale[2] = sz
|
|
418
|
+
changed = true
|
|
368
419
|
}
|
|
369
420
|
}
|
|
421
|
+
if (!changed) return
|
|
370
422
|
node._localDirty = true
|
|
371
423
|
node._scene?._schedule()
|
|
372
424
|
}
|
|
@@ -482,8 +534,15 @@ export function setVisible(node: SceneNode, visible: boolean): void {
|
|
|
482
534
|
node._scene?._schedule()
|
|
483
535
|
}
|
|
484
536
|
|
|
485
|
-
/**
|
|
486
|
-
|
|
537
|
+
/** Set a mesh's explicit draw-order key (see Mesh.renderOrder). */
|
|
538
|
+
export function setRenderOrder(mesh: Mesh, order: number): void {
|
|
539
|
+
if (mesh.renderOrder === order) return
|
|
540
|
+
mesh.renderOrder = order
|
|
541
|
+
mesh._scene?._reorder()
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/** Swap a mesh's geometry: its draw entry is rebuilt (the scene re-sorts
|
|
545
|
+
* the list, so the mesh keeps its place). */
|
|
487
546
|
export function setGeometry(mesh: Mesh, geometry: Geometry): void {
|
|
488
547
|
if (mesh.geometry === geometry) return
|
|
489
548
|
mesh.geometry = geometry
|
|
@@ -545,11 +604,25 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
545
604
|
let capture = new Map<number, Mesh>()
|
|
546
605
|
let hover = new Map<number, Mesh>()
|
|
547
606
|
|
|
548
|
-
// Live
|
|
549
|
-
//
|
|
550
|
-
//
|
|
551
|
-
|
|
607
|
+
// Live meshes (those holding a draw entry) in add order; the background
|
|
608
|
+
// entry never joins this list. Draw order is derived from it by
|
|
609
|
+
// orderEntries (order.ts) whenever orderDirty. Camera moves and
|
|
610
|
+
// transparent-mesh moves only dirty the order when two or more transparent
|
|
611
|
+
// meshes exist - fewer cannot change relative order.
|
|
612
|
+
let meshes: Mesh[] = []
|
|
613
|
+
let transparentCount = 0
|
|
614
|
+
let orderDirty = false
|
|
615
|
+
// The order last handed to the engine: a resort that lands on the same
|
|
616
|
+
// permutation (the common case under a moving camera) issues nothing.
|
|
617
|
+
let lastOrder: DrawId[] = []
|
|
552
618
|
let background: { entry: DrawId; pipeline: RenderPipelineId; program: ProgramId } | null = null
|
|
619
|
+
let sortEntries = () => {
|
|
620
|
+
orderDirty = false
|
|
621
|
+
let order = orderEntries(meshes, view, background?.entry)
|
|
622
|
+
if (order.length === lastOrder.length && order.every((id, i) => id === lastOrder[i])) return
|
|
623
|
+
lastOrder = order
|
|
624
|
+
setDrawOrder(texture, order)
|
|
625
|
+
}
|
|
553
626
|
|
|
554
627
|
// Reinsert or refit a mesh's broadphase leaf from its fresh world matrix:
|
|
555
628
|
// the local box's center/extents carried through the absolute matrix (the
|
|
@@ -566,6 +639,9 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
566
639
|
let wx = m[0] * cx + m[4] * cy + m[8] * cz + m[12]
|
|
567
640
|
let wy = m[1] * cx + m[5] * cy + m[9] * cz + m[13]
|
|
568
641
|
let wz = m[2] * cx + m[6] * cy + m[10] * cz + m[14]
|
|
642
|
+
mesh._center[0] = wx
|
|
643
|
+
mesh._center[1] = wy
|
|
644
|
+
mesh._center[2] = wz
|
|
569
645
|
let rx = Math.abs(m[0]) * ex + Math.abs(m[4]) * ey + Math.abs(m[8]) * ez
|
|
570
646
|
let ry = Math.abs(m[1]) * ex + Math.abs(m[5]) * ey + Math.abs(m[9]) * ez
|
|
571
647
|
let rz = Math.abs(m[2]) * ex + Math.abs(m[6]) * ey + Math.abs(m[10]) * ez
|
|
@@ -610,7 +686,16 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
610
686
|
// holds. Entries are untouched - uModel is camera-independent, and
|
|
611
687
|
// uCamPos is stored even when no current material declares it.
|
|
612
688
|
cameraPending = false
|
|
613
|
-
|
|
689
|
+
// The camera basis rides along: the view matrix's first two rows are
|
|
690
|
+
// the camera's world-space right and up (no clip flip - that lives in
|
|
691
|
+
// the projection), so a billboard needs no reconstruction from uViewProj.
|
|
692
|
+
setTargetParams(texture, {
|
|
693
|
+
uViewProj: viewProj,
|
|
694
|
+
uCamPos: eye,
|
|
695
|
+
uCamRight: [view[0], view[4], view[8]],
|
|
696
|
+
uCamUp: [view[1], view[5], view[9]],
|
|
697
|
+
})
|
|
698
|
+
if (transparentCount > 1) orderDirty = true
|
|
614
699
|
}
|
|
615
700
|
let walk = (node: SceneNode, parentChanged: boolean, parentVisible: boolean) => {
|
|
616
701
|
let changed = parentChanged
|
|
@@ -632,6 +717,7 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
632
717
|
mesh._hidden = !shown
|
|
633
718
|
if (shown) mesh._fresh = true
|
|
634
719
|
}
|
|
720
|
+
if (changed && mesh._transparent && transparentCount > 1) orderDirty = true
|
|
635
721
|
if (!mesh._hidden && (changed || mesh._fresh)) {
|
|
636
722
|
if (mesh.material.normalMatrix) {
|
|
637
723
|
setDrawParams(texture, mesh._entry, {
|
|
@@ -655,6 +741,7 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
655
741
|
for (let c of node.children) walk(c, changed, shown)
|
|
656
742
|
}
|
|
657
743
|
walk(root, false, true)
|
|
744
|
+
if (orderDirty) sortEntries()
|
|
658
745
|
}
|
|
659
746
|
|
|
660
747
|
let hooks: SceneHooks = {
|
|
@@ -695,7 +782,10 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
695
782
|
textures: mesh.material.textures,
|
|
696
783
|
instanceCount: 0,
|
|
697
784
|
})
|
|
698
|
-
|
|
785
|
+
meshes.push(mesh)
|
|
786
|
+
mesh._transparent = mesh.material.transparent === true
|
|
787
|
+
if (mesh._transparent) transparentCount++
|
|
788
|
+
orderDirty = true
|
|
699
789
|
mesh._hidden = true
|
|
700
790
|
mesh._fresh = true
|
|
701
791
|
this._schedule()
|
|
@@ -703,8 +793,10 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
703
793
|
_detach(mesh) {
|
|
704
794
|
if (mesh._entry !== null) {
|
|
705
795
|
if (!disposed) removeDraw(texture, mesh._entry)
|
|
706
|
-
let i =
|
|
707
|
-
if (i >= 0)
|
|
796
|
+
let i = meshes.indexOf(mesh)
|
|
797
|
+
if (i >= 0) meshes.splice(i, 1)
|
|
798
|
+
if (mesh._transparent) transparentCount--
|
|
799
|
+
orderDirty = true
|
|
708
800
|
}
|
|
709
801
|
mesh._entry = null
|
|
710
802
|
// The leaf goes with the entry: a geometry swap rebuilds the entry,
|
|
@@ -717,6 +809,10 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
717
809
|
_setParams(mesh, params) {
|
|
718
810
|
if (mesh._entry !== null && !disposed) setDrawParams(texture, mesh._entry, params)
|
|
719
811
|
},
|
|
812
|
+
_reorder() {
|
|
813
|
+
orderDirty = true
|
|
814
|
+
this._schedule()
|
|
815
|
+
},
|
|
720
816
|
}
|
|
721
817
|
|
|
722
818
|
let root = makeNode("group")
|
|
@@ -864,6 +960,9 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
864
960
|
cameraDirty = true
|
|
865
961
|
hooks._schedule()
|
|
866
962
|
},
|
|
963
|
+
setParams(params) {
|
|
964
|
+
if (!disposed) setTargetParams(texture, params)
|
|
965
|
+
},
|
|
867
966
|
setBackground(source) {
|
|
868
967
|
if (disposed) return
|
|
869
968
|
if (background !== null) {
|
|
@@ -874,9 +973,9 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
874
973
|
}
|
|
875
974
|
if (source === null) return
|
|
876
975
|
let built = backgroundPipeline(source, (opts?.label ?? "scene") + "-background")
|
|
877
|
-
// First in list order:
|
|
878
|
-
//
|
|
879
|
-
let entry = addDraw(texture, built.pipeline, null, { vertexCount: 3, before:
|
|
976
|
+
// First in list order: inserted before the first mesh entry, and every
|
|
977
|
+
// later sort keeps it there.
|
|
978
|
+
let entry = addDraw(texture, built.pipeline, null, { vertexCount: 3, before: meshes[0]?._entry ?? undefined })
|
|
880
979
|
background = { entry, pipeline: built.pipeline, program: built.program }
|
|
881
980
|
},
|
|
882
981
|
project(point) {
|