@solidrt/3d 0.0.56 → 0.0.58
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 +1052 -639
- package/README.md +2 -1
- package/examples/README.md +19 -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/examples/wireframe.tsx +107 -0
- 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/geometry-gpu.ts +45 -13
- package/src/geometry.ts +186 -5
- package/src/gltf.ts +97 -16
- package/src/index.ts +15 -10
- package/src/input.ts +79 -0
- package/src/material.ts +26 -8
- package/src/mesh.ts +5 -6
- package/src/model.ts +65 -33
- package/src/node.ts +100 -26
- package/src/orbit.ts +266 -235
- package/src/scene-pointer.ts +229 -101
- package/src/scene.ts +256 -79
- package/src/skeleton.ts +211 -0
- 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"
|
|
@@ -50,7 +50,7 @@ import { cameraParams, cameraState, ensureCamera, makeCamera, updateCamera } fro
|
|
|
50
50
|
import type { Camera, CameraState, CameraUpdate } from "./camera.ts"
|
|
51
51
|
import { makeShadowSystem } from "./scene-shadows.ts"
|
|
52
52
|
import { makePointerInput } from "./scene-pointer.ts"
|
|
53
|
-
import { geometryBounds, layoutKey, validateGeometry } from "./geometry.ts"
|
|
53
|
+
import { geometryBounds, geometryTopology, layoutKey, validateGeometry } from "./geometry.ts"
|
|
54
54
|
import type { Geometry } from "./geometry.ts"
|
|
55
55
|
import { acquireGeometryBuffers, releaseGeometryBuffers } from "./geometry-gpu.ts"
|
|
56
56
|
import { backgroundPipeline, missingAttributes, SKYBOX_FRAGMENT } from "./material.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,
|
|
@@ -1177,7 +1289,7 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1177
1289
|
// so added live it would draw at the seeded identity until then. The
|
|
1178
1290
|
// mismatch branch in sync() turns it on in the same pass that writes
|
|
1179
1291
|
// uModel.
|
|
1180
|
-
mesh._entry = addDraw(texture, mesh.material.pipeline(mesh.geometry.layout), entrySeed(mesh.material, mesh._params), {
|
|
1292
|
+
mesh._entry = addDraw(texture, mesh.material.pipeline(mesh.geometry.layout, geometryTopology(mesh.geometry)), entrySeed(mesh.material, mesh._params), {
|
|
1181
1293
|
buffer: bufs.buffer,
|
|
1182
1294
|
indexBuffer: bufs.index,
|
|
1183
1295
|
indexFormat: bufs.indexFormat,
|
|
@@ -1207,6 +1319,9 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1207
1319
|
if (v.entries.has(mesh)) return
|
|
1208
1320
|
let inst = mesh._instances
|
|
1209
1321
|
if (v.shadowFilter !== null && !v.shadowFilter(mesh)) return
|
|
1322
|
+
// Lines and points cast nothing: a one-pixel wireframe in a shadow
|
|
1323
|
+
// map is speckle on the floor, never a shadow anyone wanted.
|
|
1324
|
+
if (v.shadowFilter !== null && geometryTopology(mesh.geometry) !== "triangles") return
|
|
1210
1325
|
if ((mesh.layers & v.mask) === 0) return
|
|
1211
1326
|
let material = viewMaterial(v, mesh)
|
|
1212
1327
|
// An override pipeline cannot know a populated mesh's record layout,
|
|
@@ -1219,7 +1334,7 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1219
1334
|
checkInstancePairing(material, inst, "Shadow material")
|
|
1220
1335
|
}
|
|
1221
1336
|
let bufs = mesh._buffers!
|
|
1222
|
-
let entry = addDraw(v.texture, material.pipeline(mesh.geometry.layout), entrySeed(material, v.override !== null ? null : mesh._params), {
|
|
1337
|
+
let entry = addDraw(v.texture, material.pipeline(mesh.geometry.layout, geometryTopology(mesh.geometry)), entrySeed(material, v.override !== null ? null : mesh._params), {
|
|
1223
1338
|
buffer: bufs.buffer,
|
|
1224
1339
|
indexBuffer: bufs.index,
|
|
1225
1340
|
indexFormat: bufs.indexFormat,
|
|
@@ -1487,6 +1602,10 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1487
1602
|
},
|
|
1488
1603
|
_setLayers(mesh) {
|
|
1489
1604
|
if (mesh._buffers === null || disposed) return
|
|
1605
|
+
if (mesh._node !== null) spatial.setLayers(mesh._node, mesh.layers)
|
|
1606
|
+
if (mesh._instances !== null) {
|
|
1607
|
+
for (let n of instanceGroup(mesh as InstancedMesh)) spatial.setLayers(n, mesh.layers)
|
|
1608
|
+
}
|
|
1490
1609
|
if ((mesh.layers & sceneMask) !== 0) attachScene(mesh)
|
|
1491
1610
|
else detachScene(mesh)
|
|
1492
1611
|
for (let v of views) {
|
|
@@ -1598,6 +1717,7 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1598
1717
|
if (mesh === null || mesh._buffers === null || mesh._node === null || instance._node === null) return
|
|
1599
1718
|
spatial.setBounds(instance._node, geometryBounds(mesh.geometry))
|
|
1600
1719
|
spatial.setShape(instance._node, mesh._buffers.shape)
|
|
1720
|
+
spatial.setLayers(instance._node, mesh.layers)
|
|
1601
1721
|
// The record is the instance's placement inside the mesh (the mesh
|
|
1602
1722
|
// node is the anchor), staged by the core at the next flush.
|
|
1603
1723
|
spatial.bindMatrixRecord(instance._node, mesh._instances.buffer, instance._slot, mesh._node)
|
|
@@ -1674,13 +1794,9 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1674
1794
|
lightsDirty = true
|
|
1675
1795
|
hooks._schedule()
|
|
1676
1796
|
|
|
1677
|
-
//
|
|
1678
|
-
//
|
|
1679
|
-
|
|
1680
|
-
let pointer = makePointerInput({
|
|
1681
|
-
pick: (x, y) => scene.pick(x, y),
|
|
1682
|
-
targetSize: () => ({ width, height }),
|
|
1683
|
-
})
|
|
1797
|
+
// The scene's root listeners (scene.listen): the last stop of the
|
|
1798
|
+
// pointer walk scene-pointer.ts runs behind scene.handlers.
|
|
1799
|
+
let listeners = new Set<ScenePointerListener>()
|
|
1684
1800
|
|
|
1685
1801
|
let scene: Scene = {
|
|
1686
1802
|
texture,
|
|
@@ -1830,7 +1946,7 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1830
1946
|
// microtask still runs and finds nothing dirty - harmless.)
|
|
1831
1947
|
if (scheduled) sync()
|
|
1832
1948
|
if (disposed) return []
|
|
1833
|
-
let
|
|
1949
|
+
let f = queryFilter(rayOpts, "raycast")
|
|
1834
1950
|
let hits: Hit[] = []
|
|
1835
1951
|
rayOriginScratch[0] = origin[0]
|
|
1836
1952
|
rayOriginScratch[1] = origin[1]
|
|
@@ -1838,8 +1954,8 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1838
1954
|
rayDirScratch[0] = direction[0]
|
|
1839
1955
|
rayDirScratch[1] = direction[1]
|
|
1840
1956
|
rayDirScratch[2] = direction[2]
|
|
1841
|
-
for (let h of spatial.raycast(rayOriginScratch, rayDirScratch)) {
|
|
1842
|
-
let leaf =
|
|
1957
|
+
for (let h of spatial.raycast(rayOriginScratch, rayDirScratch, f)) {
|
|
1958
|
+
let leaf = leafOf(h.node)
|
|
1843
1959
|
if (leaf === null) continue
|
|
1844
1960
|
let hit: Hit = { mesh: leaf.mesh, distance: h.distance, point: h.point, normal: h.normal }
|
|
1845
1961
|
if (leaf.instance !== null) hit.instance = leaf.instance
|
|
@@ -1852,11 +1968,11 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1852
1968
|
overlap(volume, qopts) {
|
|
1853
1969
|
if (scheduled) sync()
|
|
1854
1970
|
if (disposed) return []
|
|
1855
|
-
let
|
|
1971
|
+
let f = queryFilter(qopts, "overlap")
|
|
1856
1972
|
let kind = packVolume(volume, "overlap")
|
|
1857
1973
|
let out: Overlap[] = []
|
|
1858
|
-
for (let h of spatial.overlap(kind, kind === "box" ? boxScratch : capsuleScratch)) {
|
|
1859
|
-
let leaf =
|
|
1974
|
+
for (let h of spatial.overlap(kind, kind === "box" ? boxScratch : capsuleScratch, f)) {
|
|
1975
|
+
let leaf = leafOf(h.node)
|
|
1860
1976
|
if (leaf === null) continue
|
|
1861
1977
|
let contact: Overlap = { mesh: leaf.mesh, point: h.point, normal: h.normal, depth: h.depth }
|
|
1862
1978
|
if (leaf.instance !== null) contact.instance = leaf.instance
|
|
@@ -1867,27 +1983,53 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1867
1983
|
sweep(volume, motion, qopts) {
|
|
1868
1984
|
if (scheduled) sync()
|
|
1869
1985
|
if (disposed) return []
|
|
1870
|
-
let
|
|
1986
|
+
let f = queryFilter(qopts, "sweep")
|
|
1871
1987
|
let kind = packVolume(volume, "sweep")
|
|
1872
1988
|
motionScratch[0] = motion[0]
|
|
1873
1989
|
motionScratch[1] = motion[1]
|
|
1874
1990
|
motionScratch[2] = motion[2]
|
|
1875
1991
|
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)
|
|
1992
|
+
for (let h of spatial.sweep(kind, kind === "box" ? boxScratch : capsuleScratch, motionScratch, f)) {
|
|
1993
|
+
let impact = impactOf(h)
|
|
1994
|
+
if (impact !== null) out.push(impact)
|
|
1882
1995
|
}
|
|
1883
1996
|
return out
|
|
1884
1997
|
},
|
|
1885
|
-
|
|
1886
|
-
|
|
1998
|
+
moveAndSlide(volume, motion, mopts) {
|
|
1999
|
+
if (scheduled) sync()
|
|
2000
|
+
let kind = packVolume(volume, "moveAndSlide")
|
|
2001
|
+
motionScratch[0] = motion[0]
|
|
2002
|
+
motionScratch[1] = motion[1]
|
|
2003
|
+
motionScratch[2] = motion[2]
|
|
2004
|
+
if (disposed) return { motion: [motion[0], motion[1], motion[2]], floor: null, wall: false, ceiling: false, hits: [] }
|
|
2005
|
+
let f = queryFilter(mopts, "moveAndSlide")
|
|
2006
|
+
let r = spatial.moveAndSlide(kind, kind === "box" ? boxScratch : capsuleScratch, motionScratch, mopts, f)
|
|
2007
|
+
let hits: Impact[] = []
|
|
2008
|
+
for (let h of r.hits) {
|
|
2009
|
+
let impact = impactOf(h)
|
|
2010
|
+
if (impact !== null) hits.push(impact)
|
|
2011
|
+
}
|
|
2012
|
+
return { motion: r.motion, floor: r.floor, wall: r.wall, ceiling: r.ceiling, hits }
|
|
2013
|
+
},
|
|
2014
|
+
get handlers() {
|
|
2015
|
+
return pointer.handlers
|
|
2016
|
+
},
|
|
2017
|
+
handlersFor(layout) {
|
|
2018
|
+
return pointer.handlersFor(layout)
|
|
2019
|
+
},
|
|
2020
|
+
listen(listener) {
|
|
2021
|
+
listeners.add(listener)
|
|
2022
|
+
return () => {
|
|
2023
|
+
listeners.delete(listener)
|
|
2024
|
+
}
|
|
2025
|
+
},
|
|
1887
2026
|
createView(vopts) {
|
|
1888
2027
|
if (disposed) throw new Error("createView: the scene is disposed")
|
|
1889
2028
|
let v = makeView(vopts, null)
|
|
1890
|
-
|
|
2029
|
+
// The view's own walk: its listeners, its camera's pick, and a
|
|
2030
|
+
// dispatch with its own capture and hover bookkeeping.
|
|
2031
|
+
let viewListeners = new Set<ScenePointerListener>()
|
|
2032
|
+
let handle: ViewHandle = {
|
|
1891
2033
|
texture: v.texture,
|
|
1892
2034
|
depthTexture: vopts.depth === "texture" && vopts.into === undefined ? depthTexture(v.texture) : null,
|
|
1893
2035
|
setCamera(update) {
|
|
@@ -1927,10 +2069,35 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1927
2069
|
}
|
|
1928
2070
|
hooks._schedule()
|
|
1929
2071
|
},
|
|
2072
|
+
pick(x, y) {
|
|
2073
|
+
if (v.disposed) return []
|
|
2074
|
+
pixelRayOf(v.camera, v.width, v.height, x, y)
|
|
2075
|
+
return scene.raycast(pickOrigin, pickDir)
|
|
2076
|
+
},
|
|
2077
|
+
get handlers() {
|
|
2078
|
+
return viewPointer.handlers
|
|
2079
|
+
},
|
|
2080
|
+
handlersFor(layout) {
|
|
2081
|
+
return viewPointer.handlersFor(layout)
|
|
2082
|
+
},
|
|
2083
|
+
listen(listener) {
|
|
2084
|
+
viewListeners.add(listener)
|
|
2085
|
+
return () => {
|
|
2086
|
+
viewListeners.delete(listener)
|
|
2087
|
+
}
|
|
2088
|
+
},
|
|
1930
2089
|
dispose() {
|
|
2090
|
+
viewListeners.clear()
|
|
1931
2091
|
disposeView(v)
|
|
1932
2092
|
},
|
|
1933
2093
|
}
|
|
2094
|
+
let viewPointer = makePointerInput({
|
|
2095
|
+
pick: (x, y) => handle.pick(x, y),
|
|
2096
|
+
targetSize: () => ({ width: v.width, height: v.height }),
|
|
2097
|
+
root: handle,
|
|
2098
|
+
listeners: viewListeners,
|
|
2099
|
+
})
|
|
2100
|
+
return handle
|
|
1934
2101
|
},
|
|
1935
2102
|
createReflectionProbe(popts) {
|
|
1936
2103
|
if (disposed) throw new Error("createReflectionProbe: the scene is disposed")
|
|
@@ -1950,6 +2117,7 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1950
2117
|
dispose() {
|
|
1951
2118
|
if (disposed) return
|
|
1952
2119
|
disposed = true
|
|
2120
|
+
listeners.clear()
|
|
1953
2121
|
// Full tree-side teardown, not just the target: every node leaves
|
|
1954
2122
|
// the scene (entries' geometry-buffer references and pick leaves
|
|
1955
2123
|
// dropped, core nodes freed), so a disposed scene leaves no
|
|
@@ -1977,6 +2145,15 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1977
2145
|
}
|
|
1978
2146
|
},
|
|
1979
2147
|
}
|
|
2148
|
+
// Pointer dispatch (scene.handlers): capture/hover bookkeeping, the
|
|
2149
|
+
// walk and the tap tracker live in scene-pointer.ts; all it needs of
|
|
2150
|
+
// the scene is pick(), the target size, and the root it ends at.
|
|
2151
|
+
let pointer = makePointerInput({
|
|
2152
|
+
pick: (x, y) => scene.pick(x, y),
|
|
2153
|
+
targetSize: () => ({ width, height }),
|
|
2154
|
+
root: scene,
|
|
2155
|
+
listeners,
|
|
2156
|
+
})
|
|
1980
2157
|
// A reflection probe over layer mask `mask` (see createReflectionProbe):
|
|
1981
2158
|
// the public object plus `finish`, which keeps the prefiltered chain and
|
|
1982
2159
|
// drops everything else - the bake's one-shot use.
|