@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/first-person.ts
CHANGED
|
@@ -1,59 +1,53 @@
|
|
|
1
|
-
// A first-person camera for a scene: a position plus yaw/pitch,
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
1
|
+
// A first-person camera for a scene: a position plus yaw/pitch, look and
|
|
2
|
+
// move in one control - Unity's FirstPersonController shape, rather than
|
|
3
|
+
// Three's split into PointerLockControls (look only) and a hand-written
|
|
4
|
+
// key loop. The control is pure (ARCHITECTURE.md: controls through an
|
|
5
|
+
// abstraction, never direct event handling): it consumes three axes,
|
|
6
|
+
// `look` (vec2), `move` (vec2) and `rise` (axis), in device-free units,
|
|
7
|
+
// and exposes pose verbs for scripted moves. Mouse look under pointer
|
|
8
|
+
// lock, drag-to-look on touch, WASD, sticks: all of that is the app's
|
|
9
|
+
// wiring through an input map, and `firstPersonBindings` in input.ts is
|
|
10
|
+
// the standard set.
|
|
6
11
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
// Move input: WASD/arrows (physical codes AND logical keys, so a layout
|
|
17
|
-
// or a synthetic event without a code both work), Q/E for down/up in fly
|
|
18
|
-
// mode, and the left stick. Walking flattens the forward vector onto the
|
|
19
|
-
// ground plane at fixed height; flying moves along the view direction.
|
|
12
|
+
// `look` deltas are element heights of travel (a drag across the element
|
|
13
|
+
// sweeps DRAG_TURNS turns) or the pointer feed's mouse-motion
|
|
14
|
+
// equivalent; a rate (a stick) turns at LOOK_RATE turns per second at
|
|
15
|
+
// full deflection. `move` is [right, forward] in the screen convention
|
|
16
|
+
// (forward is -y, as a stick pushed up reads): a rate walks at
|
|
17
|
+
// `moveSpeed`, a delta is a step in world units. `rise` moves along
|
|
18
|
+
// world up at the same speed, in fly mode only. Walking flattens the
|
|
19
|
+
// forward vector onto the ground plane at fixed height; flying moves
|
|
20
|
+
// along the view direction.
|
|
20
21
|
//
|
|
21
|
-
// Pose is plain mutable state
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
// control tracks them from the down/up pair; an `onBlur` handler drops
|
|
28
|
-
// them all, because the up never arrives once focus has left.
|
|
22
|
+
// Pose is plain mutable state; a nudge or a verb pushes it at once (a
|
|
23
|
+
// mouse move under lock needs no frame loop), and update(dt) integrates
|
|
24
|
+
// the rates from the app's own onFrame. Only `active` is reactive: the
|
|
25
|
+
// frame-loop gate, true while any rate reads non-zero (a held key, a
|
|
26
|
+
// deflected stick), so a still scene renders nothing new. update()
|
|
27
|
+
// returns whether the pose changed since the previous update.
|
|
29
28
|
//
|
|
30
29
|
// Collision is deliberately absent: a camera control cannot know the
|
|
31
30
|
// level. Clamp or reject positions through `clampPosition`.
|
|
32
31
|
//
|
|
33
32
|
// Options are read where they apply, not copied out: `fly`, `moveSpeed`,
|
|
34
|
-
// `lookSpeed`, the pitch clamps
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
33
|
+
// `lookSpeed`, the pitch clamps and `clampPosition` are re-read from the
|
|
34
|
+
// options object on every input or update, so a caller may change a field
|
|
35
|
+
// (or hand in an object of getters, which is what `<FirstPersonCamera>`
|
|
36
|
+
// does with its props) and the next move sees it - walk and fly are one
|
|
37
|
+
// control, not two mounts. Only the initial pose is copied at creation;
|
|
38
|
+
// later pose changes go through set() and the verbs.
|
|
40
39
|
|
|
41
|
-
import {
|
|
42
|
-
import {
|
|
43
|
-
import type {
|
|
40
|
+
import { untrack } from "@solidjs/signals"
|
|
41
|
+
import { createAxes } from "@solidrt/core/input"
|
|
42
|
+
import type { Axes, Vec2 } from "@solidrt/core/input"
|
|
44
43
|
import type { CameraUpdate } from "./camera.ts"
|
|
45
44
|
import type { Vec3 } from "./math.ts"
|
|
46
45
|
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
// `viewport` the drag is viewport-relative, one viewport height sweeping
|
|
50
|
-
// DRAG_TURNS turns, and without one it falls back to the mouse figure.
|
|
51
|
-
// The stick turns at LOOK_STICK radians/second at full deflection, with
|
|
52
|
-
// a dead zone below which a resting stick reads as zero.
|
|
53
|
-
const LOOK_MOUSE = 0.002
|
|
46
|
+
// One element height of drag sweeps this many turns of look (half a turn:
|
|
47
|
+
// a drag across the screen turns the walker around).
|
|
54
48
|
const DRAG_TURNS = 0.5
|
|
55
|
-
|
|
56
|
-
const
|
|
49
|
+
// Look rate at full stick deflection, turns per second.
|
|
50
|
+
const LOOK_RATE = 0.4
|
|
57
51
|
// Default walking speed, world units per second.
|
|
58
52
|
const MOVE_SPEED = 3
|
|
59
53
|
// Pitch clamps stop short of the poles so the look direction never
|
|
@@ -80,17 +74,14 @@ export type FirstPersonCameraOptions = {
|
|
|
80
74
|
maxPitch?: number
|
|
81
75
|
/** Movement speed in world units per second (default 3). */
|
|
82
76
|
moveSpeed?: number
|
|
83
|
-
/** Multiplier over the built-in
|
|
77
|
+
/** Multiplier over the built-in look sensitivities (drags, mouse motion
|
|
78
|
+
* and rates alike). */
|
|
84
79
|
lookSpeed?: number
|
|
85
80
|
/** Walk (default) keeps the height fixed and moves along the ground
|
|
86
|
-
* projection of the view; fly moves along the view itself and
|
|
87
|
-
*
|
|
88
|
-
*
|
|
81
|
+
* projection of the view; fly moves along the view itself and lets
|
|
82
|
+
* `rise` move along world up. Toggling it on a running control keeps
|
|
83
|
+
* the pose. */
|
|
89
84
|
fly?: boolean
|
|
90
|
-
/** The viewport a drag lives in: the input element's own laid-out
|
|
91
|
-
* height (the frame the recognizer's deltas arrive in); makes the drag
|
|
92
|
-
* viewport-relative. Null while unknown. */
|
|
93
|
-
viewport?: () => { height: number } | null
|
|
94
85
|
/** Constrain where a move may put the eye: called with the eye the
|
|
95
86
|
* move asks for and the eye it starts from (both fresh arrays), returns
|
|
96
87
|
* the position to use - a level's bounds, a floor height, a collision
|
|
@@ -105,6 +96,8 @@ export type FirstPersonPose = {
|
|
|
105
96
|
pitch?: number
|
|
106
97
|
}
|
|
107
98
|
|
|
99
|
+
export type FirstPersonAxes = { look: "vec2"; move: "vec2"; rise: "axis" }
|
|
100
|
+
|
|
108
101
|
export type FirstPersonCamera = {
|
|
109
102
|
/** Eye position (a fresh array per call). */
|
|
110
103
|
eye(): Vec3
|
|
@@ -112,195 +105,154 @@ export type FirstPersonCamera = {
|
|
|
112
105
|
forward(): Vec3
|
|
113
106
|
/** Pose snapshot - the shape debug commands return and set() takes. */
|
|
114
107
|
pose(): { position: Vec3; yaw: number; pitch: number }
|
|
115
|
-
/** Merge a pose in (clamps apply)
|
|
116
|
-
* update(). */
|
|
108
|
+
/** Merge a pose in (clamps apply) and push it. */
|
|
117
109
|
set(pose: FirstPersonPose): void
|
|
118
|
-
/** Whether
|
|
119
|
-
*
|
|
110
|
+
/** Whether any axis rate reads non-zero - what a frame loop should run
|
|
111
|
+
* on. Reactive. */
|
|
120
112
|
active(): boolean
|
|
121
|
-
/** Integrate
|
|
122
|
-
*
|
|
113
|
+
/** Integrate the axis rates over dt seconds and push any pose change;
|
|
114
|
+
* returns whether the pose changed since the previous update (nudges
|
|
115
|
+
* and verbs included). */
|
|
123
116
|
update(dt: number): boolean
|
|
124
|
-
/**
|
|
125
|
-
*
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
117
|
+
/** The input abstraction: `look` (vec2, element heights of drag / turns
|
|
118
|
+
* per second), `move` (vec2 [right, forward], forward = -y; world units
|
|
119
|
+
* per delta, `moveSpeed` per second) and `rise` (axis, fly only). */
|
|
120
|
+
axes: Axes<FirstPersonAxes>
|
|
121
|
+
/** Turn by radians (yaw positive left, pitch positive up; clamps
|
|
122
|
+
* apply) and push. */
|
|
123
|
+
lookBy(yaw: number, pitch: number): void
|
|
124
|
+
/** Step by world units in the walker's frame: right, forward (ground
|
|
125
|
+
* projection when walking, the view direction when flying) and up
|
|
126
|
+
* (world up, fly mode only), through `clampPosition`, and push. */
|
|
127
|
+
moveBy(right: number, forward: number, up?: number): void
|
|
134
128
|
}
|
|
135
129
|
|
|
136
|
-
|
|
130
|
+
let clampNum = (v: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, v))
|
|
137
131
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const KEY_MOVES: Record<string, Move> = {
|
|
141
|
-
KeyW: "forward", ArrowUp: "forward", w: "forward", W: "forward",
|
|
142
|
-
KeyS: "back", ArrowDown: "back", s: "back", S: "back",
|
|
143
|
-
KeyA: "left", ArrowLeft: "left", a: "left", A: "left",
|
|
144
|
-
KeyD: "right", ArrowRight: "right", d: "right", D: "right",
|
|
145
|
-
KeyE: "up", e: "up", E: "up",
|
|
146
|
-
KeyQ: "down", q: "down", Q: "down",
|
|
132
|
+
function finite(what: string, v: number): void {
|
|
133
|
+
if (!Number.isFinite(v)) throw new Error(`createFirstPersonCamera: ${what} must be a finite number, got ${v}`)
|
|
147
134
|
}
|
|
148
135
|
|
|
149
|
-
let clampNum = (v: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, v))
|
|
150
|
-
let deadzone = (v: number) => (Math.abs(v) < STICK_DEADZONE ? 0 : v)
|
|
151
|
-
|
|
152
136
|
/**
|
|
153
137
|
* Create a first-person camera driving `camera`'s position and target,
|
|
154
138
|
* where `camera` is a Scene or one of its Views (fov, near, and far stay
|
|
155
139
|
* yours via its setCamera). The initial pose applies immediately. In a
|
|
156
|
-
* component tree, prefer the `<FirstPersonCamera>` component: it
|
|
157
|
-
* scene
|
|
158
|
-
*
|
|
140
|
+
* component tree, prefer the `<FirstPersonCamera>` component: it drives
|
|
141
|
+
* the enclosing scene or view through context and takes an input map as
|
|
142
|
+
* a prop.
|
|
159
143
|
*/
|
|
160
144
|
export function createFirstPersonCamera(camera: FirstPersonTarget, options: FirstPersonCameraOptions = {}): FirstPersonCamera {
|
|
145
|
+
if (!camera || typeof camera.setCamera !== "function") throw new Error("createFirstPersonCamera: the target needs setCamera() (a Scene or a View)")
|
|
161
146
|
let position: Vec3 = options.position ? [options.position[0], options.position[1], options.position[2]] : [0, 1.6, 0]
|
|
162
147
|
let yaw = options.yaw ?? 0
|
|
163
148
|
let pitch = options.pitch ?? 0
|
|
164
149
|
// Everything below the pose is read from `options` where it applies.
|
|
165
150
|
let lookSpeed = () => options.lookSpeed ?? 1
|
|
151
|
+
let moveSpeed = () => options.moveSpeed ?? MOVE_SPEED
|
|
166
152
|
let clampPitch = () => {
|
|
167
153
|
pitch = clampNum(pitch, options.minPitch ?? -PITCH_LIMIT, options.maxPitch ?? PITCH_LIMIT)
|
|
168
154
|
}
|
|
169
|
-
|
|
170
|
-
let held = new Set<Move>()
|
|
171
|
-
let [heldCount, setHeldCount] = createSignal(0)
|
|
172
|
-
let dirty = false
|
|
155
|
+
let changed = false
|
|
173
156
|
|
|
174
157
|
let forward = (): Vec3 => {
|
|
175
158
|
let cp = Math.cos(pitch)
|
|
176
159
|
return [-Math.sin(yaw) * cp, Math.sin(pitch), -Math.cos(yaw) * cp]
|
|
177
160
|
}
|
|
178
|
-
let
|
|
161
|
+
let push = () => {
|
|
162
|
+
changed = true
|
|
179
163
|
let f = forward()
|
|
180
164
|
camera.setCamera({ position: [position[0], position[1], position[2]], target: [position[0] + f[0], position[1] + f[1], position[2] + f[2]] })
|
|
181
165
|
}
|
|
182
|
-
let look = (
|
|
183
|
-
yaw
|
|
184
|
-
pitch
|
|
166
|
+
let look = (dYaw: number, dPitch: number) => {
|
|
167
|
+
yaw += dYaw
|
|
168
|
+
pitch += dPitch
|
|
185
169
|
clampPitch()
|
|
186
|
-
dirty = true
|
|
187
170
|
}
|
|
188
|
-
|
|
189
|
-
|
|
171
|
+
// A step in the walker's frame. Walking projects the view onto the
|
|
172
|
+
// ground plane, so looking down does not slow the walk; right is always
|
|
173
|
+
// horizontal; up applies in fly mode only.
|
|
174
|
+
let step = (right: number, ahead: number, up: number) => {
|
|
175
|
+
let fly = options.fly ?? false
|
|
176
|
+
let f = forward()
|
|
177
|
+
let fx = fly ? f[0] : -Math.sin(yaw)
|
|
178
|
+
let fy = fly ? f[1] : 0
|
|
179
|
+
let fz = fly ? f[2] : -Math.cos(yaw)
|
|
180
|
+
let rx = Math.cos(yaw)
|
|
181
|
+
let rz = -Math.sin(yaw)
|
|
182
|
+
let rise = fly ? up : 0
|
|
183
|
+
let next: Vec3 = [position[0] + fx * ahead + rx * right, position[1] + fy * ahead + rise, position[2] + fz * ahead + rz * right]
|
|
190
184
|
position = options.clampPosition ? options.clampPosition(next, [position[0], position[1], position[2]]) : next
|
|
191
|
-
dirty = true
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// The stick axes of every connected pad, summed: a game with one player
|
|
195
|
-
// sees its one pad, and a pad at rest contributes nothing.
|
|
196
|
-
let sticks = () => {
|
|
197
|
-
let s = { moveX: 0, moveY: 0, lookX: 0, lookY: 0 }
|
|
198
|
-
for (let pad of gamepads()) {
|
|
199
|
-
if (!pad) continue
|
|
200
|
-
s.moveX += deadzone(pad.axes.leftX ?? 0)
|
|
201
|
-
s.moveY += deadzone(pad.axes.leftY ?? 0)
|
|
202
|
-
s.lookX += deadzone(pad.axes.rightX ?? 0)
|
|
203
|
-
s.lookY += deadzone(pad.axes.rightY ?? 0)
|
|
204
|
-
}
|
|
205
|
-
return s
|
|
206
185
|
}
|
|
207
|
-
let active = createMemo(() => {
|
|
208
|
-
let s = sticks()
|
|
209
|
-
return heldCount() > 0 || s.moveX !== 0 || s.moveY !== 0 || s.lookX !== 0 || s.lookY !== 0
|
|
210
|
-
})
|
|
211
186
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
187
|
+
let axes = createAxes<FirstPersonAxes>(
|
|
188
|
+
{ look: "vec2", move: "vec2", rise: "axis" },
|
|
189
|
+
{
|
|
190
|
+
onNudge: (name, delta) => {
|
|
191
|
+
if (name === "look") {
|
|
192
|
+
let d = delta as Vec2
|
|
193
|
+
let rel = DRAG_TURNS * 2 * Math.PI * lookSpeed()
|
|
194
|
+
look(-d[0] * rel, -d[1] * rel)
|
|
195
|
+
} else if (name === "move") {
|
|
196
|
+
let d = delta as Vec2
|
|
197
|
+
step(d[0], -d[1], 0)
|
|
198
|
+
} else {
|
|
199
|
+
step(0, 0, delta as number)
|
|
200
|
+
}
|
|
201
|
+
push()
|
|
202
|
+
},
|
|
221
203
|
},
|
|
222
|
-
|
|
204
|
+
)
|
|
223
205
|
|
|
224
206
|
clampPitch()
|
|
225
|
-
|
|
207
|
+
push()
|
|
208
|
+
changed = false
|
|
226
209
|
|
|
227
210
|
return {
|
|
228
211
|
eye: () => [position[0], position[1], position[2]],
|
|
229
212
|
forward,
|
|
230
213
|
pose: () => ({ position: [position[0], position[1], position[2]], yaw, pitch }),
|
|
231
|
-
active,
|
|
214
|
+
active: axes.active,
|
|
215
|
+
axes,
|
|
232
216
|
set(pose) {
|
|
233
217
|
if (pose.position) position = [pose.position[0], pose.position[1], pose.position[2]]
|
|
234
218
|
if (pose.yaw !== undefined) yaw = pose.yaw
|
|
235
219
|
if (pose.pitch !== undefined) pitch = pose.pitch
|
|
236
220
|
clampPitch()
|
|
237
|
-
|
|
221
|
+
push()
|
|
238
222
|
},
|
|
239
223
|
update(dt) {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
224
|
+
finite("update dt", dt)
|
|
225
|
+
let moved = false
|
|
226
|
+
let [lx, ly] = untrack(() => axes.rate("look"))
|
|
227
|
+
if (lx !== 0 || ly !== 0) {
|
|
228
|
+
let rate = LOOK_RATE * 2 * Math.PI * lookSpeed() * dt
|
|
229
|
+
look(-lx * rate, -ly * rate)
|
|
230
|
+
moved = true
|
|
244
231
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
// between the rise and the heading of the same step.
|
|
252
|
-
let fly = options.fly ?? false
|
|
253
|
-
let rise = fly ? (held.has("up") ? 1 : 0) - (held.has("down") ? 1 : 0) : 0
|
|
254
|
-
if (ahead !== 0 || side !== 0 || rise !== 0) {
|
|
255
|
-
let step = (options.moveSpeed ?? MOVE_SPEED) * dt
|
|
256
|
-
let f = forward()
|
|
257
|
-
// Walking projects the view onto the ground plane, so looking down
|
|
258
|
-
// does not slow the walk; right is always horizontal.
|
|
259
|
-
let fx = fly ? f[0] : -Math.sin(yaw)
|
|
260
|
-
let fy = fly ? f[1] : 0
|
|
261
|
-
let fz = fly ? f[2] : -Math.cos(yaw)
|
|
262
|
-
let rx = Math.cos(yaw)
|
|
263
|
-
let rz = -Math.sin(yaw)
|
|
264
|
-
moveBy((fx * ahead + rx * side) * step, (fy * ahead + rise) * step, (fz * ahead + rz * side) * step)
|
|
232
|
+
let [mx, my] = untrack(() => axes.rate("move"))
|
|
233
|
+
let rise = untrack(() => axes.rate("rise"))
|
|
234
|
+
if (mx !== 0 || my !== 0 || rise !== 0) {
|
|
235
|
+
let speed = moveSpeed() * dt
|
|
236
|
+
step(mx * speed, -my * speed, rise * speed)
|
|
237
|
+
moved = true
|
|
265
238
|
}
|
|
266
|
-
if (
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
return
|
|
239
|
+
if (moved) push()
|
|
240
|
+
let result = changed
|
|
241
|
+
changed = false
|
|
242
|
+
return result
|
|
270
243
|
},
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
}
|
|
284
|
-
},
|
|
285
|
-
onPointerUp(e) {
|
|
286
|
-
transform.handlers.onPointerUp(e)
|
|
287
|
-
},
|
|
288
|
-
onKeyDown(e) {
|
|
289
|
-
let move = KEY_MOVES[e.code] ?? KEY_MOVES[e.key]
|
|
290
|
-
if (!move) return
|
|
291
|
-
held.add(move)
|
|
292
|
-
setHeldCount(held.size)
|
|
293
|
-
},
|
|
294
|
-
onKeyUp(e) {
|
|
295
|
-
let move = KEY_MOVES[e.code] ?? KEY_MOVES[e.key]
|
|
296
|
-
if (!move) return
|
|
297
|
-
held.delete(move)
|
|
298
|
-
setHeldCount(held.size)
|
|
299
|
-
},
|
|
300
|
-
onBlur() {
|
|
301
|
-
held.clear()
|
|
302
|
-
setHeldCount(0)
|
|
303
|
-
},
|
|
244
|
+
lookBy(dYaw, dPitch) {
|
|
245
|
+
finite("lookBy yaw", dYaw)
|
|
246
|
+
finite("lookBy pitch", dPitch)
|
|
247
|
+
look(dYaw, dPitch)
|
|
248
|
+
push()
|
|
249
|
+
},
|
|
250
|
+
moveBy(right, ahead, up = 0) {
|
|
251
|
+
finite("moveBy right", right)
|
|
252
|
+
finite("moveBy forward", ahead)
|
|
253
|
+
finite("moveBy up", up)
|
|
254
|
+
step(right, ahead, up)
|
|
255
|
+
push()
|
|
304
256
|
},
|
|
305
257
|
}
|
|
306
258
|
}
|
package/src/geometry-gpu.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { createBuffer, destroyBuffer } from "@solidrt/core/gpu"
|
|
|
15
15
|
import type { BufferId, IndexFormat } from "@solidrt/core/gpu"
|
|
16
16
|
import { createShape, destroyShape } from "flux:spatial"
|
|
17
17
|
import type { ShapeId } from "flux:spatial"
|
|
18
|
-
import { layoutStride } from "./geometry.ts"
|
|
18
|
+
import { geometryTopology, layoutStride } from "./geometry.ts"
|
|
19
19
|
import type { Geometry } from "./geometry.ts"
|
|
20
20
|
|
|
21
21
|
/** An acquired reference to a geometry's GPU buffers: what a draw entry
|
|
@@ -26,14 +26,45 @@ export type GeometryBuffers = {
|
|
|
26
26
|
buffer: BufferId
|
|
27
27
|
index: BufferId
|
|
28
28
|
indexFormat: IndexFormat
|
|
29
|
-
/** The picking shape (positions at 0, uv at 6 of every layout)
|
|
30
|
-
|
|
29
|
+
/** The picking shape (positions at 0, uv at 6 of every layout); null
|
|
30
|
+
* for anything but a triangle list, which picks by its box. */
|
|
31
|
+
shape: ShapeId | null
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
type GpuEntry = GeometryBuffers & { geometry: Geometry; refs: number }
|
|
34
|
+
type GpuEntry = GeometryBuffers & { geometry: Geometry; vertices: Float32Array; refs: number }
|
|
34
35
|
|
|
35
36
|
let entries = new WeakMap<Geometry, GpuEntry>()
|
|
36
37
|
|
|
38
|
+
/** Vertex uploads keyed by the array itself: geometries sharing one
|
|
39
|
+
* vertex array (a wireframe or edges geometry over its source's) share
|
|
40
|
+
* one GPU buffer, each entry holding a reference to it. */
|
|
41
|
+
let vertexUploads = new WeakMap<Float32Array, { buffer: BufferId; refs: number }>()
|
|
42
|
+
|
|
43
|
+
function acquireVertices(geometry: Geometry): BufferId {
|
|
44
|
+
let upload = vertexUploads.get(geometry.vertices)
|
|
45
|
+
if (upload === undefined) {
|
|
46
|
+
upload = {
|
|
47
|
+
buffer: createBuffer(geometry.vertices, {
|
|
48
|
+
autoFree: false,
|
|
49
|
+
label: geometry.label ? geometry.label + "-verts" : undefined,
|
|
50
|
+
}),
|
|
51
|
+
refs: 0,
|
|
52
|
+
}
|
|
53
|
+
vertexUploads.set(geometry.vertices, upload)
|
|
54
|
+
}
|
|
55
|
+
upload.refs++
|
|
56
|
+
return upload.buffer
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function releaseVertices(vertices: Float32Array): void {
|
|
60
|
+
let upload = vertexUploads.get(vertices)
|
|
61
|
+
if (upload === undefined) return
|
|
62
|
+
upload.refs--
|
|
63
|
+
if (upload.refs > 0) return
|
|
64
|
+
vertexUploads.delete(vertices)
|
|
65
|
+
destroyBuffer(upload.buffer)
|
|
66
|
+
}
|
|
67
|
+
|
|
37
68
|
/** The geometry's GPU buffers, created on first use, plus the index format
|
|
38
69
|
* the draw entry must bind them with. Takes a reference - pair every
|
|
39
70
|
* acquire with a releaseGeometryBuffers of the returned token when the
|
|
@@ -43,16 +74,17 @@ export function acquireGeometryBuffers(geometry: Geometry): GeometryBuffers {
|
|
|
43
74
|
if (entry === undefined) {
|
|
44
75
|
entry = {
|
|
45
76
|
geometry,
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
label: geometry.label ? geometry.label + "-verts" : undefined,
|
|
49
|
-
}),
|
|
77
|
+
vertices: geometry.vertices,
|
|
78
|
+
buffer: acquireVertices(geometry),
|
|
50
79
|
index: createBuffer(geometry.indices, {
|
|
51
80
|
autoFree: false,
|
|
52
81
|
label: geometry.label ? geometry.label + "-indices" : undefined,
|
|
53
82
|
}),
|
|
54
83
|
indexFormat: geometry.indices instanceof Uint32Array ? "uint32" : "uint16",
|
|
55
|
-
shape:
|
|
84
|
+
shape:
|
|
85
|
+
geometryTopology(geometry) === "triangles"
|
|
86
|
+
? createShape(geometry.vertices, layoutStride(geometry.layout), 0, 6, geometry.indices)
|
|
87
|
+
: null,
|
|
56
88
|
refs: 0,
|
|
57
89
|
}
|
|
58
90
|
entries.set(geometry, entry)
|
|
@@ -74,9 +106,9 @@ export function releaseGeometryBuffers(acquired: GeometryBuffers): void {
|
|
|
74
106
|
queueMicrotask(() => {
|
|
75
107
|
if (entries.get(entry.geometry) !== entry || entry.refs > 0) return
|
|
76
108
|
entries.delete(entry.geometry)
|
|
77
|
-
|
|
109
|
+
releaseVertices(entry.vertices)
|
|
78
110
|
destroyBuffer(entry.index)
|
|
79
|
-
destroyShape(entry.shape)
|
|
111
|
+
if (entry.shape !== null) destroyShape(entry.shape)
|
|
80
112
|
})
|
|
81
113
|
}
|
|
82
114
|
|
|
@@ -91,7 +123,7 @@ export function disposeGeometry(geometry: Geometry): void {
|
|
|
91
123
|
let entry = entries.get(geometry)
|
|
92
124
|
if (entry === undefined) return
|
|
93
125
|
entries.delete(geometry)
|
|
94
|
-
|
|
126
|
+
releaseVertices(entry.vertices)
|
|
95
127
|
destroyBuffer(entry.index)
|
|
96
|
-
destroyShape(entry.shape)
|
|
128
|
+
if (entry.shape !== null) destroyShape(entry.shape)
|
|
97
129
|
}
|