@solidrt/3d 0.0.67 → 0.0.69
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 +62 -23
- package/README.md +30 -27
- package/package.json +3 -3
- package/src/components/group.tsx +2 -2
- package/src/components/index.ts +1 -1
- package/src/components/instanced-mesh.tsx +2 -2
- package/src/components/lod.tsx +2 -2
- package/src/components/mesh.tsx +2 -2
- package/src/components/node-props.ts +4 -4
- package/src/components/scene.tsx +6 -1
- package/src/components/sprite.tsx +2 -2
- package/src/first-person.ts +3 -1
- package/src/index.ts +4 -4
- package/src/material.ts +73 -7
- package/src/mesh.ts +59 -6
- package/src/node.ts +12 -4
- package/src/orbit.ts +13 -6
- package/src/scene-pointer.ts +1 -1
- package/src/scene-shadows.ts +1 -1
- package/src/scene.ts +136 -14
package/AGENTS.md
CHANGED
|
@@ -91,7 +91,9 @@ Two layers. The imperative core is Solid-free: `createScene`,
|
|
|
91
91
|
over the spatial core (`flux:spatial`, `alloy/src/spatial/`): every node
|
|
92
92
|
in a scene has a core node, JS keeps the LOCAL position/quaternion/scale
|
|
93
93
|
as the readable truth and forwards each write, and the core's flush
|
|
94
|
-
(one call per
|
|
94
|
+
(one call per frame, in the publish pass of core's `onBeforeRender`
|
|
95
|
+
ahead of the paint; at the microtask for a write made outside a frame)
|
|
96
|
+
recomputes only the moved subtrees and writes
|
|
95
97
|
each entry's uModel (plus uNormal for materials declaring it) and its
|
|
96
98
|
visibility switch itself - a move costs its subtree, never the scene.
|
|
97
99
|
ONE `setTargetParams` (the shared uViewProj + uCamPos) per camera
|
|
@@ -215,7 +217,10 @@ mesh: `frustumCulled: false` (Three's name; `setCulling`) for geometry
|
|
|
215
217
|
a vertex stage moves beyond its box - a fullscreen quad, a custom
|
|
216
218
|
displacement - and `cullMargin` (world units, Godot's
|
|
217
219
|
`extra_cull_margin`) for bounded displacement such as wind. Sprites
|
|
218
|
-
cull by their quad's reach at any facing
|
|
220
|
+
cull by their quad's reach at any facing; a sprite whose material (or
|
|
221
|
+
`params`) floors its screen size exceeds that reach far away, so the
|
|
222
|
+
floor coming on switches the mesh's culling off, one way. A SKINNED
|
|
223
|
+
part is culled by
|
|
219
224
|
the union of its joints' boxes (the bake computes each joint's
|
|
220
225
|
influence box in joint space, `ModelSkin.jointBounds`, .sol3m VERSION
|
|
221
226
|
5; the joint nodes carry them as culling-only bounds, outside the
|
|
@@ -399,7 +404,8 @@ unmount lets them go in; nearest declaring ancestor wins) and cascades
|
|
|
399
404
|
nothing unless they declare `from`/`exit` -
|
|
400
405
|
a squad of instances spawning in is one `stagger` on their group, and
|
|
401
406
|
for the nodes straight under the scene root it is `<Scene stagger>` /
|
|
402
|
-
`createScene({ stagger })`
|
|
407
|
+
`createScene({ stagger })` / `scene.setStagger(ms)` (a declaration on
|
|
408
|
+
`scene.root`). A populated
|
|
403
409
|
mesh unmounts as `destroy(mesh)` then `disposeInstances(mesh)`, and the
|
|
404
410
|
dispose waits for a mesh still animating out, so an `<Instance>`'s exit
|
|
405
411
|
plays through its `<InstancedMesh>`'s unmount (`model.dispose` waits the
|
|
@@ -987,7 +993,15 @@ box's twelve triangles, so its hits carry the struck face's `normal` and
|
|
|
987
993
|
no `face`/`uv` - and a ray from inside it meets the far side, the
|
|
988
994
|
surface contract overlap/sweep share. Both methods
|
|
989
995
|
flush pending writes first (the lookAt/project immediacy contract), and
|
|
990
|
-
both skip invisible meshes.
|
|
996
|
+
both skip invisible meshes. A sprite under a screen-size clamp
|
|
997
|
+
(`sprite({ minScreenPx, maxScreenPx })`) draws at a size the index does
|
|
998
|
+
not know - the vertex stage scales it by the pixels per unit at its
|
|
999
|
+
depth under the camera drawing it - so `pick` (which has a camera; a
|
|
1000
|
+
view's pick its own) replaces the ray's hits on clamped sprites with an
|
|
1001
|
+
exact ray-quad test at the drawn size, the quad rebuilt as the stage
|
|
1002
|
+
builds it (camera right/up, or a fixed-y sprite's yaw basis), hits
|
|
1003
|
+
carrying the plane distance and a normal facing the ray. `raycast` is a
|
|
1004
|
+
world query and keeps the index's unit-box answer.
|
|
991
1005
|
|
|
992
1006
|
### Collision
|
|
993
1007
|
|
|
@@ -1312,7 +1326,7 @@ opts out of the scene's fog (all four library materials take it).
|
|
|
1312
1326
|
#### sprite
|
|
1313
1327
|
|
|
1314
1328
|
`sprite({ color?, map?, transparent?, blend?, billboard?, shape?,
|
|
1315
|
-
falloff? })` - unlit on a quad
|
|
1329
|
+
falloff?, minScreenPx?, maxScreenPx? })` - unlit on a quad
|
|
1316
1330
|
that turns to face the camera IN THE VERTEX STAGE (off the shared
|
|
1317
1331
|
uCamRight/uCamUp, or uCamPos for `billboard: "fixed-y"`, which yaws
|
|
1318
1332
|
only and stays upright on world y - Godot's BILLBOARD_FIXED_Y, the
|
|
@@ -1326,7 +1340,18 @@ facing), so hits carry no normal/face/uv. `shape: "radial"` is a
|
|
|
1326
1340
|
procedural falloff over the quad's inscribed disc, `(1 - 2|uv -
|
|
1327
1341
|
0.5|)^falloff` (default 1; 2 a soft puff) multiplying color and alpha,
|
|
1328
1342
|
so a glow, flare or puff needs no texture; it composes with a `map`
|
|
1329
|
-
and with `blend: "add"`. `
|
|
1343
|
+
and with `blend: "add"`. `minScreenPx`/`maxScreenPx` clamp the quad's
|
|
1344
|
+
smaller axis on SCREEN (pixels; 0 = off), scaled uniformly about its
|
|
1345
|
+
center in the vertex stage by the pixels one world unit covers at the
|
|
1346
|
+
quad's depth (depth alone, from the shared uniforms, so perspective
|
|
1347
|
+
and orthographic alike): a floor is the marker that stays findable at
|
|
1348
|
+
distance, a ceiling the label that must not balloon, equal bounds a
|
|
1349
|
+
constant screen size (Three's `sizeAttenuation: false`, `scale` then
|
|
1350
|
+
the aspect alone). The clamp is core's `screenSizeScale`, shared with
|
|
1351
|
+
@solidrt/2d's sprites; the per-entry `uScreenPx`, so a per-mesh
|
|
1352
|
+
`params` can override it. `scene.pick`/`view.pick` test the drawn
|
|
1353
|
+
quad (below), and a floored sprite's mesh turns its frustum culling
|
|
1354
|
+
off (one way; `setCulling` turns it back on). `examples/sprites.tsx`.
|
|
1330
1355
|
|
|
1331
1356
|
#### shaderMaterial
|
|
1332
1357
|
|
|
@@ -1398,10 +1423,14 @@ vertex stream stepped per instance: on the mesh it is an `InstanceStream`
|
|
|
1398
1423
|
(the layout, a byte mirror, a mesh-owned GPU buffer), written through
|
|
1399
1424
|
`instanceAttribute(mesh, name)` - the accessor `geometryAttribute`
|
|
1400
1425
|
returns, record index in, values as the shader sees them, the codec
|
|
1401
|
-
packing the bytes -
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1426
|
+
packing the bytes - or in bulk through the mirror itself, `records(mesh,
|
|
1427
|
+
stream?)` (a Float32Array over an all-float layout, bytes otherwise;
|
|
1428
|
+
read it at use time, growth replaces it), and published with
|
|
1429
|
+
`updateRecords(mesh, { stream?, first?, count? })`, which is
|
|
1430
|
+
`updateVertices` for records: one coalesced buffer write per dirty
|
|
1431
|
+
stream at the scene's sync, so ten moved records of ten thousand cost
|
|
1432
|
+
ten - the same `records`/`updateRecords` pair as @solidrt/2d's records
|
|
1433
|
+
layer. Its meshes come from `createInstancedMesh`
|
|
1405
1434
|
(the first buffer is the core-written matrix, the rest are streams; the
|
|
1406
1435
|
second is the STYLE record, below) or `createRecordMesh` (every buffer a
|
|
1407
1436
|
stream); a `createMesh` mesh is rejected at add(). `instanceStyle: [..]`
|
|
@@ -1519,11 +1548,13 @@ through `instanceAttribute`. `count` picks how many draw (default all).
|
|
|
1519
1548
|
`setRecords(mesh, records, count?)` rewrites from the start (count
|
|
1520
1549
|
defaults to the records written; more than capacity GROWS: capacity
|
|
1521
1550
|
doubles into replacement buffers, the entry is re-pointed via
|
|
1522
|
-
`setDrawBuffers`, the old ones are freed), `
|
|
1523
|
-
`updateRecords(mesh, { first, count })`
|
|
1524
|
-
against capacity, so write ahead and dial
|
|
1525
|
-
`setRecordCount(mesh, n)` is the population dial (clamped to
|
|
1526
|
-
frame-rate-safe).
|
|
1551
|
+
`setDrawBuffers`, the old ones are freed), `records(mesh)` (the mirror)
|
|
1552
|
+
or `instanceAttribute` + `updateRecords(mesh, { first, count })`
|
|
1553
|
+
rewrites a few (the range is against capacity, so write ahead and dial
|
|
1554
|
+
after), and `setRecordCount(mesh, n)` is the population dial (clamped to
|
|
1555
|
+
capacity; frame-rate-safe). The @solidrt/2d records layer speaks the
|
|
1556
|
+
same three verbs over its sprites, `setRecords` alone staying the
|
|
1557
|
+
mesh's (a layer's population is addSprite's). Records are opaque data (position/yaw/tint/whatever
|
|
1527
1558
|
your shader reads), so the
|
|
1528
1559
|
library cannot know where they place instances: a record mesh has NO
|
|
1529
1560
|
picking leaf unless you pass `bounds` (local, covering the population) -
|
|
@@ -3014,8 +3045,9 @@ successors) as ordinary content, the model-loading split repeated:
|
|
|
3014
3045
|
meshes skip uModel writes; the fresh matrix is
|
|
3015
3046
|
written on unhide. A freshly attached entry starts off the same way and
|
|
3016
3047
|
the core's flush turns it on when it writes uModel - never add one
|
|
3017
|
-
live: it has no world matrix yet, and drawn before the sync
|
|
3018
|
-
|
|
3048
|
+
live: it has no world matrix yet, and drawn before the sync (which runs
|
|
3049
|
+
ahead of the paint for a write made in a frame, at the microtask
|
|
3050
|
+
otherwise) it would flash at the world origin for a frame.
|
|
3019
3051
|
- Instancing pairs strictly at add(), like layout: an instanced material
|
|
3020
3052
|
needs a createInstancedMesh or createRecordMesh mesh and vice versa,
|
|
3021
3053
|
and every instance buffer the material declares must be a layout the
|
|
@@ -3039,8 +3071,14 @@ successors) as ordinary content, the model-loading split repeated:
|
|
|
3039
3071
|
`data` and the scene's sync publishes the `dirty` range (`updateRecords`
|
|
3040
3072
|
marks it for accessor writes) - so a write on a mesh outside any scene
|
|
3041
3073
|
shows once it is added (the dirty range waits), and reading the GPU
|
|
3042
|
-
buffer back
|
|
3043
|
-
|
|
3074
|
+
buffer back between the write and the sync lags the mirror. The sync
|
|
3075
|
+
runs in the publish pass of core's `onBeforeRender`, ahead of the
|
|
3076
|
+
frame's paint (at the microtask for a write made outside a frame), so
|
|
3077
|
+
a `setRecords` made in onFrame is in that frame's picture with its
|
|
3078
|
+
count, its buffers and its bytes together - the count alone
|
|
3079
|
+
(`setRecordCount`) reaches the engine at once. Growth replaces
|
|
3080
|
+
`data`: hold the accessor, or re-read `records(mesh)` at use time,
|
|
3081
|
+
never a hoisted view.
|
|
3044
3082
|
- The sugar is for FEW records, the mirror is for MANY. `setInstanceStyle`
|
|
3045
3083
|
over an all-float layout is indexed stores; over a packed layout (the
|
|
3046
3084
|
stock `float16x4` color included) it is a codec call per component,
|
|
@@ -3050,10 +3088,11 @@ successors) as ordinary content, the model-loading split repeated:
|
|
|
3050
3088
|
color, 9 ms through the accessor, 5 ms through the mirror - and that
|
|
3051
3089
|
5 ms is the app's own per-instance tint math, the write itself is
|
|
3052
3090
|
free. A population restyled per frame writes the mirror in bulk:
|
|
3053
|
-
|
|
3054
|
-
`Float16Array` over the stock color, indexed by slot
|
|
3055
|
-
filled in a plain loop, then ONE `updateRecords(mesh,
|
|
3056
|
-
})` - zero per-component calls, the same shape as
|
|
3091
|
+
`records(mesh)` (a Float32Array over an all-float layout, bytes
|
|
3092
|
+
otherwise - a `Float16Array` over the stock color, indexed by slot
|
|
3093
|
+
times components) filled in a plain loop, then ONE `updateRecords(mesh,
|
|
3094
|
+
{ first, count })` - zero per-component calls, the same shape as
|
|
3095
|
+
`setRecords`.
|
|
3057
3096
|
- Instanced casters: `castShadow` on a populated mesh needs a depth pass
|
|
3058
3097
|
with the instance placement in it - the stock materials' `instanced`
|
|
3059
3098
|
carries one, a custom class needs `shadowVertex` (see shadows below);
|
package/README.md
CHANGED
|
@@ -60,9 +60,12 @@ drag, and hover enter/leave pairs - hit testing runs over a BVH the
|
|
|
60
60
|
scene maintains incrementally, so events put no ceiling on scene size.
|
|
61
61
|
Underneath sit `scene.pick(x, y)` (the camera ray through a pixel,
|
|
62
62
|
`project()`'s inverse) and `scene.raycast(origin, direction)`; hits are
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
per triangle, carrying the face, its uv and a normal facing the ray.
|
|
64
|
+
`scene.overlap`, `scene.sweep` and `scene.moveAndSlide` ask the same
|
|
65
|
+
index what a volume touches, where a moving one first touches, and how a
|
|
66
|
+
character body slides through it. A scene also takes a `background` -
|
|
67
|
+
fragment GLSL drawn inside its own pass behind the meshes, or a skybox
|
|
68
|
+
cube - replacing the stacked backdrop-texture pattern.
|
|
66
69
|
Custom materials get a standard uniform set - per-mesh `uModel`/`uNormal`,
|
|
67
70
|
shared `uViewProj`/`uCamPos`/`uCamRight`/`uCamUp`, each written once per
|
|
68
71
|
change - plus your own uniforms: scene-wide via `scene.setParams` (one write
|
|
@@ -83,27 +86,27 @@ every fleet only JS can step (`examples/instanced.tsx`). And
|
|
|
83
86
|
blinn, fresnel, a standard vertex stage) to compose your own lit looks
|
|
84
87
|
from plain template literals.
|
|
85
88
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
solids
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
89
|
+
In the package: materials (`unlit`, Blinn-Phong `phong`, metal/rough
|
|
90
|
+
`standard` with surface maps and image-based lighting, `sprite` for
|
|
91
|
+
camera-facing quads with a screen-size clamp, and `shaderMaterial` for
|
|
92
|
+
your own GLSL as a first-class material), lights as graph nodes
|
|
93
|
+
(directional, spot, point and hemisphere, up to eight, with cascaded
|
|
94
|
+
directional, spot and six-face point shadows), geometry generators (box,
|
|
95
|
+
plane, circle, ring, sphere, capsule, cylinder, cone, torus, torus knot,
|
|
96
|
+
the platonic solids with `detail` subdivision, so the icosahedron is also
|
|
97
|
+
the icosphere), a profile kit for custom solids (`extrude` with bevels,
|
|
98
|
+
`lathe`, polyline `sweep`/`tube` with mitred joints, flat `polygon`, with
|
|
99
|
+
`fillet`/`roundRect`/`triangulate` helpers), geometry as data
|
|
100
|
+
(`transformGeometry` bakes a placement into vertices, `mergeGeometries`
|
|
101
|
+
concatenates parts, `withAttribute`/`withColors` append named channels
|
|
102
|
+
materials read by name, morph targets), models (`loadGltf`/`loadModel`,
|
|
103
|
+
skins, a clip mixer with root motion, a baked `.sol3m` container with
|
|
104
|
+
compressed textures), Gaussian splats, level of detail, views of one
|
|
105
|
+
scene (split screen, a minimap, a reflection probe), environment and sky
|
|
106
|
+
(skybox, equirect panoramas, baked environments, fog, bloom, tone
|
|
107
|
+
mapping), camera controls (`<OrbitCamera>` and `<FirstPersonCamera>`
|
|
108
|
+
driven by an input map the app binds, shots and blends between them),
|
|
109
|
+
picking with pointer events, and the collision queries. Full usage notes
|
|
110
|
+
and traps: [AGENTS.md](AGENTS.md); runnable examples:
|
|
111
|
+
[examples/](examples/); the ranked list of what is next:
|
|
112
|
+
`okf/notes/3d-roadmap.md`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidrt/3d",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.69",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"funding": "https://github.com/sponsors/wellawaretech",
|
|
6
6
|
"author": "Antoine van Wel",
|
|
@@ -20,10 +20,10 @@
|
|
|
20
20
|
"AGENTS.md"
|
|
21
21
|
],
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@solidrt/test": "0.0.
|
|
23
|
+
"@solidrt/test": "0.0.69"
|
|
24
24
|
},
|
|
25
25
|
"peerDependencies": {
|
|
26
26
|
"@solidjs/signals": "2.0.0-rc.13",
|
|
27
|
-
"@solidrt/core": "0.0.
|
|
27
|
+
"@solidrt/core": "0.0.69"
|
|
28
28
|
}
|
|
29
29
|
}
|
package/src/components/group.tsx
CHANGED
|
@@ -2,11 +2,11 @@ import { onCleanup, untrack, useContext } from "@solidrt/core"
|
|
|
2
2
|
import type { ParentComponent } from "@solidrt/core"
|
|
3
3
|
import { SceneContext, provide } from "./context.tsx"
|
|
4
4
|
import { syncNode } from "./node-props.ts"
|
|
5
|
-
import type { TransformProps,
|
|
5
|
+
import type { TransformProps, BubblingNodePointerProps } from "./node-props.ts"
|
|
6
6
|
import { add, createGroup, destroy } from "../node.ts"
|
|
7
7
|
import type { SceneNode } from "../node.ts"
|
|
8
8
|
|
|
9
|
-
export type GroupProps = TransformProps &
|
|
9
|
+
export type GroupProps = TransformProps & BubblingNodePointerProps & { ref?: (node: SceneNode) => void }
|
|
10
10
|
|
|
11
11
|
/** A transform node: children inherit its position/rotation/scale. Its
|
|
12
12
|
* pointer events are the ones bubbling up from a hit descendant; a group
|
package/src/components/index.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
export { useScene } from "./context.tsx"
|
|
18
18
|
export type { CameraTarget } from "./context.tsx"
|
|
19
|
-
export type {
|
|
19
|
+
export type { BubblingNodePointerProps, NodePointerProps, TransformProps } from "./node-props.ts"
|
|
20
20
|
export { Scene } from "./scene.tsx"
|
|
21
21
|
export type { ScenePointerProps, SceneProps } from "./scene.tsx"
|
|
22
22
|
export { View3d } from "./view3d.tsx"
|
|
@@ -2,7 +2,7 @@ import { createContext, createEffect, onCleanup, untrack, useContext } from "@so
|
|
|
2
2
|
import type { ParentComponent } from "@solidrt/core"
|
|
3
3
|
import { SceneContext, provide } from "./context.tsx"
|
|
4
4
|
import { syncNode } from "./node-props.ts"
|
|
5
|
-
import type { TransformProps,
|
|
5
|
+
import type { TransformProps, NodePointerProps } from "./node-props.ts"
|
|
6
6
|
import { syncMesh } from "./mesh.tsx"
|
|
7
7
|
import type { PopulatedMeshProps } from "./mesh.tsx"
|
|
8
8
|
import { add, destroy, setMorphWeights } from "../node.ts"
|
|
@@ -92,7 +92,7 @@ export let InstancedMesh: ParentComponent<InstancedMeshProps> = props => {
|
|
|
92
92
|
)
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
export type InstanceProps = TransformProps &
|
|
95
|
+
export type InstanceProps = TransformProps & NodePointerProps & {
|
|
96
96
|
/** The instance's style record (setInstanceStyle as a prop): one value
|
|
97
97
|
* per component of the material's second instance buffer, in order -
|
|
98
98
|
* `[r, g, b, a]` under a stock material's `instanceColors`. Reactive;
|
package/src/components/lod.tsx
CHANGED
|
@@ -3,7 +3,7 @@ import type { ParentComponent } from "@solidrt/core"
|
|
|
3
3
|
import { LodContext, SceneContext, provide } from "./context.tsx"
|
|
4
4
|
import type { LodRegistry } from "./context.tsx"
|
|
5
5
|
import { syncNode } from "./node-props.ts"
|
|
6
|
-
import type { TransformProps,
|
|
6
|
+
import type { TransformProps, BubblingNodePointerProps } from "./node-props.ts"
|
|
7
7
|
import { syncMesh } from "./mesh.tsx"
|
|
8
8
|
import type { PopulatedMeshProps } from "./mesh.tsx"
|
|
9
9
|
import { InstancedMeshContext } from "./instanced-mesh.tsx"
|
|
@@ -15,7 +15,7 @@ import { disposeInstances, setCastShadow } from "../mesh.ts"
|
|
|
15
15
|
import type { InstancedMesh as InstancedMeshNode } from "../mesh.ts"
|
|
16
16
|
|
|
17
17
|
export type LodProps = TransformProps &
|
|
18
|
-
|
|
18
|
+
BubblingNodePointerProps & {
|
|
19
19
|
/** The cross-fade band fraction (createLod's `fade`); default 0. */
|
|
20
20
|
fade?: number
|
|
21
21
|
ref?: (node: SceneNode) => void
|
package/src/components/mesh.tsx
CHANGED
|
@@ -2,7 +2,7 @@ import { createEffect, onCleanup, untrack, useContext } from "@solidrt/core"
|
|
|
2
2
|
import type { VoidComponent } from "@solidrt/core"
|
|
3
3
|
import { SceneContext } from "./context.tsx"
|
|
4
4
|
import { syncNode } from "./node-props.ts"
|
|
5
|
-
import type { TransformProps,
|
|
5
|
+
import type { TransformProps, NodePointerProps } from "./node-props.ts"
|
|
6
6
|
import { add, destroy, setMorphWeights } from "../node.ts"
|
|
7
7
|
import type { MorphWeights } from "../node.ts"
|
|
8
8
|
import { createMesh, setCastShadow, setCulling, setEnvironment, setGeometry, setLayers, setMaterial, setMeshParams, setRenderOrder } from "../mesh.ts"
|
|
@@ -12,7 +12,7 @@ import type { ShaderParams } from "@solidrt/core/gpu"
|
|
|
12
12
|
import type { Geometry } from "../geometry.ts"
|
|
13
13
|
import type { Material } from "../material.ts"
|
|
14
14
|
|
|
15
|
-
export type MeshProps = TransformProps &
|
|
15
|
+
export type MeshProps = TransformProps & NodePointerProps & {
|
|
16
16
|
geometry: Geometry
|
|
17
17
|
material: Material
|
|
18
18
|
/** Per-mesh uniforms for a custom material (setMeshParams as a prop).
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createEffect, onCleanup, useContext } from "@solidrt/core"
|
|
2
2
|
import { LodContext } from "./context.tsx"
|
|
3
3
|
import { setTransform, setTransition, setVisible } from "../node.ts"
|
|
4
|
-
import type { NodePointerEvent, NodeTapEvent, NodeWheelEvent, SceneNode, TransitionEndEvent } from "../node.ts"
|
|
4
|
+
import type { HoverHandlers, NodePointerEvent, NodeTapEvent, NodeWheelEvent, SceneNode, TransitionEndEvent } from "../node.ts"
|
|
5
5
|
import type { NodeTransition } from "flux:spatial"
|
|
6
6
|
import type { Quat, Vec3 } from "../math.ts"
|
|
7
7
|
|
|
@@ -36,7 +36,7 @@ export type TransformProps = {
|
|
|
36
36
|
* the built-in <Scene> leaf does (opt out with events={false}); an `output`
|
|
37
37
|
* leaf spreads them itself.
|
|
38
38
|
*/
|
|
39
|
-
export type
|
|
39
|
+
export type NodePointerProps = BubblingNodePointerProps & {
|
|
40
40
|
/** Hover: enter/leave pair on the struck node alone, so only a hit
|
|
41
41
|
* target - a mesh or an instance - takes them; `<Group>` and `<Lod>`
|
|
42
42
|
* bubble and omit the pair. */
|
|
@@ -46,7 +46,7 @@ export type PointerEventProps = BubblingPointerEventProps & {
|
|
|
46
46
|
|
|
47
47
|
/** The pointer props every node takes: the events that bubble from the
|
|
48
48
|
* hit up through the Groups above it, without the hover pair. */
|
|
49
|
-
export type
|
|
49
|
+
export type BubblingNodePointerProps = {
|
|
50
50
|
onPointerDown?: (event: NodePointerEvent) => void
|
|
51
51
|
onPointerMove?: (event: NodePointerEvent) => void
|
|
52
52
|
onPointerUp?: (event: NodePointerEvent) => void
|
|
@@ -60,7 +60,7 @@ export type BubblingPointerEventProps = {
|
|
|
60
60
|
onTap?: (event: NodeTapEvent) => void
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
export function syncNode(node: SceneNode, props: TransformProps &
|
|
63
|
+
export function syncNode(node: SceneNode & HoverHandlers, props: TransformProps & NodePointerProps): void {
|
|
64
64
|
// One effect for the transform and the handlers: re-assigning eight
|
|
65
65
|
// handler fields on a transform write is free, an effect of its own is
|
|
66
66
|
// not (a node component's mount cost is mostly its effects and its
|
package/src/components/scene.tsx
CHANGED
|
@@ -63,7 +63,7 @@ export type SceneProps = ScenePointerProps & {
|
|
|
63
63
|
/** Stagger (ms) for the nodes straight under the scene root: their
|
|
64
64
|
* enters and exits beginning in one frame are spaced by `index *
|
|
65
65
|
* stagger`, the whole-scene form of `<Group transition={{ stagger }}>`
|
|
66
|
-
* (see SceneOptions.stagger).
|
|
66
|
+
* (see SceneOptions.stagger). Live (scene.setStagger). */
|
|
67
67
|
stagger?: number
|
|
68
68
|
/** Scene-wide fog (scene.setFog): linear `{ color, near, far }` or exp2
|
|
69
69
|
* `{ color, density }`, optionally thinning above `height` by
|
|
@@ -256,6 +256,11 @@ export let Scene: ParentComponent<SceneProps> = props => {
|
|
|
256
256
|
l => scene.setLayers(l ?? 1),
|
|
257
257
|
{ defer: true },
|
|
258
258
|
)
|
|
259
|
+
createEffect(
|
|
260
|
+
() => props.stagger,
|
|
261
|
+
ms => scene.setStagger(ms ?? null),
|
|
262
|
+
{ defer: true },
|
|
263
|
+
)
|
|
259
264
|
untrack(() => props.ref)?.(scene)
|
|
260
265
|
// The pointer feed the app handed in listens at the scene's root, and
|
|
261
266
|
// children reach it through useScene().pointer. Fixed at creation, like
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { onCleanup, untrack, useContext } from "@solidrt/core"
|
|
2
2
|
import type { VoidComponent } from "@solidrt/core"
|
|
3
3
|
import { SceneContext } from "./context.tsx"
|
|
4
|
-
import type { TransformProps,
|
|
4
|
+
import type { TransformProps, NodePointerProps } from "./node-props.ts"
|
|
5
5
|
import { syncMesh } from "./mesh.tsx"
|
|
6
6
|
import { add, destroy } from "../node.ts"
|
|
7
7
|
import { createSprite } from "../mesh.ts"
|
|
@@ -9,7 +9,7 @@ import type { Mesh as MeshNode } from "../mesh.ts"
|
|
|
9
9
|
import type { ShaderParams } from "@solidrt/core/gpu"
|
|
10
10
|
import type { Material } from "../material.ts"
|
|
11
11
|
|
|
12
|
-
export type SpriteProps = TransformProps &
|
|
12
|
+
export type SpriteProps = TransformProps & NodePointerProps & {
|
|
13
13
|
/** A `sprite()` material (any material draws, only a sprite one turns). */
|
|
14
14
|
material: Material
|
|
15
15
|
/** Per-mesh uniforms, merge semantics - as on Mesh. */
|
package/src/first-person.ts
CHANGED
|
@@ -29,7 +29,9 @@
|
|
|
29
29
|
//
|
|
30
30
|
// Pose is plain mutable state; a nudge or a verb pushes it at once (a
|
|
31
31
|
// mouse move under lock needs no frame loop), and update(dt) integrates
|
|
32
|
-
// the rates and the glide from the app's own onFrame
|
|
32
|
+
// the rates and the glide from the app's own onFrame (onBeforeRender when
|
|
33
|
+
// the camera rides something the frame moves, so it sees the frame's
|
|
34
|
+
// final pose). Only `active` is
|
|
33
35
|
// reactive: the frame-loop gate, true while any rate reads non-zero (a
|
|
34
36
|
// held key, a deflected stick) or a glide is in flight, so a still scene
|
|
35
37
|
// renders nothing new. update() returns whether the pose changed since
|
package/src/index.ts
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
// PerspectiveCamera) on top. See AGENTS.md for the model and the traps.
|
|
7
7
|
|
|
8
8
|
export { add, createGroup, destroy, getMorphNames, getMorphWeights, getRotation, getTransform, lookAt, remove, setMorphWeights, setTransform, setTransition, setVisible, worldPosition } from "./node.ts"
|
|
9
|
-
export type { LodConfig, MorphWeights, NodePointerEvent, NodeTapEvent, NodeWheelEvent, SceneEventBase, SceneNode, ScenePointerEvent, ScenePointerListener, SceneTapEvent, SceneWheelEvent, TransformUpdate, TransitionEndEvent } from "./node.ts"
|
|
10
|
-
export { addInstance, createInstancedMesh, createMesh, createRecordMesh, createSprite, disposeInstances, setCastShadow, setCulling, setDrawRange, setEnvironment, setGeometry, setInstanceStyle, setLayers, setMaterial, setMeshParams, setRecordCount, setRecords, setRenderOrder, instanceAttribute, updateRecords, INSTANCE_FLOATS } from "./mesh.ts"
|
|
9
|
+
export type { HoverHandlers, LodConfig, MorphWeights, NodePointerEvent, NodeTapEvent, NodeWheelEvent, SceneEventBase, SceneNode, ScenePointerEvent, ScenePointerListener, SceneTapEvent, SceneWheelEvent, TransformUpdate, TransitionEndEvent } from "./node.ts"
|
|
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"
|
|
12
12
|
export { createDirectionalLight, createHemisphereLight, createPointLight, createSpotLight, setLight } from "./light.ts"
|
|
13
13
|
// The shader-source caps, also on the /glsl subpath next to the sources they size.
|
|
@@ -62,8 +62,8 @@ export type {
|
|
|
62
62
|
MeshProps,
|
|
63
63
|
OrbitCameraProps,
|
|
64
64
|
PerspectiveCameraProps,
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
NodePointerProps,
|
|
66
|
+
BubblingNodePointerProps,
|
|
67
67
|
GroupProps,
|
|
68
68
|
PointLightProps,
|
|
69
69
|
RecordMeshProps,
|
package/src/material.ts
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
// @solidrt/core/gpu) stays first-class beneath both.
|
|
21
21
|
|
|
22
22
|
import {
|
|
23
|
+
checkScreenSize,
|
|
23
24
|
compileShader,
|
|
24
25
|
createRenderPipeline,
|
|
25
26
|
destroyProgram,
|
|
@@ -28,6 +29,7 @@ import {
|
|
|
28
29
|
glsl,
|
|
29
30
|
linkProgram,
|
|
30
31
|
programAttributes,
|
|
32
|
+
SCREEN_SIZE_GLSL,
|
|
31
33
|
} from "@solidrt/core/gpu"
|
|
32
34
|
import type {
|
|
33
35
|
BlendMode,
|
|
@@ -97,6 +99,10 @@ export type Material = {
|
|
|
97
99
|
* like them, so the solids fill the depth before the fragments that
|
|
98
100
|
* cannot early-z. */
|
|
99
101
|
cutout?: boolean
|
|
102
|
+
/** How a `sprite()` material's quad faces the camera (set by sprite()):
|
|
103
|
+
* what the scene's pick rebuilds to test a screen-size-clamped sprite
|
|
104
|
+
* at its drawn size. Absent on every other material. */
|
|
105
|
+
billboard?: "full" | "fixed-y"
|
|
100
106
|
/** The instance buffers, when the material's pipeline declares them
|
|
101
107
|
* (shaderMaterialClass's `instanceBuffers`; the stock materials'
|
|
102
108
|
* `instanced`), one layout per buffer after the geometry's. Such a
|
|
@@ -945,6 +951,27 @@ export type SpriteOptions = Omit<UnlitOptions, "instanced" | "instanceColors" |
|
|
|
945
951
|
* soft puff, 0.5 a hard-edged disc), the per-entry `uFalloff`. Needs
|
|
946
952
|
* `shape`. */
|
|
947
953
|
falloff?: number
|
|
954
|
+
/**
|
|
955
|
+
* The screen-size clamp's floor, pixels (default 0: off): however far
|
|
956
|
+
* the camera is, the quad draws at least this big on its smaller axis,
|
|
957
|
+
* scaled uniformly about its center so its aspect holds - a marker or
|
|
958
|
+
* handle that stays findable at distance. The vertex stage scales by
|
|
959
|
+
* the pixels one world unit covers at the quad's depth (the depth
|
|
960
|
+
* alone, no foreshortening), so it is right under perspective and
|
|
961
|
+
* orthographic cameras alike, and `pick` tests the drawn quad. The
|
|
962
|
+
* per-entry `uScreenPx.x`; a per-mesh `params` may override the pair.
|
|
963
|
+
* A floored sprite's mesh turns its frustum culling off (its quad can
|
|
964
|
+
* exceed its box far away). Finite, >= 0.
|
|
965
|
+
*/
|
|
966
|
+
minScreenPx?: number
|
|
967
|
+
/**
|
|
968
|
+
* The clamp's ceiling, pixels (default 0: off): the quad draws at most
|
|
969
|
+
* this big on its smaller axis however close the camera comes. Equal
|
|
970
|
+
* to `minScreenPx` it is a constant screen size, Three's
|
|
971
|
+
* `sizeAttenuation: false`, and `scale` then carries the aspect alone.
|
|
972
|
+
* The per-entry `uScreenPx.y`. Never below a floor that is on.
|
|
973
|
+
*/
|
|
974
|
+
maxScreenPx?: number
|
|
948
975
|
}
|
|
949
976
|
|
|
950
977
|
// The radial shape as a tier-2 surface on the unlit fragment: the
|
|
@@ -962,11 +989,29 @@ const RADIAL_SURFACE = glsl`
|
|
|
962
989
|
|
|
963
990
|
// The billboard vertex stages: the unit quad's corners placed along the
|
|
964
991
|
// camera axes at the mesh's world position, with the quad's size read
|
|
965
|
-
// off uModel's column lengths so `scale` sizes the sprite like any mesh
|
|
966
|
-
//
|
|
967
|
-
//
|
|
968
|
-
//
|
|
969
|
-
// the
|
|
992
|
+
// off uModel's column lengths so `scale` sizes the sprite like any mesh,
|
|
993
|
+
// then clamped on screen (core's screenSizeScale over uScreenPx, the
|
|
994
|
+
// material's minScreenPx/maxScreenPx; identity at [0, 0]). The rotation
|
|
995
|
+
// part of uModel is otherwise ignored (the camera decides the facing).
|
|
996
|
+
// Fixed-y takes the yaw from the camera-to-center direction flattened
|
|
997
|
+
// onto XZ; straight above or below there is no yaw to take, so the quad
|
|
998
|
+
// falls back to facing +z rather than dividing by zero. The scene's pick
|
|
999
|
+
// rebuilds exactly this quad for a clamped sprite (scene.ts
|
|
1000
|
+
// clampedSpriteHit); the two must agree.
|
|
1001
|
+
//
|
|
1002
|
+
// The pixels one world unit covers at the quad's depth: the center and
|
|
1003
|
+
// the center one camera-up unit away, projected, are half a viewport
|
|
1004
|
+
// height apart per NDC unit. Depth alone, no foreshortening, from the
|
|
1005
|
+
// shared uniforms only, and the same under an orthographic camera (clip
|
|
1006
|
+
// w is 1 there).
|
|
1007
|
+
const SPRITE_PX_PER_UNIT = glsl`
|
|
1008
|
+
float spritePxPerUnit(vec3 center) {
|
|
1009
|
+
vec4 c = uViewProj * vec4(center, 1.0);
|
|
1010
|
+
vec4 u = uViewProj * vec4(center + uCamUp, 1.0);
|
|
1011
|
+
return abs(u.y / u.w - c.y / c.w) * 0.5 * uViewport.y;
|
|
1012
|
+
}
|
|
1013
|
+
`
|
|
1014
|
+
|
|
970
1015
|
const SPRITE_VERTEX_SRC = glsl`
|
|
971
1016
|
in vec3 aPos;
|
|
972
1017
|
in vec2 aUV;
|
|
@@ -976,10 +1021,15 @@ const SPRITE_VERTEX_SRC = glsl`
|
|
|
976
1021
|
uniform mat4 uViewProj;
|
|
977
1022
|
uniform vec3 uCamRight;
|
|
978
1023
|
uniform vec3 uCamUp;
|
|
1024
|
+
uniform vec2 uViewport;
|
|
1025
|
+
uniform vec2 uScreenPx;
|
|
1026
|
+
${SCREEN_SIZE_GLSL}
|
|
1027
|
+
${SPRITE_PX_PER_UNIT}
|
|
979
1028
|
|
|
980
1029
|
void main() {
|
|
981
1030
|
vec3 center = uModel[3].xyz;
|
|
982
1031
|
vec2 size = vec2(length(uModel[0].xyz), length(uModel[1].xyz));
|
|
1032
|
+
size *= screenSizeScale(size, spritePxPerUnit(center), uScreenPx);
|
|
983
1033
|
vec3 world = center + uCamRight * (aPos.x * size.x) + uCamUp * (aPos.y * size.y);
|
|
984
1034
|
vWorldPos = world;
|
|
985
1035
|
gl_Position = uViewProj * vec4(world, 1.0);
|
|
@@ -995,10 +1045,16 @@ const SPRITE_FIXED_Y_VERTEX_SRC = glsl`
|
|
|
995
1045
|
uniform mat4 uModel;
|
|
996
1046
|
uniform mat4 uViewProj;
|
|
997
1047
|
uniform vec3 uCamPos;
|
|
1048
|
+
uniform vec3 uCamUp;
|
|
1049
|
+
uniform vec2 uViewport;
|
|
1050
|
+
uniform vec2 uScreenPx;
|
|
1051
|
+
${SCREEN_SIZE_GLSL}
|
|
1052
|
+
${SPRITE_PX_PER_UNIT}
|
|
998
1053
|
|
|
999
1054
|
void main() {
|
|
1000
1055
|
vec3 center = uModel[3].xyz;
|
|
1001
1056
|
vec2 size = vec2(length(uModel[0].xyz), length(uModel[1].xyz));
|
|
1057
|
+
size *= screenSizeScale(size, spritePxPerUnit(center), uScreenPx);
|
|
1002
1058
|
vec3 toCam = uCamPos - center;
|
|
1003
1059
|
toCam.y = 0.0;
|
|
1004
1060
|
float len = length(toCam);
|
|
@@ -1021,6 +1077,11 @@ let spriteClasses = new Map<string, ShaderMaterialClass>()
|
|
|
1021
1077
|
* plane. Unlike unlit, `transparent` defaults to TRUE - sprites are cutouts
|
|
1022
1078
|
* far more often than not (Three's SpriteMaterial default) - pass false
|
|
1023
1079
|
* for an opaque one. Culling is off: a camera-facing quad has no back.
|
|
1080
|
+
* `minScreenPx`/`maxScreenPx` clamp the quad's size on screen (the
|
|
1081
|
+
* per-entry uScreenPx, so sprites sharing the material share the clamp
|
|
1082
|
+
* and one draw; a per-mesh `params` override takes a mesh its own); the
|
|
1083
|
+
* scene's pick tests the drawn quad, and a floored sprite's mesh is never
|
|
1084
|
+
* frustum-culled.
|
|
1024
1085
|
*/
|
|
1025
1086
|
export function sprite(opts: SpriteOptions = {}): Material {
|
|
1026
1087
|
// Excluded by the type and checked for a JS caller: the billboard stage
|
|
@@ -1039,6 +1100,9 @@ export function sprite(opts: SpriteOptions = {}): Material {
|
|
|
1039
1100
|
if (opts.shape !== undefined && !radial) throw new Error('sprite: shape must be "radial", got ' + opts.shape)
|
|
1040
1101
|
if (opts.falloff !== undefined && !radial) throw new Error("sprite: falloff needs shape")
|
|
1041
1102
|
if (opts.falloff !== undefined && !(Number.isFinite(opts.falloff) && opts.falloff > 0)) throw new Error("sprite: falloff must be a positive number, got " + opts.falloff)
|
|
1103
|
+
let minScreenPx = opts.minScreenPx ?? 0
|
|
1104
|
+
let maxScreenPx = opts.maxScreenPx ?? 0
|
|
1105
|
+
checkScreenSize("sprite", minScreenPx, maxScreenPx)
|
|
1042
1106
|
let key = [map, transparent, blend, fixedY, fog, radial].join("|")
|
|
1043
1107
|
let cls = spriteClasses.get(key)
|
|
1044
1108
|
if (cls === undefined) {
|
|
@@ -1058,9 +1122,11 @@ export function sprite(opts: SpriteOptions = {}): Material {
|
|
|
1058
1122
|
})
|
|
1059
1123
|
spriteClasses.set(key, cls)
|
|
1060
1124
|
}
|
|
1061
|
-
let params: ShaderParams = { uColor }
|
|
1125
|
+
let params: ShaderParams = { uColor, uScreenPx: [minScreenPx, maxScreenPx] }
|
|
1062
1126
|
if (radial) params.uFalloff = opts.falloff ?? 1
|
|
1063
|
-
|
|
1127
|
+
let material = cls.instance({ params, textures: map ? { uMap: opts.map! } : undefined })
|
|
1128
|
+
material.billboard = fixedY ? "fixed-y" : "full"
|
|
1129
|
+
return material
|
|
1064
1130
|
}
|
|
1065
1131
|
|
|
1066
1132
|
/** The attributes `material` reads that `layout` does not carry (name and
|
package/src/mesh.ts
CHANGED
|
@@ -15,9 +15,9 @@ import type { Material } from "./material.ts"
|
|
|
15
15
|
import type { TransformUpdate } from "./math.ts"
|
|
16
16
|
import * as spatial from "flux:spatial"
|
|
17
17
|
import { activateMorph, afterFree, createWeightsTexture, enterScene, leaveScene, makeNode, rebindMorph, remove, resetMorph, setTransform } from "./node.ts"
|
|
18
|
-
import type { SceneNode } from "./node.ts"
|
|
18
|
+
import type { HoverHandlers, SceneNode } from "./node.ts"
|
|
19
19
|
|
|
20
|
-
export type Mesh = SceneNode & {
|
|
20
|
+
export type Mesh = SceneNode & HoverHandlers & {
|
|
21
21
|
kind: "mesh"
|
|
22
22
|
geometry: Geometry
|
|
23
23
|
material: Material
|
|
@@ -176,9 +176,10 @@ export type InstanceSlots = { slots: (InstanceNode | null)[]; free: number[] }
|
|
|
176
176
|
* attribute list the material declared for it (any vertex format,
|
|
177
177
|
* tightly packed), `data` the JS mirror of `capacity` records - a
|
|
178
178
|
* Float32Array over an all-float layout, a Uint8Array otherwise
|
|
179
|
-
* (vertexView's rule
|
|
180
|
-
*
|
|
181
|
-
*
|
|
179
|
+
* (vertexView's rule; `records(mesh, stream)` hands it out for bulk
|
|
180
|
+
* writes) - and `buffer` the mesh-owned GPU copy. Writes go through
|
|
181
|
+
* instanceAttribute's accessor (or setInstanceStyle / setRecords, which
|
|
182
|
+
* encode for you) or the mirror itself, land in the mirror, and publish as ONE
|
|
182
183
|
* coalesced buffer write per stream at the scene's next sync over the
|
|
183
184
|
* `dirty` byte range - a frame-rate path, like setTransform. Growth
|
|
184
185
|
* replaces `data` and `buffer` (the accessor follows; a held `data`
|
|
@@ -226,7 +227,7 @@ export type RecordMesh = Mesh & { _instances: MeshInstances & { matrix: null; no
|
|
|
226
227
|
* Slot-bound to its mesh: created by addInstance, destroyed by
|
|
227
228
|
* destroy; the generic add/remove reject it.
|
|
228
229
|
*/
|
|
229
|
-
export type InstanceNode = SceneNode & {
|
|
230
|
+
export type InstanceNode = SceneNode & HoverHandlers & {
|
|
230
231
|
kind: "instance"
|
|
231
232
|
/** The owning mesh; null once removed (the handle is inert). */
|
|
232
233
|
mesh: InstancedMesh | null
|
|
@@ -287,9 +288,35 @@ export function createSprite(material: Material): Mesh {
|
|
|
287
288
|
if (spriteQuad === undefined) spriteQuad = plane({ label: "sprite" })
|
|
288
289
|
let mesh = createMesh(spriteQuad, material)
|
|
289
290
|
mesh._sprite = true
|
|
291
|
+
uncullFlooredSprite(mesh)
|
|
290
292
|
return mesh
|
|
291
293
|
}
|
|
292
294
|
|
|
295
|
+
/**
|
|
296
|
+
* The screen-size clamp a sprite mesh draws with, `[minPx, maxPx]` (0 =
|
|
297
|
+
* off): the mesh's own `uScreenPx` param over its material's (a sprite
|
|
298
|
+
* material always carries one). null when neither bound is on, or the
|
|
299
|
+
* material is not a sprite material.
|
|
300
|
+
*/
|
|
301
|
+
export function spriteScreenPx(mesh: Mesh): [number, number] | null {
|
|
302
|
+
let px = mesh._params?.uScreenPx ?? mesh.material.params.uScreenPx
|
|
303
|
+
if (px === undefined || typeof px === "number") return null
|
|
304
|
+
let min = px[0] ?? 0
|
|
305
|
+
let max = px[1] ?? 0
|
|
306
|
+
return min > 0 || max > 0 ? [min, max] : null
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// A floored sprite draws larger than its unit box far from the camera,
|
|
310
|
+
// so the frustum test would hide it while its quad is on screen: a floor
|
|
311
|
+
// coming on (at creation, a material swap, a params override) switches
|
|
312
|
+
// the mesh's culling off. Only ever off - an app that wants it back says
|
|
313
|
+
// so with setCulling.
|
|
314
|
+
function uncullFlooredSprite(mesh: Mesh): void {
|
|
315
|
+
if (!mesh._sprite || !mesh.frustumCulled) return
|
|
316
|
+
let px = spriteScreenPx(mesh)
|
|
317
|
+
if (px !== null && px[0] > 0) setCulling(mesh, { frustumCulled: false })
|
|
318
|
+
}
|
|
319
|
+
|
|
293
320
|
/** The local box picking and sorting work from: explicit instance bounds
|
|
294
321
|
* when the mesh is instanced (null without them - no leaf, no hits), the
|
|
295
322
|
* unit box for a sprite, the geometry's own bounds otherwise. */
|
|
@@ -866,6 +893,30 @@ export function instanceAttribute(mesh: InstancedMesh | RecordMesh, name: string
|
|
|
866
893
|
return null
|
|
867
894
|
}
|
|
868
895
|
|
|
896
|
+
/**
|
|
897
|
+
* The mirror of record stream `stream` (default the first: a record
|
|
898
|
+
* mesh's records, an instanced mesh's style records) - the raw power
|
|
899
|
+
* path for a population written in bulk: `capacity` records at the
|
|
900
|
+
* layout's stride, a Float32Array over an all-float layout, bytes
|
|
901
|
+
* otherwise (write those through instanceAttribute's codecs, or a typed
|
|
902
|
+
* view of your own over the format). Write it, then updateRecords the
|
|
903
|
+
* range. Read it AT USE TIME: growth (setRecords past capacity,
|
|
904
|
+
* addInstance past it) replaces the mirror, and a hoisted view becomes a
|
|
905
|
+
* dead copy whose writes publish nothing. Throws on a transferred
|
|
906
|
+
* population (no mirror) and a disposed one. @solidrt/2d's
|
|
907
|
+
* records(layer).
|
|
908
|
+
*/
|
|
909
|
+
export function records(mesh: InstancedMesh | RecordMesh, stream = 0): ArrayBufferView {
|
|
910
|
+
let inst: MeshInstances | null = mesh._instances
|
|
911
|
+
if (inst === null) throw new Error("records: the mesh's instances are disposed")
|
|
912
|
+
if (inst.transfer) throw new Error("records: the records were transferred at creation (transfer: true) - there is no mirror to read or write")
|
|
913
|
+
let s = inst.streams[stream]
|
|
914
|
+
if (!Number.isInteger(stream) || s === undefined) {
|
|
915
|
+
throw new Error("records: stream " + stream + " is out of range; the mesh has record streams 0.." + (inst.streams.length - 1))
|
|
916
|
+
}
|
|
917
|
+
return s.data
|
|
918
|
+
}
|
|
919
|
+
|
|
869
920
|
/** Options of `updateRecords`: the stream (default 0) and the record
|
|
870
921
|
* range (default the whole stream, capacity wide). */
|
|
871
922
|
export type UpdateRecordsOptions = { stream?: number; first?: number; count?: number }
|
|
@@ -1336,6 +1387,7 @@ export function setDrawRange(mesh: Mesh, first: number, count?: number): void {
|
|
|
1336
1387
|
export function setMaterial(mesh: Mesh, material: Material): void {
|
|
1337
1388
|
if (mesh.material === material) return
|
|
1338
1389
|
mesh.material = material
|
|
1390
|
+
uncullFlooredSprite(mesh)
|
|
1339
1391
|
rebuildEntry(mesh)
|
|
1340
1392
|
}
|
|
1341
1393
|
|
|
@@ -1359,5 +1411,6 @@ function rebuildEntry(mesh: Mesh): void {
|
|
|
1359
1411
|
export function setMeshParams(mesh: Mesh, params: ShaderParams): void {
|
|
1360
1412
|
if (mesh._params === null) mesh._params = {}
|
|
1361
1413
|
Object.assign(mesh._params, params)
|
|
1414
|
+
if (params.uScreenPx !== undefined) uncullFlooredSprite(mesh)
|
|
1362
1415
|
mesh._scene?._setParams(mesh, params)
|
|
1363
1416
|
}
|
package/src/node.ts
CHANGED
|
@@ -180,6 +180,15 @@ export type SceneHooks = {
|
|
|
180
180
|
* cross-fade band. Re-applied whenever the node enters a scene. */
|
|
181
181
|
export type LodConfig = { levels: { node: SceneNode | null; size: number }[]; fade: number }
|
|
182
182
|
|
|
183
|
+
/** The hover pair, on the nodes a ray can strike (Mesh, InstanceNode):
|
|
184
|
+
* enter/leave fire on the struck node alone and never bubble, so a group
|
|
185
|
+
* or a light carries none - the 2d package's Sprite against its
|
|
186
|
+
* SpriteGroup. */
|
|
187
|
+
export type HoverHandlers = {
|
|
188
|
+
onPointerEnter?: (event: NodePointerEvent) => void
|
|
189
|
+
onPointerLeave?: (event: NodePointerEvent) => void
|
|
190
|
+
}
|
|
191
|
+
|
|
183
192
|
export type SceneNode = {
|
|
184
193
|
kind: "group" | "mesh" | "light" | "instance"
|
|
185
194
|
parent: SceneNode | null
|
|
@@ -198,14 +207,13 @@ export type SceneNode = {
|
|
|
198
207
|
* nearest hit - the struck instance of an instanced mesh, else the mesh
|
|
199
208
|
* - bubble through its ancestors and end at the scene's listeners
|
|
200
209
|
* (stopPropagation stops the walk, and a stopped down claims the whole
|
|
201
|
-
* press); enter/leave
|
|
202
|
-
* the element showing the scene
|
|
210
|
+
* press); enter/leave are the struck node's own (HoverHandlers, on Mesh
|
|
211
|
+
* and InstanceNode). Events flow once the element showing the scene
|
|
212
|
+
* carries `scene.handlers`. */
|
|
203
213
|
onPointerDown?: (event: NodePointerEvent) => void
|
|
204
214
|
onPointerMove?: (event: NodePointerEvent) => void
|
|
205
215
|
onPointerUp?: (event: NodePointerEvent) => void
|
|
206
216
|
onPointerCancel?: (event: NodePointerEvent) => void
|
|
207
|
-
onPointerEnter?: (event: NodePointerEvent) => void
|
|
208
|
-
onPointerLeave?: (event: NodePointerEvent) => void
|
|
209
217
|
/** The wheel over the node (NodeWheelEvent: `deltaX`/`deltaY`), bubbling
|
|
210
218
|
* like down/move/up. */
|
|
211
219
|
onWheel?: (event: NodeWheelEvent) => void
|
package/src/orbit.ts
CHANGED
|
@@ -101,7 +101,9 @@
|
|
|
101
101
|
// at once (the next paint carries the new camera, no frame loop needed
|
|
102
102
|
// for a drag), and update(dt) integrates the rates, the auto-orbit, the
|
|
103
103
|
// motion, the follow, the lanes and the occlusion return from the app's
|
|
104
|
-
// own onFrame
|
|
104
|
+
// own onFrame (onBeforeRender when it follows something the frame moves,
|
|
105
|
+
// so it sees the frame's final pose) - the control registers no frame
|
|
106
|
+
// loop of its own. Only
|
|
105
107
|
// `orbiting` and `active` are reactive: slow UI state a HUD reads, and
|
|
106
108
|
// the frame-loop gate. update() returns whether the camera changed since
|
|
107
109
|
// the previous update (pushes included), so per-frame dependents
|
|
@@ -303,8 +305,10 @@ export type OrbitCamera = {
|
|
|
303
305
|
active(): boolean
|
|
304
306
|
/** Integrate the axis rates, the auto-orbit, the motion, the follow,
|
|
305
307
|
* the lanes and the occlusion return over dt seconds and push any
|
|
306
|
-
* camera change. Call from onFrame
|
|
307
|
-
*
|
|
308
|
+
* camera change. Call from onFrame - or from onBeforeRender when the
|
|
309
|
+
* control follows something the frame moves, so it reads that frame's
|
|
310
|
+
* final pose; returns whether the camera changed since the previous
|
|
311
|
+
* update (nudges and verbs included). */
|
|
308
312
|
update(dt: number): boolean
|
|
309
313
|
/** The input abstraction: `rotate` (vec2, element heights of drag /
|
|
310
314
|
* turns per second), `zoom` (axis, octaves, positive in), `pan` (vec2,
|
|
@@ -325,9 +329,12 @@ export type OrbitCamera = {
|
|
|
325
329
|
* camera's right and up (or the ground plane, `panPlane`), through
|
|
326
330
|
* `clampPose`, and push. */
|
|
327
331
|
panBy(right: number, up: number): void
|
|
328
|
-
/** Re-seat the pivot
|
|
329
|
-
*
|
|
330
|
-
*
|
|
332
|
+
/** Re-seat the pivot (the orbit target, the point the camera turns
|
|
333
|
+
* about - the 2d camera's pose point, which that control places ON
|
|
334
|
+
* SCREEN by its `pivot` fraction and moves in world space by its pose)
|
|
335
|
+
* at a world point without moving the picture: the point is projected
|
|
336
|
+
* onto the view axis and becomes the target at that depth. Points at
|
|
337
|
+
* or behind the eye are ignored. */
|
|
331
338
|
setPivot(point: Vec3): void
|
|
332
339
|
/** Re-seat the pivot at a world point EXACTLY, without moving the
|
|
333
340
|
* picture: the target becomes the point, the distance its depth, and
|
package/src/scene-pointer.ts
CHANGED
|
@@ -116,7 +116,7 @@ export function makePointerInput(deps: PointerInputDeps): PointerInput {
|
|
|
116
116
|
let sameHit = (hit: Hit, target: Target): boolean => hit.mesh === target.mesh && (hit.instance ?? null) === target.instance
|
|
117
117
|
let sameTarget = (a: Target | null, b: Target | null): boolean => (a === null ? b === null : b !== null && a.mesh === b.mesh && a.instance === b.instance)
|
|
118
118
|
// The node the walk starts at, and the one enter/leave fire on.
|
|
119
|
-
let nodeOf = (target: Target):
|
|
119
|
+
let nodeOf = (target: Target): Mesh | InstanceNode => target.instance ?? target.mesh
|
|
120
120
|
// The captured target's own hit, if the ray still strikes it.
|
|
121
121
|
let hitOn = (target: Target | null, x: number, y: number): Hit | null => {
|
|
122
122
|
if (target === null) return null
|
package/src/scene-shadows.ts
CHANGED
|
@@ -99,7 +99,7 @@ export type ShadowSystemDeps<V extends ShadowView> = {
|
|
|
99
99
|
disposeView(view: V): void
|
|
100
100
|
/** The light set changed shape: the scene owes a writeLights. */
|
|
101
101
|
markLightsDirty(): void
|
|
102
|
-
/** Schedule the scene's sync
|
|
102
|
+
/** Schedule the scene's sync. */
|
|
103
103
|
schedule(): void
|
|
104
104
|
}
|
|
105
105
|
|
package/src/scene.ts
CHANGED
|
@@ -13,15 +13,17 @@
|
|
|
13
13
|
// coverage (stored and skipped until a declaring material arrives), so no
|
|
14
14
|
// bookkeeping tracks who reads them. scene.setParams merges app-owned
|
|
15
15
|
// names into the same set.
|
|
16
|
-
// Mutations batch
|
|
17
|
-
//
|
|
16
|
+
// Mutations batch, so a burst of writes (a whole subtree moved, many
|
|
17
|
+
// effects in one flush) syncs once: in the publish pass of core's
|
|
18
|
+
// onBeforeRender, ahead of the frame's paint, or at a microtask for a
|
|
19
|
+
// write made outside a frame.
|
|
18
20
|
//
|
|
19
21
|
// Rendering itself belongs to the runtime: the target is an ordinary
|
|
20
22
|
// `render: "auto"` draw target that re-renders when its entries change, so
|
|
21
23
|
// a static scene costs zero passes and this module registers no frame
|
|
22
24
|
// loop. Continuous animation is the app's onFrame writing transforms -
|
|
23
|
-
// each write lands in the core, the
|
|
24
|
-
// renders once.
|
|
25
|
+
// each write lands in the core, the sync ahead of the paint flushes it,
|
|
26
|
+
// and the frame renders once.
|
|
25
27
|
//
|
|
26
28
|
// Still in JS this stage (see okf/backlog/spatial-core.md): the picking
|
|
27
29
|
// broadphase and its leaves, the transparent sort's centers and the light
|
|
@@ -36,11 +38,11 @@
|
|
|
36
38
|
// and pointer subsystems are scene-shadows.ts / scene-pointer.ts,
|
|
37
39
|
// built here with the scene's seams as their deps.
|
|
38
40
|
|
|
39
|
-
import { addDraw, createCubeDrawTarget, createDrawTarget, depthTexture, destroyProgram, destroyRenderPipeline, destroyTexture, removeDraw, renderTarget, setDrawBuffers, setDrawParams, setDrawRange, setDrawTextures, setTargetParams, setTargetRect, setTargetSize, setTargetTextures } from "@solidrt/core/gpu"
|
|
41
|
+
import { addDraw, createCubeDrawTarget, createDrawTarget, depthTexture, destroyProgram, destroyRenderPipeline, destroyTexture, removeDraw, renderTarget, screenSizeScale, setDrawBuffers, setDrawParams, setDrawRange, setDrawTextures, setTargetParams, setTargetRect, setTargetSize, setTargetTextures } from "@solidrt/core/gpu"
|
|
40
42
|
import * as spatial from "flux:spatial"
|
|
41
43
|
import type { BindDrawOptions, Impact as CoreImpact, NodeId, QueryFilter } from "flux:spatial"
|
|
42
44
|
import type { BufferId, DrawId, FilterMode, InstanceOrder, ProgramId, RenderPipelineId, ShaderParams, TextureBindings, TextureId, WrapMode } from "@solidrt/core/gpu"
|
|
43
|
-
import { getOwner, onCleanup } from "@solidrt/core"
|
|
45
|
+
import { getOwner, onBeforeRender, onCleanup, runWithOwner } from "@solidrt/core"
|
|
44
46
|
import type { PointerEvent as ElementPointerEvent, WheelEvent as ElementWheelEvent } from "@solidrt/core"
|
|
45
47
|
import { copy, mat4, transformPoint } from "./math.ts"
|
|
46
48
|
import { linearColor, premultipliedColor } from "./color.ts"
|
|
@@ -61,9 +63,9 @@ import type { EnvironmentOptions, Prefilter } from "./environment.ts"
|
|
|
61
63
|
|
|
62
64
|
export type { EnvironmentOptions } from "./environment.ts"
|
|
63
65
|
import type { Material } from "./material.ts"
|
|
64
|
-
import { activateMorph, fillTransform, freeLeaving, leaveScene, makeNode, setTransition } from "./node.ts"
|
|
66
|
+
import { activateMorph, fillTransform, freeLeaving, leaveScene, makeNode, setTransition, worldInto } from "./node.ts"
|
|
65
67
|
import type { SceneHooks, SceneNode, ScenePointerListener } from "./node.ts"
|
|
66
|
-
import { checkInstancePairing, checkMask, checkOrderPairing, drawCount, instanceBinding, localBounds, markLiveRecords, publishRecords } from "./mesh.ts"
|
|
68
|
+
import { checkInstancePairing, checkMask, checkOrderPairing, drawCount, instanceBinding, localBounds, markLiveRecords, publishRecords, spriteScreenPx } from "./mesh.ts"
|
|
67
69
|
import type { InstancedMesh, InstanceNode, Mesh, MeshInstances, ResolvedInstanceOrder } from "./mesh.ts"
|
|
68
70
|
import type { CastingLight, Light } from "./light.ts"
|
|
69
71
|
|
|
@@ -470,7 +472,7 @@ export type SceneOptions = {
|
|
|
470
472
|
* { stagger })` at creation: every node added straight under the root
|
|
471
473
|
* that enters or leaves in one frame is spaced by `index * stagger`,
|
|
472
474
|
* the whole-scene form of a Group's stagger (nested groups declaring
|
|
473
|
-
* their own win for what is under them). */
|
|
475
|
+
* their own win for what is under them). See setStagger. */
|
|
474
476
|
stagger?: number
|
|
475
477
|
/** `autoFree: false` opts out of owner-scoped auto-dispose (then call dispose yourself). */
|
|
476
478
|
autoFree?: boolean
|
|
@@ -482,7 +484,8 @@ export type SceneOptions = {
|
|
|
482
484
|
/** Where a per-scene budget error goes: the light cap and the
|
|
483
485
|
* shadow-slot budget are checked over the settled set at the sync and
|
|
484
486
|
* reported once per change of the set, the scene rendering on with
|
|
485
|
-
* what fits. Without a handler the sync throws it,
|
|
487
|
+
* what fits. Without a handler the sync throws it, and it is only
|
|
488
|
+
* reported (logged by the frame's publish pass, or uncaught from the
|
|
486
489
|
* microtask): the `<Scene>` component hands one in and rethrows inside
|
|
487
490
|
* the tree, so the app's error boundary shows it. */
|
|
488
491
|
onError?: (error: Error) => void
|
|
@@ -827,6 +830,9 @@ export type Scene = {
|
|
|
827
830
|
* measured projected size, for the scene, its views and its shadow
|
|
828
831
|
* tiles. Below 1 switches to far levels sooner. */
|
|
829
832
|
setLodBias(bias: number): void
|
|
833
|
+
/** Stagger (ms) on the scene's root node, live (SceneOptions.stagger
|
|
834
|
+
* is its creation form): null removes it. The 2d layer's setStagger. */
|
|
835
|
+
setStagger(ms: number | null): void
|
|
830
836
|
/** The level a LOD group draws in the scene's own render (the thing to
|
|
831
837
|
* read while tuning thresholds): the index into its levels, nearest
|
|
832
838
|
* first, `levels.length` when culled past the last one, null before
|
|
@@ -1583,8 +1589,9 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1583
1589
|
let inst = mesh._instances
|
|
1584
1590
|
let bufs = mesh._buffers!
|
|
1585
1591
|
// The entry starts switched off: it has no world matrix yet - the walk
|
|
1586
|
-
// in sync() computes one - and _schedule() defers that to
|
|
1587
|
-
//
|
|
1592
|
+
// in sync() computes one - and _schedule() defers that to the sync
|
|
1593
|
+
// (the frame's publish pass, or a microtask), so added live it would
|
|
1594
|
+
// draw at the seeded identity until then. The
|
|
1588
1595
|
// mismatch branch in sync() turns it on in the same pass that writes
|
|
1589
1596
|
// uModel.
|
|
1590
1597
|
let morph = morphEntry(mesh.material, mesh)
|
|
@@ -2270,6 +2277,21 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
2270
2277
|
// first frame.
|
|
2271
2278
|
lightsDirty = true
|
|
2272
2279
|
hooks._schedule()
|
|
2280
|
+
// The publish pass of every frame runs the pending sync ahead of the
|
|
2281
|
+
// paint, so a write made anywhere in the frame's JS is in that frame's
|
|
2282
|
+
// picture: the count, the buffers and the bytes of a record write land
|
|
2283
|
+
// together, and nothing shows a frame late. The microtask stays for
|
|
2284
|
+
// writes made outside a frame. Registered outside any owner: the scene's
|
|
2285
|
+
// own dispose unhooks it (an autoFree: false scene outlives the owner it
|
|
2286
|
+
// was created in).
|
|
2287
|
+
let unhook = runWithOwner(null, () =>
|
|
2288
|
+
onBeforeRender(
|
|
2289
|
+
() => {
|
|
2290
|
+
if (scheduled) sync()
|
|
2291
|
+
},
|
|
2292
|
+
{ publish: true },
|
|
2293
|
+
),
|
|
2294
|
+
)
|
|
2273
2295
|
|
|
2274
2296
|
// The scene's root listeners (scene.listen): the last stop of the
|
|
2275
2297
|
// pointer walk scene-pointer.ts runs behind scene.handlers.
|
|
@@ -2303,6 +2325,101 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
2303
2325
|
return hits
|
|
2304
2326
|
}
|
|
2305
2327
|
|
|
2328
|
+
// Sprites under a screen-size clamp (the sprite material's
|
|
2329
|
+
// minScreenPx/maxScreenPx) draw at a size the index does not know: the
|
|
2330
|
+
// vertex stage scales the quad by the pixels one unit covers at its
|
|
2331
|
+
// depth under THE CAMERA DRAWING IT. So a pick - which has a camera -
|
|
2332
|
+
// replaces the ray's hits on them with an exact ray-quad test at the
|
|
2333
|
+
// drawn size: the quad's basis as the stage builds it (camera right and
|
|
2334
|
+
// up, or a fixed-y sprite's yaw basis), the scale from core's
|
|
2335
|
+
// screenSizeScale over the stage's own pixels-per-unit derivation. The
|
|
2336
|
+
// world-space raycast keeps the index's answer. Reads pickOrigin/pickDir
|
|
2337
|
+
// as pixelRayOf left them; `th` is the target's pixel height the stage
|
|
2338
|
+
// sees as uViewport.y.
|
|
2339
|
+
let spriteWorld = mat4()
|
|
2340
|
+
let spriteClip: Vec4 = [0, 0, 0, 0]
|
|
2341
|
+
let spriteProbe: Vec3 = [0, 0, 0]
|
|
2342
|
+
let shown = (node: SceneNode | null): boolean => {
|
|
2343
|
+
for (; node !== null; node = node.parent) if (!node.visible) return false
|
|
2344
|
+
return true
|
|
2345
|
+
}
|
|
2346
|
+
let clampedSpriteHit = (mesh: Mesh, px: [number, number], cam: Camera, th: number): Hit | null => {
|
|
2347
|
+
let m = worldInto(spriteWorld, mesh)
|
|
2348
|
+
let cx = m[12], cy = m[13], cz = m[14]
|
|
2349
|
+
let w = Math.hypot(m[0], m[1], m[2])
|
|
2350
|
+
let h = Math.hypot(m[4], m[5], m[6])
|
|
2351
|
+
let v = cam.view
|
|
2352
|
+
let upX = v[1], upY = v[5], upZ = v[9]
|
|
2353
|
+
spriteProbe[0] = cx
|
|
2354
|
+
spriteProbe[1] = cy
|
|
2355
|
+
spriteProbe[2] = cz
|
|
2356
|
+
transformPoint(spriteClip, cam.viewProj, spriteProbe)
|
|
2357
|
+
let cw = spriteClip[3]
|
|
2358
|
+
if (!(cw > 1e-6)) return null
|
|
2359
|
+
let centerNdcY = spriteClip[1] / cw
|
|
2360
|
+
spriteProbe[0] = cx + upX
|
|
2361
|
+
spriteProbe[1] = cy + upY
|
|
2362
|
+
spriteProbe[2] = cz + upZ
|
|
2363
|
+
transformPoint(spriteClip, cam.viewProj, spriteProbe)
|
|
2364
|
+
let pxPerUnit = Math.abs(spriteClip[1] / spriteClip[3] - centerNdcY) * 0.5 * th
|
|
2365
|
+
let s = screenSizeScale(w, h, pxPerUnit, px[0], px[1])
|
|
2366
|
+
let halfW = (w * s) / 2
|
|
2367
|
+
let halfH = (h * s) / 2
|
|
2368
|
+
let rx: number, ry: number, rz: number
|
|
2369
|
+
if (mesh.material.billboard === "fixed-y") {
|
|
2370
|
+
let tx = cam.eye[0] - cx
|
|
2371
|
+
let tz = cam.eye[2] - cz
|
|
2372
|
+
let len = Math.hypot(tx, tz)
|
|
2373
|
+
if (len > 1e-6) {
|
|
2374
|
+
rx = tz / len
|
|
2375
|
+
ry = 0
|
|
2376
|
+
rz = -tx / len
|
|
2377
|
+
} else {
|
|
2378
|
+
rx = 1
|
|
2379
|
+
ry = 0
|
|
2380
|
+
rz = 0
|
|
2381
|
+
}
|
|
2382
|
+
upX = 0
|
|
2383
|
+
upY = 1
|
|
2384
|
+
upZ = 0
|
|
2385
|
+
} else {
|
|
2386
|
+
rx = v[0]
|
|
2387
|
+
ry = v[4]
|
|
2388
|
+
rz = v[8]
|
|
2389
|
+
}
|
|
2390
|
+
// The quad's normal, right x up; the ray meets the plane at t.
|
|
2391
|
+
let nx = ry * upZ - rz * upY
|
|
2392
|
+
let ny = rz * upX - rx * upZ
|
|
2393
|
+
let nz = rx * upY - ry * upX
|
|
2394
|
+
let ox = pickOrigin[0], oy = pickOrigin[1], oz = pickOrigin[2]
|
|
2395
|
+
let dx = pickDir[0], dy = pickDir[1], dz = pickDir[2]
|
|
2396
|
+
let denom = dx * nx + dy * ny + dz * nz
|
|
2397
|
+
if (Math.abs(denom) < 1e-12) return null
|
|
2398
|
+
let t = ((cx - ox) * nx + (cy - oy) * ny + (cz - oz) * nz) / denom
|
|
2399
|
+
if (t <= 0) return null
|
|
2400
|
+
let hx = ox + t * dx, hy = oy + t * dy, hz = oz + t * dz
|
|
2401
|
+
let lx = (hx - cx) * rx + (hy - cy) * ry + (hz - cz) * rz
|
|
2402
|
+
let ly = (hx - cx) * upX + (hy - cy) * upY + (hz - cz) * upZ
|
|
2403
|
+
if (Math.abs(lx) > halfW || Math.abs(ly) > halfH) return null
|
|
2404
|
+
// Facing the ray, like every core hit; distance along the normalized ray.
|
|
2405
|
+
let flip = denom > 0 ? -1 : 1
|
|
2406
|
+
return { mesh, distance: t * Math.hypot(dx, dy, dz), point: [hx, hy, hz], normal: [nx * flip, ny * flip, nz * flip] }
|
|
2407
|
+
}
|
|
2408
|
+
let clampSpriteHits = (hits: Hit[], cam: Camera, th: number): Hit[] => {
|
|
2409
|
+
let out: Hit[] | null = null
|
|
2410
|
+
for (let mesh of meshes) {
|
|
2411
|
+
if (!mesh._sprite) continue
|
|
2412
|
+
let px = spriteScreenPx(mesh)
|
|
2413
|
+
if (px === null) continue
|
|
2414
|
+
if (out === null) out = hits.filter(hit => !(hit.mesh._sprite && spriteScreenPx(hit.mesh) !== null))
|
|
2415
|
+
if ((mesh.layers & sceneMask) === 0 || !shown(mesh)) continue
|
|
2416
|
+
let hit = clampedSpriteHit(mesh, px, cam, th)
|
|
2417
|
+
if (hit !== null) out.push(hit)
|
|
2418
|
+
}
|
|
2419
|
+
if (out === null) return hits
|
|
2420
|
+
return out.sort((a, b) => a.distance - b.distance)
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2306
2423
|
let scene: Scene = {
|
|
2307
2424
|
texture: resolve === null ? texture : resolve.target,
|
|
2308
2425
|
hdrTexture: texture,
|
|
@@ -2323,6 +2440,10 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
2323
2440
|
hooks._schedule()
|
|
2324
2441
|
},
|
|
2325
2442
|
size: () => ({ width, height }),
|
|
2443
|
+
setStagger(ms) {
|
|
2444
|
+
if (disposed) return
|
|
2445
|
+
setTransition(root, ms === null ? null : { stagger: ms })
|
|
2446
|
+
},
|
|
2326
2447
|
setParams(params) {
|
|
2327
2448
|
if (disposed) return
|
|
2328
2449
|
Object.assign(sceneParams, params)
|
|
@@ -2443,7 +2564,7 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
2443
2564
|
},
|
|
2444
2565
|
pick(x, y) {
|
|
2445
2566
|
pixelRay(x, y)
|
|
2446
|
-
return raycastAs(texture, pickOrigin, pickDir, undefined)
|
|
2567
|
+
return clampSpriteHits(raycastAs(texture, pickOrigin, pickDir, undefined), camera, height)
|
|
2447
2568
|
},
|
|
2448
2569
|
unproject(x, y, w, out = [0, 0, 0]) {
|
|
2449
2570
|
pixelRay(x, y)
|
|
@@ -2590,7 +2711,7 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
2590
2711
|
pick(x, y) {
|
|
2591
2712
|
if (v.disposed) return []
|
|
2592
2713
|
pixelRayOf(v.camera, v.width, v.height, x, y)
|
|
2593
|
-
return raycastAs(v.texture, pickOrigin, pickDir, undefined)
|
|
2714
|
+
return clampSpriteHits(raycastAs(v.texture, pickOrigin, pickDir, undefined), v.camera, v.height)
|
|
2594
2715
|
},
|
|
2595
2716
|
project(point) {
|
|
2596
2717
|
return projectWith(v.camera, v.width, v.height, point)
|
|
@@ -2656,6 +2777,7 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
2656
2777
|
dispose() {
|
|
2657
2778
|
if (disposed) return
|
|
2658
2779
|
disposed = true
|
|
2780
|
+
unhook()
|
|
2659
2781
|
listeners.clear()
|
|
2660
2782
|
// Full tree-side teardown, not just the target: every node leaves
|
|
2661
2783
|
// the scene (entries' geometry-buffer references and pick leaves
|