@solidrt/3d 0.0.56 → 0.0.57
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 +199 -117
- package/README.md +2 -1
- package/examples/README.md +13 -8
- package/examples/collision.tsx +17 -4
- package/examples/first-person.tsx +20 -10
- package/examples/fleet.tsx +11 -5
- package/examples/model.tsx +3 -3
- package/examples/pick.tsx +144 -34
- package/examples/probe.tsx +10 -4
- package/examples/scene-views.tsx +49 -58
- package/examples/sky-lit.tsx +10 -4
- package/examples/skybox.tsx +10 -4
- package/examples/standard.tsx +10 -4
- package/package.json +2 -2
- package/src/collision.ts +14 -193
- package/src/components/context.tsx +35 -0
- package/src/components/directional-light.tsx +48 -0
- package/src/components/first-person-camera.tsx +72 -0
- package/src/components/group.tsx +18 -0
- package/src/components/hemisphere-light.tsx +24 -0
- package/src/components/index.ts +46 -0
- package/src/components/instanced-mesh.tsx +105 -0
- package/src/components/mesh.tsx +92 -0
- package/src/components/node-props.ts +90 -0
- package/src/components/orbit-camera.tsx +75 -0
- package/src/components/perspective-camera.tsx +32 -0
- package/src/components/point-light.tsx +52 -0
- package/src/components/record-mesh.tsx +68 -0
- package/src/components/scene.tsx +281 -0
- package/src/components/spot-light.tsx +65 -0
- package/src/components/sprite.tsx +38 -0
- package/src/components/view3d.tsx +152 -0
- package/src/first-person.ts +140 -188
- package/src/index.ts +13 -9
- package/src/input.ts +70 -0
- package/src/node.ts +98 -25
- package/src/orbit.ts +266 -235
- package/src/scene-pointer.ts +229 -101
- package/src/scene.ts +250 -76
- package/src/components.tsx +0 -1101
package/src/scene.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// The retained scene: plain objects, no signals - the hot path (a moved
|
|
2
2
|
// node) is flat imperative code, and reactivity stays at the component
|
|
3
|
-
// boundary (components
|
|
3
|
+
// boundary (components/). The transform hierarchy itself lives in the
|
|
4
4
|
// spatial core (flux:spatial): every node in a scene has a core node, JS
|
|
5
5
|
// keeps the LOCAL transform as the readable source of truth and forwards
|
|
6
6
|
// each write, and the core's flush recomputes only the moved subtrees and
|
|
@@ -38,10 +38,10 @@
|
|
|
38
38
|
|
|
39
39
|
import { addDraw, createCubeDrawTarget, createDrawTarget, depthTexture, destroyProgram, destroyRenderPipeline, destroyTexture, removeDraw, renderTarget, setDrawBuffers, setDrawOrder, setDrawParams, setDrawTextures, setTargetParams, setTargetRect, setTargetSize, setTargetTextures } from "@solidrt/core/gpu"
|
|
40
40
|
import * as spatial from "flux:spatial"
|
|
41
|
-
import type { NodeId } from "flux:spatial"
|
|
41
|
+
import type { Impact as CoreImpact, NodeId, QueryFilter } from "flux:spatial"
|
|
42
42
|
import type { DrawId, FilterMode, ProgramId, RenderPipelineId, ShaderParams, TextureId, WrapMode } from "@solidrt/core/gpu"
|
|
43
43
|
import { getOwner, onCleanup } from "@solidrt/core"
|
|
44
|
-
import type { PointerEvent as ElementPointerEvent } from "@solidrt/core"
|
|
44
|
+
import type { PointerEvent as ElementPointerEvent, WheelEvent as ElementWheelEvent } from "@solidrt/core"
|
|
45
45
|
import { copy, mat4, transformPoint } from "./math.ts"
|
|
46
46
|
import { linearColor } from "./color.ts"
|
|
47
47
|
import type { Mat4, Quat, Vec3, Vec4 } from "./math.ts"
|
|
@@ -59,7 +59,7 @@ import type { Prefilter } from "./environment.ts"
|
|
|
59
59
|
import type { Material } from "./material.ts"
|
|
60
60
|
import { orderEntries } from "./order.ts"
|
|
61
61
|
import { fillTransform, leaveScene, makeNode, worldInto } from "./node.ts"
|
|
62
|
-
import type { SceneHooks, SceneNode,
|
|
62
|
+
import type { SceneHooks, SceneNode, ScenePointerListener } from "./node.ts"
|
|
63
63
|
import { checkInstancePairing, checkMask, instanceBinding, localBounds, publishInstanceStyle } from "./mesh.ts"
|
|
64
64
|
import type { InstancedMesh, InstanceNode, Mesh } from "./mesh.ts"
|
|
65
65
|
import type { CastingLight, Light } from "./light.ts"
|
|
@@ -183,15 +183,52 @@ export type Overlap = { mesh: Mesh; instance?: InstanceNode; point: Vec3; normal
|
|
|
183
183
|
* Godot's KinematicCollision3D). `instance` as on Overlap. */
|
|
184
184
|
export type Impact = { mesh: Mesh; instance?: InstanceNode; time: number; point: Vec3; normal: Vec3 }
|
|
185
185
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
186
|
+
export type MoveOptions = QueryOptions & {
|
|
187
|
+
/** The direction floors face (default [0, 1, 0]). */
|
|
188
|
+
up?: Vec3
|
|
189
|
+
/** Largest angle (radians) between a contact normal and `up` that still
|
|
190
|
+
* counts as floor (default 45 degrees). Steeper contacts are walls, and
|
|
191
|
+
* the body slides down them. */
|
|
192
|
+
floorMaxAngle?: number
|
|
193
|
+
/** Most contacts one call slides along (default 6). */
|
|
194
|
+
maxSlides?: number
|
|
195
|
+
/** Gap kept from every surface, world units (default 0.01). */
|
|
196
|
+
skin?: number
|
|
197
|
+
/** How far below the body a floor is pulled to when the motion does not
|
|
198
|
+
* rise (default 0.1; 0 disables): what keeps a walker on a ramp going
|
|
199
|
+
* down, and what decides `floor` at the end of the move - a body that
|
|
200
|
+
* ends higher than this above its floor is airborne, whatever it
|
|
201
|
+
* touched on the way. A rising motion (a jump) never snaps. */
|
|
202
|
+
floorSnap?: number
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export type MoveResult = {
|
|
206
|
+
/** The displacement the body gets: add it to the body's position. */
|
|
207
|
+
motion: Vec3
|
|
208
|
+
/** The unit normal of the floor the body ends the move on - within
|
|
209
|
+
* `floorSnap` below it, snapped onto - else null (airborne, or on a
|
|
210
|
+
* slope too steep to stand on). With `floorSnap: 0` it is a floor met
|
|
211
|
+
* during the move instead. */
|
|
212
|
+
floor: Vec3 | null
|
|
213
|
+
/** Whether a wall (a contact steeper than a floor and flatter than a
|
|
214
|
+
* ceiling) or a ceiling was met. */
|
|
215
|
+
wall: boolean
|
|
216
|
+
ceiling: boolean
|
|
217
|
+
/** Every contact met, in order, the floor snap's last. */
|
|
218
|
+
hits: Impact[]
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Element handlers wiring a scene's (or a view's) pointer events: spread
|
|
222
|
+
* onto whatever element shows its texture (the built-in `<Scene>` and
|
|
223
|
+
* `<View3d>` leaves wire them automatically). `scene.handlers` expects
|
|
224
|
+
* the leaf laid out at the target size; a split-resolution leaf
|
|
225
|
+
* (supersampling) uses scene.handlersFor. */
|
|
190
226
|
export type SceneHandlers = {
|
|
191
227
|
onPointerDown(event: ElementPointerEvent): void
|
|
192
228
|
onPointerMove(event: ElementPointerEvent): void
|
|
193
229
|
onPointerUp(event: ElementPointerEvent): void
|
|
194
230
|
onPointerLeave(event: ElementPointerEvent): void
|
|
231
|
+
onWheel(event: ElementWheelEvent): void
|
|
195
232
|
}
|
|
196
233
|
|
|
197
234
|
/**
|
|
@@ -447,7 +484,7 @@ export type ViewOptions = {
|
|
|
447
484
|
}
|
|
448
485
|
|
|
449
486
|
/** A second rendering of a scene from its own camera; see Scene.createView. */
|
|
450
|
-
export type
|
|
487
|
+
export type ViewHandle = {
|
|
451
488
|
/** The view's output, an ordinary texture id. */
|
|
452
489
|
texture: TextureId
|
|
453
490
|
/** The view target's depth as a sampler-only texture id when created
|
|
@@ -469,6 +506,22 @@ export type View = {
|
|
|
469
506
|
/** Replace the view's layer mask (bitmask): entries for newly admitted
|
|
470
507
|
* meshes attach, masked-out ones detach. */
|
|
471
508
|
setLayers(mask: number): void
|
|
509
|
+
/** The camera ray through one of the VIEW's pixels, hits nearest first:
|
|
510
|
+
* scene.pick through this view's camera and size, so a mesh under a
|
|
511
|
+
* minimap is picked where the minimap shows it (the scene's query
|
|
512
|
+
* filters apply: visibility, the view's own layer mask is not one). */
|
|
513
|
+
pick(x: number, y: number): Hit[]
|
|
514
|
+
/** Element pointer handlers for the element showing `texture`: the
|
|
515
|
+
* scene's dispatch with THIS VIEW as the root of the walk - nodes get
|
|
516
|
+
* their ordinary handlers, picked through the view's camera, the
|
|
517
|
+
* view's `listen` is the last stop, `x`/`y` are view pixels. The
|
|
518
|
+
* `<View3d>` leaf carries them; an `output` leaf spreads them itself. */
|
|
519
|
+
handlers: SceneHandlers
|
|
520
|
+
/** handlers for a leaf laid out at another size; exactly
|
|
521
|
+
* scene.handlersFor. */
|
|
522
|
+
handlersFor(layout: () => { width: number; height: number }): SceneHandlers
|
|
523
|
+
/** A listener at this view's root; exactly scene.listen. */
|
|
524
|
+
listen(listener: ScenePointerListener): () => void
|
|
472
525
|
/** Destroy the view's target (its entries die with it). Idempotent;
|
|
473
526
|
* views also die with their scene. */
|
|
474
527
|
dispose(): void
|
|
@@ -708,6 +761,21 @@ export type Scene = {
|
|
|
708
761
|
* a wall. A zero motion touches nothing. Same testing as overlap().
|
|
709
762
|
*/
|
|
710
763
|
sweep(volume: Volume, motion: Vec3, opts?: QueryOptions): Impact[]
|
|
764
|
+
/**
|
|
765
|
+
* Move a body `motion` through the scene's colliders, sliding along
|
|
766
|
+
* what it hits (Godot's move_and_slide, Unity's CharacterController.Move):
|
|
767
|
+
* a capsule for a character, a sphere for a ball, a box for a crate.
|
|
768
|
+
* The body first pushes out of anything it starts inside, then sweeps
|
|
769
|
+
* and slides up to `maxSlides` times, then, unless the motion rises,
|
|
770
|
+
* snaps down onto a floor within `floorSnap` - the floor it reports is
|
|
771
|
+
* the one it ends on. One core call per body per frame; the loop's
|
|
772
|
+
* sweeps and overlaps never leave the core. Colliders are whatever
|
|
773
|
+
* `opts` selects (`layers`/`meshes`, as for sweep). Gravity is the
|
|
774
|
+
* caller's: fold the fall into `motion` each frame, zero it while
|
|
775
|
+
* `floor` is set (the snap keeps reporting the floor while the body
|
|
776
|
+
* stands still); a walkable floor absorbs the vertical part.
|
|
777
|
+
*/
|
|
778
|
+
moveAndSlide(volume: Volume, motion: Vec3, opts?: MoveOptions): MoveResult
|
|
711
779
|
/**
|
|
712
780
|
* pick()'s ray half (Unity's ScreenPointToRay, Godot's
|
|
713
781
|
* project_ray_origin/normal): the camera ray through a scene pixel,
|
|
@@ -719,17 +787,25 @@ export type Scene = {
|
|
|
719
787
|
*/
|
|
720
788
|
screenRay(x: number, y: number): ScreenRay
|
|
721
789
|
/**
|
|
722
|
-
* Element pointer handlers driving the
|
|
723
|
-
* (onPointerDown/Move/Up/Enter/Leave on nodes)
|
|
724
|
-
*
|
|
725
|
-
*
|
|
726
|
-
*
|
|
727
|
-
*
|
|
728
|
-
*
|
|
729
|
-
*
|
|
730
|
-
*
|
|
731
|
-
*
|
|
732
|
-
* the
|
|
790
|
+
* Element pointer handlers driving the node event fields
|
|
791
|
+
* (onPointerDown/Move/Up/Enter/Leave/Wheel/Tap on nodes) and the
|
|
792
|
+
* scene's listeners: spread onto the element that shows
|
|
793
|
+
* `scene.texture`. The `<Scene>` component's built-in leaf carries them
|
|
794
|
+
* automatically; with `output` (or imperative use), spread them
|
|
795
|
+
* yourself: `<texture src={scene.texture} {...scene.handlers} />`.
|
|
796
|
+
* The element event model one tree deeper, with the scene as the root
|
|
797
|
+
* of the walk: the nearest hit is the target (the struck instance of an
|
|
798
|
+
* instanced mesh, else the mesh), down/move/up/wheel bubble through its
|
|
799
|
+
* ancestors and end at the scene's listeners (`listen`); over empty
|
|
800
|
+
* space the walk is the scene alone. stopPropagation stops the walk,
|
|
801
|
+
* and a stopped down claims the whole press. Capture is per pointer to
|
|
802
|
+
* the press target, the scene included: a drag keeps delivering to what
|
|
803
|
+
* it pressed, with `point`/`distance` null while the ray misses it.
|
|
804
|
+
* Enter/leave pair on the struck node alone, on hover changes. Taps are
|
|
805
|
+
* synthesized (a same-target release within the slop, alone for the
|
|
806
|
+
* press, `tapCount` for repeats). Hover reacts to pointer MOTION - a
|
|
807
|
+
* mesh animating under a still pointer fires nothing until the pointer
|
|
808
|
+
* moves (the element hit-test has the same limit).
|
|
733
809
|
*
|
|
734
810
|
* Coordinates assume the leaf is LAID OUT at the target size - true for
|
|
735
811
|
* the built-in leaf and a d-texture at natural size, under any ancestor
|
|
@@ -743,6 +819,19 @@ export type Scene = {
|
|
|
743
819
|
* layout just works: `scene.handlersFor(() => ({ width: w(), height:
|
|
744
820
|
* h() }))`. */
|
|
745
821
|
handlersFor(layout: () => { width: number; height: number }): SceneHandlers
|
|
822
|
+
/**
|
|
823
|
+
* Add a listener at the root of the pointer walk (the `<Scene>`
|
|
824
|
+
* component's onPointerDown/Move/Up, onWheel and onTap props do this):
|
|
825
|
+
* it sees every event the node chain lets through, hit or miss -
|
|
826
|
+
* `event.mesh` is the hit it bubbled from, null over empty space - with
|
|
827
|
+
* `x`/`y` in scene pixels. Listeners all run, in registration order
|
|
828
|
+
* (the root is the last stop, nothing is left to claim); a stopped DOWN
|
|
829
|
+
* claims the whole press for the chain, so that pointer's move, up and
|
|
830
|
+
* tap never arrive. Returns the remover. A pointer feed listens here
|
|
831
|
+
* through feedPointer, so a camera control bound to the feed respects a
|
|
832
|
+
* mesh's claim.
|
|
833
|
+
*/
|
|
834
|
+
listen(listener: ScenePointerListener): () => void
|
|
746
835
|
/**
|
|
747
836
|
* A second rendering of this scene: its own draw target and camera,
|
|
748
837
|
* the same meshes and lights. Each mesh gets one entry in the view's
|
|
@@ -756,7 +845,7 @@ export type Scene = {
|
|
|
756
845
|
* or pointer events. Views die with the scene; `view.dispose()` drops
|
|
757
846
|
* one early.
|
|
758
847
|
*/
|
|
759
|
-
createView(opts: ViewOptions):
|
|
848
|
+
createView(opts: ViewOptions): ViewHandle
|
|
760
849
|
/**
|
|
761
850
|
* A reflection probe: the scene rendered into a cube map from a point,
|
|
762
851
|
* six faces at 90 degrees - Three's CubeCamera, Unity's and Godot's
|
|
@@ -849,23 +938,44 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
849
938
|
// map turns a hit's core node back into the mesh, or the instance of an
|
|
850
939
|
// instanced mesh (each instance node is its own leaf).
|
|
851
940
|
let byNode = new Map<NodeId, Mesh | InstanceNode>()
|
|
852
|
-
// The
|
|
853
|
-
//
|
|
854
|
-
//
|
|
855
|
-
//
|
|
856
|
-
//
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
941
|
+
// The filter raycast/overlap/sweep hand the core: the scene root (the
|
|
942
|
+
// arena is shared with other scenes and 2d layers), the query mask (the
|
|
943
|
+
// scene's by default, so an undrawn layer needs an explicit opts.layers
|
|
944
|
+
// to report) and, for an include-list, the meshes' nodes - an instanced
|
|
945
|
+
// mesh's instances each being a leaf of their own.
|
|
946
|
+
// One filter object per scene, refilled per query (the bindings read it
|
|
947
|
+
// synchronously), so a query allocates nothing but its hits.
|
|
948
|
+
let filter: QueryFilter = {}
|
|
949
|
+
let queryFilter = (qopts: QueryOptions | undefined, site: string): QueryFilter => {
|
|
950
|
+
filter.root = root._node!
|
|
951
|
+
filter.layers = qopts?.layers !== undefined ? checkMask(qopts.layers, site) : sceneMask
|
|
952
|
+
if (qopts?.meshes === undefined) {
|
|
953
|
+
filter.nodes = undefined
|
|
954
|
+
} else {
|
|
955
|
+
let nodes: NodeId[] = []
|
|
956
|
+
for (let mesh of qopts.meshes) {
|
|
957
|
+
if (mesh._node !== null) nodes.push(mesh._node)
|
|
958
|
+
if (mesh._instances !== null) nodes.push(...instanceGroup(mesh as InstancedMesh))
|
|
959
|
+
}
|
|
960
|
+
filter.nodes = nodes
|
|
868
961
|
}
|
|
962
|
+
return filter
|
|
963
|
+
}
|
|
964
|
+
// A hit's core node back to its mesh (and instance).
|
|
965
|
+
let leafOf = (node: NodeId): { mesh: Mesh; instance: InstanceNode | null } | null => {
|
|
966
|
+
let leaf = byNode.get(node)
|
|
967
|
+
if (leaf === undefined) return null
|
|
968
|
+
let instance = leaf.kind === "instance" ? leaf : null
|
|
969
|
+
let mesh = instance !== null ? instance.mesh : (leaf as Mesh)
|
|
970
|
+
if (mesh === null) return null
|
|
971
|
+
return { mesh, instance }
|
|
972
|
+
}
|
|
973
|
+
let impactOf = (h: CoreImpact): Impact | null => {
|
|
974
|
+
let leaf = leafOf(h.node)
|
|
975
|
+
if (leaf === null) return null
|
|
976
|
+
let impact: Impact = { mesh: leaf.mesh, time: h.time, point: h.point, normal: h.normal }
|
|
977
|
+
if (leaf.instance !== null) impact.instance = leaf.instance
|
|
978
|
+
return impact
|
|
869
979
|
}
|
|
870
980
|
// Nodes whose transform changed since the last sync (deduped by the
|
|
871
981
|
// _moved flag): what the light and transparent-order bookkeeping
|
|
@@ -1070,44 +1180,46 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1070
1180
|
let clip: Vec4 = [0, 0, 0, 0]
|
|
1071
1181
|
let pickOrigin: Vec3 = [0, 0, 0]
|
|
1072
1182
|
|
|
1073
|
-
// The camera ray through a
|
|
1183
|
+
// The camera ray through a pixel of `cam` over a tw x th target, into the
|
|
1184
|
+
// pickOrigin/pickDir
|
|
1074
1185
|
// scratches. The direction keeps a camera-forward component of 1
|
|
1075
1186
|
// (perspective: (cx, cy, -1) in the camera frame; ortho: unit forward),
|
|
1076
1187
|
// so origin + w * direction is the world point at camera-forward
|
|
1077
1188
|
// distance w - what unproject() banks on. pick()/screenRay()/
|
|
1078
1189
|
// unproject() all cast exactly this ray.
|
|
1079
|
-
let
|
|
1080
|
-
ensureCamera(
|
|
1081
|
-
let v =
|
|
1082
|
-
let o =
|
|
1190
|
+
let pixelRayOf = (cam: Camera, tw: number, th: number, x: number, y: number): void => {
|
|
1191
|
+
ensureCamera(cam, tw, th)
|
|
1192
|
+
let v = cam.view
|
|
1193
|
+
let o = cam.ortho
|
|
1083
1194
|
if (o === null) {
|
|
1084
1195
|
// The camera-frame ray through the pixel, inverting project()'s
|
|
1085
1196
|
// mapping: the baked y-down clip flip is why pixel y converts with
|
|
1086
1197
|
// no negation there and one here.
|
|
1087
|
-
let f = 1 / Math.tan(((
|
|
1088
|
-
let cx = (((x /
|
|
1089
|
-
let cy = -((y /
|
|
1198
|
+
let f = 1 / Math.tan(((cam.fov * Math.PI) / 180) / 2)
|
|
1199
|
+
let cx = (((x / tw) * 2 - 1) * (tw / th)) / f
|
|
1200
|
+
let cy = -((y / th) * 2 - 1) / f
|
|
1090
1201
|
// The view's upper 3x3 rows are the camera axes, so its transpose
|
|
1091
1202
|
// carries the camera-space direction (cx, cy, -1) to world.
|
|
1092
1203
|
pickDir[0] = cx * v[0] + cy * v[1] - v[2]
|
|
1093
1204
|
pickDir[1] = cx * v[4] + cy * v[5] - v[6]
|
|
1094
1205
|
pickDir[2] = cx * v[8] + cy * v[9] - v[10]
|
|
1095
|
-
pickOrigin[0] =
|
|
1096
|
-
pickOrigin[1] =
|
|
1097
|
-
pickOrigin[2] =
|
|
1206
|
+
pickOrigin[0] = cam.eye[0]
|
|
1207
|
+
pickOrigin[1] = cam.eye[1]
|
|
1208
|
+
pickOrigin[2] = cam.eye[2]
|
|
1098
1209
|
return
|
|
1099
1210
|
}
|
|
1100
1211
|
// Orthographic: every ray runs along the camera's forward axis; the
|
|
1101
1212
|
// pixel picks where on the camera plane it starts (top row = top).
|
|
1102
|
-
let cx = o.left + (x /
|
|
1103
|
-
let cy = o.top + (y /
|
|
1104
|
-
pickOrigin[0] =
|
|
1105
|
-
pickOrigin[1] =
|
|
1106
|
-
pickOrigin[2] =
|
|
1213
|
+
let cx = o.left + (x / tw) * (o.right - o.left)
|
|
1214
|
+
let cy = o.top + (y / th) * (o.bottom - o.top)
|
|
1215
|
+
pickOrigin[0] = cam.eye[0] + cx * v[0] + cy * v[1]
|
|
1216
|
+
pickOrigin[1] = cam.eye[1] + cx * v[4] + cy * v[5]
|
|
1217
|
+
pickOrigin[2] = cam.eye[2] + cx * v[8] + cy * v[9]
|
|
1107
1218
|
pickDir[0] = -v[2]
|
|
1108
1219
|
pickDir[1] = -v[6]
|
|
1109
1220
|
pickDir[2] = -v[10]
|
|
1110
1221
|
}
|
|
1222
|
+
let pixelRay = (x: number, y: number): void => pixelRayOf(camera, width, height, x, y)
|
|
1111
1223
|
|
|
1112
1224
|
// Views (scene.createView): more targets drawing the same meshes from
|
|
1113
1225
|
// their own cameras. A view holds one entry per mesh in its target,
|
|
@@ -1487,6 +1599,10 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1487
1599
|
},
|
|
1488
1600
|
_setLayers(mesh) {
|
|
1489
1601
|
if (mesh._buffers === null || disposed) return
|
|
1602
|
+
if (mesh._node !== null) spatial.setLayers(mesh._node, mesh.layers)
|
|
1603
|
+
if (mesh._instances !== null) {
|
|
1604
|
+
for (let n of instanceGroup(mesh as InstancedMesh)) spatial.setLayers(n, mesh.layers)
|
|
1605
|
+
}
|
|
1490
1606
|
if ((mesh.layers & sceneMask) !== 0) attachScene(mesh)
|
|
1491
1607
|
else detachScene(mesh)
|
|
1492
1608
|
for (let v of views) {
|
|
@@ -1598,6 +1714,7 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1598
1714
|
if (mesh === null || mesh._buffers === null || mesh._node === null || instance._node === null) return
|
|
1599
1715
|
spatial.setBounds(instance._node, geometryBounds(mesh.geometry))
|
|
1600
1716
|
spatial.setShape(instance._node, mesh._buffers.shape)
|
|
1717
|
+
spatial.setLayers(instance._node, mesh.layers)
|
|
1601
1718
|
// The record is the instance's placement inside the mesh (the mesh
|
|
1602
1719
|
// node is the anchor), staged by the core at the next flush.
|
|
1603
1720
|
spatial.bindMatrixRecord(instance._node, mesh._instances.buffer, instance._slot, mesh._node)
|
|
@@ -1674,13 +1791,9 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1674
1791
|
lightsDirty = true
|
|
1675
1792
|
hooks._schedule()
|
|
1676
1793
|
|
|
1677
|
-
//
|
|
1678
|
-
//
|
|
1679
|
-
|
|
1680
|
-
let pointer = makePointerInput({
|
|
1681
|
-
pick: (x, y) => scene.pick(x, y),
|
|
1682
|
-
targetSize: () => ({ width, height }),
|
|
1683
|
-
})
|
|
1794
|
+
// The scene's root listeners (scene.listen): the last stop of the
|
|
1795
|
+
// pointer walk scene-pointer.ts runs behind scene.handlers.
|
|
1796
|
+
let listeners = new Set<ScenePointerListener>()
|
|
1684
1797
|
|
|
1685
1798
|
let scene: Scene = {
|
|
1686
1799
|
texture,
|
|
@@ -1830,7 +1943,7 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1830
1943
|
// microtask still runs and finds nothing dirty - harmless.)
|
|
1831
1944
|
if (scheduled) sync()
|
|
1832
1945
|
if (disposed) return []
|
|
1833
|
-
let
|
|
1946
|
+
let f = queryFilter(rayOpts, "raycast")
|
|
1834
1947
|
let hits: Hit[] = []
|
|
1835
1948
|
rayOriginScratch[0] = origin[0]
|
|
1836
1949
|
rayOriginScratch[1] = origin[1]
|
|
@@ -1838,8 +1951,8 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1838
1951
|
rayDirScratch[0] = direction[0]
|
|
1839
1952
|
rayDirScratch[1] = direction[1]
|
|
1840
1953
|
rayDirScratch[2] = direction[2]
|
|
1841
|
-
for (let h of spatial.raycast(rayOriginScratch, rayDirScratch)) {
|
|
1842
|
-
let leaf =
|
|
1954
|
+
for (let h of spatial.raycast(rayOriginScratch, rayDirScratch, f)) {
|
|
1955
|
+
let leaf = leafOf(h.node)
|
|
1843
1956
|
if (leaf === null) continue
|
|
1844
1957
|
let hit: Hit = { mesh: leaf.mesh, distance: h.distance, point: h.point, normal: h.normal }
|
|
1845
1958
|
if (leaf.instance !== null) hit.instance = leaf.instance
|
|
@@ -1852,11 +1965,11 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1852
1965
|
overlap(volume, qopts) {
|
|
1853
1966
|
if (scheduled) sync()
|
|
1854
1967
|
if (disposed) return []
|
|
1855
|
-
let
|
|
1968
|
+
let f = queryFilter(qopts, "overlap")
|
|
1856
1969
|
let kind = packVolume(volume, "overlap")
|
|
1857
1970
|
let out: Overlap[] = []
|
|
1858
|
-
for (let h of spatial.overlap(kind, kind === "box" ? boxScratch : capsuleScratch)) {
|
|
1859
|
-
let leaf =
|
|
1971
|
+
for (let h of spatial.overlap(kind, kind === "box" ? boxScratch : capsuleScratch, f)) {
|
|
1972
|
+
let leaf = leafOf(h.node)
|
|
1860
1973
|
if (leaf === null) continue
|
|
1861
1974
|
let contact: Overlap = { mesh: leaf.mesh, point: h.point, normal: h.normal, depth: h.depth }
|
|
1862
1975
|
if (leaf.instance !== null) contact.instance = leaf.instance
|
|
@@ -1867,27 +1980,53 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1867
1980
|
sweep(volume, motion, qopts) {
|
|
1868
1981
|
if (scheduled) sync()
|
|
1869
1982
|
if (disposed) return []
|
|
1870
|
-
let
|
|
1983
|
+
let f = queryFilter(qopts, "sweep")
|
|
1871
1984
|
let kind = packVolume(volume, "sweep")
|
|
1872
1985
|
motionScratch[0] = motion[0]
|
|
1873
1986
|
motionScratch[1] = motion[1]
|
|
1874
1987
|
motionScratch[2] = motion[2]
|
|
1875
1988
|
let out: Impact[] = []
|
|
1876
|
-
for (let h of spatial.sweep(kind, kind === "box" ? boxScratch : capsuleScratch, motionScratch)) {
|
|
1877
|
-
let
|
|
1878
|
-
if (
|
|
1879
|
-
let impact: Impact = { mesh: leaf.mesh, time: h.time, point: h.point, normal: h.normal }
|
|
1880
|
-
if (leaf.instance !== null) impact.instance = leaf.instance
|
|
1881
|
-
out.push(impact)
|
|
1989
|
+
for (let h of spatial.sweep(kind, kind === "box" ? boxScratch : capsuleScratch, motionScratch, f)) {
|
|
1990
|
+
let impact = impactOf(h)
|
|
1991
|
+
if (impact !== null) out.push(impact)
|
|
1882
1992
|
}
|
|
1883
1993
|
return out
|
|
1884
1994
|
},
|
|
1885
|
-
|
|
1886
|
-
|
|
1995
|
+
moveAndSlide(volume, motion, mopts) {
|
|
1996
|
+
if (scheduled) sync()
|
|
1997
|
+
let kind = packVolume(volume, "moveAndSlide")
|
|
1998
|
+
motionScratch[0] = motion[0]
|
|
1999
|
+
motionScratch[1] = motion[1]
|
|
2000
|
+
motionScratch[2] = motion[2]
|
|
2001
|
+
if (disposed) return { motion: [motion[0], motion[1], motion[2]], floor: null, wall: false, ceiling: false, hits: [] }
|
|
2002
|
+
let f = queryFilter(mopts, "moveAndSlide")
|
|
2003
|
+
let r = spatial.moveAndSlide(kind, kind === "box" ? boxScratch : capsuleScratch, motionScratch, mopts, f)
|
|
2004
|
+
let hits: Impact[] = []
|
|
2005
|
+
for (let h of r.hits) {
|
|
2006
|
+
let impact = impactOf(h)
|
|
2007
|
+
if (impact !== null) hits.push(impact)
|
|
2008
|
+
}
|
|
2009
|
+
return { motion: r.motion, floor: r.floor, wall: r.wall, ceiling: r.ceiling, hits }
|
|
2010
|
+
},
|
|
2011
|
+
get handlers() {
|
|
2012
|
+
return pointer.handlers
|
|
2013
|
+
},
|
|
2014
|
+
handlersFor(layout) {
|
|
2015
|
+
return pointer.handlersFor(layout)
|
|
2016
|
+
},
|
|
2017
|
+
listen(listener) {
|
|
2018
|
+
listeners.add(listener)
|
|
2019
|
+
return () => {
|
|
2020
|
+
listeners.delete(listener)
|
|
2021
|
+
}
|
|
2022
|
+
},
|
|
1887
2023
|
createView(vopts) {
|
|
1888
2024
|
if (disposed) throw new Error("createView: the scene is disposed")
|
|
1889
2025
|
let v = makeView(vopts, null)
|
|
1890
|
-
|
|
2026
|
+
// The view's own walk: its listeners, its camera's pick, and a
|
|
2027
|
+
// dispatch with its own capture and hover bookkeeping.
|
|
2028
|
+
let viewListeners = new Set<ScenePointerListener>()
|
|
2029
|
+
let handle: ViewHandle = {
|
|
1891
2030
|
texture: v.texture,
|
|
1892
2031
|
depthTexture: vopts.depth === "texture" && vopts.into === undefined ? depthTexture(v.texture) : null,
|
|
1893
2032
|
setCamera(update) {
|
|
@@ -1927,10 +2066,35 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1927
2066
|
}
|
|
1928
2067
|
hooks._schedule()
|
|
1929
2068
|
},
|
|
2069
|
+
pick(x, y) {
|
|
2070
|
+
if (v.disposed) return []
|
|
2071
|
+
pixelRayOf(v.camera, v.width, v.height, x, y)
|
|
2072
|
+
return scene.raycast(pickOrigin, pickDir)
|
|
2073
|
+
},
|
|
2074
|
+
get handlers() {
|
|
2075
|
+
return viewPointer.handlers
|
|
2076
|
+
},
|
|
2077
|
+
handlersFor(layout) {
|
|
2078
|
+
return viewPointer.handlersFor(layout)
|
|
2079
|
+
},
|
|
2080
|
+
listen(listener) {
|
|
2081
|
+
viewListeners.add(listener)
|
|
2082
|
+
return () => {
|
|
2083
|
+
viewListeners.delete(listener)
|
|
2084
|
+
}
|
|
2085
|
+
},
|
|
1930
2086
|
dispose() {
|
|
2087
|
+
viewListeners.clear()
|
|
1931
2088
|
disposeView(v)
|
|
1932
2089
|
},
|
|
1933
2090
|
}
|
|
2091
|
+
let viewPointer = makePointerInput({
|
|
2092
|
+
pick: (x, y) => handle.pick(x, y),
|
|
2093
|
+
targetSize: () => ({ width: v.width, height: v.height }),
|
|
2094
|
+
root: handle,
|
|
2095
|
+
listeners: viewListeners,
|
|
2096
|
+
})
|
|
2097
|
+
return handle
|
|
1934
2098
|
},
|
|
1935
2099
|
createReflectionProbe(popts) {
|
|
1936
2100
|
if (disposed) throw new Error("createReflectionProbe: the scene is disposed")
|
|
@@ -1950,6 +2114,7 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1950
2114
|
dispose() {
|
|
1951
2115
|
if (disposed) return
|
|
1952
2116
|
disposed = true
|
|
2117
|
+
listeners.clear()
|
|
1953
2118
|
// Full tree-side teardown, not just the target: every node leaves
|
|
1954
2119
|
// the scene (entries' geometry-buffer references and pick leaves
|
|
1955
2120
|
// dropped, core nodes freed), so a disposed scene leaves no
|
|
@@ -1977,6 +2142,15 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1977
2142
|
}
|
|
1978
2143
|
},
|
|
1979
2144
|
}
|
|
2145
|
+
// Pointer dispatch (scene.handlers): capture/hover bookkeeping, the
|
|
2146
|
+
// walk and the tap tracker live in scene-pointer.ts; all it needs of
|
|
2147
|
+
// the scene is pick(), the target size, and the root it ends at.
|
|
2148
|
+
let pointer = makePointerInput({
|
|
2149
|
+
pick: (x, y) => scene.pick(x, y),
|
|
2150
|
+
targetSize: () => ({ width, height }),
|
|
2151
|
+
root: scene,
|
|
2152
|
+
listeners,
|
|
2153
|
+
})
|
|
1980
2154
|
// A reflection probe over layer mask `mask` (see createReflectionProbe):
|
|
1981
2155
|
// the public object plus `finish`, which keeps the prefiltered chain and
|
|
1982
2156
|
// drops everything else - the bake's one-shot use.
|