@solidrt/3d 0.0.55 → 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 +1206 -224
- package/README.md +16 -7
- package/examples/README.md +88 -9
- package/examples/aim.tsx +2 -3
- package/examples/cascades.tsx +1 -4
- package/examples/collision.tsx +263 -0
- package/examples/first-person.tsx +129 -0
- package/examples/fleet.tsx +200 -0
- package/examples/fog.tsx +2 -3
- package/examples/instanced.tsx +72 -32
- package/examples/lamps.tsx +92 -0
- package/examples/lit.tsx +3 -5
- package/examples/materials.tsx +150 -0
- package/examples/model-load.tsx +95 -0
- package/examples/model.glb +0 -0
- package/examples/model.tsx +35 -9
- package/examples/pick.tsx +145 -37
- package/examples/probe.tsx +107 -0
- package/examples/scene-background.tsx +2 -4
- package/examples/scene-basic.tsx +2 -4
- package/examples/scene-views.tsx +68 -48
- package/examples/shadows.tsx +2 -4
- package/examples/sky-lit.tsx +92 -0
- package/examples/skybox.tsx +134 -0
- package/examples/sprites.tsx +4 -6
- package/examples/standard.tsx +138 -0
- package/examples/sweep-paths.tsx +2 -4
- package/package.json +3 -3
- package/src/camera.ts +148 -0
- package/src/clip.ts +96 -0
- package/src/collision.ts +20 -0
- package/src/color.ts +32 -0
- 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/environment-bake.ts +492 -0
- package/src/environment.ts +325 -0
- package/src/first-person.ts +258 -0
- package/src/geometry.ts +6 -2
- package/src/glsl.ts +1343 -82
- package/src/gltf.ts +432 -112
- package/src/index.ts +61 -9
- package/src/input.ts +70 -0
- package/src/light.ts +383 -0
- package/src/material.ts +667 -232
- package/src/math.ts +110 -0
- package/src/mesh.ts +728 -0
- package/src/mixer.ts +476 -0
- package/src/model-file.ts +80 -15
- package/src/model.ts +204 -35
- package/src/node.ts +579 -0
- package/src/orbit.ts +292 -212
- package/src/scene-pointer.ts +294 -0
- package/src/scene-shadows.ts +471 -0
- package/src/scene.ts +1435 -1576
- package/tools/environment.ts +69 -0
- package/tools/model.ts +4 -3
- package/src/components.tsx +0 -420
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
|
|
@@ -27,350 +27,208 @@
|
|
|
27
27
|
// broadphase and its leaves, the transparent sort's centers and the light
|
|
28
28
|
// params. They read world matrices back from the core, and only for the
|
|
29
29
|
// subtrees that moved since they last looked.
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
//
|
|
31
|
+
// This file is the scene half: createScene and everything one scene
|
|
32
|
+
// instance owns (targets, views, sync, picking). The node layer it
|
|
33
|
+
// renders - the graph, transforms, meshes, lights - lives in node.ts,
|
|
34
|
+
// mesh.ts and light.ts, talking back through the SceneHooks seam
|
|
35
|
+
// (node.ts); the camera record is camera.ts, and the per-scene shadow
|
|
36
|
+
// and pointer subsystems are scene-shadows.ts / scene-pointer.ts,
|
|
37
|
+
// built here with the scene's seams as their deps.
|
|
38
|
+
|
|
39
|
+
import { addDraw, createCubeDrawTarget, createDrawTarget, depthTexture, destroyProgram, destroyRenderPipeline, destroyTexture, removeDraw, renderTarget, setDrawBuffers, setDrawOrder, setDrawParams, setDrawTextures, setTargetParams, setTargetRect, setTargetSize, setTargetTextures } from "@solidrt/core/gpu"
|
|
32
40
|
import * as spatial from "flux:spatial"
|
|
33
|
-
import type { NodeId,
|
|
34
|
-
import {
|
|
35
|
-
import type { BufferId, DrawId, FilterMode, ProgramId, RenderPipelineId, ShaderParams, TextureId, VertexAttribute, WrapMode } from "@solidrt/core/gpu"
|
|
41
|
+
import type { Impact as CoreImpact, NodeId, QueryFilter } from "flux:spatial"
|
|
42
|
+
import type { DrawId, FilterMode, ProgramId, RenderPipelineId, ShaderParams, TextureId, WrapMode } from "@solidrt/core/gpu"
|
|
36
43
|
import { getOwner, onCleanup } from "@solidrt/core"
|
|
37
|
-
import type { PointerEvent as ElementPointerEvent } from "@solidrt/core"
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
import {
|
|
41
|
-
import
|
|
42
|
-
import {
|
|
43
|
-
import {
|
|
44
|
-
import {
|
|
45
|
-
import
|
|
44
|
+
import type { PointerEvent as ElementPointerEvent, WheelEvent as ElementWheelEvent } from "@solidrt/core"
|
|
45
|
+
import { copy, mat4, transformPoint } from "./math.ts"
|
|
46
|
+
import { linearColor } from "./color.ts"
|
|
47
|
+
import type { Mat4, Quat, Vec3, Vec4 } from "./math.ts"
|
|
48
|
+
import { MAX_LIGHTS, MAX_SHADOW_MAPS } from "./glsl.ts"
|
|
49
|
+
import { cameraParams, cameraState, ensureCamera, makeCamera, updateCamera } from "./camera.ts"
|
|
50
|
+
import type { Camera, CameraState, CameraUpdate } from "./camera.ts"
|
|
51
|
+
import { makeShadowSystem } from "./scene-shadows.ts"
|
|
52
|
+
import { makePointerInput } from "./scene-pointer.ts"
|
|
53
|
+
import { geometryBounds, layoutKey, validateGeometry } from "./geometry.ts"
|
|
46
54
|
import type { Geometry } from "./geometry.ts"
|
|
47
|
-
import {
|
|
48
|
-
import {
|
|
55
|
+
import { acquireGeometryBuffers, releaseGeometryBuffers } from "./geometry-gpu.ts"
|
|
56
|
+
import { backgroundPipeline, missingAttributes, SKYBOX_FRAGMENT } from "./material.ts"
|
|
57
|
+
import { createEnvironmentPlaceholder, createPrefilter, probeFormat } from "./environment.ts"
|
|
58
|
+
import type { Prefilter } from "./environment.ts"
|
|
49
59
|
import type { Material } from "./material.ts"
|
|
60
|
+
import { orderEntries } from "./order.ts"
|
|
61
|
+
import { fillTransform, leaveScene, makeNode, worldInto } from "./node.ts"
|
|
62
|
+
import type { SceneHooks, SceneNode, ScenePointerListener } from "./node.ts"
|
|
63
|
+
import { checkInstancePairing, checkMask, instanceBinding, localBounds, publishInstanceStyle } from "./mesh.ts"
|
|
64
|
+
import type { InstancedMesh, InstanceNode, Mesh } from "./mesh.ts"
|
|
65
|
+
import type { CastingLight, Light } from "./light.ts"
|
|
50
66
|
|
|
51
67
|
const IDENTITY = mat4()
|
|
52
68
|
const RESOLVED = Promise.resolve()
|
|
53
|
-
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
const CASCADE_SPLIT_LAMBDA = 0.5
|
|
57
|
-
// |y| of a light direction above this is straight up or down, where
|
|
58
|
-
// world up cannot serve as the shadow map's roll reference.
|
|
59
|
-
const VERTICAL_LIGHT = 0.99
|
|
60
|
-
// lookAt()'s default roll reference. Read-only: quatFromFrame never
|
|
61
|
-
// writes its inputs, so one shared vector is safe.
|
|
62
|
-
const WORLD_UP: Vec3 = [0, 1, 0]
|
|
63
|
-
// The FFI carriers: one transform write (position, quaternion, scale) and
|
|
64
|
-
// one world-matrix read. Values are copied at the boundary, so one of each
|
|
65
|
-
// serves every call.
|
|
66
|
-
let transformScratch = new Float32Array(10)
|
|
67
|
-
let worldRead = new Float32Array(16)
|
|
68
|
-
// lookAt()/worldPosition() scratch: nothing here outlives a single call.
|
|
69
|
-
let worldScratch = mat4()
|
|
70
|
-
let localScratch = mat4()
|
|
69
|
+
|
|
70
|
+
// raycast()'s FFI carriers; values are copied at the boundary, so one of
|
|
71
|
+
// each serves every call.
|
|
71
72
|
let rayOriginScratch = new Float32Array(3)
|
|
72
73
|
let rayDirScratch = new Float32Array(3)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
function
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
let
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
74
|
+
// overlap()/sweep()'s carriers: the volume in flux:spatial's packed
|
|
75
|
+
// layout, one array per kind (capsule: a, b, radius; box: center, half
|
|
76
|
+
// extents, rotation), and the sweep's motion.
|
|
77
|
+
let capsuleScratch = new Float32Array(7)
|
|
78
|
+
let boxScratch = new Float32Array(10)
|
|
79
|
+
let motionScratch = new Float32Array(3)
|
|
80
|
+
const IDENTITY_QUAT: Quat = [0, 0, 0, 1]
|
|
81
|
+
|
|
82
|
+
/** Pack a volume into its kind's scratch array and name the kind; a
|
|
83
|
+
* sphere is a capsule whose segment is one point. */
|
|
84
|
+
function packVolume(volume: Volume, site: string): "capsule" | "box" {
|
|
85
|
+
if ("halfExtents" in volume) {
|
|
86
|
+
let s = boxScratch
|
|
87
|
+
let h = volume.halfExtents
|
|
88
|
+
if (!(h[0] >= 0 && h[1] >= 0 && h[2] >= 0)) throw new Error(site + ": halfExtents must be >= 0, got " + h)
|
|
89
|
+
let q = volume.rotation ?? IDENTITY_QUAT
|
|
90
|
+
s[0] = volume.center[0]
|
|
91
|
+
s[1] = volume.center[1]
|
|
92
|
+
s[2] = volume.center[2]
|
|
93
|
+
s[3] = h[0]
|
|
94
|
+
s[4] = h[1]
|
|
95
|
+
s[5] = h[2]
|
|
96
|
+
s[6] = q[0]
|
|
97
|
+
s[7] = q[1]
|
|
98
|
+
s[8] = q[2]
|
|
99
|
+
s[9] = q[3]
|
|
100
|
+
return "box"
|
|
101
|
+
}
|
|
102
|
+
if (!(volume.radius >= 0)) throw new Error(site + ": radius must be >= 0, got " + volume.radius)
|
|
103
|
+
let s = capsuleScratch
|
|
104
|
+
let a = "center" in volume ? volume.center : volume.a
|
|
105
|
+
let b = "center" in volume ? volume.center : volume.b
|
|
106
|
+
s[0] = a[0]
|
|
107
|
+
s[1] = a[1]
|
|
108
|
+
s[2] = a[2]
|
|
109
|
+
s[3] = b[0]
|
|
110
|
+
s[4] = b[1]
|
|
111
|
+
s[5] = b[2]
|
|
112
|
+
s[6] = volume.radius
|
|
113
|
+
return "capsule"
|
|
96
114
|
}
|
|
97
|
-
|
|
98
|
-
let upScratch: Vec3 = [0, 0, 0]
|
|
115
|
+
|
|
99
116
|
// pick()'s camera-ray scratch.
|
|
100
117
|
let pickDir: Vec3 = [0, 0, 0]
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
let
|
|
104
|
-
let scaleScratch: Vec3 = [1, 1, 1]
|
|
105
|
-
|
|
106
|
-
// The scene half a node needs to reach: attach/detach entries and schedule
|
|
107
|
-
// a sync. Kept separate from the public Scene type so internals stay off
|
|
108
|
-
// the app-facing surface. The camera (uViewProj + uCamPos) is written
|
|
109
|
-
// through the shared channel only when it changes - attach never re-seeds
|
|
110
|
-
// it, because target state survives entry churn.
|
|
111
|
-
type SceneHooks = {
|
|
112
|
-
_schedule(): void
|
|
113
|
-
_attach(mesh: Mesh): void
|
|
114
|
-
_detach(mesh: Mesh): void
|
|
115
|
-
_attachLight(light: Light): void
|
|
116
|
-
_detachLight(light: Light): void
|
|
117
|
-
_lightChanged(): void
|
|
118
|
-
_setParams(mesh: Mesh, params: ShaderParams): void
|
|
119
|
-
_setCount(mesh: Mesh): void
|
|
120
|
-
/** Re-point the mesh's entry at its (replaced) instance buffer. */
|
|
121
|
-
_setBuffer(mesh: Mesh): void
|
|
122
|
-
/** The mesh's castShadow flag changed: re-evaluate the filtered views. */
|
|
123
|
-
_setCast(mesh: Mesh): void
|
|
124
|
-
/** A light's castShadow/shadow options changed. */
|
|
125
|
-
_shadowChanged(light: DirectionalLight): void
|
|
126
|
-
_reorder(): void
|
|
127
|
-
/** The node's transform changed (for the sort and light bookkeeping). */
|
|
128
|
-
_moved(node: SceneNode): void
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export type SceneNode = {
|
|
132
|
-
kind: "group" | "mesh" | "light"
|
|
133
|
-
parent: SceneNode | null
|
|
134
|
-
children: SceneNode[]
|
|
135
|
-
/** Read freely; write through setTransform/setVisible so changes sync. */
|
|
136
|
-
position: Vec3
|
|
137
|
-
/** The stored rotation, always a UNIT quaternion. Euler triples convert
|
|
138
|
-
* on the way in (setTransform's `rotation`) and out (getRotation) - there
|
|
139
|
-
* is no second rotation field to fall out of step with this one. */
|
|
140
|
-
quaternion: Quat
|
|
141
|
-
scale: Vec3
|
|
142
|
-
visible: boolean
|
|
143
|
-
/** Pointer event handlers - plain fields, assign freely (they touch no
|
|
144
|
-
* GPU state, so they need no setTransform-style write path; components
|
|
145
|
-
* sync their props here). Down/move/up dispatch on the hit mesh and
|
|
146
|
-
* bubble through its ancestors (stopPropagation stops the walk);
|
|
147
|
-
* enter/leave fire on the mesh alone. Events flow once the element
|
|
148
|
-
* showing the scene carries `scene.handlers`. */
|
|
149
|
-
onPointerDown?: (event: ScenePointerEvent) => void
|
|
150
|
-
onPointerMove?: (event: ScenePointerEvent) => void
|
|
151
|
-
onPointerUp?: (event: ScenePointerEvent) => void
|
|
152
|
-
onPointerEnter?: (event: ScenePointerEvent) => void
|
|
153
|
-
onPointerLeave?: (event: ScenePointerEvent) => void
|
|
154
|
-
/** A declared transition (setTransition) settled naturally on one
|
|
155
|
-
* component; a cancel, snap or scene leave never fires. */
|
|
156
|
-
onTransitionEnd?: (event: TransitionEndEvent) => void
|
|
157
|
-
/** The core node while in a scene (created at add, freed at remove). */
|
|
158
|
-
_node: NodeId | null
|
|
159
|
-
_moved: boolean
|
|
160
|
-
_scene: SceneHooks | null
|
|
161
|
-
/** The declared transition, re-applied on every scene enter. */
|
|
162
|
-
_transition: NodeTransition | string | null
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/** The orthographic frustum a casting light renders its shadow map from,
|
|
166
|
-
* in the light's own space (x right, y up, looking along its direction),
|
|
167
|
-
* Three's DirectionalLightShadow camera. Everything outside it is lit. */
|
|
168
|
-
export type ShadowCamera = { left: number; right: number; top: number; bottom: number; near: number; far: number }
|
|
169
|
-
|
|
170
|
-
export type ShadowOptions = {
|
|
171
|
-
/** Shadow map resolution in texels, square (default 1024). */
|
|
172
|
-
mapSize?: number
|
|
173
|
-
/** Depth bias against acne, in the map's 0..1 depth (default 0). */
|
|
174
|
-
bias?: number
|
|
175
|
-
/** Offset a receiving point along its normal before the lookup, in
|
|
176
|
-
* world units (default 0) - the acne fix that keeps contact shadows. */
|
|
177
|
-
normalBias?: number
|
|
178
|
-
/** The light frustum; absent keys keep the defaults +-5, 0.5..500.
|
|
179
|
-
* Ignored by a cascaded light (its frustums follow the scene camera). */
|
|
180
|
-
camera?: Partial<ShadowCamera>
|
|
181
|
-
/** Split the shadow into this many cascades (1..MAX_CASCADES, default
|
|
182
|
-
* 1 = the `camera` box). With more, the light renders one map per slice
|
|
183
|
-
* of the SCENE camera's frustum (near .. far, tightest first), each
|
|
184
|
-
* `mapSize` texels wide and fitted every time the camera or the light
|
|
185
|
-
* moves, and a receiver samples the tightest one that has the point:
|
|
186
|
-
* sharp contact shadows near the camera and coarser ones toward the
|
|
187
|
-
* horizon, for a scene the box cannot cover at one map's resolution.
|
|
188
|
-
* Views (`scene.createView`) sample the same maps, fitted to the scene
|
|
189
|
-
* camera, not their own. Each cascade is a tile of the atlas. */
|
|
190
|
-
cascades?: number
|
|
191
|
-
/** How far from the scene camera a cascaded light shadows, in world
|
|
192
|
-
* units (default null = the camera's far). The cascades span
|
|
193
|
-
* near..distance and a point past it is lit, so pulling it in sharpens
|
|
194
|
-
* every cascade; it also bounds the maps' depth range, which is what
|
|
195
|
-
* `bias` is measured against. A box light ignores it. */
|
|
196
|
-
distance?: number | null
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/** A directional light node: parallel rays travelling along `direction`
|
|
200
|
-
* in the node's LOCAL space, so a parent's rotation turns the light with
|
|
201
|
-
* it (the default `[0, -1, 0]` is a sun straight overhead; the length is
|
|
202
|
-
* ignored). Scale does not affect it, and neither does position UNLESS
|
|
203
|
-
* it casts: a casting light's shadow camera sits at its WORLD position
|
|
204
|
-
* looking along its world direction (Three's rule), so place a casting
|
|
205
|
-
* sun above the scene. Write through setLight. */
|
|
206
|
-
export type DirectionalLight = SceneNode & {
|
|
207
|
-
kind: "light"
|
|
208
|
-
type: "directional"
|
|
209
|
-
direction: Vec3
|
|
210
|
-
/** Linear [r, g, b] 0..1. */
|
|
211
|
-
color: Vec3
|
|
212
|
-
intensity: number
|
|
213
|
-
/** Render a shadow map from this light (any directional light may;
|
|
214
|
-
* each map is a full extra pass over the casting meshes); meshes with
|
|
215
|
-
* `castShadow` draw into it, `lit` materials read it (unless
|
|
216
|
-
* `receiveShadow: false`). */
|
|
217
|
-
castShadow: boolean
|
|
218
|
-
/** The resolved shadow options (read; write through setLight). */
|
|
219
|
-
shadow: { mapSize: number; bias: number; normalBias: number; camera: ShadowCamera; cascades: number; distance: number | null }
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/** The ambient term: a sky/ground gradient by the WORLD normal's
|
|
223
|
-
* vertical tilt (fixed to world up, not the node's). One per scene - the
|
|
224
|
-
* last attached wins. Write through setLight. */
|
|
225
|
-
export type HemisphereLight = SceneNode & {
|
|
226
|
-
kind: "light"
|
|
227
|
-
type: "hemisphere"
|
|
228
|
-
sky: Vec3
|
|
229
|
-
ground: Vec3
|
|
230
|
-
intensity: number
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
export type Light = DirectionalLight | HemisphereLight
|
|
234
|
-
|
|
235
|
-
export type Mesh = SceneNode & {
|
|
236
|
-
kind: "mesh"
|
|
237
|
-
geometry: Geometry
|
|
238
|
-
material: Material
|
|
239
|
-
/** Explicit draw-order key (default 0), Three's name: lower draws first.
|
|
240
|
-
* Sorts within the opaque group and within the transparent group; the
|
|
241
|
-
* transparent group always follows the opaque one. Set with setRenderOrder. */
|
|
242
|
-
renderOrder: number
|
|
243
|
-
/** Draw into the scene's shadow map (default false, Three's default).
|
|
244
|
-
* Set with setCastShadow. A casting instanced mesh is skipped (the
|
|
245
|
-
* depth pass cannot know its record layout). */
|
|
246
|
-
castShadow: boolean
|
|
247
|
-
_entry: DrawId | null
|
|
248
|
-
/** The geometry-buffer reference the entry was built from, acquired at
|
|
249
|
-
* attach and what _detach releases - like _transparent, a snapshot,
|
|
250
|
-
* because setGeometry swaps mesh.geometry before the rebuild. */
|
|
251
|
-
_buffers: GeometryBuffers | null
|
|
252
|
-
/** material.transparent as of the last attach - the entry's actual
|
|
253
|
-
* pipeline state, and what _detach counts against (setMaterial swaps
|
|
254
|
-
* mesh.material before the rebuild). */
|
|
255
|
-
_transparent: boolean
|
|
256
|
-
/** World-space center of the local bounds, refreshed at sort time: the
|
|
257
|
-
* transparent sort key. */
|
|
258
|
-
_center: Vec3
|
|
259
|
-
_params: ShaderParams | null
|
|
260
|
-
/** Instance state when the mesh was made by createInstancedMesh; null on
|
|
261
|
-
* an ordinary mesh. */
|
|
262
|
-
_instances: MeshInstances | null
|
|
263
|
-
/** True for a createSprite mesh: its quad faces the camera in the
|
|
264
|
-
* vertex stage, so it picks by a unit box instead of its flat triangles. */
|
|
265
|
-
_sprite: boolean
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/** The per-mesh half of instancing: the record buffer and its bookkeeping.
|
|
269
|
-
* Read the public fields freely; write through setInstances /
|
|
270
|
-
* setInstanceCount so the draw range follows. */
|
|
271
|
-
export type MeshInstances = {
|
|
272
|
-
/** The GPU record buffer, owned by the mesh (disposeInstances frees it). */
|
|
273
|
-
buffer: BufferId
|
|
274
|
-
/** Floats per record - the material's instanceAttributes summed. */
|
|
275
|
-
stride: number
|
|
276
|
-
/** Records the buffer has room for; doubles when setInstances writes
|
|
277
|
-
* more (a replacement buffer, never a resize). */
|
|
278
|
-
capacity: number
|
|
279
|
-
/** The buffer label, carried to replacement buffers on growth. */
|
|
280
|
-
label: string | undefined
|
|
281
|
-
/** Records currently drawn (the entry's instanceCount while visible). */
|
|
282
|
-
count: number
|
|
283
|
-
/** Explicit LOCAL bounds covering the whole population ([minX, minY,
|
|
284
|
-
* minZ, maxX, maxY, maxZ]), or null: the mesh then has no picking leaf -
|
|
285
|
-
* records are opaque data, so the library cannot derive where the
|
|
286
|
-
* instances are. */
|
|
287
|
-
bounds: Float32Array | null
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/** A mesh from createInstancedMesh: an ordinary Mesh whose entry draws
|
|
291
|
-
* `instances.count` copies of the geometry, one record each. */
|
|
292
|
-
export type InstancedMesh = Mesh & { _instances: MeshInstances }
|
|
118
|
+
// worldInto's out for the light/center readbacks below; nothing here
|
|
119
|
+
// outlives a single call.
|
|
120
|
+
let worldScratch = mat4()
|
|
293
121
|
|
|
294
122
|
/** One picking intersection, Three's intersect result: the mesh, the
|
|
295
123
|
* camera-ray distance in world units, the world-space point, and for a
|
|
296
124
|
* triangle hit (every ordinary mesh - the test is per triangle, so a ray
|
|
297
125
|
* through a knot's hole misses) the world-space geometric `normal` facing
|
|
298
126
|
* the ray, the triangle index `face` and the interpolated texture `uv`.
|
|
299
|
-
* An instanced mesh is picked
|
|
300
|
-
*
|
|
301
|
-
*
|
|
127
|
+
* An instanced mesh is picked per instance, by the geometry's triangles
|
|
128
|
+
* placed as that instance (`instance` names it, Three's instanceId). A
|
|
129
|
+
* record mesh is picked by the faces of its explicit population box and
|
|
130
|
+
* a sprite by those of a unit box around its center, so their hits carry
|
|
131
|
+
* the struck face's `normal` and no `face`/`uv`. */
|
|
302
132
|
export type Hit = {
|
|
303
133
|
mesh: Mesh
|
|
134
|
+
/** The instance struck, for an instanced mesh. */
|
|
135
|
+
instance?: InstanceNode
|
|
304
136
|
distance: number
|
|
305
137
|
point: Vec3
|
|
306
|
-
normal
|
|
138
|
+
normal: Vec3
|
|
307
139
|
face?: number
|
|
308
140
|
uv?: [number, number]
|
|
309
141
|
}
|
|
310
142
|
|
|
311
|
-
/**
|
|
312
|
-
|
|
313
|
-
|
|
143
|
+
/** A pixel's camera ray (screenRay): `direction` is NOT normalized - its
|
|
144
|
+
* camera-forward component is 1, so `origin + w * direction` is the world
|
|
145
|
+
* point at camera-forward distance `w` (unproject's mapping). */
|
|
146
|
+
export type ScreenRay = { origin: Vec3; direction: Vec3 }
|
|
147
|
+
|
|
148
|
+
/** Filters for one raycast/overlap/sweep query. */
|
|
149
|
+
export type QueryOptions = {
|
|
150
|
+
/**
|
|
151
|
+
* Layer mask replacing the scene's for this query (Unity's layerMask,
|
|
152
|
+
* Three's raycaster.layers): hits come from meshes whose `layers`
|
|
153
|
+
* intersect it, wherever the scene mask would look. A mask the scene
|
|
154
|
+
* excludes makes a ray-only mesh - a low-poly collision mesh living
|
|
155
|
+
* undrawn in the same scene, the physics-collider pattern.
|
|
156
|
+
*/
|
|
157
|
+
layers?: number
|
|
158
|
+
/** Only these meshes report hits (Three's intersectObjects, an
|
|
159
|
+
* include-list); composes with `layers`. */
|
|
160
|
+
meshes?: Mesh[]
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** A query volume for overlap()/sweep(): a sphere, a capsule (the radius
|
|
164
|
+
* swept along the segment a-b) or an oriented box (half extents along
|
|
165
|
+
* the axes of `rotation`, identity when absent) - Unity's Overlap/Cast
|
|
166
|
+
* trio, Godot's Sphere/Capsule/BoxShape3D. */
|
|
167
|
+
export type Sphere = { center: Vec3; radius: number }
|
|
168
|
+
export type Capsule = { a: Vec3; b: Vec3; radius: number }
|
|
169
|
+
export type OrientedBox = { center: Vec3; halfExtents: Vec3; rotation?: Quat }
|
|
170
|
+
export type Volume = Sphere | Capsule | OrientedBox
|
|
171
|
+
|
|
172
|
+
/** One mesh an overlap() volume touches: its deepest contact - the point
|
|
173
|
+
* on the mesh, the unit direction out of it, and the depth along that
|
|
174
|
+
* direction that clears the contact (Unity's ComputePenetration, Godot's
|
|
175
|
+
* get_rest_info, per mesh). `instance` names the instance touched on an
|
|
176
|
+
* instanced mesh, which reports once per instance. */
|
|
177
|
+
export type Overlap = { mesh: Mesh; instance?: InstanceNode; point: Vec3; normal: Vec3; depth: number }
|
|
178
|
+
|
|
179
|
+
/** One mesh a sweep() volume touches on its way: `time` is the fraction
|
|
180
|
+
* of the motion at first touch (0 for a volume already in contact and
|
|
181
|
+
* moving in), `point` the touch point on the mesh, `normal` the unit
|
|
182
|
+
* normal there facing the volume (Unity's RaycastHit from a cast,
|
|
183
|
+
* Godot's KinematicCollision3D). `instance` as on Overlap. */
|
|
184
|
+
export type Impact = { mesh: Mesh; instance?: InstanceNode; time: number; point: Vec3; normal: Vec3 }
|
|
185
|
+
|
|
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
|
|
314
203
|
}
|
|
315
204
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
/**
|
|
329
|
-
|
|
330
|
-
/** Camera-ray distance to `point` in world units, or null with it. */
|
|
331
|
-
distance: number | null
|
|
332
|
-
/** Pointer position in scene pixels - project()'s coordinate space. */
|
|
333
|
-
x: number
|
|
334
|
-
y: number
|
|
335
|
-
pointerId: number
|
|
336
|
-
pointerType: string
|
|
337
|
-
button?: number
|
|
338
|
-
shiftKey: boolean
|
|
339
|
-
ctrlKey: boolean
|
|
340
|
-
altKey: boolean
|
|
341
|
-
metaKey: boolean
|
|
342
|
-
/** Stops the bubble walk after the current handler. */
|
|
343
|
-
stopPropagation(): void
|
|
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[]
|
|
344
219
|
}
|
|
345
220
|
|
|
346
|
-
/** Element handlers wiring a scene's pointer events: spread
|
|
347
|
-
* element shows
|
|
348
|
-
* automatically). `scene.handlers` expects
|
|
349
|
-
* size; a split-resolution leaf
|
|
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. */
|
|
350
226
|
export type SceneHandlers = {
|
|
351
227
|
onPointerDown(event: ElementPointerEvent): void
|
|
352
228
|
onPointerMove(event: ElementPointerEvent): void
|
|
353
229
|
onPointerUp(event: ElementPointerEvent): void
|
|
354
230
|
onPointerLeave(event: ElementPointerEvent): void
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
/** An orthographic projection's view-space extents, in world units (the
|
|
358
|
-
* same box at every depth). */
|
|
359
|
-
export type OrthoExtent = { left: number; right: number; top: number; bottom: number }
|
|
360
|
-
|
|
361
|
-
export type CameraUpdate = {
|
|
362
|
-
/** Vertical field of view in DEGREES (default 60). */
|
|
363
|
-
fov?: number
|
|
364
|
-
near?: number
|
|
365
|
-
far?: number
|
|
366
|
-
position?: Vec3
|
|
367
|
-
target?: Vec3
|
|
368
|
-
up?: Vec3
|
|
369
|
-
/** An orthographic projection with these extents (`fov` is then
|
|
370
|
-
* ignored); null returns to perspective. Three's OrthographicCamera as
|
|
371
|
-
* a camera option: a top-down map, an isometric view, a shadow-map
|
|
372
|
-
* light. */
|
|
373
|
-
ortho?: OrthoExtent | null
|
|
231
|
+
onWheel(event: ElementWheelEvent): void
|
|
374
232
|
}
|
|
375
233
|
|
|
376
234
|
/**
|
|
@@ -403,7 +261,8 @@ export type FogOptions = (
|
|
|
403
261
|
density: number
|
|
404
262
|
}
|
|
405
263
|
) & {
|
|
406
|
-
/** Straight [r, g, b], 0..1
|
|
264
|
+
/** Straight [r, g, b], 0..1, sRGB like every color option (decoded to
|
|
265
|
+
* linear light for the mix). */
|
|
407
266
|
color: [number, number, number]
|
|
408
267
|
/** World y at and below which the fog is full; default 0. Only acts
|
|
409
268
|
* with a `heightFalloff`. */
|
|
@@ -413,13 +272,164 @@ export type FogOptions = (
|
|
|
413
272
|
heightFalloff?: number
|
|
414
273
|
}
|
|
415
274
|
|
|
275
|
+
// The shared-param writes a fog value compiles to (null = the three zeros
|
|
276
|
+
// that turn the factor off) - one source for scene.setFog and a view's own
|
|
277
|
+
// `fog`, so both validate and spell the uniforms identically.
|
|
278
|
+
function fogParams(fog: FogOptions | null): ShaderParams {
|
|
279
|
+
if (fog === null) return { uFogInv: 0, uFogDensity: 0, uFogHeightFalloff: 0 }
|
|
280
|
+
let color = linearColor(fog.color)
|
|
281
|
+
let height = fog.height ?? 0
|
|
282
|
+
let falloff = fog.heightFalloff ?? 0
|
|
283
|
+
if (!Number.isFinite(height) || !Number.isFinite(falloff) || falloff < 0) {
|
|
284
|
+
throw new Error(`fog: height must be finite and heightFalloff finite and >= 0, got ${height} / ${falloff}`)
|
|
285
|
+
}
|
|
286
|
+
let params: ShaderParams = { uFogColor: color, uFogHeight: height, uFogHeightFalloff: falloff }
|
|
287
|
+
if ("density" in fog) {
|
|
288
|
+
let { density } = fog
|
|
289
|
+
if (!Number.isFinite(density) || density <= 0) {
|
|
290
|
+
throw new Error(`fog: density must be finite and > 0, got ${density}`)
|
|
291
|
+
}
|
|
292
|
+
return { ...params, uFogInv: 0, uFogDensity: density }
|
|
293
|
+
}
|
|
294
|
+
let { near, far } = fog
|
|
295
|
+
if (!(Number.isFinite(near) && Number.isFinite(far)) || far <= near) {
|
|
296
|
+
throw new Error(`fog: near/far must be finite with near < far, got ${near}..${far}`)
|
|
297
|
+
}
|
|
298
|
+
return { ...params, uFogNear: near, uFogInv: 1 / (far - near), uFogDensity: 0 }
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// `params` minus `names` - the scene-params fan-out around a view's own
|
|
302
|
+
// names. Returns the input untouched (no copy) when nothing intersects.
|
|
303
|
+
function withoutNames(params: ShaderParams, names: Set<string>): ShaderParams {
|
|
304
|
+
let hit = false
|
|
305
|
+
for (let k of names) {
|
|
306
|
+
if (k in params) {
|
|
307
|
+
hit = true
|
|
308
|
+
break
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (!hit) return params
|
|
312
|
+
let out: ShaderParams = {}
|
|
313
|
+
for (let k of Object.keys(params)) if (!names.has(k)) out[k] = params[k]!
|
|
314
|
+
return out
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* A skybox: the scene's background sampled from a cube map along each
|
|
319
|
+
* pixel's view ray (setBackground's object form). Three's
|
|
320
|
+
* `scene.background = cubeTexture` with `backgroundIntensity` and
|
|
321
|
+
* `backgroundRotation`; Unity's Skybox/Cubemap `_Exposure` and
|
|
322
|
+
* `_Rotation`; Godot's `background_energy_multiplier` and `sky_rotation`.
|
|
323
|
+
* Under an orthographic camera every pixel looks the same way, so the
|
|
324
|
+
* skybox is one flat color there (a sky needs perspective in every
|
|
325
|
+
* engine).
|
|
326
|
+
*/
|
|
327
|
+
export type SkyboxOptions = {
|
|
328
|
+
/** A cube map (createCubeTexture, faces in +X, -X, +Y, -Y, +Z, -Z
|
|
329
|
+
* order), sampled as GL defines it: each face is what a lookup in that
|
|
330
|
+
* direction returns. No lookup flip (Godot's and Unity's convention):
|
|
331
|
+
* a Three-style face set (each face as seen from inside) is mirrored
|
|
332
|
+
* per image at load, not in the shader. A 2D texture id throws. */
|
|
333
|
+
cube: TextureId
|
|
334
|
+
/** Multiplier on the sampled color, >= 0; default 1. */
|
|
335
|
+
intensity?: number
|
|
336
|
+
/** Turn about world y in RADIANS (default 0): the sky turns as a node
|
|
337
|
+
* with rotation [0, r, 0] would. Radians like node rotation and Three;
|
|
338
|
+
* Unity's `_Rotation` is degrees. */
|
|
339
|
+
rotation?: number
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* The scene's environment: a cube map every `lit` material with
|
|
344
|
+
* `reflectivity` mirrors (scene.setEnvironment). The same three fields
|
|
345
|
+
* as a skybox, and typically the same cube - Three's `scene.environment`
|
|
346
|
+
* with `environmentIntensity` and `environmentRotation`; Unity's
|
|
347
|
+
* environment reflections source, Godot's reflected light from the sky.
|
|
348
|
+
*/
|
|
349
|
+
export type EnvironmentOptions = {
|
|
350
|
+
/** A cube map (loadEnvironment's baked chain; equirectToCube for an LDR
|
|
351
|
+
* panorama; createCubeTexture with `mipmap: true` for a hand-baked
|
|
352
|
+
* sky), faces in +X, -X, +Y, -Y, +Z, -Z order, looked up like the
|
|
353
|
+
* skybox (no flip). A 2D texture id throws. */
|
|
354
|
+
cube: TextureId
|
|
355
|
+
/** Multiplier on the reflected color, >= 0; default 1. */
|
|
356
|
+
intensity?: number
|
|
357
|
+
/** Turn about world y in RADIANS (default 0), the skybox's convention:
|
|
358
|
+
* the environment turns as a node with rotation [0, r, 0] would. */
|
|
359
|
+
rotation?: number
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// The uniform turn a rotated cube map is looked up through: the INVERSE
|
|
363
|
+
// of the sky's turn, because a lookup along view direction v must find
|
|
364
|
+
// the texel that sat at R(-r) v before the sky turned by +r.
|
|
365
|
+
function cubeTurn(rotation: number): Mat4 {
|
|
366
|
+
let c = Math.cos(rotation)
|
|
367
|
+
let n = Math.sin(rotation)
|
|
368
|
+
// prettier-ignore
|
|
369
|
+
return [
|
|
370
|
+
c, 0, n, 0,
|
|
371
|
+
0, 1, 0, 0,
|
|
372
|
+
-n, 0, c, 0,
|
|
373
|
+
0, 0, 0, 1,
|
|
374
|
+
]
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function checkCubeKnobs(o: { intensity?: number; rotation?: number }, site: string): { intensity: number; rotation: number } {
|
|
378
|
+
let intensity = o.intensity ?? 1
|
|
379
|
+
let rotation = o.rotation ?? 0
|
|
380
|
+
if (!Number.isFinite(intensity) || intensity < 0) throw new Error(site + ": intensity must be a finite number >= 0, got " + intensity)
|
|
381
|
+
if (!Number.isFinite(rotation)) throw new Error(site + ": rotation must be a finite angle in radians, got " + rotation)
|
|
382
|
+
return { intensity, rotation }
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// The entry params a skybox compiles to.
|
|
386
|
+
function skyboxParams(sky: SkyboxOptions, site: string): ShaderParams {
|
|
387
|
+
let k = checkCubeKnobs(sky, site)
|
|
388
|
+
return { uSkyIntensity: k.intensity, uSkyRotation: cubeTurn(k.rotation) }
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// The shared params an environment compiles to (null = off: uEnvOn 0
|
|
392
|
+
// makes every reflective material's term vanish; the set ENVIRONMENT in
|
|
393
|
+
// `@solidrt/3d/glsl` declares).
|
|
394
|
+
function environmentParams(env: EnvironmentOptions | null): ShaderParams {
|
|
395
|
+
if (env === null) return { uEnvIntensity: 0, uEnvRotation: cubeTurn(0), uEnvOn: 0 }
|
|
396
|
+
let k = checkCubeKnobs(env, "scene.setEnvironment")
|
|
397
|
+
return { uEnvIntensity: k.intensity, uEnvRotation: cubeTurn(k.rotation), uEnvOn: 1 }
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** The output tone mapping (setToneMapping): "none" clamps, "aces" is the
|
|
401
|
+
* filmic curve every engine ships (Three's ACESFilmic, Godot's ACES,
|
|
402
|
+
* Unity's ACES). */
|
|
403
|
+
export type ToneMapping = "none" | "aces"
|
|
404
|
+
|
|
405
|
+
// The uToneMapping value per mode; the OUTPUT set branches on it.
|
|
406
|
+
const TONE_MAPPING_CODE: Record<ToneMapping, number> = { none: 0, aces: 1 }
|
|
407
|
+
|
|
416
408
|
export type SceneOptions = {
|
|
409
|
+
/** The target's clear, written as given: encoded pixels, untouched by
|
|
410
|
+
* exposure and tone mapping (a GLSL background or skybox goes through
|
|
411
|
+
* both). */
|
|
417
412
|
clearColor?: [number, number, number, number]
|
|
418
413
|
/** Scene-wide fog; see setFog. */
|
|
419
414
|
fog?: FogOptions
|
|
420
|
-
/**
|
|
421
|
-
|
|
422
|
-
|
|
415
|
+
/** Output tone mapping, default "none"; see setToneMapping. */
|
|
416
|
+
toneMapping?: ToneMapping
|
|
417
|
+
/** Output exposure, default 1; see setExposure. */
|
|
418
|
+
exposure?: number
|
|
419
|
+
/** The scene target's layer mask (bitmask, default 1): the scene draws
|
|
420
|
+
* the meshes whose `layers` intersect it. Live via scene.setLayers. */
|
|
421
|
+
layers?: number
|
|
422
|
+
/** The scene target's depth storage: true (default) for a buffer,
|
|
423
|
+
* "texture" for a sampleable one exposed as `scene.depthTexture` - the
|
|
424
|
+
* input for a depth-reading post effect in `output` (depth fog, SSAO,
|
|
425
|
+
* depth of field). Not with `samples` (the engine has no multisampled
|
|
426
|
+
* sampleable depth): render larger and display smaller instead. */
|
|
427
|
+
depth?: true | "texture"
|
|
428
|
+
/** The background drawn behind the meshes, inside the scene's own pass:
|
|
429
|
+
* fragment GLSL or a skybox - see setBackground. */
|
|
430
|
+
background?: string | SkyboxOptions
|
|
431
|
+
/** The cube map reflective materials mirror; see setEnvironment. */
|
|
432
|
+
environment?: EnvironmentOptions
|
|
423
433
|
label?: string
|
|
424
434
|
/** `autoFree: false` opts out of owner-scoped auto-dispose (then call dispose yourself). */
|
|
425
435
|
autoFree?: boolean
|
|
@@ -442,6 +452,17 @@ export type ViewOptions = {
|
|
|
442
452
|
* draws in add order (no renderOrder or transparent sort).
|
|
443
453
|
*/
|
|
444
454
|
overrideMaterial?: Material
|
|
455
|
+
/** The view's layer mask (bitmask, default 1): the view draws the
|
|
456
|
+
* meshes whose `layers` intersect it - a minimap admitting marker
|
|
457
|
+
* meshes only. Live via view.setLayers. */
|
|
458
|
+
layers?: number
|
|
459
|
+
/**
|
|
460
|
+
* The view's own fog: FogOptions overrides the scene's, null turns fog
|
|
461
|
+
* off in this view (an unfogged minimap over a fogged scene). Absent
|
|
462
|
+
* follows the scene. The fog names become view-owned params - the
|
|
463
|
+
* scene's setParams/setFog fan-out skips them (see View.setParams).
|
|
464
|
+
*/
|
|
465
|
+
fog?: FogOptions | null
|
|
445
466
|
/** The view target's depth storage: true (default) for a buffer,
|
|
446
467
|
* "texture" for a sampleable one exposed as `view.depthTexture`. Not
|
|
447
468
|
* with `into` (the depth is the parent's). */
|
|
@@ -463,7 +484,7 @@ export type ViewOptions = {
|
|
|
463
484
|
}
|
|
464
485
|
|
|
465
486
|
/** A second rendering of a scene from its own camera; see Scene.createView. */
|
|
466
|
-
export type
|
|
487
|
+
export type ViewHandle = {
|
|
467
488
|
/** The view's output, an ordinary texture id. */
|
|
468
489
|
texture: TextureId
|
|
469
490
|
/** The view target's depth as a sampler-only texture id when created
|
|
@@ -471,25 +492,127 @@ export type View = {
|
|
|
471
492
|
depthTexture: TextureId | null
|
|
472
493
|
/** Partial camera update, exactly scene.setCamera. */
|
|
473
494
|
setCamera(update: CameraUpdate): void
|
|
495
|
+
/** Current camera state, exactly scene.camera. */
|
|
496
|
+
camera(): CameraState
|
|
474
497
|
setSize(width: number, height: number): void
|
|
475
498
|
/** Move and resize a view created `into` an atlas (top-left origin);
|
|
476
499
|
* throws on a view with a target of its own. */
|
|
477
500
|
setRect(rect: { x: number; y: number; width: number; height: number }): void
|
|
478
|
-
/** View-owned shared params on the view's target
|
|
479
|
-
*
|
|
501
|
+
/** View-owned shared params on the view's target. Names written here
|
|
502
|
+
* (and the `fog` option's) become the view's OWN: the scene's
|
|
503
|
+
* setParams/setFog fan-out skips them from then on, so a view override
|
|
504
|
+
* is never clobbered by the next scene-wide write. */
|
|
480
505
|
setParams(params: ShaderParams): void
|
|
506
|
+
/** Replace the view's layer mask (bitmask): entries for newly admitted
|
|
507
|
+
* meshes attach, masked-out ones detach. */
|
|
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
|
|
481
525
|
/** Destroy the view's target (its entries die with it). Idempotent;
|
|
482
526
|
* views also die with their scene. */
|
|
483
527
|
dispose(): void
|
|
484
528
|
}
|
|
485
529
|
|
|
530
|
+
/** A dynamic environment rendered from a point in the scene; see
|
|
531
|
+
* Scene.createReflectionProbe. */
|
|
532
|
+
export type ReflectionProbeOptions = {
|
|
533
|
+
/** Where the probe looks out from, world space (live via setPosition). */
|
|
534
|
+
position: Vec3
|
|
535
|
+
/** Face edge in texels; default 128 (Unity's probe default). */
|
|
536
|
+
size?: number
|
|
537
|
+
/** The face cameras' near and far planes (defaults 0.1 and 100). */
|
|
538
|
+
near?: number
|
|
539
|
+
far?: number
|
|
540
|
+
/** Layer mask (bitmask, default 1): which meshes the probe sees. */
|
|
541
|
+
layers?: number
|
|
542
|
+
clearColor?: [number, number, number, number]
|
|
543
|
+
/** Convolve the faces into the roughness chain after each update
|
|
544
|
+
* (default true: Unity's and Godot's probes; `standard` blurs it by
|
|
545
|
+
* roughness like a baked environment). `false` keeps the sharp cube
|
|
546
|
+
* alone - Three's CubeCamera - at a sixth of the passes. */
|
|
547
|
+
prefilter?: boolean
|
|
548
|
+
label?: string
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
export type ReflectionProbe = {
|
|
552
|
+
/** The cube map: what `environment={{ cube }}` and `background={{
|
|
553
|
+
* cube }}` take. The prefiltered chain (roughness 0 sharp at level 0,
|
|
554
|
+
* blurred below, the same rule as a baked .srte), or with `prefilter:
|
|
555
|
+
* false` the sharp faces alone - then `standard` reflects it at its
|
|
556
|
+
* roughness 0 look whatever its roughness. */
|
|
557
|
+
cube: TextureId
|
|
558
|
+
setPosition(position: Vec3): void
|
|
559
|
+
/** Render the six faces now - six passes over the scene's draw list,
|
|
560
|
+
* seeing the meshes where the last frame's flush placed them - then
|
|
561
|
+
* (unless `prefilter: false`) the chain: one small pass per face per
|
|
562
|
+
* level. Call it when the surroundings changed (every frame for a
|
|
563
|
+
* moving scene, once for a still one). The probe's own cube is never
|
|
564
|
+
* sampled by the faces it renders (a black environment stands in): one
|
|
565
|
+
* bounce. */
|
|
566
|
+
update(): void
|
|
567
|
+
/** Destroy the cube (idempotent; probes also die with the scene). */
|
|
568
|
+
dispose(): void
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// The face cameras of a reflection probe, GL order (+X, -X, +Y, -Y, +Z,
|
|
572
|
+
// -Z): the direction each looks along and its up vector, chosen so that
|
|
573
|
+
// through the x-mirrored projection (Camera.mirror) each face lands in
|
|
574
|
+
// GL's own cube layout - plain world-up cameras, unlike Three's rolled
|
|
575
|
+
// CubeCamera set, because target images here are stored top-down.
|
|
576
|
+
const PROBE_FACE_DIRECTION: Vec3[] = [
|
|
577
|
+
[1, 0, 0],
|
|
578
|
+
[-1, 0, 0],
|
|
579
|
+
[0, 1, 0],
|
|
580
|
+
[0, -1, 0],
|
|
581
|
+
[0, 0, 1],
|
|
582
|
+
[0, 0, -1],
|
|
583
|
+
]
|
|
584
|
+
const PROBE_FACE_UP: Vec3[] = [
|
|
585
|
+
[0, 1, 0],
|
|
586
|
+
[0, 1, 0],
|
|
587
|
+
[0, 0, -1],
|
|
588
|
+
[0, 0, 1],
|
|
589
|
+
[0, 1, 0],
|
|
590
|
+
[0, 1, 0],
|
|
591
|
+
]
|
|
592
|
+
// Default face edge of a reflection probe (Unity's default resolution),
|
|
593
|
+
// and of a baked background (the bake tool's default).
|
|
594
|
+
const PROBE_SIZE = 128
|
|
595
|
+
// A cube face spans a quarter turn.
|
|
596
|
+
const PROBE_FOV = 90
|
|
597
|
+
// The output stage of a cube that holds light rather than display pixels
|
|
598
|
+
// (a probe's faces, a baked sky): no sRGB encode, no tone mapping, unit
|
|
599
|
+
// exposure - so the environment lookups read radiance.
|
|
600
|
+
const LINEAR_OUTPUT: ShaderParams = { uOutputEncode: 0, uToneMapping: TONE_MAPPING_CODE.none, uExposure: 1 }
|
|
601
|
+
|
|
486
602
|
export type Scene = {
|
|
487
603
|
/** The scene's output: an ordinary texture id (`<texture src>`). */
|
|
488
604
|
texture: TextureId
|
|
605
|
+
/** The scene target's depth as a sampler-only texture id when created
|
|
606
|
+
* with `depth: "texture"`, else null. */
|
|
607
|
+
depthTexture: TextureId | null
|
|
489
608
|
/** The tree root; add(scene.root, node) attaches top-level nodes. */
|
|
490
609
|
root: SceneNode
|
|
491
610
|
/** Partial camera update; absent keys keep their current value. */
|
|
492
611
|
setCamera(update: CameraUpdate): void
|
|
612
|
+
/** The camera as the next frame draws it: setCamera's own fields, a
|
|
613
|
+
* fresh snapshot per call (arrays are copies - mutate freely, write
|
|
614
|
+
* back through setCamera). Reflects a pending setCamera immediately. */
|
|
615
|
+
camera(): CameraState
|
|
493
616
|
setSize(width: number, height: number): void
|
|
494
617
|
/**
|
|
495
618
|
* Scene-wide uniforms: merge app-owned names into the target's SHARED
|
|
@@ -513,58 +636,176 @@ export type Scene = {
|
|
|
513
636
|
* The background is not fogged: match colors (see FogOptions).
|
|
514
637
|
*/
|
|
515
638
|
setFog(fog: FogOptions | null): void
|
|
639
|
+
/** Replace the scene target's layer mask (bitmask): entries for newly
|
|
640
|
+
* admitted meshes attach, masked-out ones detach. Shadow views follow
|
|
641
|
+
* this mask - what the scene cannot see must not darken it. */
|
|
642
|
+
setLayers(mask: number): void
|
|
643
|
+
/**
|
|
644
|
+
* Set, replace, or remove (null) the scene's background, drawn as the
|
|
645
|
+
* FIRST entry of the scene's own pass - one target, no second texture
|
|
646
|
+
* layer, no separate resize plumbing. It draws with depth off before
|
|
647
|
+
* every mesh and covers the whole target, so the clearColor stops being
|
|
648
|
+
* visible. Two forms:
|
|
649
|
+
*
|
|
650
|
+
* Fragment GLSL. The fragment gets the shader-target contract (vUV 0..1
|
|
651
|
+
* top-left origin, iResolution, fragColor; no `#version` line means the
|
|
652
|
+
* standard preamble), so a source written for createShaderTexture ports
|
|
653
|
+
* verbatim, PLUS `vRay`: the world-space view ray through the pixel
|
|
654
|
+
* (unnormalized; normalize it), which makes a directional sky - a
|
|
655
|
+
* horizon gradient, a sun disc, stars - a few lines of fragment code.
|
|
656
|
+
* The background is an ordinary scene entry, so it may also declare
|
|
657
|
+
* `uniform vec3 uCamPos` (the ray's origin) and any name written through
|
|
658
|
+
* scene.setParams (an app clock for an animated sky).
|
|
659
|
+
*
|
|
660
|
+
* A skybox (SkyboxOptions): `{ cube, intensity?, rotation? }` samples a
|
|
661
|
+
* cube map along the same ray. Replacing one skybox with another
|
|
662
|
+
* rewrites the entry's params and textures without recompiling, so
|
|
663
|
+
* `rotation` can animate from the reactive prop.
|
|
664
|
+
*
|
|
665
|
+
* Three's `scene.background = color` is `clearColor` here; a 2D image
|
|
666
|
+
* form can arrive later as a non-breaking widening.
|
|
667
|
+
*/
|
|
668
|
+
setBackground(source: string | SkyboxOptions | null): void
|
|
669
|
+
/**
|
|
670
|
+
* Set, replace, or remove (null) the scene's environment: the cube map
|
|
671
|
+
* every `lit` material created with `reflectivity` mirrors, blurred by
|
|
672
|
+
* its `shininess`. Scene-level like Three's `scene.environment`,
|
|
673
|
+
* Unity's environment reflections and Godot's sky-lit reflections: one
|
|
674
|
+
* cube bound on every target the scene draws into, one shared-params
|
|
675
|
+
* write for intensity and rotation, however many meshes reflect (a
|
|
676
|
+
* custom material composes ENVIRONMENT from `@solidrt/3d/glsl`). No
|
|
677
|
+
* per-material map. Typically the skybox's own cube, turned with it.
|
|
678
|
+
* Not an ambient light source yet: the hemisphere light stays the
|
|
679
|
+
* ambient term.
|
|
680
|
+
*/
|
|
681
|
+
setEnvironment(env: EnvironmentOptions | null): void
|
|
682
|
+
/**
|
|
683
|
+
* The output stage's tone mapping, applied by every library material,
|
|
684
|
+
* the skybox and a GLSL background that ends with outputColor: "none"
|
|
685
|
+
* (default) clamps the linear result, "aces" compresses highlights on
|
|
686
|
+
* the filmic curve. One shared-params write (`uToneMapping`, the OUTPUT
|
|
687
|
+
* set in `@solidrt/3d/glsl` declares), like Three's
|
|
688
|
+
* renderer.toneMapping and Godot's Environment tonemap; a custom
|
|
689
|
+
* fragment that writes fragColor directly is untouched. The clearColor
|
|
690
|
+
* is not tone mapped: with a curve on, draw the backdrop as a
|
|
691
|
+
* background.
|
|
692
|
+
*/
|
|
693
|
+
setToneMapping(mode: ToneMapping): void
|
|
516
694
|
/**
|
|
517
|
-
*
|
|
518
|
-
*
|
|
519
|
-
*
|
|
520
|
-
*
|
|
521
|
-
* iResolution, fragColor; no `#version` line means the standard
|
|
522
|
-
* preamble), so a source written for createShaderTexture ports verbatim.
|
|
523
|
-
* It draws with depth off before every mesh and covers the whole target,
|
|
524
|
-
* so the clearColor stops being visible. Three's `scene.background =
|
|
525
|
-
* color` is `clearColor` here; the texture form can arrive later as a
|
|
526
|
-
* non-breaking widening. No app-driven uniforms in v1 - a background is
|
|
527
|
-
* static art (anything animated is a mesh's own shaderMaterial).
|
|
695
|
+
* The output stage's exposure (default 1): the linear result is scaled
|
|
696
|
+
* by it before tone mapping, so a scene lit in physical-ish units is
|
|
697
|
+
* brought into range here rather than by dimming every light. One
|
|
698
|
+
* shared-params write (`uExposure`), like Three's toneMappingExposure.
|
|
528
699
|
*/
|
|
529
|
-
|
|
700
|
+
setExposure(exposure: number): void
|
|
530
701
|
/**
|
|
531
702
|
* Project a world point to scene pixels: origin top-left, y down - the
|
|
532
703
|
* output texture's own coordinate space, ready for overlay layout (HUD
|
|
533
|
-
* markers, labels). `w` is the
|
|
534
|
-
*
|
|
535
|
-
*
|
|
536
|
-
*
|
|
704
|
+
* markers, labels). `w` is the point's camera-forward distance in world
|
|
705
|
+
* units under either projection (useful for depth-ordering or
|
|
706
|
+
* distance-scaling markers, and unproject's exact input). Returns null
|
|
707
|
+
* for a point at or behind a PERSPECTIVE camera's plane - such a point
|
|
708
|
+
* has no place on screen; a parallel projection places every point, so
|
|
709
|
+
* an orthographic camera never returns null and `w` may be zero or
|
|
710
|
+
* negative there (negative near is legal ortho). Reflects a pending
|
|
711
|
+
* setCamera immediately.
|
|
537
712
|
*/
|
|
538
713
|
project(point: Vec3): { x: number; y: number; w: number } | null
|
|
714
|
+
/**
|
|
715
|
+
* project()'s exact inverse (Unity's ScreenToWorldPoint, Godot's
|
|
716
|
+
* project_position): the world point at scene pixel (`x`, `y`) and
|
|
717
|
+
* camera-forward distance `w`, copied into `out` (or a fresh Vec3).
|
|
718
|
+
* project()'s `w` round-trips under either projection - the
|
|
719
|
+
* drag-at-depth recipe: project the grabbed point once, keep its `w`,
|
|
720
|
+
* unproject each pointer move to slide the object at its original
|
|
721
|
+
* depth. Reflects a pending setCamera immediately, like project().
|
|
722
|
+
*/
|
|
723
|
+
unproject(x: number, y: number, w: number, out?: Vec3): Vec3
|
|
539
724
|
/** The camera's view-projection matrix, copied into `out` (or a fresh
|
|
540
725
|
* mat4). The batch escape hatch; for single points use project(). */
|
|
541
726
|
viewProj(out?: Mat4): Mat4
|
|
542
727
|
/**
|
|
543
728
|
* Cast the camera ray through a scene pixel (top-left origin, y down -
|
|
544
729
|
* project()'s space, the inverse direction) and return every visible
|
|
545
|
-
* mesh it hits, nearest first.
|
|
546
|
-
*
|
|
547
|
-
*
|
|
548
|
-
*
|
|
549
|
-
*
|
|
550
|
-
*
|
|
730
|
+
* mesh it hits, nearest first. An ordinary mesh tests per triangle
|
|
731
|
+
* against its geometry (hits carry `face`, `uv`, a world-space `normal`
|
|
732
|
+
* facing the ray, and a ray through a knot's hole misses); an instanced
|
|
733
|
+
* mesh or sprite is tested by its box's faces (a `normal`, no
|
|
734
|
+
* `face`/`uv`). Broadphase runs over a BVH kept in step
|
|
735
|
+
* by the sync walk, and a large geometry's triangles are BVH-indexed
|
|
736
|
+
* too (built by the first ray that reaches it), so merged static
|
|
737
|
+
* geometry stays cheap to query. Reflects pending setTransform/add
|
|
738
|
+
* writes immediately (the sync is flushed).
|
|
551
739
|
*/
|
|
552
740
|
pick(x: number, y: number): Hit[]
|
|
553
741
|
/** pick()'s world-space half: the same query along an arbitrary ray.
|
|
554
742
|
* `direction` need not be normalized; distances are world units. */
|
|
555
|
-
raycast(origin: Vec3, direction: Vec3): Hit[]
|
|
743
|
+
raycast(origin: Vec3, direction: Vec3, opts?: QueryOptions): Hit[]
|
|
744
|
+
/**
|
|
745
|
+
* Every mesh the volume touches (Unity's OverlapSphere/Box/Capsule,
|
|
746
|
+
* Godot's intersect_shape), each with its deepest contact. Tested per
|
|
747
|
+
* triangle in world space against the same shapes raycast() uses, so
|
|
748
|
+
* an undrawn collider layer answers through `opts.layers`, and any
|
|
749
|
+
* transform holds; an instanced mesh or sprite counts by its box.
|
|
750
|
+
* Surfaces, not solids: a volume wholly inside a closed mesh with no
|
|
751
|
+
* triangle in reach touches nothing. Unordered; reflects pending writes
|
|
752
|
+
* like raycast().
|
|
753
|
+
*/
|
|
754
|
+
overlap(volume: Volume, opts?: QueryOptions): Overlap[]
|
|
556
755
|
/**
|
|
557
|
-
*
|
|
558
|
-
*
|
|
559
|
-
*
|
|
560
|
-
*
|
|
561
|
-
*
|
|
562
|
-
*
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
*
|
|
567
|
-
*
|
|
756
|
+
* The volume moved by `motion`: every mesh it touches on the way, at
|
|
757
|
+
* its first touch, earliest first (Unity's SphereCast/CapsuleCast/
|
|
758
|
+
* BoxCast, Godot's cast_motion). A volume already in contact reports
|
|
759
|
+
* time 0 while the motion closes in, and nothing while it leaves or
|
|
760
|
+
* slides along the contact - what lets a move-and-slide proceed along
|
|
761
|
+
* a wall. A zero motion touches nothing. Same testing as overlap().
|
|
762
|
+
*/
|
|
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
|
|
779
|
+
/**
|
|
780
|
+
* pick()'s ray half (Unity's ScreenPointToRay, Godot's
|
|
781
|
+
* project_ray_origin/normal): the camera ray through a scene pixel,
|
|
782
|
+
* fresh arrays each call, for intersection work pick() cannot do - a
|
|
783
|
+
* drag plane, a ground grid, a raycast with the hits filtered
|
|
784
|
+
* yourself. `direction`'s camera-forward component is 1 (see
|
|
785
|
+
* ScreenRay), and raycast() takes it as-is. Reflects a pending
|
|
786
|
+
* setCamera immediately.
|
|
787
|
+
*/
|
|
788
|
+
screenRay(x: number, y: number): ScreenRay
|
|
789
|
+
/**
|
|
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).
|
|
568
809
|
*
|
|
569
810
|
* Coordinates assume the leaf is LAID OUT at the target size - true for
|
|
570
811
|
* the built-in leaf and a d-texture at natural size, under any ancestor
|
|
@@ -578,6 +819,19 @@ export type Scene = {
|
|
|
578
819
|
* layout just works: `scene.handlersFor(() => ({ width: w(), height:
|
|
579
820
|
* h() }))`. */
|
|
580
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
|
|
581
835
|
/**
|
|
582
836
|
* A second rendering of this scene: its own draw target and camera,
|
|
583
837
|
* the same meshes and lights. Each mesh gets one entry in the view's
|
|
@@ -586,11 +840,43 @@ export type Scene = {
|
|
|
586
840
|
* nothing per view. Views share the scene's geometry buffers and
|
|
587
841
|
* (unless `overrideMaterial`) its materials; the light set and
|
|
588
842
|
* scene.setParams names fan out to every view, view.setParams is the
|
|
589
|
-
* view's own channel.
|
|
590
|
-
*
|
|
591
|
-
* events. Views die with the scene; `view.dispose()` drops
|
|
843
|
+
* view's own channel. A view's backdrop is its clearColor (the scene's
|
|
844
|
+
* background draws on probes, not on views), and a view has no picking
|
|
845
|
+
* or pointer events. Views die with the scene; `view.dispose()` drops
|
|
846
|
+
* one early.
|
|
847
|
+
*/
|
|
848
|
+
createView(opts: ViewOptions): ViewHandle
|
|
849
|
+
/**
|
|
850
|
+
* A reflection probe: the scene rendered into a cube map from a point,
|
|
851
|
+
* six faces at 90 degrees - Three's CubeCamera, Unity's and Godot's
|
|
852
|
+
* realtime ReflectionProbe - for the environment a moving object
|
|
853
|
+
* mirrors its surroundings through. A view under the hood (one entry
|
|
854
|
+
* list, the light set and scene params fanned out) that also draws the
|
|
855
|
+
* scene's background behind the meshes, as a probe does in every
|
|
856
|
+
* engine, rendered only by
|
|
857
|
+
* `probe.update()`, which the app calls when the surroundings moved:
|
|
858
|
+
* six scene passes each time, then the GPU prefilter into the roughness
|
|
859
|
+
* chain (unless `prefilter: false`), so `standard` blurs it by
|
|
860
|
+
* roughness exactly as it does a baked environment. Probes die with
|
|
861
|
+
* the scene.
|
|
592
862
|
*/
|
|
593
|
-
|
|
863
|
+
createReflectionProbe(opts: ReflectionProbeOptions): ReflectionProbe
|
|
864
|
+
/**
|
|
865
|
+
* Bake the background into an environment: a reflection probe at the
|
|
866
|
+
* origin that sees no mesh (layer mask 0), so its six `size` faces
|
|
867
|
+
* (default 128) hold the background alone - the GLSL sky or the skybox,
|
|
868
|
+
* exactly as the scene draws it, written LINEAR (uOutputEncode 0, no tone mapping, unit exposure,
|
|
869
|
+
* so a sky ending in outputColor bakes its light; one writing fragColor
|
|
870
|
+
* raw bakes those bytes as light) - then GGX-prefiltered into the chain
|
|
871
|
+
* `standard` samples by roughness: Godot's sky-to-radiance bake, a
|
|
872
|
+
* procedural sky lighting the scene, or a hi-res LDR skybox reduced to
|
|
873
|
+
* an environment. A snapshot: bake again when the sky changed. Returns
|
|
874
|
+
* the cube for `environment={{ cube }}` - a mipmapped cube draw target,
|
|
875
|
+
* 8-bit linear like a probe, NOT auto-freed (an environment normally
|
|
876
|
+
* lives as long as the app; destroyTexture it otherwise). Throws
|
|
877
|
+
* without a background.
|
|
878
|
+
*/
|
|
879
|
+
bakeBackground(size?: number): TextureId
|
|
594
880
|
/** Destroy the target (entries die with it). Idempotent. Material
|
|
595
881
|
* pipelines are shared and survive (app-lifetime, see material.ts);
|
|
596
882
|
* geometry buffers are reference-counted and freed with their last
|
|
@@ -598,79 +884,6 @@ export type Scene = {
|
|
|
598
884
|
dispose(): void
|
|
599
885
|
}
|
|
600
886
|
|
|
601
|
-
// A camera: the scene's own and one per view, the same state and the same
|
|
602
|
-
// one-shared-write contract. `dirty` = the matrices need recomputing (a
|
|
603
|
-
// setCamera or a resize), `pending` = the GPU write is owed to the next
|
|
604
|
-
// sync. The recompute is split from the sync so project()/viewProj() see a
|
|
605
|
-
// fresh matrix right after setCamera, before the microtask runs.
|
|
606
|
-
type Camera = {
|
|
607
|
-
fov: number
|
|
608
|
-
near: number
|
|
609
|
-
far: number
|
|
610
|
-
eye: Vec3
|
|
611
|
-
target: Vec3
|
|
612
|
-
up: Vec3
|
|
613
|
-
ortho: OrthoExtent | null
|
|
614
|
-
dirty: boolean
|
|
615
|
-
pending: boolean
|
|
616
|
-
proj: Mat4
|
|
617
|
-
view: Mat4
|
|
618
|
-
viewProj: Mat4
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
function makeCamera(): Camera {
|
|
622
|
-
return {
|
|
623
|
-
fov: 60,
|
|
624
|
-
near: 0.1,
|
|
625
|
-
far: 100,
|
|
626
|
-
eye: [0, 0, 3],
|
|
627
|
-
target: [0, 0, 0],
|
|
628
|
-
up: [0, 1, 0],
|
|
629
|
-
ortho: null,
|
|
630
|
-
dirty: true,
|
|
631
|
-
pending: false,
|
|
632
|
-
proj: mat4(),
|
|
633
|
-
view: mat4(),
|
|
634
|
-
viewProj: mat4(),
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
function updateCamera(cam: Camera, update: CameraUpdate): void {
|
|
639
|
-
if (update.fov !== undefined) cam.fov = update.fov
|
|
640
|
-
if (update.near !== undefined) cam.near = update.near
|
|
641
|
-
if (update.far !== undefined) cam.far = update.far
|
|
642
|
-
if (update.position) cam.eye = [update.position[0], update.position[1], update.position[2]]
|
|
643
|
-
if (update.target) cam.target = [update.target[0], update.target[1], update.target[2]]
|
|
644
|
-
if (update.up) cam.up = [update.up[0], update.up[1], update.up[2]]
|
|
645
|
-
if (update.ortho !== undefined) {
|
|
646
|
-
let o = update.ortho
|
|
647
|
-
cam.ortho = o === null ? null : { left: o.left, right: o.right, top: o.top, bottom: o.bottom }
|
|
648
|
-
}
|
|
649
|
-
cam.dirty = true
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
function ensureCamera(cam: Camera, width: number, height: number): void {
|
|
653
|
-
if (!cam.dirty) return
|
|
654
|
-
cam.dirty = false
|
|
655
|
-
cam.pending = true
|
|
656
|
-
let o = cam.ortho
|
|
657
|
-
if (o === null) perspective(cam.proj, (cam.fov * Math.PI) / 180, width / height, cam.near, cam.far)
|
|
658
|
-
else orthographic(cam.proj, o.left, o.right, o.top, o.bottom, cam.near, cam.far)
|
|
659
|
-
lookAtMatrix(cam.view, cam.eye, cam.target, cam.up)
|
|
660
|
-
multiply(cam.viewProj, cam.proj, cam.view)
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
// The camera is target state: one shared write, whatever the target holds.
|
|
664
|
-
// Entries are untouched - uModel is camera-independent, and uCamPos is
|
|
665
|
-
// stored even when no current material declares it. The camera basis rides
|
|
666
|
-
// along: the view matrix's first two rows are the camera's world-space
|
|
667
|
-
// right and up (no clip flip - that lives in the projection), so a
|
|
668
|
-
// billboard needs no reconstruction from uViewProj.
|
|
669
|
-
function cameraParams(cam: Camera): ShaderParams {
|
|
670
|
-
let v = cam.view
|
|
671
|
-
return { uViewProj: cam.viewProj, uCamPos: cam.eye, uCamRight: [v[0], v[4], v[8]], uCamUp: [v[1], v[5], v[9]] }
|
|
672
|
-
}
|
|
673
|
-
|
|
674
887
|
// A material reads attributes by name; the geometry's layout must carry
|
|
675
888
|
// every one it declares (the pipeline is built for that layout, so a
|
|
676
889
|
// missing channel would have no home) - an error, like the rest of the
|
|
@@ -695,602 +908,6 @@ function entrySeed(material: Material, params: ShaderParams | null): ShaderParam
|
|
|
695
908
|
: { uModel: IDENTITY, ...material.params, ...params }
|
|
696
909
|
}
|
|
697
910
|
|
|
698
|
-
// The uShadowAtlas binding while nothing casts: one white
|
|
699
|
-
// texel (depth 1, never shadowed), shared by every scene for the app.
|
|
700
|
-
let placeholder: TextureId | undefined
|
|
701
|
-
|
|
702
|
-
function shadowPlaceholder(): TextureId {
|
|
703
|
-
if (placeholder === undefined) {
|
|
704
|
-
placeholder = createTexture(new Uint8Array([255, 255, 255, 255]), 1, 1, { autoFree: false, label: "scene-shadow-none" })
|
|
705
|
-
}
|
|
706
|
-
return placeholder
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
function makeNode(kind: SceneNode["kind"]): SceneNode {
|
|
710
|
-
return {
|
|
711
|
-
kind,
|
|
712
|
-
parent: null,
|
|
713
|
-
children: [],
|
|
714
|
-
position: [0, 0, 0],
|
|
715
|
-
quaternion: [0, 0, 0, 1],
|
|
716
|
-
scale: [1, 1, 1],
|
|
717
|
-
visible: true,
|
|
718
|
-
_node: null,
|
|
719
|
-
_moved: false,
|
|
720
|
-
_scene: null,
|
|
721
|
-
_transition: null,
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
export function createGroup(): SceneNode {
|
|
726
|
-
return makeNode("group")
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
export type DirectionalLightOptions = {
|
|
730
|
-
direction?: Vec3
|
|
731
|
-
color?: Vec3
|
|
732
|
-
intensity?: number
|
|
733
|
-
castShadow?: boolean
|
|
734
|
-
/** Shadow-map options, merged key by key (setLight keeps unmentioned ones). */
|
|
735
|
-
shadow?: ShadowOptions
|
|
736
|
-
}
|
|
737
|
-
export type HemisphereLightOptions = { sky?: Vec3; ground?: Vec3; intensity?: number }
|
|
738
|
-
|
|
739
|
-
/** Every directional light may cast: the cap is MAX_LIGHTS (every map
|
|
740
|
-
* is a tile of the scene's one shadow atlas, so the pass count does not
|
|
741
|
-
* follow it, the fill does). */
|
|
742
|
-
export const MAX_SHADOWS = MAX_LIGHTS
|
|
743
|
-
export { MAX_CASCADES }
|
|
744
|
-
|
|
745
|
-
function mergeShadow(into: DirectionalLight["shadow"], update: ShadowOptions): void {
|
|
746
|
-
if (update.mapSize !== undefined) into.mapSize = update.mapSize
|
|
747
|
-
if (update.bias !== undefined) into.bias = update.bias
|
|
748
|
-
if (update.normalBias !== undefined) into.normalBias = update.normalBias
|
|
749
|
-
if (update.camera !== undefined) Object.assign(into.camera, update.camera)
|
|
750
|
-
if (update.cascades !== undefined) {
|
|
751
|
-
let n = update.cascades
|
|
752
|
-
if (!Number.isInteger(n) || n < 1 || n > MAX_CASCADES) throw new Error("shadow.cascades must be an integer from 1 to " + MAX_CASCADES)
|
|
753
|
-
into.cascades = n
|
|
754
|
-
}
|
|
755
|
-
if (update.distance !== undefined) {
|
|
756
|
-
let d = update.distance
|
|
757
|
-
if (d !== null && !(d > 0)) throw new Error("shadow.distance must be a positive number or null")
|
|
758
|
-
into.distance = d
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
export function createDirectionalLight(opts: DirectionalLightOptions = {}): DirectionalLight {
|
|
763
|
-
let light = makeNode("light") as DirectionalLight
|
|
764
|
-
light.type = "directional"
|
|
765
|
-
light.direction = [...(opts.direction ?? [0, -1, 0])] as Vec3
|
|
766
|
-
light.color = [...(opts.color ?? [1, 1, 1])] as Vec3
|
|
767
|
-
light.intensity = opts.intensity ?? 1
|
|
768
|
-
light.castShadow = opts.castShadow === true
|
|
769
|
-
light.shadow = { mapSize: 1024, bias: 0, normalBias: 0, camera: { left: -5, right: 5, top: 5, bottom: -5, near: 0.5, far: 500 }, cascades: 1, distance: null }
|
|
770
|
-
if (opts.shadow !== undefined) mergeShadow(light.shadow, opts.shadow)
|
|
771
|
-
return light
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
export function createHemisphereLight(opts: HemisphereLightOptions = {}): HemisphereLight {
|
|
775
|
-
let light = makeNode("light") as HemisphereLight
|
|
776
|
-
light.type = "hemisphere"
|
|
777
|
-
light.sky = [...(opts.sky ?? [1, 1, 1])] as Vec3
|
|
778
|
-
light.ground = [...(opts.ground ?? [0.2, 0.2, 0.2])] as Vec3
|
|
779
|
-
light.intensity = opts.intensity ?? 1
|
|
780
|
-
return light
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
/** The write path for a light's own fields (color, intensity, direction
|
|
784
|
-
* or sky/ground); absent keys keep their value. Its placement goes
|
|
785
|
-
* through setTransform like any node. Frame-rate-safe. */
|
|
786
|
-
export function setLight(light: DirectionalLight, update: DirectionalLightOptions): void
|
|
787
|
-
export function setLight(light: HemisphereLight, update: HemisphereLightOptions): void
|
|
788
|
-
export function setLight(light: Light, update: DirectionalLightOptions & HemisphereLightOptions): void {
|
|
789
|
-
if (update.intensity !== undefined) light.intensity = update.intensity
|
|
790
|
-
if (light.type === "directional") {
|
|
791
|
-
if (update.direction !== undefined) light.direction = [...update.direction] as Vec3
|
|
792
|
-
if (update.color !== undefined) light.color = [...update.color] as Vec3
|
|
793
|
-
let shadowChanged = false
|
|
794
|
-
if (update.castShadow !== undefined && update.castShadow !== light.castShadow) {
|
|
795
|
-
light.castShadow = update.castShadow
|
|
796
|
-
shadowChanged = true
|
|
797
|
-
}
|
|
798
|
-
if (update.shadow !== undefined) {
|
|
799
|
-
mergeShadow(light.shadow, update.shadow)
|
|
800
|
-
shadowChanged = true
|
|
801
|
-
}
|
|
802
|
-
if (shadowChanged) light._scene?._shadowChanged(light)
|
|
803
|
-
} else {
|
|
804
|
-
if (update.sky !== undefined) light.sky = [...update.sky] as Vec3
|
|
805
|
-
if (update.ground !== undefined) light.ground = [...update.ground] as Vec3
|
|
806
|
-
}
|
|
807
|
-
light._scene?._lightChanged()
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
export function createMesh(geometry: Geometry, material: Material): Mesh {
|
|
811
|
-
let mesh = makeNode("mesh") as Mesh
|
|
812
|
-
mesh.geometry = geometry
|
|
813
|
-
mesh.material = material
|
|
814
|
-
mesh.renderOrder = 0
|
|
815
|
-
mesh.castShadow = false
|
|
816
|
-
mesh._entry = null
|
|
817
|
-
mesh._buffers = null
|
|
818
|
-
mesh._transparent = false
|
|
819
|
-
mesh._center = [0, 0, 0]
|
|
820
|
-
mesh._params = null
|
|
821
|
-
mesh._instances = null
|
|
822
|
-
mesh._sprite = false
|
|
823
|
-
return mesh
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
// Every sprite draws the same unit quad, built once: geometry is data
|
|
827
|
-
// and its GPU buffers are acquired per mesh, so one shared value is the
|
|
828
|
-
// normal sharing story. The box is the quad's extent at any facing.
|
|
829
|
-
let spriteQuad: Geometry | undefined
|
|
830
|
-
const SPRITE_BOUNDS = new Float32Array([-0.5, -0.5, -0.5, 0.5, 0.5, 0.5])
|
|
831
|
-
|
|
832
|
-
/**
|
|
833
|
-
* A camera-facing quad, Three's `Sprite`: a unit plane drawn with a
|
|
834
|
-
* `sprite()` material (any material works, but only a sprite material
|
|
835
|
-
* turns the quad; there is no `geometry` argument). Size it with `scale` -
|
|
836
|
-
* a scale of [2, 1, 1] is a 2 x 1 world-unit quad - and place it like any
|
|
837
|
-
* mesh; its rotation is ignored, the camera decides the facing. Picking
|
|
838
|
-
* is by a unit box around the center (the quad's reach at any facing, an
|
|
839
|
-
* approximation), so hits carry no normal/face/uv.
|
|
840
|
-
*/
|
|
841
|
-
export function createSprite(material: Material): Mesh {
|
|
842
|
-
if (spriteQuad === undefined) spriteQuad = plane({ label: "sprite" })
|
|
843
|
-
let mesh = createMesh(spriteQuad, material)
|
|
844
|
-
mesh._sprite = true
|
|
845
|
-
return mesh
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
/** The local box picking and sorting work from: explicit instance bounds
|
|
849
|
-
* when the mesh is instanced (null without them - no leaf, no hits), the
|
|
850
|
-
* unit box for a sprite, the geometry's own bounds otherwise. */
|
|
851
|
-
function localBounds(mesh: Mesh): Float32Array | null {
|
|
852
|
-
if (mesh._instances !== null) return mesh._instances.bounds
|
|
853
|
-
return mesh._sprite ? SPRITE_BOUNDS : geometryBounds(mesh.geometry)
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
const ATTRIBUTE_FLOATS: Record<VertexAttribute["format"], number> = { f32: 1, vec2: 2, vec3: 3, vec4: 4 }
|
|
857
|
-
|
|
858
|
-
function instanceStride(attributes: VertexAttribute[]): number {
|
|
859
|
-
let stride = 0
|
|
860
|
-
for (let a of attributes) stride += ATTRIBUTE_FLOATS[a.format]
|
|
861
|
-
return stride
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
export type InstancedMeshOptions = {
|
|
865
|
-
/** LOCAL bounds covering every instance the records place ([minX, minY,
|
|
866
|
-
* minZ, maxX, maxY, maxZ] - geometryBounds' shape), copied in. Records
|
|
867
|
-
* are opaque data, so only the app knows where its instances are: with
|
|
868
|
-
* bounds the mesh picks and transparent-sorts like any other
|
|
869
|
-
* (conservatively - one box around the whole population); without, it
|
|
870
|
-
* has no picking leaf and pointer events never target it. */
|
|
871
|
-
bounds?: ArrayLike<number>
|
|
872
|
-
/** Debug label for the record buffer. */
|
|
873
|
-
label?: string
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
/**
|
|
877
|
-
* A mesh drawing `geometry` once per record of `records`: one draw entry,
|
|
878
|
-
* one uModel write, N instances - the shape for forests, particles, and
|
|
879
|
-
* every fleet whose per-copy data is a few floats rather than a merged
|
|
880
|
-
* vertex buffer. The material must declare `instanceAttributes`
|
|
881
|
-
* (shaderMaterialClass); its vertex stage reads each record through those
|
|
882
|
-
* `in` variables. `records` is the interleaved attribute data (stride =
|
|
883
|
-
* the attributes' floats summed) and is uploaded here; its length is the
|
|
884
|
-
* buffer's initial capacity, which setInstances grows past on demand.
|
|
885
|
-
* `count` limits how many records draw (default all), up to capacity.
|
|
886
|
-
*
|
|
887
|
-
* The result is an ordinary Mesh: add/remove, setTransform (uModel places
|
|
888
|
-
* the whole population), setVisible (hiding zeroes the drawn count,
|
|
889
|
-
* unhiding restores it), setMeshParams and renderOrder all apply. Update
|
|
890
|
-
* records with setInstances, the drawn count with setInstanceCount, and
|
|
891
|
-
* free the record buffer with disposeInstances when done for good.
|
|
892
|
-
*/
|
|
893
|
-
export function createInstancedMesh(
|
|
894
|
-
geometry: Geometry,
|
|
895
|
-
material: Material,
|
|
896
|
-
records: Float32Array,
|
|
897
|
-
count?: number,
|
|
898
|
-
opts?: InstancedMeshOptions,
|
|
899
|
-
): InstancedMesh {
|
|
900
|
-
let attributes = material.instanceAttributes
|
|
901
|
-
if (attributes === undefined) {
|
|
902
|
-
throw new Error(
|
|
903
|
-
"createInstancedMesh: the material declares no instanceAttributes - build it with shaderMaterialClass({ instanceAttributes: [...] })",
|
|
904
|
-
)
|
|
905
|
-
}
|
|
906
|
-
let stride = instanceStride(attributes)
|
|
907
|
-
if (records.length % stride !== 0) {
|
|
908
|
-
throw new Error(
|
|
909
|
-
"createInstancedMesh: " + records.length + " floats is not a whole number of " + stride + "-float records",
|
|
910
|
-
)
|
|
911
|
-
}
|
|
912
|
-
let bounds: Float32Array | null = null
|
|
913
|
-
if (opts?.bounds !== undefined) {
|
|
914
|
-
if (opts.bounds.length !== 6) {
|
|
915
|
-
throw new Error("createInstancedMesh: bounds must be [minX, minY, minZ, maxX, maxY, maxZ]")
|
|
916
|
-
}
|
|
917
|
-
bounds = new Float32Array(6)
|
|
918
|
-
for (let i = 0; i < 6; i++) bounds[i] = opts.bounds[i]!
|
|
919
|
-
}
|
|
920
|
-
let capacity = records.length / stride
|
|
921
|
-
let mesh = createMesh(geometry, material) as InstancedMesh
|
|
922
|
-
mesh._instances = {
|
|
923
|
-
buffer: createBuffer(records, { autoFree: false, label: opts?.label }),
|
|
924
|
-
stride,
|
|
925
|
-
capacity,
|
|
926
|
-
label: opts?.label,
|
|
927
|
-
count: Math.max(0, Math.min(Math.floor(count ?? capacity), capacity)),
|
|
928
|
-
bounds,
|
|
929
|
-
}
|
|
930
|
-
return mesh
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
/**
|
|
934
|
-
* Overwrite an instanced mesh's records from the start of its buffer and
|
|
935
|
-
* (by default) draw exactly the records written - pass `count` to draw
|
|
936
|
-
* fewer, or to keep more previously written ones alive past a partial
|
|
937
|
-
* rewrite. More records than the buffer holds grow it: capacity doubles
|
|
938
|
-
* (or jumps to the records written when that is more), a new buffer is
|
|
939
|
-
* created and written, the mesh's entry is re-pointed at it and the old
|
|
940
|
-
* buffer freed - so a population grows without a new mesh, with the
|
|
941
|
-
* copies amortized like any dynamic array (size the initial records to
|
|
942
|
-
* skip them). Frame-rate-safe like setMeshParams when no growth happens.
|
|
943
|
-
*/
|
|
944
|
-
export function setInstances(mesh: InstancedMesh, records: Float32Array, count?: number): void {
|
|
945
|
-
let inst = mesh._instances
|
|
946
|
-
if (records.length % inst.stride !== 0) {
|
|
947
|
-
throw new Error("setInstances: " + records.length + " floats is not a whole number of " + inst.stride + "-float records")
|
|
948
|
-
}
|
|
949
|
-
let written = records.length / inst.stride
|
|
950
|
-
if (written > inst.capacity) {
|
|
951
|
-
let previous = inst.buffer
|
|
952
|
-
inst.capacity = Math.max(written, inst.capacity * 2)
|
|
953
|
-
inst.buffer = createBuffer(inst.capacity * inst.stride * 4, { autoFree: false, label: inst.label })
|
|
954
|
-
mesh._scene?._setBuffer(mesh)
|
|
955
|
-
destroyBuffer(previous)
|
|
956
|
-
}
|
|
957
|
-
writeBuffer(inst.buffer, records)
|
|
958
|
-
setInstanceCount(mesh, count ?? written)
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
/** Set how many records draw (clamped to [0, capacity]). The visibility
|
|
962
|
-
* switch composes: a hidden mesh stores the count and draws it on unhide. */
|
|
963
|
-
export function setInstanceCount(mesh: InstancedMesh, count: number): void {
|
|
964
|
-
let inst = mesh._instances
|
|
965
|
-
let n = Math.max(0, Math.min(Math.floor(count), inst.capacity))
|
|
966
|
-
if (n === inst.count) return
|
|
967
|
-
inst.count = n
|
|
968
|
-
mesh._scene?._setCount(mesh)
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
/**
|
|
972
|
-
* Detach the mesh (if attached) and free its record buffer. The buffer is
|
|
973
|
-
* mesh-owned with no reference count (unlike geometry buffers it is never
|
|
974
|
-
* shared), so this is the one explicit free; the mesh cannot be re-added
|
|
975
|
-
* afterwards.
|
|
976
|
-
*/
|
|
977
|
-
export function disposeInstances(mesh: InstancedMesh): void {
|
|
978
|
-
let inst: MeshInstances | null = mesh._instances
|
|
979
|
-
if (inst === null) return
|
|
980
|
-
if (mesh._scene) remove(mesh)
|
|
981
|
-
destroyBuffer(inst.buffer)
|
|
982
|
-
;(mesh as Mesh)._instances = null
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
/** Attach `child` under `parent` (re-parenting detaches it first). */
|
|
986
|
-
export function add(parent: SceneNode, child: SceneNode): void {
|
|
987
|
-
if (child.parent !== null) remove(child)
|
|
988
|
-
child.parent = parent
|
|
989
|
-
parent.children.push(child)
|
|
990
|
-
if (parent._scene) enterScene(child, parent._scene)
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
/** Detach `child` from its parent (and its meshes from the scene). */
|
|
994
|
-
export function remove(child: SceneNode): void {
|
|
995
|
-
if (child._scene) leaveScene(child)
|
|
996
|
-
let parent = child.parent
|
|
997
|
-
if (parent !== null) {
|
|
998
|
-
let i = parent.children.indexOf(child)
|
|
999
|
-
if (i >= 0) parent.children.splice(i, 1)
|
|
1000
|
-
child.parent = null
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
function enterScene(node: SceneNode, scene: SceneHooks): void {
|
|
1005
|
-
node._scene = scene
|
|
1006
|
-
node._node = spatial.createNode(fillTransform(node), node.visible)
|
|
1007
|
-
if (node._transition !== null) {
|
|
1008
|
-
spatial.setTransition(node._node, node._transition)
|
|
1009
|
-
declareTransition(node._node, node)
|
|
1010
|
-
}
|
|
1011
|
-
// The parent is in the scene already (add() enters the child only then),
|
|
1012
|
-
// and the scene root is the one node without a parent.
|
|
1013
|
-
if (node.parent !== null && node.parent._node !== null) spatial.setParent(node._node, node.parent._node)
|
|
1014
|
-
if (node.kind === "mesh") scene._attach(node as Mesh)
|
|
1015
|
-
else if (node.kind === "light") scene._attachLight(node as Light)
|
|
1016
|
-
for (let c of node.children) enterScene(c, scene)
|
|
1017
|
-
scene._schedule()
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
function leaveScene(node: SceneNode): void {
|
|
1021
|
-
let scene = node._scene
|
|
1022
|
-
if (scene && node.kind === "mesh") scene._detach(node as Mesh)
|
|
1023
|
-
else if (scene && node.kind === "light") scene._detachLight(node as Light)
|
|
1024
|
-
node._scene = null
|
|
1025
|
-
for (let c of node.children) leaveScene(c)
|
|
1026
|
-
if (node._node !== null) {
|
|
1027
|
-
declared.delete(node._node)
|
|
1028
|
-
spatial.destroyNode(node._node)
|
|
1029
|
-
node._node = null
|
|
1030
|
-
}
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
/** The node's local transform in the FFI carrier. */
|
|
1034
|
-
function fillTransform(node: SceneNode): Float32Array {
|
|
1035
|
-
let t = transformScratch
|
|
1036
|
-
t[0] = node.position[0]; t[1] = node.position[1]; t[2] = node.position[2]
|
|
1037
|
-
t[3] = node.quaternion[0]; t[4] = node.quaternion[1]; t[5] = node.quaternion[2]; t[6] = node.quaternion[3]
|
|
1038
|
-
t[7] = node.scale[0]; t[8] = node.scale[1]; t[9] = node.scale[2]
|
|
1039
|
-
return t
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
/** Forward a changed local transform to the core (no-op outside a scene:
|
|
1043
|
-
* entering pushes the whole transform). */
|
|
1044
|
-
function pushTransform(node: SceneNode): void {
|
|
1045
|
-
if (node._node === null || node._scene === null) return
|
|
1046
|
-
spatial.writeTransform(node._node, fillTransform(node))
|
|
1047
|
-
node._scene._moved(node)
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
export type { TransformUpdate } from "./math.ts"
|
|
1051
|
-
|
|
1052
|
-
/**
|
|
1053
|
-
* Declare (or with null clear) how the node's transform writes animate:
|
|
1054
|
-
* once set, setTransform writes are TARGETS the core animates toward
|
|
1055
|
-
* (position/scale per lane, rotation along the quaternion geodesic - a
|
|
1056
|
-
* spring keeps its velocity through retargets, the pursuit-safe shape),
|
|
1057
|
-
* so JS writes once per target change instead of once per frame. A spec
|
|
1058
|
-
* per component (position, rotation, scale) plus `all`; each
|
|
1059
|
-
* `{ duration, bounce? }` (a spring, the default) / `{ duration, curve }`
|
|
1060
|
-
* (a tween) / a shorthand string like "300ms ease-out". The declaration
|
|
1061
|
-
* lives on the node and re-applies whenever it enters a scene; the pose
|
|
1062
|
-
* it enters with always snaps. Clearing cancels running tracks in place
|
|
1063
|
-
* (the node keeps its mid-flight transform) and later writes snap. Each
|
|
1064
|
-
* natural settle calls the node's `onTransitionEnd` with the component
|
|
1065
|
-
* (the raw "spatialTransitionEnd" engine event on srt:events stays for
|
|
1066
|
-
* flux:spatial consumers; it carries the core id, `_node`).
|
|
1067
|
-
*/
|
|
1068
|
-
export function setTransition(node: SceneNode, transition: NodeTransition | string | null): void {
|
|
1069
|
-
node._transition = transition
|
|
1070
|
-
if (node._node !== null) {
|
|
1071
|
-
spatial.setTransition(node._node, transition)
|
|
1072
|
-
if (transition === null) declared.delete(node._node)
|
|
1073
|
-
else declareTransition(node._node, node)
|
|
1074
|
-
}
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
/**
|
|
1078
|
-
* The one write path for node transforms (so the scene knows to sync).
|
|
1079
|
-
* Values are copied in; absent keys keep their current value. This is also
|
|
1080
|
-
* the frame-rate escape hatch: call it from onFrame on a node grabbed via
|
|
1081
|
-
* `ref`, bypassing signals entirely.
|
|
1082
|
-
*
|
|
1083
|
-
* A write that changes nothing schedules nothing, so driving every node
|
|
1084
|
-
* unconditionally from onFrame costs only the compare for the nodes that
|
|
1085
|
-
* did not move. Rotation is compared after conversion, so passing an euler
|
|
1086
|
-
* equal to the node's current quaternion is also a no-op.
|
|
1087
|
-
*/
|
|
1088
|
-
export function setTransform(node: SceneNode, update: TransformUpdate): void {
|
|
1089
|
-
// A no-op write costs nothing: driving every node from onFrame is the
|
|
1090
|
-
// intended shape, and most nodes did not move. Exact compares, like
|
|
1091
|
-
// setVisible - a value that survives a float round trip unchanged is the
|
|
1092
|
-
// same value, and an epsilon would need a scale-dependent one anyway.
|
|
1093
|
-
let changed = false
|
|
1094
|
-
let p = update.position
|
|
1095
|
-
if (p && (p[0] !== node.position[0] || p[1] !== node.position[1] || p[2] !== node.position[2])) {
|
|
1096
|
-
node.position[0] = p[0]
|
|
1097
|
-
node.position[1] = p[1]
|
|
1098
|
-
node.position[2] = p[2]
|
|
1099
|
-
changed = true
|
|
1100
|
-
}
|
|
1101
|
-
if (updateRotation(rotScratch, update, "setTransform")) {
|
|
1102
|
-
let n = node.quaternion
|
|
1103
|
-
if (rotScratch[0] !== n[0] || rotScratch[1] !== n[1] || rotScratch[2] !== n[2] || rotScratch[3] !== n[3]) {
|
|
1104
|
-
n[0] = rotScratch[0]
|
|
1105
|
-
n[1] = rotScratch[1]
|
|
1106
|
-
n[2] = rotScratch[2]
|
|
1107
|
-
n[3] = rotScratch[3]
|
|
1108
|
-
changed = true
|
|
1109
|
-
}
|
|
1110
|
-
}
|
|
1111
|
-
if (update.scale !== undefined) {
|
|
1112
|
-
updateScale(scaleScratch, update.scale)
|
|
1113
|
-
if (scaleScratch[0] !== node.scale[0] || scaleScratch[1] !== node.scale[1] || scaleScratch[2] !== node.scale[2]) {
|
|
1114
|
-
node.scale[0] = scaleScratch[0]
|
|
1115
|
-
node.scale[1] = scaleScratch[1]
|
|
1116
|
-
node.scale[2] = scaleScratch[2]
|
|
1117
|
-
changed = true
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
if (!changed) return
|
|
1121
|
-
pushTransform(node)
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
/**
|
|
1125
|
-
* Aim a node at a WORLD-space point, Three's `Object3D.lookAt`: the node's
|
|
1126
|
-
* local +z ends up pointing at `target`, with `up` (world space, default
|
|
1127
|
-
* +y) choosing the roll about that axis. Ancestor transforms are undone,
|
|
1128
|
-
* so the aim holds under a rotated group - the ancestor chain is brought
|
|
1129
|
-
* up to date on the spot rather than waiting for the pending sync.
|
|
1130
|
-
*
|
|
1131
|
-
* +z because that is the library's own sweep axis (`extrude`, `sweep`,
|
|
1132
|
-
* `tube` run along z), so aiming their output needs no correction. For a
|
|
1133
|
-
* y-axis solid (`cylinder`, `cone`) reach for `quatFromTo` instead, which
|
|
1134
|
-
* takes the axis to aim as an argument.
|
|
1135
|
-
*
|
|
1136
|
-
* Writes `node.quaternion` - an ordinary rotation afterwards, readable and
|
|
1137
|
-
* overwritable by setTransform. To aim along a DIRECTION rather than at a
|
|
1138
|
-
* point, add it to the node's world position (`worldPosition`), the same
|
|
1139
|
-
* conversion Three asks for.
|
|
1140
|
-
*
|
|
1141
|
-
* Exact for rotation and uniform scale in the ancestor chain; a
|
|
1142
|
-
* non-uniformly scaled ancestor shears the frame and the aim is
|
|
1143
|
-
* approximate, exactly as in Three (both read the parent's upper 3x3 as
|
|
1144
|
-
* if it were a rotation).
|
|
1145
|
-
*/
|
|
1146
|
-
export function lookAt(node: SceneNode, target: Vec3, up: Vec3 = WORLD_UP): void {
|
|
1147
|
-
let parent = node.parent
|
|
1148
|
-
if (parent === null) {
|
|
1149
|
-
// No ancestors: parent space IS world space, aim straight from the
|
|
1150
|
-
// node's own position.
|
|
1151
|
-
aimScratch[0] = target[0] - node.position[0]
|
|
1152
|
-
aimScratch[1] = target[1] - node.position[1]
|
|
1153
|
-
aimScratch[2] = target[2] - node.position[2]
|
|
1154
|
-
quatFromFrame(node.quaternion, aimScratch, up)
|
|
1155
|
-
} else {
|
|
1156
|
-
let world = worldInto(worldScratch, parent)
|
|
1157
|
-
transformPoint(pointScratch, world, node.position)
|
|
1158
|
-
aimScratch[0] = target[0] - pointScratch[0]
|
|
1159
|
-
aimScratch[1] = target[1] - pointScratch[1]
|
|
1160
|
-
aimScratch[2] = target[2] - pointScratch[2]
|
|
1161
|
-
// World -> parent space for both vectors: rotating forward and up
|
|
1162
|
-
// rotates the frame they build, so converting the inputs is the same
|
|
1163
|
-
// as converting the resulting rotation, and needs no matrix inverse.
|
|
1164
|
-
unrotate(aimScratch, world, aimScratch)
|
|
1165
|
-
unrotate(upScratch, world, up)
|
|
1166
|
-
quatFromFrame(node.quaternion, aimScratch, upScratch)
|
|
1167
|
-
}
|
|
1168
|
-
pushTransform(node)
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
|
-
/**
|
|
1172
|
-
* A node's rotation as Euler radians in XYZ order, copied into `out` (or a
|
|
1173
|
-
* fresh Vec3). A convenience for reading and debugging, NOT a peer of
|
|
1174
|
-
* `node.quaternion`: the conversion is lossy in the sense that it cannot
|
|
1175
|
-
* recover the triple that was written (see eulerFromQuat), only a triple
|
|
1176
|
-
* that means the same rotation. Anything composing or interpolating
|
|
1177
|
-
* rotations should work with the quaternion.
|
|
1178
|
-
*/
|
|
1179
|
-
export function getRotation(node: SceneNode, out: Vec3 = [0, 0, 0]): Vec3 {
|
|
1180
|
-
return eulerFromQuat(out, node.quaternion)
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
/**
|
|
1184
|
-
* A node's position in world space, copied into `out` (or a fresh Vec3) -
|
|
1185
|
-
* Three's `getWorldPosition`. Brings the ancestor chain up to date first,
|
|
1186
|
-
* so it is exact before the pending sync has run.
|
|
1187
|
-
*/
|
|
1188
|
-
export function worldPosition(node: SceneNode, out: Vec3 = [0, 0, 0]): Vec3 {
|
|
1189
|
-
let world = worldInto(worldScratch, node)
|
|
1190
|
-
out[0] = world[12]
|
|
1191
|
-
out[1] = world[13]
|
|
1192
|
-
out[2] = world[14]
|
|
1193
|
-
return out
|
|
1194
|
-
}
|
|
1195
|
-
|
|
1196
|
-
/**
|
|
1197
|
-
* `out` = node's world matrix as the tree stands now. In a scene that is
|
|
1198
|
-
* one core read (pending writes included, nothing cleared); outside one
|
|
1199
|
-
* the chain is composed here. Scene membership is subtree-closed, so the
|
|
1200
|
-
* recursion meets a core node at the first in-scene ancestor at the
|
|
1201
|
-
* latest. One shared local scratch serves any depth - each frame uses it
|
|
1202
|
-
* only after its recursive call has returned.
|
|
1203
|
-
*/
|
|
1204
|
-
function worldInto(out: Mat4, node: SceneNode): Mat4 {
|
|
1205
|
-
if (node._node !== null) {
|
|
1206
|
-
spatial.worldMatrix(node._node, worldRead)
|
|
1207
|
-
for (let i = 0; i < 16; i++) out[i] = worldRead[i]!
|
|
1208
|
-
return out
|
|
1209
|
-
}
|
|
1210
|
-
if (node.parent === null) identity(out)
|
|
1211
|
-
else worldInto(out, node.parent)
|
|
1212
|
-
return multiply(out, out, compose(localScratch, node.position, node.quaternion, node.scale))
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
/**
|
|
1216
|
-
* `out` = v with m's rotation undone: the transpose of m's upper 3x3 with
|
|
1217
|
-
* its columns normalized, so uniform scale divides out. out may alias v.
|
|
1218
|
-
*/
|
|
1219
|
-
function unrotate(out: Vec3, m: Mat4, v: Vec3): Vec3 {
|
|
1220
|
-
let x = v[0], y = v[1], z = v[2]
|
|
1221
|
-
let l0 = Math.hypot(m[0], m[1], m[2]) || 1
|
|
1222
|
-
let l1 = Math.hypot(m[4], m[5], m[6]) || 1
|
|
1223
|
-
let l2 = Math.hypot(m[8], m[9], m[10]) || 1
|
|
1224
|
-
out[0] = (m[0] * x + m[1] * y + m[2] * z) / l0
|
|
1225
|
-
out[1] = (m[4] * x + m[5] * y + m[6] * z) / l1
|
|
1226
|
-
out[2] = (m[8] * x + m[9] * y + m[10] * z) / l2
|
|
1227
|
-
return out
|
|
1228
|
-
}
|
|
1229
|
-
|
|
1230
|
-
/** Show or hide a node and its whole subtree (a hidden mesh costs one
|
|
1231
|
-
* `instanceCount: 0` draw range - the entry stays, drawing nothing). */
|
|
1232
|
-
export function setVisible(node: SceneNode, visible: boolean): void {
|
|
1233
|
-
if (node.visible === visible) return
|
|
1234
|
-
node.visible = visible
|
|
1235
|
-
if (node._node !== null) {
|
|
1236
|
-
spatial.setVisible(node._node, visible)
|
|
1237
|
-
node._scene?._schedule()
|
|
1238
|
-
}
|
|
1239
|
-
}
|
|
1240
|
-
|
|
1241
|
-
/** Set a mesh's explicit draw-order key (see Mesh.renderOrder). */
|
|
1242
|
-
export function setRenderOrder(mesh: Mesh, order: number): void {
|
|
1243
|
-
if (mesh.renderOrder === order) return
|
|
1244
|
-
mesh.renderOrder = order
|
|
1245
|
-
mesh._scene?._reorder()
|
|
1246
|
-
}
|
|
1247
|
-
|
|
1248
|
-
/** Draw the mesh into the scene's shadow map, or stop (see Mesh.castShadow). */
|
|
1249
|
-
export function setCastShadow(mesh: Mesh, cast: boolean): void {
|
|
1250
|
-
if (mesh.castShadow === cast) return
|
|
1251
|
-
mesh.castShadow = cast
|
|
1252
|
-
mesh._scene?._setCast(mesh)
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
|
-
/** Swap a mesh's geometry: its draw entry is rebuilt (the scene re-sorts
|
|
1256
|
-
* the list, so the mesh keeps its place). */
|
|
1257
|
-
export function setGeometry(mesh: Mesh, geometry: Geometry): void {
|
|
1258
|
-
if (mesh._sprite) throw new Error("setGeometry: a sprite draws the shared unit quad and takes no geometry")
|
|
1259
|
-
if (mesh.geometry === geometry) return
|
|
1260
|
-
mesh.geometry = geometry
|
|
1261
|
-
rebuildEntry(mesh)
|
|
1262
|
-
}
|
|
1263
|
-
|
|
1264
|
-
/** Swap a mesh's material: its draw entry is rebuilt. */
|
|
1265
|
-
export function setMaterial(mesh: Mesh, material: Material): void {
|
|
1266
|
-
if (mesh.material === material) return
|
|
1267
|
-
mesh.material = material
|
|
1268
|
-
rebuildEntry(mesh)
|
|
1269
|
-
}
|
|
1270
|
-
|
|
1271
|
-
function rebuildEntry(mesh: Mesh): void {
|
|
1272
|
-
let scene = mesh._scene
|
|
1273
|
-
if (scene) {
|
|
1274
|
-
scene._detach(mesh)
|
|
1275
|
-
scene._attach(mesh)
|
|
1276
|
-
}
|
|
1277
|
-
}
|
|
1278
|
-
|
|
1279
|
-
/**
|
|
1280
|
-
* Write per-mesh uniforms - the channel for a custom material's app-driven
|
|
1281
|
-
* values (a time, a per-object tint). Names must be
|
|
1282
|
-
* declared and used by the mesh's material shaders (unknown names throw at
|
|
1283
|
-
* the call site, the engine's validation contract). Values persist on the
|
|
1284
|
-
* mesh: they survive geometry/material entry rebuilds and re-apply then.
|
|
1285
|
-
* Also the frame-rate path - like setTransform, call it from onFrame
|
|
1286
|
-
* freely.
|
|
1287
|
-
*/
|
|
1288
|
-
export function setMeshParams(mesh: Mesh, params: ShaderParams): void {
|
|
1289
|
-
if (mesh._params === null) mesh._params = {}
|
|
1290
|
-
Object.assign(mesh._params, params)
|
|
1291
|
-
mesh._scene?._setParams(mesh, params)
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
911
|
/**
|
|
1295
912
|
* Create a scene rendering into a depth-buffered draw target of the given
|
|
1296
913
|
* size. Returns the scene handle; `scene.texture` is the output. Inside a
|
|
@@ -1298,8 +915,15 @@ export function setMeshParams(mesh: Mesh, params: ShaderParams): void {
|
|
|
1298
915
|
* `autoFree: false`); outside one, call `dispose()` yourself.
|
|
1299
916
|
*/
|
|
1300
917
|
export function createScene(width: number, height: number, opts?: SceneOptions): Scene {
|
|
918
|
+
let depthMode = opts?.depth ?? true
|
|
919
|
+
if (depthMode === "texture" && (opts?.samples ?? 1) > 1) {
|
|
920
|
+
throw new Error('createScene: depth "texture" cannot combine with samples (no multisampled sampleable depth) - render larger and display smaller instead')
|
|
921
|
+
}
|
|
922
|
+
// The scene target's layer mask; shadow views follow it (a mesh the
|
|
923
|
+
// scene cannot see must not darken it).
|
|
924
|
+
let sceneMask = checkMask(opts?.layers ?? 1, "createScene")
|
|
1301
925
|
let texture = createDrawTarget(width, height, null, {
|
|
1302
|
-
depth:
|
|
926
|
+
depth: depthMode,
|
|
1303
927
|
clearColor: opts?.clearColor,
|
|
1304
928
|
filter: opts?.filter,
|
|
1305
929
|
wrap: opts?.wrap,
|
|
@@ -1311,15 +935,63 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1311
935
|
let scheduled = false
|
|
1312
936
|
|
|
1313
937
|
// Picking: the index and the narrowphase live in the spatial core; this
|
|
1314
|
-
// map turns a hit's core node back into the mesh
|
|
1315
|
-
//
|
|
1316
|
-
let byNode = new Map<NodeId, Mesh>()
|
|
938
|
+
// map turns a hit's core node back into the mesh, or the instance of an
|
|
939
|
+
// instanced mesh (each instance node is its own leaf).
|
|
940
|
+
let byNode = new Map<NodeId, Mesh | InstanceNode>()
|
|
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
|
|
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
|
|
979
|
+
}
|
|
1317
980
|
// Nodes whose transform changed since the last sync (deduped by the
|
|
1318
981
|
// _moved flag): what the light and transparent-order bookkeeping
|
|
1319
982
|
// reacts to, since which meshes moved is the core's knowledge now.
|
|
1320
983
|
let moved: SceneNode[] = []
|
|
1321
|
-
|
|
1322
|
-
let
|
|
984
|
+
// Instanced meshes with style writes to publish at the next sync.
|
|
985
|
+
let styleDirty = new Set<InstancedMesh>()
|
|
986
|
+
// Instanced meshes whose cull group - their live instance nodes - changed
|
|
987
|
+
// since the last sync: one setCullGroup per mesh however many entered
|
|
988
|
+
// or left.
|
|
989
|
+
let groupDirty = new Set<InstancedMesh>()
|
|
990
|
+
let instanceGroup = (mesh: InstancedMesh): NodeId[] => {
|
|
991
|
+
let members: NodeId[] = []
|
|
992
|
+
for (let n of mesh._instances.nodes.slots) if (n !== null && n._node !== null) members.push(n._node)
|
|
993
|
+
return members
|
|
994
|
+
}
|
|
1323
995
|
|
|
1324
996
|
// Live meshes (those holding a draw entry) in add order; the background
|
|
1325
997
|
// entry never joins this list. Draw order is derived from it by
|
|
@@ -1333,47 +1005,78 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1333
1005
|
// light params at the end of the sync - one write, however many meshes.
|
|
1334
1006
|
let lights: Light[] = []
|
|
1335
1007
|
let lightsDirty = false
|
|
1336
|
-
// uLightDir
|
|
1337
|
-
// shared-slot
|
|
1338
|
-
//
|
|
1339
|
-
// vector
|
|
1008
|
+
// uLightDir and uLightPos are CORE-DRIVEN: each light's slots are
|
|
1009
|
+
// shared-slot sinks following the node's world transform - the
|
|
1010
|
+
// direction slot (bindDirectionSlot) with -direction as the local
|
|
1011
|
+
// vector (the shader wants the vector TOWARD the light; a spot's axis
|
|
1012
|
+
// the same way), the position slot (bindPositionSlot) for the
|
|
1013
|
+
// positional types - so a light that merely moves costs no JS.
|
|
1340
1014
|
// This rewrite runs on attach/detach/field changes (and a new view)
|
|
1341
|
-
// only and owns the rest:
|
|
1342
|
-
// scene state, so it lands on the scene
|
|
1015
|
+
// only and owns the rest: types, colors, cone/falloff params, count,
|
|
1016
|
+
// hemisphere. The light set is scene state, so it lands on the scene
|
|
1017
|
+
// target and every view target.
|
|
1343
1018
|
let vecScratch = new Float32Array(3)
|
|
1019
|
+
// SPOT_PENUMBRA_MIN floors the cone's inner-outer cosine window so a
|
|
1020
|
+
// hard rim (penumbra 0) is still a defined smoothstep edge pair.
|
|
1021
|
+
const SPOT_PENUMBRA_MIN = 1e-3
|
|
1344
1022
|
let writeLights = () => {
|
|
1345
1023
|
lightsDirty = false
|
|
1346
1024
|
let sky: Vec3 = [0, 0, 0]
|
|
1347
1025
|
let ground: Vec3 = [0, 0, 0]
|
|
1026
|
+
let types: number[] = []
|
|
1348
1027
|
let colors: number[] = []
|
|
1028
|
+
let coneFalloff: number[] = []
|
|
1349
1029
|
let bias: number[] = []
|
|
1350
1030
|
let normalBias: number[] = []
|
|
1031
|
+
let radius: number[] = []
|
|
1351
1032
|
let count = 0
|
|
1352
1033
|
for (let light of lights) {
|
|
1353
1034
|
if (light.type === "hemisphere") {
|
|
1354
1035
|
let k = light.intensity
|
|
1355
|
-
|
|
1356
|
-
|
|
1036
|
+
let s = linearColor(light.sky)
|
|
1037
|
+
let g = linearColor(light.ground)
|
|
1038
|
+
sky = [s[0]! * k, s[1]! * k, s[2]! * k]
|
|
1039
|
+
ground = [g[0]! * k, g[1]! * k, g[2]! * k]
|
|
1357
1040
|
continue
|
|
1358
1041
|
}
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1042
|
+
if (light.type !== "point") {
|
|
1043
|
+
vecScratch[0] = -light.direction[0]
|
|
1044
|
+
vecScratch[1] = -light.direction[1]
|
|
1045
|
+
vecScratch[2] = -light.direction[2]
|
|
1046
|
+
spatial.bindDirectionSlot(light._node!, texture, "uLightDir", MAX_LIGHTS * 3, count, vecScratch)
|
|
1047
|
+
for (let v of views) spatial.bindDirectionSlot(light._node!, v.texture, "uLightDir", MAX_LIGHTS * 3, count, vecScratch)
|
|
1048
|
+
}
|
|
1049
|
+
if (light.type !== "directional") {
|
|
1050
|
+
spatial.bindPositionSlot(light._node!, texture, "uLightPos", MAX_LIGHTS * 3, count)
|
|
1051
|
+
for (let v of views) spatial.bindPositionSlot(light._node!, v.texture, "uLightPos", MAX_LIGHTS * 3, count)
|
|
1052
|
+
}
|
|
1053
|
+
types.push(light.type === "directional" ? 0 : light.type === "spot" ? 1 : 2)
|
|
1054
|
+
let c = linearColor(light.color)
|
|
1365
1055
|
let k = light.intensity
|
|
1366
|
-
colors.push(c[0] * k, c[1] * k, c[2] * k)
|
|
1056
|
+
colors.push(c[0]! * k, c[1]! * k, c[2]! * k)
|
|
1057
|
+
if (light.type === "spot") {
|
|
1058
|
+
let pen = Math.max(light.penumbra, SPOT_PENUMBRA_MIN)
|
|
1059
|
+
let rad = (light.angle * Math.PI) / 180
|
|
1060
|
+
coneFalloff.push(Math.cos(rad * (1 - pen)), Math.cos(rad), light.distance, light.decay)
|
|
1061
|
+
} else if (light.type === "point") {
|
|
1062
|
+
coneFalloff.push(0, 0, light.distance, light.decay)
|
|
1063
|
+
} else {
|
|
1064
|
+
coneFalloff.push(0, 0, 0, 0)
|
|
1065
|
+
}
|
|
1367
1066
|
bias.push(light.shadow.bias)
|
|
1368
1067
|
normalBias.push(light.shadow.normalBias)
|
|
1068
|
+
radius.push(light.shadow.radius)
|
|
1369
1069
|
count++
|
|
1370
1070
|
}
|
|
1371
1071
|
for (let i = count; i < MAX_LIGHTS; i++) {
|
|
1072
|
+
types.push(0)
|
|
1372
1073
|
colors.push(0, 0, 0)
|
|
1074
|
+
coneFalloff.push(0, 0, 0, 0)
|
|
1373
1075
|
bias.push(0)
|
|
1374
1076
|
normalBias.push(0)
|
|
1077
|
+
radius.push(1)
|
|
1375
1078
|
}
|
|
1376
|
-
// The shadow set rides with the lights. Per
|
|
1079
|
+
// The shadow set rides with the lights. Per casting light i: its
|
|
1377
1080
|
// map slots as uShadowFirst[i] + uShadowCount[i] (0 = a receiving
|
|
1378
1081
|
// material draws that light plain) and its biases; per map slot j its
|
|
1379
1082
|
// tile of the atlas as uShadowRect[j] in atlas UV (the whole map in
|
|
@@ -1383,12 +1086,14 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1383
1086
|
let first: number[] = new Array(MAX_LIGHTS).fill(0)
|
|
1384
1087
|
let counts: number[] = new Array(MAX_LIGHTS).fill(0)
|
|
1385
1088
|
let rects: number[] = []
|
|
1386
|
-
let atlas =
|
|
1387
|
-
let maps: Record<string, TextureId> = {
|
|
1388
|
-
|
|
1089
|
+
let atlas = shadowSys.atlas()
|
|
1090
|
+
let maps: Record<string, TextureId> = {
|
|
1091
|
+
uShadowAtlas: atlas !== null ? depthTexture(atlas.texture) : shadowSys.placeholder(),
|
|
1092
|
+
uEnv: environment ?? envPlaceholder,
|
|
1093
|
+
}
|
|
1094
|
+
shadowSys.forEachShadowSlot((slot, i, c, r) => {
|
|
1389
1095
|
if (c === 0) first[i] = slot
|
|
1390
1096
|
counts[i] = counts[i]! + 1
|
|
1391
|
-
let r = shadow.rects[c]!
|
|
1392
1097
|
let a = atlas!
|
|
1393
1098
|
rects.push(r.x / a.width, r.y / a.height, r.width / a.width, r.height / a.height)
|
|
1394
1099
|
})
|
|
@@ -1397,29 +1102,55 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1397
1102
|
uHemiSky: sky,
|
|
1398
1103
|
uHemiGround: ground,
|
|
1399
1104
|
uLightCount: count,
|
|
1105
|
+
uLightType: types,
|
|
1400
1106
|
uLightColor: colors,
|
|
1107
|
+
uLightParams: coneFalloff,
|
|
1401
1108
|
uShadowFirst: first,
|
|
1402
1109
|
uShadowCount: counts,
|
|
1403
1110
|
uShadowBias: bias,
|
|
1404
1111
|
uShadowNormalBias: normalBias,
|
|
1112
|
+
uShadowRadius: radius,
|
|
1405
1113
|
uShadowRect: rects,
|
|
1406
1114
|
}
|
|
1407
1115
|
receivingTargets(t => {
|
|
1408
1116
|
setTargetParams(t, params)
|
|
1409
|
-
|
|
1117
|
+
// A probe never samples its own cube while rendering it (a same-pass
|
|
1118
|
+
// feedback the engine rejects): its faces see a black environment.
|
|
1119
|
+
setTargetTextures(t, ownsEnvironment(t) ? { ...maps, uEnv: envPlaceholder } : maps)
|
|
1410
1120
|
})
|
|
1411
1121
|
// A slot change (a light attached, detached or reordered) moves every
|
|
1412
1122
|
// matrix too: rewrite the whole array once.
|
|
1413
|
-
|
|
1123
|
+
shadowSys.markMatricesDirty()
|
|
1414
1124
|
}
|
|
1415
1125
|
let orderDirty = false
|
|
1416
1126
|
// The order last handed to the engine: a resort that lands on the same
|
|
1417
1127
|
// permutation (the common case under a moving camera) issues nothing.
|
|
1418
1128
|
let lastOrder: DrawId[] = []
|
|
1419
|
-
|
|
1129
|
+
// `skybox`: the entry runs the library's cube-map fragment (its params
|
|
1130
|
+
// and textures are rewritable in place); false = app GLSL.
|
|
1131
|
+
// `sky` is the skybox source (its cube and knobs) for the bake, null
|
|
1132
|
+
// for a GLSL background.
|
|
1133
|
+
// `entries` is the background's draw entry per target that draws it:
|
|
1134
|
+
// the scene's own and every sky view's (a reflection probe), each first
|
|
1135
|
+
// in its list.
|
|
1136
|
+
let background: { pipeline: RenderPipelineId; program: ProgramId; sky: SkyboxOptions | null; entries: Map<TextureId, DrawId> } | null = null
|
|
1137
|
+
// Add the current background to `target` before entry `before` (its
|
|
1138
|
+
// first mesh entry; undefined appends, right for a target with none yet).
|
|
1139
|
+
let attachBackground = (target: TextureId, before: DrawId | undefined) => {
|
|
1140
|
+
if (background === null) return
|
|
1141
|
+
let sky = background.sky
|
|
1142
|
+
let params = sky === null ? null : skyboxParams(sky, "scene.setBackground")
|
|
1143
|
+
let textures = sky === null ? undefined : { uSky: sky.cube }
|
|
1144
|
+
background.entries.set(target, addDraw(target, background.pipeline, params, { vertexCount: 3, before, textures }))
|
|
1145
|
+
}
|
|
1146
|
+
// The environment cube bound as uEnv on every receiving target (the
|
|
1147
|
+
// light rewrite seeds it on new targets); null binds the placeholder,
|
|
1148
|
+
// the scene's own 1x1 black cube.
|
|
1149
|
+
let environment: TextureId | null = null
|
|
1150
|
+
let envPlaceholder = createEnvironmentPlaceholder((opts?.label ?? "scene") + "-env-none")
|
|
1420
1151
|
let sortEntries = () => {
|
|
1421
1152
|
orderDirty = false
|
|
1422
|
-
let order = orderEntries(meshes, camera.view, background?.
|
|
1153
|
+
let order = orderEntries(meshes, camera.view, background?.entries.get(texture))
|
|
1423
1154
|
if (order.length === lastOrder.length && order.every((id, i) => id === lastOrder[i])) return
|
|
1424
1155
|
lastOrder = order
|
|
1425
1156
|
setDrawOrder(texture, order)
|
|
@@ -1449,6 +1180,47 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1449
1180
|
let clip: Vec4 = [0, 0, 0, 0]
|
|
1450
1181
|
let pickOrigin: Vec3 = [0, 0, 0]
|
|
1451
1182
|
|
|
1183
|
+
// The camera ray through a pixel of `cam` over a tw x th target, into the
|
|
1184
|
+
// pickOrigin/pickDir
|
|
1185
|
+
// scratches. The direction keeps a camera-forward component of 1
|
|
1186
|
+
// (perspective: (cx, cy, -1) in the camera frame; ortho: unit forward),
|
|
1187
|
+
// so origin + w * direction is the world point at camera-forward
|
|
1188
|
+
// distance w - what unproject() banks on. pick()/screenRay()/
|
|
1189
|
+
// unproject() all cast exactly this ray.
|
|
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
|
|
1194
|
+
if (o === null) {
|
|
1195
|
+
// The camera-frame ray through the pixel, inverting project()'s
|
|
1196
|
+
// mapping: the baked y-down clip flip is why pixel y converts with
|
|
1197
|
+
// no negation there and one here.
|
|
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
|
|
1201
|
+
// The view's upper 3x3 rows are the camera axes, so its transpose
|
|
1202
|
+
// carries the camera-space direction (cx, cy, -1) to world.
|
|
1203
|
+
pickDir[0] = cx * v[0] + cy * v[1] - v[2]
|
|
1204
|
+
pickDir[1] = cx * v[4] + cy * v[5] - v[6]
|
|
1205
|
+
pickDir[2] = cx * v[8] + cy * v[9] - v[10]
|
|
1206
|
+
pickOrigin[0] = cam.eye[0]
|
|
1207
|
+
pickOrigin[1] = cam.eye[1]
|
|
1208
|
+
pickOrigin[2] = cam.eye[2]
|
|
1209
|
+
return
|
|
1210
|
+
}
|
|
1211
|
+
// Orthographic: every ray runs along the camera's forward axis; the
|
|
1212
|
+
// pixel picks where on the camera plane it starts (top row = top).
|
|
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]
|
|
1218
|
+
pickDir[0] = -v[2]
|
|
1219
|
+
pickDir[1] = -v[6]
|
|
1220
|
+
pickDir[2] = -v[10]
|
|
1221
|
+
}
|
|
1222
|
+
let pixelRay = (x: number, y: number): void => pixelRayOf(camera, width, height, x, y)
|
|
1223
|
+
|
|
1452
1224
|
// Views (scene.createView): more targets drawing the same meshes from
|
|
1453
1225
|
// their own cameras. A view holds one entry per mesh in its target,
|
|
1454
1226
|
// bound as one more draw sink of the mesh's core node, so the flush
|
|
@@ -1460,149 +1232,115 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1460
1232
|
width: number
|
|
1461
1233
|
height: number
|
|
1462
1234
|
override: Material | null
|
|
1463
|
-
/**
|
|
1464
|
-
*
|
|
1465
|
-
|
|
1235
|
+
/** Non-null marks a SHADOW view and names its caster set (m =>
|
|
1236
|
+
* m.castShadow). Re-evaluated per mesh by _setCast; also picks the
|
|
1237
|
+
* caster's own shadow material variant over the depth override. */
|
|
1238
|
+
shadowFilter: ((mesh: Mesh) => boolean) | null
|
|
1239
|
+
/** Layer mask: the view draws the meshes whose `layers` intersect
|
|
1240
|
+
* it. A shadow view's follows the scene's. */
|
|
1241
|
+
mask: number
|
|
1242
|
+
/** Names the view set itself (view.setParams, the fog option): the
|
|
1243
|
+
* scene's fan-out skips them so a view override survives scene-wide
|
|
1244
|
+
* writes. */
|
|
1245
|
+
ownNames: Set<string>
|
|
1466
1246
|
camera: Camera
|
|
1467
1247
|
entries: Map<Mesh, DrawId>
|
|
1468
1248
|
orderDirty: boolean
|
|
1469
1249
|
lastOrder: DrawId[]
|
|
1250
|
+
/** A reflection probe's target is a cube draw target of this face
|
|
1251
|
+
* edge, rendered face by face through probe.update(); null for a
|
|
1252
|
+
* 2D view. */
|
|
1253
|
+
cube: number | null
|
|
1254
|
+
/** A probe's public cube: the chain prefiltered from its target, or
|
|
1255
|
+
* the target itself when sharp (null for a 2D view). When it is the
|
|
1256
|
+
* scene's environment, the probe's own faces bind the placeholder. */
|
|
1257
|
+
probeCube: TextureId | null
|
|
1258
|
+
/** Whether the scene's background draws on this target, first in its
|
|
1259
|
+
* list (a probe: the sky behind the meshes); a plain view's backdrop
|
|
1260
|
+
* is its clearColor. */
|
|
1261
|
+
sky: boolean
|
|
1470
1262
|
disposed: boolean
|
|
1471
1263
|
}
|
|
1472
1264
|
let views: ViewRecord[] = []
|
|
1473
1265
|
// Every name scene.setParams has merged so far, replayed on a new view.
|
|
1474
1266
|
let sceneParams: ShaderParams = {}
|
|
1475
|
-
// The shadows, one per casting directional light: the internal views
|
|
1476
|
-
// rendering its maps (tiles of the shadow atlas, depth override, casting
|
|
1477
|
-
// meshes only; one for a box light, `shadow.cascades` for a cascaded
|
|
1478
|
-
// one, tightest first) and `rects`, each tile's place in the atlas in
|
|
1479
|
-
// texels. `lastWorld` is the light's world matrix the shadow cameras
|
|
1480
|
-
// were last placed from; `dirty` forces a re-place (options changed).
|
|
1481
|
-
type ShadowRect = { x: number; y: number; width: number; height: number }
|
|
1482
|
-
type Shadow = { light: DirectionalLight; views: ViewRecord[]; lastWorld: Mat4; dirty: boolean; rects: ShadowRect[] }
|
|
1483
|
-
let shadows = new Map<DirectionalLight, Shadow>()
|
|
1484
|
-
// The map slots: every casting light's maps in light order, a light's
|
|
1485
|
-
// cascades consecutive and tightest first. The ONE enumeration the
|
|
1486
|
-
// receiving side is dealt by - rects and first/count in writeLights,
|
|
1487
|
-
// matrices in sync - so uShadowFirst/uShadowCount agree with both.
|
|
1488
|
-
// `i` is the light's directional index (its uShadow*[i] slot).
|
|
1489
|
-
let forEachShadowSlot = (fn: (slot: number, i: number, shadow: Shadow, cascade: number) => void): void => {
|
|
1490
|
-
let slot = 0
|
|
1491
|
-
let i = 0
|
|
1492
|
-
for (let light of lights) {
|
|
1493
|
-
if (light.type !== "directional") continue
|
|
1494
|
-
let shadow = shadows.get(light)
|
|
1495
|
-
if (shadow !== undefined) for (let c = 0; c < shadow.views.length; c++) fn(slot++, i, shadow, c)
|
|
1496
|
-
i++
|
|
1497
|
-
}
|
|
1498
|
-
}
|
|
1499
|
-
// The shadow atlas: ONE depth-texture target every casting light's map
|
|
1500
|
-
// is a tile of, so N maps render as one pass and receivers sample one
|
|
1501
|
-
// sampler through per-map rects (uShadowRect). Created with the first
|
|
1502
|
-
// caster, destroyed with the last. Laid out as a grid of cells the
|
|
1503
|
-
// largest mapSize wide, scaled down uniformly when that would exceed
|
|
1504
|
-
// the device's texture size: tile size follows the budget.
|
|
1505
|
-
let shadowAtlas: { texture: TextureId; width: number; height: number } | null = null
|
|
1506
|
-
let shadowLayout = (count: number, maxSize: number) => {
|
|
1507
|
-
let cols = Math.ceil(Math.sqrt(count))
|
|
1508
|
-
let rows = Math.ceil(count / cols)
|
|
1509
|
-
let scale = Math.min(1, limits.maxTextureSize / (cols * maxSize), limits.maxTextureSize / (rows * maxSize))
|
|
1510
|
-
let cell = Math.max(1, Math.floor(maxSize * scale))
|
|
1511
|
-
return { cols, cell, scale, width: cols * cell, height: rows * cell }
|
|
1512
|
-
}
|
|
1513
|
-
// Place every shadow tile for the current caster set plus `adding` (not
|
|
1514
|
-
// yet in `shadows`; its rects are returned for the view creates), in
|
|
1515
|
-
// light order, a light's cascades consecutive. Sizes the atlas, moves
|
|
1516
|
-
// tiles whose place changed, and drops the atlas when nothing casts.
|
|
1517
|
-
// The rects reach receivers through the next light rewrite.
|
|
1518
|
-
let placeShadows = (adding: DirectionalLight | null): ShadowRect[] | null => {
|
|
1519
|
-
let casters: DirectionalLight[] = []
|
|
1520
|
-
for (let l of lights) if (l.type === "directional" && (shadows.has(l) || l === adding)) casters.push(l)
|
|
1521
|
-
if (adding !== null && !casters.includes(adding)) casters.push(adding)
|
|
1522
|
-
lightsDirty = true
|
|
1523
|
-
if (casters.length === 0) {
|
|
1524
|
-
if (shadowAtlas !== null) {
|
|
1525
|
-
destroyTexture(shadowAtlas.texture)
|
|
1526
|
-
shadowAtlas = null
|
|
1527
|
-
}
|
|
1528
|
-
return null
|
|
1529
|
-
}
|
|
1530
|
-
let maxSize = 1
|
|
1531
|
-
let tiles = 0
|
|
1532
|
-
for (let l of casters) {
|
|
1533
|
-
maxSize = Math.max(maxSize, l.shadow.mapSize)
|
|
1534
|
-
tiles += l.shadow.cascades
|
|
1535
|
-
}
|
|
1536
|
-
let lay = shadowLayout(tiles, maxSize)
|
|
1537
|
-
if (shadowAtlas === null) {
|
|
1538
|
-
shadowAtlas = {
|
|
1539
|
-
texture: createDrawTarget(lay.width, lay.height, null, {
|
|
1540
|
-
depth: "texture",
|
|
1541
|
-
clearColor: [1, 1, 1, 1],
|
|
1542
|
-
label: (opts?.label ?? "scene") + "-shadow-atlas",
|
|
1543
|
-
autoFree: false,
|
|
1544
|
-
}),
|
|
1545
|
-
width: lay.width,
|
|
1546
|
-
height: lay.height,
|
|
1547
|
-
}
|
|
1548
|
-
} else if (shadowAtlas.width !== lay.width || shadowAtlas.height !== lay.height) {
|
|
1549
|
-
setTargetSize(shadowAtlas.texture, lay.width, lay.height)
|
|
1550
|
-
shadowAtlas.width = lay.width
|
|
1551
|
-
shadowAtlas.height = lay.height
|
|
1552
|
-
}
|
|
1553
|
-
let placed: ShadowRect[] | null = null
|
|
1554
|
-
let k = 0
|
|
1555
|
-
for (let l of casters) {
|
|
1556
|
-
let size = Math.max(1, Math.floor(l.shadow.mapSize * lay.scale))
|
|
1557
|
-
let shadow = shadows.get(l)
|
|
1558
|
-
for (let c = 0; c < l.shadow.cascades; c++, k++) {
|
|
1559
|
-
let rect: ShadowRect = { x: (k % lay.cols) * lay.cell, y: Math.floor(k / lay.cols) * lay.cell, width: size, height: size }
|
|
1560
|
-
if (shadow === undefined) {
|
|
1561
|
-
if (placed === null) placed = []
|
|
1562
|
-
placed.push(rect)
|
|
1563
|
-
continue
|
|
1564
|
-
}
|
|
1565
|
-
let r = shadow.rects[c]!
|
|
1566
|
-
if (r.x === rect.x && r.y === rect.y && r.width === rect.width && r.height === rect.height) continue
|
|
1567
|
-
shadow.rects[c] = rect
|
|
1568
|
-
let view = shadow.views[c]!
|
|
1569
|
-
setTargetRect(view.texture, rect)
|
|
1570
|
-
view.width = rect.width
|
|
1571
|
-
view.height = rect.height
|
|
1572
|
-
// A tile's texel size moved: the cascade fit snaps to it.
|
|
1573
|
-
shadow.dirty = true
|
|
1574
|
-
}
|
|
1575
|
-
}
|
|
1576
|
-
return placed
|
|
1577
|
-
}
|
|
1578
|
-
let shadowDir: Vec3 = [0, 0, 0]
|
|
1579
|
-
// uShadowMatrix is one array param (the engine writes whole arrays), so
|
|
1580
|
-
// any shadow camera move rewrites all MAX_SHADOW_MAPS matrices, identity
|
|
1581
|
-
// in the slots that are not dealt.
|
|
1582
|
-
let shadowMatrices: number[] = new Array(MAX_SHADOW_MAPS * 16).fill(0)
|
|
1583
|
-
let shadowMatricesDirty = false
|
|
1584
1267
|
// Every target a receiving material can draw into: the scene's and each
|
|
1585
1268
|
// view's but the shadow views (binding a target's own depth into it
|
|
1586
1269
|
// would be same-pass feedback).
|
|
1587
1270
|
let receivingTargets = (fn: (target: TextureId) => void) => {
|
|
1588
1271
|
fn(texture)
|
|
1589
|
-
for (let v of views) if (v.
|
|
1272
|
+
for (let v of views) if (v.shadowFilter === null) fn(v.texture)
|
|
1273
|
+
}
|
|
1274
|
+
// Whether receiving target `t` is the probe whose cube is the scene's
|
|
1275
|
+
// environment (the faces, or the chain prefiltered from them): it
|
|
1276
|
+
// samples the placeholder instead of itself.
|
|
1277
|
+
let ownsEnvironment = (t: TextureId) => environment !== null && views.some(v => v.texture === t && v.probeCube === environment)
|
|
1278
|
+
// The mesh's entry in the scene's OWN target - what mesh._entry is.
|
|
1279
|
+
// Created when the scene mask admits the mesh, dropped when it stops:
|
|
1280
|
+
// the same lifecycle a view entry has, so `_buffers` (not `_entry`) is
|
|
1281
|
+
// the attached-to-the-scene sentinel.
|
|
1282
|
+
let attachScene = (mesh: Mesh) => {
|
|
1283
|
+
if (mesh._entry !== null) return
|
|
1284
|
+
if ((mesh.layers & sceneMask) === 0) return
|
|
1285
|
+
let inst = mesh._instances
|
|
1286
|
+
let bufs = mesh._buffers!
|
|
1287
|
+
// The entry starts switched off: it has no world matrix yet - the walk
|
|
1288
|
+
// in sync() computes one - and _schedule() defers that to a microtask,
|
|
1289
|
+
// so added live it would draw at the seeded identity until then. The
|
|
1290
|
+
// mismatch branch in sync() turns it on in the same pass that writes
|
|
1291
|
+
// uModel.
|
|
1292
|
+
mesh._entry = addDraw(texture, mesh.material.pipeline(mesh.geometry.layout), entrySeed(mesh.material, mesh._params), {
|
|
1293
|
+
buffer: bufs.buffer,
|
|
1294
|
+
indexBuffer: bufs.index,
|
|
1295
|
+
indexFormat: bufs.indexFormat,
|
|
1296
|
+
textures: mesh._textures !== null ? { ...mesh.material.textures, ...mesh._textures } : mesh.material.textures,
|
|
1297
|
+
...(inst !== null ? instanceBinding(mesh.material, inst) : {}),
|
|
1298
|
+
instanceCount: 0,
|
|
1299
|
+
})
|
|
1300
|
+
// The core turns the entry on (with the world matrix) at the next
|
|
1301
|
+
// flush, and off again whenever the node or an ancestor hides.
|
|
1302
|
+
spatial.bindDraw(mesh._node!, texture, mesh._entry, mesh.material.normalMatrix === true, inst !== null ? inst.count : 1)
|
|
1303
|
+
orderDirty = true
|
|
1590
1304
|
}
|
|
1305
|
+
let detachScene = (mesh: Mesh) => {
|
|
1306
|
+
if (mesh._entry === null) return
|
|
1307
|
+
if (mesh._node !== null) spatial.unbindDraw(mesh._node, texture)
|
|
1308
|
+
if (!disposed) removeDraw(texture, mesh._entry)
|
|
1309
|
+
mesh._entry = null
|
|
1310
|
+
orderDirty = true
|
|
1311
|
+
}
|
|
1312
|
+
// The material a view draws a mesh with: a shadow view lets a caster's
|
|
1313
|
+
// material pick its own depth variant (its cull side, cutout, skinning
|
|
1314
|
+
// - or the class's shadowVertex, the instanced placement); any other
|
|
1315
|
+
// override view draws exactly what it was given.
|
|
1316
|
+
let viewMaterial = (v: ViewRecord, mesh: Mesh): Material =>
|
|
1317
|
+
v.override !== null ? (v.shadowFilter !== null ? (mesh.material.shadow ?? v.override) : v.override) : mesh.material
|
|
1591
1318
|
let attachView = (v: ViewRecord, mesh: Mesh) => {
|
|
1319
|
+
if (v.entries.has(mesh)) return
|
|
1592
1320
|
let inst = mesh._instances
|
|
1593
|
-
if (v.
|
|
1594
|
-
if (
|
|
1595
|
-
|
|
1596
|
-
//
|
|
1597
|
-
//
|
|
1598
|
-
|
|
1321
|
+
if (v.shadowFilter !== null && !v.shadowFilter(mesh)) return
|
|
1322
|
+
if ((mesh.layers & v.mask) === 0) return
|
|
1323
|
+
let material = viewMaterial(v, mesh)
|
|
1324
|
+
// An override pipeline cannot know a populated mesh's record layout,
|
|
1325
|
+
// so the mesh is skipped - unless the variant chosen above is
|
|
1326
|
+
// instanced itself (a class with shadowVertex, a stock material's
|
|
1327
|
+
// instanced depth pass), whose attributes must fit the records like
|
|
1328
|
+
// the main material's did at add().
|
|
1329
|
+
if (v.override !== null && inst !== null) {
|
|
1330
|
+
if (material.instanceAttributes === undefined) return
|
|
1331
|
+
checkInstancePairing(material, inst, "Shadow material")
|
|
1332
|
+
}
|
|
1599
1333
|
let bufs = mesh._buffers!
|
|
1600
1334
|
let entry = addDraw(v.texture, material.pipeline(mesh.geometry.layout), entrySeed(material, v.override !== null ? null : mesh._params), {
|
|
1601
1335
|
buffer: bufs.buffer,
|
|
1602
1336
|
indexBuffer: bufs.index,
|
|
1603
1337
|
indexFormat: bufs.indexFormat,
|
|
1604
|
-
|
|
1605
|
-
|
|
1338
|
+
// The mesh's own bindings ride with its own material and with any
|
|
1339
|
+
// skinned stand-in (a skinned shadow variant declares uBones and
|
|
1340
|
+
// needs the mesh's palette); other override programs may not
|
|
1341
|
+
// declare the names, and per-entry bindings validate strictly.
|
|
1342
|
+
textures: (material === mesh.material || material.skinned === true) && mesh._textures !== null ? { ...material.textures, ...mesh._textures } : material.textures,
|
|
1343
|
+
...(inst !== null ? instanceBinding(material, inst) : {}),
|
|
1606
1344
|
instanceCount: 0,
|
|
1607
1345
|
})
|
|
1608
1346
|
spatial.bindDraw(mesh._node!, v.texture, entry, material.normalMatrix === true, inst !== null ? inst.count : 1)
|
|
@@ -1620,7 +1358,7 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1620
1358
|
let sortView = (v: ViewRecord) => {
|
|
1621
1359
|
v.orderDirty = false
|
|
1622
1360
|
if (v.override !== null) return
|
|
1623
|
-
let order = orderEntries(meshes, v.camera.view,
|
|
1361
|
+
let order = orderEntries(meshes, v.camera.view, background?.entries.get(v.texture), m => v.entries.get(m as Mesh) ?? null)
|
|
1624
1362
|
if (order.length === v.lastOrder.length && order.every((id, i) => id === v.lastOrder[i])) return
|
|
1625
1363
|
v.lastOrder = order
|
|
1626
1364
|
setDrawOrder(v.texture, order)
|
|
@@ -1628,158 +1366,140 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1628
1366
|
let disposeView = (v: ViewRecord) => {
|
|
1629
1367
|
if (v.disposed) return
|
|
1630
1368
|
v.disposed = true
|
|
1369
|
+
background?.entries.delete(v.texture)
|
|
1631
1370
|
for (let mesh of v.entries.keys()) if (mesh._node !== null) spatial.unbindDraw(mesh._node, v.texture)
|
|
1632
1371
|
v.entries.clear()
|
|
1633
1372
|
for (let light of lights) if (light.type === "directional" && light._node !== null) spatial.unbindSlot(light._node, v.texture)
|
|
1373
|
+
spatial.setFrustum(v.texture, null)
|
|
1634
1374
|
// Drain the zeroed direction slots while the target still exists.
|
|
1635
1375
|
spatial.flush()
|
|
1636
1376
|
destroyTexture(v.texture)
|
|
1637
1377
|
let i = views.indexOf(v)
|
|
1638
1378
|
if (i >= 0) views.splice(i, 1)
|
|
1639
1379
|
}
|
|
1380
|
+
// The six face renders of cube target `target` from `position` through
|
|
1381
|
+
// `cam`, a mirrored 90-degree camera (PROBE_FACE_DIRECTION and
|
|
1382
|
+
// PROBE_FACE_UP): each face's camera lands in the target's params,
|
|
1383
|
+
// `perFace` runs (a probe's sort), then the face renders. With `chain`
|
|
1384
|
+
// (a `mipmap: true` target, the prefilter's source) the first five faces
|
|
1385
|
+
// render into level 0 explicitly - an explicit level regenerates nothing
|
|
1386
|
+
// - and only the last face's plain render rebuilds the cube's chain: one
|
|
1387
|
+
// generation per six faces instead of six (a whole-cube generateMipmap
|
|
1388
|
+
// is the expensive step on some drivers).
|
|
1389
|
+
let renderCubeFaces = (target: TextureId, cam: Camera, size: number, position: Vec3, chain: boolean, perFace?: (face: number) => void) => {
|
|
1390
|
+
let last = PROBE_FACE_DIRECTION.length - 1
|
|
1391
|
+
for (let face = 0; face <= last; face++) {
|
|
1392
|
+
let d = PROBE_FACE_DIRECTION[face]!
|
|
1393
|
+
updateCamera(cam, { position, target: [position[0] + d[0], position[1] + d[1], position[2] + d[2]], up: PROBE_FACE_UP[face] })
|
|
1394
|
+
ensureCamera(cam, size, size)
|
|
1395
|
+
cam.pending = false
|
|
1396
|
+
setTargetParams(target, cameraParams(cam))
|
|
1397
|
+
perFace?.(face)
|
|
1398
|
+
if (chain && face < last) renderTarget(target, face, 0)
|
|
1399
|
+
else renderTarget(target, face)
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1640
1402
|
// A view record: the target, seeded with everything the scene target
|
|
1641
1403
|
// already holds (the light set - rewritten for every target, the simple
|
|
1642
1404
|
// write - the merged scene params, the shadow map binding), then one
|
|
1643
1405
|
// entry per mesh the filter admits.
|
|
1644
|
-
let makeView = (
|
|
1406
|
+
let makeView = (
|
|
1407
|
+
vopts: ViewOptions,
|
|
1408
|
+
shadowFilter: ((mesh: Mesh) => boolean) | null,
|
|
1409
|
+
cube: number | null = null,
|
|
1410
|
+
mipmap = false,
|
|
1411
|
+
sky = false,
|
|
1412
|
+
): ViewRecord => {
|
|
1645
1413
|
let override = vopts.overrideMaterial ?? null
|
|
1646
1414
|
if (override !== null) {
|
|
1647
1415
|
for (let mesh of meshes) if (mesh._instances === null) checkLayout(override, mesh.geometry, "View override material")
|
|
1648
1416
|
}
|
|
1649
1417
|
let tiled = vopts.into !== undefined
|
|
1650
1418
|
let v: ViewRecord = {
|
|
1651
|
-
texture:
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1419
|
+
texture:
|
|
1420
|
+
cube !== null
|
|
1421
|
+
? createCubeDrawTarget(cube, null, {
|
|
1422
|
+
depth: true,
|
|
1423
|
+
// The prefilter reads the faces at the lod of each sample's
|
|
1424
|
+
// solid angle: a generated chain, refreshed per face render.
|
|
1425
|
+
mipmap,
|
|
1426
|
+
// Linear radiance, HDR where the device renders half float.
|
|
1427
|
+
format: probeFormat(),
|
|
1428
|
+
clearColor: vopts.clearColor,
|
|
1429
|
+
label: vopts.label ?? (opts?.label ?? "scene") + "-probe",
|
|
1430
|
+
autoFree: false,
|
|
1431
|
+
})
|
|
1432
|
+
: createDrawTarget(vopts.width, vopts.height, null, {
|
|
1433
|
+
depth: tiled ? undefined : (vopts.depth ?? true),
|
|
1434
|
+
clearColor: vopts.clearColor,
|
|
1435
|
+
filter: vopts.filter,
|
|
1436
|
+
wrap: vopts.wrap,
|
|
1437
|
+
samples: vopts.samples,
|
|
1438
|
+
label: vopts.label ?? (opts?.label ?? "scene") + "-view",
|
|
1439
|
+
autoFree: false,
|
|
1440
|
+
into: vopts.into,
|
|
1441
|
+
x: vopts.x,
|
|
1442
|
+
y: vopts.y,
|
|
1443
|
+
}),
|
|
1663
1444
|
width: vopts.width,
|
|
1664
1445
|
height: vopts.height,
|
|
1665
1446
|
override,
|
|
1666
|
-
|
|
1447
|
+
shadowFilter,
|
|
1448
|
+
mask: shadowFilter !== null ? sceneMask : checkMask(vopts.layers ?? 1, "createView"),
|
|
1449
|
+
ownNames: new Set(),
|
|
1667
1450
|
camera: makeCamera(),
|
|
1668
1451
|
entries: new Map(),
|
|
1669
1452
|
orderDirty: true,
|
|
1670
1453
|
lastOrder: [],
|
|
1454
|
+
cube,
|
|
1455
|
+
probeCube: null,
|
|
1456
|
+
sky,
|
|
1671
1457
|
disposed: false,
|
|
1672
1458
|
}
|
|
1673
1459
|
views.push(v)
|
|
1460
|
+
// The background goes in first (no mesh entry exists yet), where every
|
|
1461
|
+
// later sort keeps it.
|
|
1462
|
+
if (sky) attachBackground(v.texture, undefined)
|
|
1674
1463
|
// The light rewrite seeds the shadow set (maps, casts, biases,
|
|
1675
1464
|
// matrices) on the new target too.
|
|
1676
1465
|
lightsDirty = true
|
|
1677
|
-
|
|
1466
|
+
// The view's own fog claims its names before the scene-params seed,
|
|
1467
|
+
// so the seed (and every later fan-out) leaves them to the view.
|
|
1468
|
+
let ownFog = vopts.fog !== undefined ? fogParams(vopts.fog) : null
|
|
1469
|
+
if (ownFog !== null) for (let k of Object.keys(ownFog)) v.ownNames.add(k)
|
|
1470
|
+
setTargetParams(v.texture, ownFog === null ? sceneParams : withoutNames(sceneParams, v.ownNames))
|
|
1471
|
+
if (ownFog !== null) setTargetParams(v.texture, ownFog)
|
|
1678
1472
|
for (let mesh of meshes) attachView(v, mesh)
|
|
1679
1473
|
hooks._schedule()
|
|
1680
1474
|
return v
|
|
1681
1475
|
}
|
|
1682
|
-
//
|
|
1683
|
-
//
|
|
1684
|
-
//
|
|
1685
|
-
//
|
|
1686
|
-
let
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
overrideMaterial: shadowDepthMaterial(),
|
|
1699
|
-
clearColor: [1, 1, 1, 1],
|
|
1700
|
-
label: (opts?.label ?? "scene") + "-shadow",
|
|
1701
|
-
},
|
|
1702
|
-
m => m.castShadow,
|
|
1703
|
-
),
|
|
1704
|
-
)
|
|
1705
|
-
shadows.set(light, { light, views, lastWorld: mat4(), dirty: true, rects })
|
|
1706
|
-
lightsDirty = true
|
|
1707
|
-
}
|
|
1708
|
-
let destroyShadow = (light: DirectionalLight) => {
|
|
1709
|
-
let shadow = shadows.get(light)
|
|
1710
|
-
if (shadow === undefined) return
|
|
1711
|
-
shadows.delete(light)
|
|
1712
|
-
for (let v of shadow.views) disposeView(v)
|
|
1713
|
-
placeShadows(null)
|
|
1714
|
-
lightsDirty = true
|
|
1715
|
-
hooks._schedule()
|
|
1716
|
-
}
|
|
1717
|
-
// Place a shadow's cameras from its light's world matrix. A box light:
|
|
1718
|
-
// at its world position, looking along its world direction, the light
|
|
1719
|
-
// frustum as the orthographic extents. Compared against the matrix it
|
|
1720
|
-
// was last placed from, so a scene animating elsewhere rewrites nothing
|
|
1721
|
-
// here. A cascaded light also follows the scene camera (`cameraMoved`).
|
|
1722
|
-
let cascadeScratch = mat4()
|
|
1723
|
-
let cascadeCenter: Vec3 = [0, 0, 0]
|
|
1724
|
-
let placeShadowCamera = (shadow: Shadow, cameraMoved: boolean) => {
|
|
1725
|
-
let light = shadow.light
|
|
1726
|
-
let m = worldInto(worldScratch, light)
|
|
1727
|
-
let cascaded = shadow.views.length > 1
|
|
1728
|
-
if (!shadow.dirty && !(cascaded && cameraMoved) && m.every((x, i) => x === shadow.lastWorld[i])) return
|
|
1729
|
-
shadow.dirty = false
|
|
1730
|
-
copy(shadow.lastWorld, m)
|
|
1731
|
-
transformVector(shadowDir, m, light.direction)
|
|
1732
|
-
let len = Math.hypot(shadowDir[0], shadowDir[1], shadowDir[2]) || 1
|
|
1733
|
-
let d: Vec3 = [shadowDir[0] / len, shadowDir[1] / len, shadowDir[2] / len]
|
|
1734
|
-
// A sun straight down is the common case and the degenerate one for
|
|
1735
|
-
// world up: roll about z then (the map's orientation is invisible).
|
|
1736
|
-
let up: Vec3 = Math.abs(d[1]) > VERTICAL_LIGHT ? [0, 0, 1] : [0, 1, 0]
|
|
1737
|
-
if (!cascaded) {
|
|
1738
|
-
let c = light.shadow.camera
|
|
1739
|
-
updateCamera(shadow.views[0]!.camera, {
|
|
1740
|
-
position: [m[12], m[13], m[14]],
|
|
1741
|
-
target: [m[12] + d[0], m[13] + d[1], m[14] + d[2]],
|
|
1742
|
-
up,
|
|
1743
|
-
ortho: { left: c.left, right: c.right, top: c.top, bottom: c.bottom },
|
|
1744
|
-
near: c.near,
|
|
1745
|
-
far: c.far,
|
|
1746
|
-
})
|
|
1747
|
-
return
|
|
1748
|
-
}
|
|
1749
|
-
// Cascades: the scene camera's range near..far (far capped by
|
|
1750
|
-
// shadow.distance) sliced by cascadeSplit, each slice's bounding
|
|
1751
|
-
// sphere (frustumSliceSphere) as an orthographic box looking along
|
|
1752
|
-
// the light, its centre snapped to the map's texel grid in light
|
|
1753
|
-
// space (snapToGrid) so the shadow edges do not swim as the camera
|
|
1754
|
-
// moves. The box reaches back toward the light by the whole range,
|
|
1755
|
-
// so a caster outside the slice still casts into it.
|
|
1756
|
-
let n = shadow.views.length
|
|
1757
|
-
let near = camera.near
|
|
1758
|
-
let far = Math.min(camera.far, light.shadow.distance ?? Infinity)
|
|
1759
|
-
if (!(far > near)) far = near + 1
|
|
1760
|
-
// The light's rotation only: rows are its right, up and back axes.
|
|
1761
|
-
let basis = lookAtMatrix(cascadeScratch, [0, 0, 0], d, up)
|
|
1762
|
-
let aspect = width / height
|
|
1763
|
-
let zn = near
|
|
1764
|
-
for (let c = 0; c < n; c++) {
|
|
1765
|
-
let zf = cascadeSplit(near, far, c, n, CASCADE_SPLIT_LAMBDA)
|
|
1766
|
-
let radius = frustumSliceSphere(cascadeCenter, camera, aspect, zn, zf)
|
|
1767
|
-
zn = zf
|
|
1768
|
-
let view = shadow.views[c]!
|
|
1769
|
-
// A texel is 2r / mapSize world units.
|
|
1770
|
-
snapToGrid(cascadeCenter, cascadeCenter, basis, (2 * radius) / view.width)
|
|
1771
|
-
let back = radius + far
|
|
1772
|
-
updateCamera(view.camera, {
|
|
1773
|
-
position: [cascadeCenter[0] - d[0] * back, cascadeCenter[1] - d[1] * back, cascadeCenter[2] - d[2] * back],
|
|
1774
|
-
target: cascadeCenter,
|
|
1775
|
-
up,
|
|
1776
|
-
ortho: { left: -radius, right: radius, top: radius, bottom: -radius },
|
|
1777
|
-
near: 0,
|
|
1778
|
-
far: back + radius,
|
|
1779
|
-
})
|
|
1780
|
-
}
|
|
1781
|
-
}
|
|
1476
|
+
// The shadow subsystem: the atlas, the per-caster shadow views (made
|
|
1477
|
+
// through makeView above) and their map cameras. The deps close over
|
|
1478
|
+
// this scene instance; writeLights reads the dealt slots back through
|
|
1479
|
+
// forEachShadowSlot/atlas and sync drives placeCameras/flushMatrices.
|
|
1480
|
+
let shadowSys = makeShadowSystem({
|
|
1481
|
+
lights,
|
|
1482
|
+
camera,
|
|
1483
|
+
targetSize: () => ({ width, height }),
|
|
1484
|
+
label: opts?.label ?? "scene",
|
|
1485
|
+
makeView,
|
|
1486
|
+
disposeView,
|
|
1487
|
+
markLightsDirty: () => {
|
|
1488
|
+
lightsDirty = true
|
|
1489
|
+
},
|
|
1490
|
+
schedule: () => hooks._schedule(),
|
|
1491
|
+
})
|
|
1782
1492
|
|
|
1493
|
+
// A camera write is also the target's frustum for the core's culling:
|
|
1494
|
+
// the scene's own, every view's, every shadow tile's - set before the
|
|
1495
|
+
// flush below, which switches the entries outside it off. Cube face
|
|
1496
|
+
// renders (probes) set no frustum: six cameras share one target and the
|
|
1497
|
+
// core would only ever see the last.
|
|
1498
|
+
let frustumScratch = new Float32Array(16)
|
|
1499
|
+
let setFrustum = (target: TextureId, cam: Camera) => {
|
|
1500
|
+
frustumScratch.set(cam.viewProj)
|
|
1501
|
+
spatial.setFrustum(target, frustumScratch)
|
|
1502
|
+
}
|
|
1783
1503
|
let sync = () => {
|
|
1784
1504
|
scheduled = false
|
|
1785
1505
|
if (disposed) return
|
|
@@ -1788,35 +1508,39 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1788
1508
|
if (camera.pending) {
|
|
1789
1509
|
camera.pending = false
|
|
1790
1510
|
setTargetParams(texture, cameraParams(camera))
|
|
1511
|
+
setFrustum(texture, camera)
|
|
1791
1512
|
if (transparentCount > 1) orderDirty = true
|
|
1792
1513
|
}
|
|
1793
|
-
|
|
1514
|
+
shadowSys.placeCameras(cameraMoved)
|
|
1794
1515
|
for (let v of views) {
|
|
1795
1516
|
ensureCamera(v.camera, v.width, v.height)
|
|
1796
1517
|
if (v.camera.pending) {
|
|
1797
1518
|
v.camera.pending = false
|
|
1798
1519
|
setTargetParams(v.texture, cameraParams(v.camera))
|
|
1520
|
+
setFrustum(v.texture, v.camera)
|
|
1799
1521
|
if (transparentCount > 1) v.orderDirty = true
|
|
1800
|
-
if (v.
|
|
1522
|
+
if (v.shadowFilter !== null) shadowSys.markMatricesDirty()
|
|
1801
1523
|
}
|
|
1802
1524
|
}
|
|
1803
1525
|
// Light bookkeeping first, so a fresh direction-slot bind is seeded
|
|
1804
1526
|
// by the flush below in the same sync.
|
|
1805
1527
|
if (lightsDirty) writeLights()
|
|
1528
|
+
// The instanced meshes' style writes since the last sync: one
|
|
1529
|
+
// coalesced buffer write per mesh.
|
|
1530
|
+
if (styleDirty.size > 0) {
|
|
1531
|
+
for (let m of styleDirty) if (m._instances !== null) publishInstanceStyle(m)
|
|
1532
|
+
styleDirty.clear()
|
|
1533
|
+
}
|
|
1534
|
+
// An instanced mesh without explicit bounds is culled by the union of
|
|
1535
|
+
// its instances' boxes: the core's cull group (a skinned part's joint
|
|
1536
|
+
// group, generalized) over the live instance nodes.
|
|
1537
|
+
if (groupDirty.size > 0) {
|
|
1538
|
+
for (let m of groupDirty) if (m._node !== null && m._buffers !== null && m._instances !== null) spatial.setCullGroup(m._node, instanceGroup(m))
|
|
1539
|
+
groupDirty.clear()
|
|
1540
|
+
}
|
|
1806
1541
|
// The matrices that render the maps are the ones receivers look up
|
|
1807
1542
|
// with: one array to every receiving target per shadow-camera move.
|
|
1808
|
-
|
|
1809
|
-
shadowMatricesDirty = false
|
|
1810
|
-
let dealt = 0
|
|
1811
|
-
forEachShadowSlot((slot, _i, shadow, c) => {
|
|
1812
|
-
let m = shadow.views[c]!.camera.viewProj
|
|
1813
|
-
for (let k = 0; k < 16; k++) shadowMatrices[slot * 16 + k] = m[k]!
|
|
1814
|
-
dealt = slot + 1
|
|
1815
|
-
})
|
|
1816
|
-
for (let slot = dealt; slot < MAX_SHADOW_MAPS; slot++) for (let k = 0; k < 16; k++) shadowMatrices[slot * 16 + k] = IDENTITY[k]!
|
|
1817
|
-
let params: ShaderParams = { uShadowMatrix: shadowMatrices }
|
|
1818
|
-
receivingTargets(t => setTargetParams(t, params))
|
|
1819
|
-
}
|
|
1543
|
+
shadowSys.flushMatrices(params => receivingTargets(t => setTargetParams(t, params)))
|
|
1820
1544
|
// The core recomputes the moved subtrees and writes every entry's
|
|
1821
1545
|
// uModel/uNormal, visibility switch and direction slots.
|
|
1822
1546
|
spatial.flush()
|
|
@@ -1846,49 +1570,43 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1846
1570
|
},
|
|
1847
1571
|
_attachLight(light) {
|
|
1848
1572
|
if (disposed) return
|
|
1849
|
-
if (light.type
|
|
1850
|
-
throw new Error("A scene takes at most " + MAX_LIGHTS + " directional
|
|
1573
|
+
if (light.type !== "hemisphere" && lights.filter(l => l.type !== "hemisphere").length >= MAX_LIGHTS) {
|
|
1574
|
+
throw new Error("A scene takes at most " + MAX_LIGHTS + " lights (directional, spot and point together)")
|
|
1851
1575
|
}
|
|
1852
1576
|
lights.push(light)
|
|
1853
1577
|
lightsDirty = true
|
|
1854
|
-
if (light.type
|
|
1578
|
+
if (light.type !== "hemisphere" && light.castShadow) shadowSys.createShadow(light)
|
|
1855
1579
|
},
|
|
1856
1580
|
_detachLight(light) {
|
|
1857
1581
|
let i = lights.indexOf(light)
|
|
1858
1582
|
if (i >= 0) lights.splice(i, 1)
|
|
1859
1583
|
lightsDirty = true
|
|
1860
|
-
if (light.type
|
|
1584
|
+
if (light.type !== "hemisphere") shadowSys.destroyShadow(light)
|
|
1861
1585
|
hooks._schedule()
|
|
1862
1586
|
},
|
|
1863
1587
|
_shadowChanged(light) {
|
|
1864
1588
|
if (disposed) return
|
|
1865
|
-
|
|
1866
|
-
if (shadow !== undefined) {
|
|
1867
|
-
if (!light.castShadow) {
|
|
1868
|
-
destroyShadow(light)
|
|
1869
|
-
return
|
|
1870
|
-
}
|
|
1871
|
-
// A cascade count change is a different view set: rebuild it. A
|
|
1872
|
-
// mapSize change re-places every tile (the grid cell follows the
|
|
1873
|
-
// largest map).
|
|
1874
|
-
if (shadow.views.length !== light.shadow.cascades) {
|
|
1875
|
-
destroyShadow(light)
|
|
1876
|
-
createShadow(light)
|
|
1877
|
-
return
|
|
1878
|
-
}
|
|
1879
|
-
placeShadows(null)
|
|
1880
|
-
shadow.dirty = true
|
|
1881
|
-
lightsDirty = true
|
|
1882
|
-
hooks._schedule()
|
|
1883
|
-
} else if (light.castShadow) {
|
|
1884
|
-
createShadow(light)
|
|
1885
|
-
}
|
|
1589
|
+
shadowSys.shadowChanged(light)
|
|
1886
1590
|
},
|
|
1887
1591
|
_setCast(mesh) {
|
|
1888
|
-
if (mesh.
|
|
1592
|
+
if (mesh._buffers === null || disposed) return
|
|
1593
|
+
for (let v of views) {
|
|
1594
|
+
if (v.shadowFilter === null) continue
|
|
1595
|
+
if (v.shadowFilter(mesh)) attachView(v, mesh)
|
|
1596
|
+
else detachView(v, mesh)
|
|
1597
|
+
}
|
|
1598
|
+
hooks._schedule()
|
|
1599
|
+
},
|
|
1600
|
+
_setLayers(mesh) {
|
|
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
|
+
}
|
|
1606
|
+
if ((mesh.layers & sceneMask) !== 0) attachScene(mesh)
|
|
1607
|
+
else detachScene(mesh)
|
|
1889
1608
|
for (let v of views) {
|
|
1890
|
-
if (v.
|
|
1891
|
-
if (v.filter(mesh)) attachView(v, mesh)
|
|
1609
|
+
if ((mesh.layers & v.mask) !== 0) attachView(v, mesh)
|
|
1892
1610
|
else detachView(v, mesh)
|
|
1893
1611
|
}
|
|
1894
1612
|
hooks._schedule()
|
|
@@ -1907,53 +1625,59 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1907
1625
|
if (v.override !== null && mesh._instances === null) checkLayout(v.override, mesh.geometry, "View override material")
|
|
1908
1626
|
}
|
|
1909
1627
|
// Instancing pairs the same way layout does: the pipeline's instance
|
|
1910
|
-
// attributes describe the mesh's record
|
|
1628
|
+
// attributes describe the mesh's record buffers, so one without the
|
|
1911
1629
|
// other (or a record stride from a different attribute list) would
|
|
1912
1630
|
// bind garbage - errors here, at add().
|
|
1913
1631
|
let inst = mesh._instances
|
|
1914
|
-
|
|
1915
|
-
if (instAttrs !== undefined && inst === null) {
|
|
1632
|
+
if (mesh.material.instanceAttributes !== undefined && inst === null) {
|
|
1916
1633
|
throw new Error(
|
|
1917
|
-
"Material declares instanceAttributes - create its meshes with createInstancedMesh
|
|
1634
|
+
"Material declares instanceAttributes - create its meshes with createInstancedMesh or createRecordMesh, not createMesh",
|
|
1918
1635
|
)
|
|
1919
1636
|
}
|
|
1920
|
-
if (inst !== null)
|
|
1921
|
-
if (instAttrs === undefined) {
|
|
1922
|
-
throw new Error("Instanced mesh with a non-instanced material - the material must declare instanceAttributes")
|
|
1923
|
-
}
|
|
1924
|
-
let stride = instanceStride(instAttrs)
|
|
1925
|
-
if (stride !== inst.stride) {
|
|
1926
|
-
throw new Error(
|
|
1927
|
-
"Instanced mesh records are " + inst.stride + " floats but the material's instanceAttributes take " + stride,
|
|
1928
|
-
)
|
|
1929
|
-
}
|
|
1930
|
-
}
|
|
1637
|
+
if (inst !== null) checkInstancePairing(mesh.material, inst, "Mesh material")
|
|
1931
1638
|
let bufs = acquireGeometryBuffers(mesh.geometry)
|
|
1932
1639
|
mesh._buffers = bufs
|
|
1933
|
-
|
|
1934
|
-
// in sync() computes one - and _schedule() defers that to a microtask,
|
|
1935
|
-
// so added live it would draw at the seeded identity until then. The
|
|
1936
|
-
// mismatch branch in sync() turns it on in the same pass that writes
|
|
1937
|
-
// uModel.
|
|
1938
|
-
mesh._entry = addDraw(texture, mesh.material.pipeline(mesh.geometry.layout), entrySeed(mesh.material, mesh._params), {
|
|
1939
|
-
buffer: bufs.buffer,
|
|
1940
|
-
indexBuffer: bufs.index,
|
|
1941
|
-
indexFormat: bufs.indexFormat,
|
|
1942
|
-
textures: mesh.material.textures,
|
|
1943
|
-
instanceBuffer: inst !== null ? inst.buffer : undefined,
|
|
1944
|
-
instanceCount: 0,
|
|
1945
|
-
})
|
|
1946
|
-
// The core turns the entry on (with the world matrix) at the next
|
|
1947
|
-
// flush, and off again whenever the node or an ancestor hides.
|
|
1948
|
-
spatial.bindDraw(mesh._node!, texture, mesh._entry, mesh.material.normalMatrix === true, inst !== null ? inst.count : 1)
|
|
1640
|
+
attachScene(mesh)
|
|
1949
1641
|
for (let v of views) attachView(v, mesh)
|
|
1642
|
+
// The style mirror may have been written while the mesh was out of
|
|
1643
|
+
// a scene (or into a buffer this scene never saw): republish it.
|
|
1644
|
+
if (inst !== null && inst.style !== null) {
|
|
1645
|
+
inst.style.dirty = inst.count > 0 ? [0, inst.count * inst.style.stride] : null
|
|
1646
|
+
if (inst.style.dirty !== null) styleDirty.add(mesh as InstancedMesh)
|
|
1647
|
+
}
|
|
1950
1648
|
// Picking: the local box puts the node in the core index; an
|
|
1951
|
-
// ordinary mesh also gets its geometry's triangle shape,
|
|
1952
|
-
//
|
|
1953
|
-
//
|
|
1954
|
-
//
|
|
1649
|
+
// ordinary mesh also gets its geometry's triangle shape, a
|
|
1650
|
+
// populated one is box-only (its instances are the leaves that
|
|
1651
|
+
// pick, each with the shape; a record mesh's records are opaque, so
|
|
1652
|
+
// without explicit bounds it is not picked at all), as is a sprite
|
|
1653
|
+
// (its triangles lie wherever the camera is, not where the geometry
|
|
1654
|
+
// says).
|
|
1955
1655
|
spatial.setBounds(mesh._node!, localBounds(mesh))
|
|
1956
1656
|
spatial.setShape(mesh._node!, inst === null && !mesh._sprite ? bufs.shape : null)
|
|
1657
|
+
// A rebuilt entry (setGeometry) re-boxes the live instances to the
|
|
1658
|
+
// new geometry; their record bindings are untouched. On a first
|
|
1659
|
+
// entry none has a core node yet - they enter after the mesh.
|
|
1660
|
+
if (inst !== null && inst.nodes !== null) {
|
|
1661
|
+
for (let n of inst.nodes.slots) {
|
|
1662
|
+
if (n !== null && n._node !== null) {
|
|
1663
|
+
spatial.setBounds(n._node, geometryBounds(mesh.geometry))
|
|
1664
|
+
spatial.setShape(n._node, bufs.shape)
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
// Culling: the gate and margin only when off the defaults, and a
|
|
1669
|
+
// skinned part's joint group (joints entered before it: a model
|
|
1670
|
+
// adds its node tree before its parts).
|
|
1671
|
+
if (!mesh.frustumCulled || mesh.cullMargin !== 0) spatial.setCull(mesh._node!, mesh.frustumCulled, mesh.cullMargin)
|
|
1672
|
+
if (mesh._cullJoints !== null) {
|
|
1673
|
+
let joints: NodeId[] = []
|
|
1674
|
+
for (let j of mesh._cullJoints) if (j._node !== null) joints.push(j._node)
|
|
1675
|
+
spatial.setCullGroup(mesh._node!, joints)
|
|
1676
|
+
}
|
|
1677
|
+
// An instanced mesh without explicit bounds is culled by its
|
|
1678
|
+
// instances' union, the same group over the instance nodes - set
|
|
1679
|
+
// at the sync, once they have entered (they enter after the mesh).
|
|
1680
|
+
if (inst !== null && inst.nodes !== null && inst.bounds === null) groupDirty.add(mesh as InstancedMesh)
|
|
1957
1681
|
byNode.set(mesh._node!, mesh)
|
|
1958
1682
|
meshes.push(mesh)
|
|
1959
1683
|
mesh._transparent = mesh.material.transparent === true
|
|
@@ -1962,16 +1686,17 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1962
1686
|
this._schedule()
|
|
1963
1687
|
},
|
|
1964
1688
|
_detach(mesh) {
|
|
1965
|
-
if (mesh.
|
|
1689
|
+
if (mesh._buffers !== null) {
|
|
1690
|
+
styleDirty.delete(mesh as InstancedMesh)
|
|
1691
|
+
groupDirty.delete(mesh as InstancedMesh)
|
|
1966
1692
|
for (let v of views) detachView(v, mesh)
|
|
1693
|
+
detachScene(mesh)
|
|
1967
1694
|
if (mesh._node !== null) {
|
|
1968
1695
|
spatial.setShape(mesh._node, null)
|
|
1969
1696
|
spatial.setBounds(mesh._node, null)
|
|
1970
|
-
spatial.unbindDraw(mesh._node, texture)
|
|
1971
1697
|
byNode.delete(mesh._node)
|
|
1972
1698
|
}
|
|
1973
|
-
|
|
1974
|
-
if (mesh._buffers !== null) releaseGeometryBuffers(mesh._buffers)
|
|
1699
|
+
releaseGeometryBuffers(mesh._buffers)
|
|
1975
1700
|
mesh._buffers = null
|
|
1976
1701
|
let i = meshes.indexOf(mesh)
|
|
1977
1702
|
if (i >= 0) meshes.splice(i, 1)
|
|
@@ -1980,9 +1705,36 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1980
1705
|
}
|
|
1981
1706
|
mesh._entry = null
|
|
1982
1707
|
},
|
|
1708
|
+
_attachInstance(instance) {
|
|
1709
|
+
if (disposed) return
|
|
1710
|
+
let mesh = instance.mesh
|
|
1711
|
+
// The mesh entered first (parents enter before children), so its
|
|
1712
|
+
// geometry buffers and core node are live - a mesh masked out of
|
|
1713
|
+
// the scene included, whose instances still pick.
|
|
1714
|
+
if (mesh === null || mesh._buffers === null || mesh._node === null || instance._node === null) return
|
|
1715
|
+
spatial.setBounds(instance._node, geometryBounds(mesh.geometry))
|
|
1716
|
+
spatial.setShape(instance._node, mesh._buffers.shape)
|
|
1717
|
+
spatial.setLayers(instance._node, mesh.layers)
|
|
1718
|
+
// The record is the instance's placement inside the mesh (the mesh
|
|
1719
|
+
// node is the anchor), staged by the core at the next flush.
|
|
1720
|
+
spatial.bindMatrixRecord(instance._node, mesh._instances.buffer, instance._slot, mesh._node)
|
|
1721
|
+
byNode.set(instance._node, instance)
|
|
1722
|
+
if (mesh._instances.bounds === null) groupDirty.add(mesh)
|
|
1723
|
+
this._schedule()
|
|
1724
|
+
},
|
|
1725
|
+
_detachInstance(instance) {
|
|
1726
|
+
// The core node is destroyed right after: its box, shape and record
|
|
1727
|
+
// binding go with it, and the slot hides at the next flush. The
|
|
1728
|
+
// mesh's group loses the member at the sync (a mesh leaving with
|
|
1729
|
+
// its instances is skipped there: its node is gone by then).
|
|
1730
|
+
if (instance._node !== null) byNode.delete(instance._node)
|
|
1731
|
+
let mesh = instance.mesh
|
|
1732
|
+
if (mesh !== null && mesh._instances !== null && mesh._instances.bounds === null) groupDirty.add(mesh)
|
|
1733
|
+
this._schedule()
|
|
1734
|
+
},
|
|
1983
1735
|
_setParams(mesh, params) {
|
|
1984
|
-
if (mesh.
|
|
1985
|
-
setDrawParams(texture, mesh._entry, params)
|
|
1736
|
+
if (mesh._buffers === null || disposed) return
|
|
1737
|
+
if (mesh._entry !== null) setDrawParams(texture, mesh._entry, params)
|
|
1986
1738
|
// A view drawing the mesh's own material carries its params too; an
|
|
1987
1739
|
// overridden view has none of them.
|
|
1988
1740
|
for (let v of views) {
|
|
@@ -1993,22 +1745,29 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
1993
1745
|
_setCount(mesh) {
|
|
1994
1746
|
// The core composes the count with the visibility switch: a hidden
|
|
1995
1747
|
// entry stays at 0 and the unhide restores the new count.
|
|
1996
|
-
if (mesh.
|
|
1748
|
+
if (mesh._buffers !== null && mesh._node !== null && !disposed && mesh._instances !== null) {
|
|
1997
1749
|
spatial.setDrawCount(mesh._node, mesh._instances.count)
|
|
1998
1750
|
}
|
|
1999
1751
|
},
|
|
2000
1752
|
_setBuffer(mesh) {
|
|
2001
|
-
// The
|
|
2002
|
-
//
|
|
2003
|
-
// the old
|
|
2004
|
-
|
|
2005
|
-
|
|
1753
|
+
// The entries keep their range (at most the old capacity, so the
|
|
1754
|
+
// larger buffers always pass the swap's bounds check); the caller
|
|
1755
|
+
// destroys the old buffers after this, which the entries held alive
|
|
1756
|
+
// until now. Each entry swaps the slots its own material binds.
|
|
1757
|
+
let inst = mesh._instances
|
|
1758
|
+
if (mesh._buffers !== null && !disposed && inst !== null) {
|
|
1759
|
+
if (mesh._entry !== null) setDrawBuffers(texture, mesh._entry, instanceBinding(mesh.material, inst))
|
|
2006
1760
|
for (let v of views) {
|
|
2007
1761
|
let entry = v.entries.get(mesh)
|
|
2008
|
-
if (entry !== undefined) setDrawBuffers(v.texture, entry,
|
|
1762
|
+
if (entry !== undefined) setDrawBuffers(v.texture, entry, instanceBinding(viewMaterial(v, mesh), inst))
|
|
2009
1763
|
}
|
|
2010
1764
|
}
|
|
2011
1765
|
},
|
|
1766
|
+
_setStyle(mesh) {
|
|
1767
|
+
if (disposed || mesh._buffers === null) return
|
|
1768
|
+
styleDirty.add(mesh)
|
|
1769
|
+
this._schedule()
|
|
1770
|
+
},
|
|
2012
1771
|
_reorder() {
|
|
2013
1772
|
orderDirty = true
|
|
2014
1773
|
for (let v of views) v.orderDirty = true
|
|
@@ -2032,134 +1791,19 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
2032
1791
|
lightsDirty = true
|
|
2033
1792
|
hooks._schedule()
|
|
2034
1793
|
|
|
2035
|
-
//
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
type InternalEvent = ScenePointerEvent & { _stopped: boolean }
|
|
2039
|
-
|
|
2040
|
-
let makeEvent = (e: ElementPointerEvent, mesh: Mesh, x: number, y: number, point: Vec3 | null, distance: number | null): InternalEvent => {
|
|
2041
|
-
let event: InternalEvent = {
|
|
2042
|
-
mesh,
|
|
2043
|
-
currentTarget: mesh,
|
|
2044
|
-
point,
|
|
2045
|
-
distance,
|
|
2046
|
-
x,
|
|
2047
|
-
y,
|
|
2048
|
-
pointerId: e.pointerId,
|
|
2049
|
-
pointerType: e.pointerType,
|
|
2050
|
-
button: e.button,
|
|
2051
|
-
shiftKey: e.shiftKey,
|
|
2052
|
-
ctrlKey: e.ctrlKey,
|
|
2053
|
-
altKey: e.altKey,
|
|
2054
|
-
metaKey: e.metaKey,
|
|
2055
|
-
_stopped: false,
|
|
2056
|
-
stopPropagation() {
|
|
2057
|
-
event._stopped = true
|
|
2058
|
-
},
|
|
2059
|
-
}
|
|
2060
|
-
return event
|
|
2061
|
-
}
|
|
2062
|
-
|
|
2063
|
-
let bubble = (name: BubbleName, event: InternalEvent): void => {
|
|
2064
|
-
for (let n: SceneNode | null = event.mesh; n !== null && !event._stopped; n = n.parent) {
|
|
2065
|
-
let handler = n[name]
|
|
2066
|
-
if (handler) {
|
|
2067
|
-
event.currentTarget = n
|
|
2068
|
-
handler(event)
|
|
2069
|
-
}
|
|
2070
|
-
}
|
|
2071
|
-
}
|
|
2072
|
-
|
|
2073
|
-
// The captured mesh's own hit, if the ray still strikes it.
|
|
2074
|
-
let hitOn = (mesh: Mesh, x: number, y: number): Hit | null => {
|
|
2075
|
-
for (let h of scene.pick(x, y)) if (h.mesh === mesh) return h
|
|
2076
|
-
return null
|
|
2077
|
-
}
|
|
2078
|
-
|
|
2079
|
-
// localX/localY arrive in the leaf's LAYOUT frame (the hit test undoes
|
|
2080
|
-
// every transform above it, design-size fits included), so a leaf laid out at
|
|
2081
|
-
// the target size - the built-in <Scene> leaf, a d-texture at natural
|
|
2082
|
-
// size - is already in scene pixels. Only a leaf deliberately laid out at
|
|
2083
|
-
// a DIFFERENT size (the supersampling pattern) needs the ratio, and only
|
|
2084
|
-
// the app knows that layout: handlersFor takes it.
|
|
2085
|
-
let makeHandlers = (layout: (() => { width: number; height: number }) | null): SceneHandlers => {
|
|
2086
|
-
let eventX = 0
|
|
2087
|
-
let eventY = 0
|
|
2088
|
-
let toScene = (e: ElementPointerEvent): void => {
|
|
2089
|
-
if (layout === null) {
|
|
2090
|
-
eventX = e.localX
|
|
2091
|
-
eventY = e.localY
|
|
2092
|
-
return
|
|
2093
|
-
}
|
|
2094
|
-
let l = layout()
|
|
2095
|
-
eventX = e.localX * (l.width > 0 ? width / l.width : 1)
|
|
2096
|
-
eventY = e.localY * (l.height > 0 ? height / l.height : 1)
|
|
2097
|
-
}
|
|
2098
|
-
return {
|
|
2099
|
-
onPointerDown(e) {
|
|
2100
|
-
toScene(e)
|
|
2101
|
-
let hit = scene.pick(eventX, eventY)[0]
|
|
2102
|
-
if (hit === undefined) return
|
|
2103
|
-
capture.set(e.pointerId, hit.mesh)
|
|
2104
|
-
bubble("onPointerDown", makeEvent(e, hit.mesh, eventX, eventY, hit.point, hit.distance))
|
|
2105
|
-
},
|
|
2106
|
-
onPointerMove(e) {
|
|
2107
|
-
toScene(e)
|
|
2108
|
-
let captured = capture.get(e.pointerId)
|
|
2109
|
-
if (captured !== undefined) {
|
|
2110
|
-
let hit = hitOn(captured, eventX, eventY)
|
|
2111
|
-
bubble("onPointerMove", makeEvent(e, captured, eventX, eventY, hit ? hit.point : null, hit ? hit.distance : null))
|
|
2112
|
-
return
|
|
2113
|
-
}
|
|
2114
|
-
let hit = scene.pick(eventX, eventY)[0]
|
|
2115
|
-
let prev = hover.get(e.pointerId)
|
|
2116
|
-
if (prev !== hit?.mesh) {
|
|
2117
|
-
if (prev !== undefined) {
|
|
2118
|
-
hover.delete(e.pointerId)
|
|
2119
|
-
prev.onPointerLeave?.(makeEvent(e, prev, eventX, eventY, null, null))
|
|
2120
|
-
}
|
|
2121
|
-
if (hit !== undefined) {
|
|
2122
|
-
hover.set(e.pointerId, hit.mesh)
|
|
2123
|
-
hit.mesh.onPointerEnter?.(makeEvent(e, hit.mesh, eventX, eventY, hit.point, hit.distance))
|
|
2124
|
-
}
|
|
2125
|
-
}
|
|
2126
|
-
if (hit !== undefined) {
|
|
2127
|
-
bubble("onPointerMove", makeEvent(e, hit.mesh, eventX, eventY, hit.point, hit.distance))
|
|
2128
|
-
}
|
|
2129
|
-
},
|
|
2130
|
-
onPointerUp(e) {
|
|
2131
|
-
toScene(e)
|
|
2132
|
-
let captured = capture.get(e.pointerId)
|
|
2133
|
-
if (captured !== undefined) {
|
|
2134
|
-
capture.delete(e.pointerId)
|
|
2135
|
-
let hit = hitOn(captured, eventX, eventY)
|
|
2136
|
-
bubble("onPointerUp", makeEvent(e, captured, eventX, eventY, hit ? hit.point : null, hit ? hit.distance : null))
|
|
2137
|
-
return
|
|
2138
|
-
}
|
|
2139
|
-
let hit = scene.pick(eventX, eventY)[0]
|
|
2140
|
-
if (hit !== undefined) {
|
|
2141
|
-
bubble("onPointerUp", makeEvent(e, hit.mesh, eventX, eventY, hit.point, hit.distance))
|
|
2142
|
-
}
|
|
2143
|
-
},
|
|
2144
|
-
onPointerLeave(e) {
|
|
2145
|
-
let prev = hover.get(e.pointerId)
|
|
2146
|
-
if (prev !== undefined) {
|
|
2147
|
-
hover.delete(e.pointerId)
|
|
2148
|
-
toScene(e)
|
|
2149
|
-
prev.onPointerLeave?.(makeEvent(e, prev, eventX, eventY, null, null))
|
|
2150
|
-
}
|
|
2151
|
-
},
|
|
2152
|
-
}
|
|
2153
|
-
}
|
|
2154
|
-
let handlers = makeHandlers(null)
|
|
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>()
|
|
2155
1797
|
|
|
2156
1798
|
let scene: Scene = {
|
|
2157
1799
|
texture,
|
|
1800
|
+
depthTexture: depthMode === "texture" ? depthTexture(texture) : null,
|
|
2158
1801
|
root,
|
|
2159
1802
|
setCamera(update) {
|
|
2160
1803
|
updateCamera(camera, update)
|
|
2161
1804
|
hooks._schedule()
|
|
2162
1805
|
},
|
|
1806
|
+
camera: () => cameraState(camera),
|
|
2163
1807
|
setSize(w, h) {
|
|
2164
1808
|
if (disposed || (w === width && h === height)) return
|
|
2165
1809
|
width = w
|
|
@@ -2172,48 +1816,85 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
2172
1816
|
if (disposed) return
|
|
2173
1817
|
Object.assign(sceneParams, params)
|
|
2174
1818
|
setTargetParams(texture, params)
|
|
2175
|
-
for (let v of views)
|
|
1819
|
+
for (let v of views) {
|
|
1820
|
+
// A view's own names (view.setParams, its fog) win over the
|
|
1821
|
+
// scene-wide fan-out.
|
|
1822
|
+
let fanned = v.ownNames.size === 0 ? params : withoutNames(params, v.ownNames)
|
|
1823
|
+
if (fanned !== params && Object.keys(fanned).length === 0) continue
|
|
1824
|
+
setTargetParams(v.texture, fanned)
|
|
1825
|
+
}
|
|
2176
1826
|
},
|
|
2177
1827
|
setFog(fog) {
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
let
|
|
2185
|
-
|
|
2186
|
-
|
|
1828
|
+
scene.setParams(fogParams(fog))
|
|
1829
|
+
},
|
|
1830
|
+
setLayers(mask) {
|
|
1831
|
+
checkMask(mask, "scene.setLayers")
|
|
1832
|
+
if (disposed || sceneMask === mask) return
|
|
1833
|
+
sceneMask = mask
|
|
1834
|
+
for (let mesh of meshes) {
|
|
1835
|
+
if ((mesh.layers & mask) !== 0) attachScene(mesh)
|
|
1836
|
+
else detachScene(mesh)
|
|
2187
1837
|
}
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
if (
|
|
2192
|
-
|
|
1838
|
+
// Shadow views follow the scene's mask: what the scene cannot see
|
|
1839
|
+
// must not darken it.
|
|
1840
|
+
for (let v of views) {
|
|
1841
|
+
if (v.shadowFilter === null) continue
|
|
1842
|
+
v.mask = mask
|
|
1843
|
+
for (let mesh of meshes) {
|
|
1844
|
+
if ((mesh.layers & mask) !== 0) attachView(v, mesh)
|
|
1845
|
+
else detachView(v, mesh)
|
|
2193
1846
|
}
|
|
2194
|
-
scene.setParams({ ...params, uFogInv: 0, uFogDensity: density })
|
|
2195
|
-
return
|
|
2196
|
-
}
|
|
2197
|
-
let { near, far } = fog
|
|
2198
|
-
if (!(Number.isFinite(near) && Number.isFinite(far)) || far <= near) {
|
|
2199
|
-
throw new Error(`setFog: near/far must be finite with near < far, got ${near}..${far}`)
|
|
2200
1847
|
}
|
|
2201
|
-
|
|
1848
|
+
hooks._schedule()
|
|
2202
1849
|
},
|
|
2203
1850
|
setBackground(source) {
|
|
2204
1851
|
if (disposed) return
|
|
1852
|
+
// A skybox replacing a skybox keeps its entries: same program, new
|
|
1853
|
+
// params and cube on every target.
|
|
1854
|
+
if (source !== null && typeof source !== "string" && background !== null && background.sky !== null) {
|
|
1855
|
+
let params = skyboxParams(source, "scene.setBackground")
|
|
1856
|
+
for (let [target, entry] of background.entries) {
|
|
1857
|
+
setDrawParams(target, entry, params)
|
|
1858
|
+
setDrawTextures(target, entry, { uSky: source.cube })
|
|
1859
|
+
}
|
|
1860
|
+
background.sky = source
|
|
1861
|
+
return
|
|
1862
|
+
}
|
|
1863
|
+
let sky = source === null || typeof source === "string" ? null : source
|
|
1864
|
+
if (sky !== null) skyboxParams(sky, "scene.setBackground")
|
|
2205
1865
|
if (background !== null) {
|
|
2206
|
-
|
|
1866
|
+
for (let [target, entry] of background.entries) removeDraw(target, entry)
|
|
2207
1867
|
destroyRenderPipeline(background.pipeline)
|
|
2208
1868
|
destroyProgram(background.program)
|
|
2209
1869
|
background = null
|
|
2210
1870
|
}
|
|
2211
1871
|
if (source === null) return
|
|
2212
|
-
let
|
|
2213
|
-
|
|
1872
|
+
let label = opts?.label ?? "scene"
|
|
1873
|
+
let built = sky === null ? backgroundPipeline(source as string, label + "-background") : backgroundPipeline(SKYBOX_FRAGMENT, label + "-skybox")
|
|
1874
|
+
background = { pipeline: built.pipeline, program: built.program, sky, entries: new Map() }
|
|
1875
|
+
// First in list order on every target that draws it: inserted before
|
|
1876
|
+
// the first mesh ENTRY (a layers-masked mesh has none), and every
|
|
2214
1877
|
// later sort keeps it there.
|
|
2215
|
-
|
|
2216
|
-
|
|
1878
|
+
attachBackground(texture, meshes.find(m => m._entry !== null)?._entry ?? undefined)
|
|
1879
|
+
for (let v of views) if (v.sky && !v.disposed) attachBackground(v.texture, v.lastOrder[0] ?? v.entries.values().next().value)
|
|
1880
|
+
},
|
|
1881
|
+
setEnvironment(env) {
|
|
1882
|
+
if (disposed) return
|
|
1883
|
+
let params = environmentParams(env)
|
|
1884
|
+
environment = env === null ? null : env.cube
|
|
1885
|
+
let cube = environment ?? envPlaceholder
|
|
1886
|
+
// A probe's own faces never sample the probe (see writeLights).
|
|
1887
|
+
receivingTargets(t => setTargetTextures(t, { uEnv: ownsEnvironment(t) ? envPlaceholder : cube }))
|
|
1888
|
+
scene.setParams(params)
|
|
1889
|
+
},
|
|
1890
|
+
setToneMapping(mode) {
|
|
1891
|
+
let code = TONE_MAPPING_CODE[mode]
|
|
1892
|
+
if (code === undefined) throw new Error('scene.setToneMapping: expected "none" or "aces", got ' + mode)
|
|
1893
|
+
scene.setParams({ uToneMapping: code })
|
|
1894
|
+
},
|
|
1895
|
+
setExposure(exposure) {
|
|
1896
|
+
if (!Number.isFinite(exposure) || exposure < 0) throw new Error("scene.setExposure: expected a finite number >= 0, got " + exposure)
|
|
1897
|
+
scene.setParams({ uExposure: exposure })
|
|
2217
1898
|
},
|
|
2218
1899
|
project(point) {
|
|
2219
1900
|
ensureCamera(camera, width, height)
|
|
@@ -2221,50 +1902,48 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
2221
1902
|
let w = clip[3]
|
|
2222
1903
|
if (w < 1e-6) return null
|
|
2223
1904
|
// perspective() bakes the y-down clip flip, so NDC maps straight to
|
|
2224
|
-
// top-left-origin pixels with no negation here.
|
|
2225
|
-
|
|
2226
|
-
|
|
1905
|
+
// top-left-origin pixels with no negation here.
|
|
1906
|
+
let x = ((clip[0] / w) * 0.5 + 0.5) * width
|
|
1907
|
+
let y = ((clip[1] / w) * 0.5 + 0.5) * height
|
|
1908
|
+
if (camera.ortho !== null) {
|
|
1909
|
+
// An orthographic clip w is 1 everywhere (every point projects, the
|
|
1910
|
+
// divides above are no-ops) and carries no depth, so `w` reports
|
|
1911
|
+
// the camera-forward distance off the view row instead - the same
|
|
1912
|
+
// meaning as the perspective clip w, and unproject's exact input.
|
|
1913
|
+
let v = camera.view
|
|
1914
|
+
w = -(v[2] * point[0] + v[6] * point[1] + v[10] * point[2] + v[14])
|
|
1915
|
+
}
|
|
1916
|
+
return { x, y, w }
|
|
2227
1917
|
},
|
|
2228
1918
|
viewProj(out) {
|
|
2229
1919
|
ensureCamera(camera, width, height)
|
|
2230
1920
|
return copy(out ?? mat4(), camera.viewProj)
|
|
2231
1921
|
},
|
|
2232
1922
|
pick(x, y) {
|
|
2233
|
-
|
|
2234
|
-
let v = camera.view
|
|
2235
|
-
let o = camera.ortho
|
|
2236
|
-
if (o === null) {
|
|
2237
|
-
// The camera-frame ray through the pixel, inverting project()'s
|
|
2238
|
-
// mapping: the baked y-down clip flip is why pixel y converts with
|
|
2239
|
-
// no negation there and one here.
|
|
2240
|
-
let f = 1 / Math.tan(((camera.fov * Math.PI) / 180) / 2)
|
|
2241
|
-
let cx = (((x / width) * 2 - 1) * (width / height)) / f
|
|
2242
|
-
let cy = -((y / height) * 2 - 1) / f
|
|
2243
|
-
// The view's upper 3x3 rows are the camera axes, so its transpose
|
|
2244
|
-
// carries the camera-space direction (cx, cy, -1) to world.
|
|
2245
|
-
pickDir[0] = cx * v[0] + cy * v[1] - v[2]
|
|
2246
|
-
pickDir[1] = cx * v[4] + cy * v[5] - v[6]
|
|
2247
|
-
pickDir[2] = cx * v[8] + cy * v[9] - v[10]
|
|
2248
|
-
return scene.raycast(camera.eye, pickDir)
|
|
2249
|
-
}
|
|
2250
|
-
// Orthographic: every ray runs along the camera's forward axis; the
|
|
2251
|
-
// pixel picks where on the camera plane it starts (top row = top).
|
|
2252
|
-
let cx = o.left + (x / width) * (o.right - o.left)
|
|
2253
|
-
let cy = o.top + (y / height) * (o.bottom - o.top)
|
|
2254
|
-
pickOrigin[0] = camera.eye[0] + cx * v[0] + cy * v[1]
|
|
2255
|
-
pickOrigin[1] = camera.eye[1] + cx * v[4] + cy * v[5]
|
|
2256
|
-
pickOrigin[2] = camera.eye[2] + cx * v[8] + cy * v[9]
|
|
2257
|
-
pickDir[0] = -v[2]
|
|
2258
|
-
pickDir[1] = -v[6]
|
|
2259
|
-
pickDir[2] = -v[10]
|
|
1923
|
+
pixelRay(x, y)
|
|
2260
1924
|
return scene.raycast(pickOrigin, pickDir)
|
|
2261
1925
|
},
|
|
2262
|
-
|
|
1926
|
+
unproject(x, y, w, out = [0, 0, 0]) {
|
|
1927
|
+
pixelRay(x, y)
|
|
1928
|
+
out[0] = pickOrigin[0] + w * pickDir[0]
|
|
1929
|
+
out[1] = pickOrigin[1] + w * pickDir[1]
|
|
1930
|
+
out[2] = pickOrigin[2] + w * pickDir[2]
|
|
1931
|
+
return out
|
|
1932
|
+
},
|
|
1933
|
+
screenRay(x, y) {
|
|
1934
|
+
pixelRay(x, y)
|
|
1935
|
+
return {
|
|
1936
|
+
origin: [pickOrigin[0], pickOrigin[1], pickOrigin[2]],
|
|
1937
|
+
direction: [pickDir[0], pickDir[1], pickDir[2]],
|
|
1938
|
+
}
|
|
1939
|
+
},
|
|
1940
|
+
raycast(origin, direction, rayOpts) {
|
|
2263
1941
|
// Flush pending writes: picking sees the tree as the app just wrote
|
|
2264
1942
|
// it, the same immediacy contract as lookAt()/project(). (The queued
|
|
2265
1943
|
// microtask still runs and finds nothing dirty - harmless.)
|
|
2266
1944
|
if (scheduled) sync()
|
|
2267
1945
|
if (disposed) return []
|
|
1946
|
+
let f = queryFilter(rayOpts, "raycast")
|
|
2268
1947
|
let hits: Hit[] = []
|
|
2269
1948
|
rayOriginScratch[0] = origin[0]
|
|
2270
1949
|
rayOriginScratch[1] = origin[1]
|
|
@@ -2272,31 +1951,89 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
2272
1951
|
rayDirScratch[0] = direction[0]
|
|
2273
1952
|
rayDirScratch[1] = direction[1]
|
|
2274
1953
|
rayDirScratch[2] = direction[2]
|
|
2275
|
-
for (let h of spatial.raycast(rayOriginScratch, rayDirScratch)) {
|
|
2276
|
-
let
|
|
2277
|
-
if (
|
|
2278
|
-
let hit: Hit = { mesh, distance: h.distance, point: h.point }
|
|
2279
|
-
if (
|
|
1954
|
+
for (let h of spatial.raycast(rayOriginScratch, rayDirScratch, f)) {
|
|
1955
|
+
let leaf = leafOf(h.node)
|
|
1956
|
+
if (leaf === null) continue
|
|
1957
|
+
let hit: Hit = { mesh: leaf.mesh, distance: h.distance, point: h.point, normal: h.normal }
|
|
1958
|
+
if (leaf.instance !== null) hit.instance = leaf.instance
|
|
2280
1959
|
if (h.face !== undefined) hit.face = h.face
|
|
2281
1960
|
if (h.uv !== undefined) hit.uv = h.uv
|
|
2282
1961
|
hits.push(hit)
|
|
2283
1962
|
}
|
|
2284
1963
|
return hits
|
|
2285
1964
|
},
|
|
2286
|
-
|
|
1965
|
+
overlap(volume, qopts) {
|
|
1966
|
+
if (scheduled) sync()
|
|
1967
|
+
if (disposed) return []
|
|
1968
|
+
let f = queryFilter(qopts, "overlap")
|
|
1969
|
+
let kind = packVolume(volume, "overlap")
|
|
1970
|
+
let out: Overlap[] = []
|
|
1971
|
+
for (let h of spatial.overlap(kind, kind === "box" ? boxScratch : capsuleScratch, f)) {
|
|
1972
|
+
let leaf = leafOf(h.node)
|
|
1973
|
+
if (leaf === null) continue
|
|
1974
|
+
let contact: Overlap = { mesh: leaf.mesh, point: h.point, normal: h.normal, depth: h.depth }
|
|
1975
|
+
if (leaf.instance !== null) contact.instance = leaf.instance
|
|
1976
|
+
out.push(contact)
|
|
1977
|
+
}
|
|
1978
|
+
return out
|
|
1979
|
+
},
|
|
1980
|
+
sweep(volume, motion, qopts) {
|
|
1981
|
+
if (scheduled) sync()
|
|
1982
|
+
if (disposed) return []
|
|
1983
|
+
let f = queryFilter(qopts, "sweep")
|
|
1984
|
+
let kind = packVolume(volume, "sweep")
|
|
1985
|
+
motionScratch[0] = motion[0]
|
|
1986
|
+
motionScratch[1] = motion[1]
|
|
1987
|
+
motionScratch[2] = motion[2]
|
|
1988
|
+
let out: 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)
|
|
1992
|
+
}
|
|
1993
|
+
return out
|
|
1994
|
+
},
|
|
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
|
+
},
|
|
2287
2014
|
handlersFor(layout) {
|
|
2288
|
-
return
|
|
2015
|
+
return pointer.handlersFor(layout)
|
|
2016
|
+
},
|
|
2017
|
+
listen(listener) {
|
|
2018
|
+
listeners.add(listener)
|
|
2019
|
+
return () => {
|
|
2020
|
+
listeners.delete(listener)
|
|
2021
|
+
}
|
|
2289
2022
|
},
|
|
2290
2023
|
createView(vopts) {
|
|
2291
2024
|
if (disposed) throw new Error("createView: the scene is disposed")
|
|
2292
2025
|
let v = makeView(vopts, null)
|
|
2293
|
-
|
|
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 = {
|
|
2294
2030
|
texture: v.texture,
|
|
2295
2031
|
depthTexture: vopts.depth === "texture" && vopts.into === undefined ? depthTexture(v.texture) : null,
|
|
2296
2032
|
setCamera(update) {
|
|
2297
2033
|
updateCamera(v.camera, update)
|
|
2298
2034
|
hooks._schedule()
|
|
2299
2035
|
},
|
|
2036
|
+
camera: () => cameraState(v.camera),
|
|
2300
2037
|
setSize(w, h) {
|
|
2301
2038
|
if (v.disposed || (w === v.width && h === v.height)) return
|
|
2302
2039
|
v.width = w
|
|
@@ -2315,16 +2052,69 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
2315
2052
|
hooks._schedule()
|
|
2316
2053
|
},
|
|
2317
2054
|
setParams(params) {
|
|
2318
|
-
if (
|
|
2055
|
+
if (v.disposed) return
|
|
2056
|
+
for (let k of Object.keys(params)) v.ownNames.add(k)
|
|
2057
|
+
setTargetParams(v.texture, params)
|
|
2058
|
+
},
|
|
2059
|
+
setLayers(mask) {
|
|
2060
|
+
checkMask(mask, "view.setLayers")
|
|
2061
|
+
if (v.disposed || v.mask === mask) return
|
|
2062
|
+
v.mask = mask
|
|
2063
|
+
for (let mesh of meshes) {
|
|
2064
|
+
if ((mesh.layers & mask) !== 0) attachView(v, mesh)
|
|
2065
|
+
else detachView(v, mesh)
|
|
2066
|
+
}
|
|
2067
|
+
hooks._schedule()
|
|
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
|
+
}
|
|
2319
2085
|
},
|
|
2320
2086
|
dispose() {
|
|
2087
|
+
viewListeners.clear()
|
|
2321
2088
|
disposeView(v)
|
|
2322
2089
|
},
|
|
2323
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
|
|
2098
|
+
},
|
|
2099
|
+
createReflectionProbe(popts) {
|
|
2100
|
+
if (disposed) throw new Error("createReflectionProbe: the scene is disposed")
|
|
2101
|
+
let { cube, setPosition, update, dispose } = makeProbe(popts, checkMask(popts.layers ?? 1, "createReflectionProbe"))
|
|
2102
|
+
return { cube, setPosition, update, dispose }
|
|
2103
|
+
},
|
|
2104
|
+
bakeBackground(size = PROBE_SIZE) {
|
|
2105
|
+
if (disposed) throw new Error("bakeBackground: the scene is disposed")
|
|
2106
|
+
if (background === null) throw new Error("scene.bakeBackground: the scene has no background to bake")
|
|
2107
|
+
if (!Number.isInteger(size) || size < 1) throw new Error("scene.bakeBackground: size must be a positive integer, got " + size)
|
|
2108
|
+
// A probe that sees no mesh (mask 0) draws the background alone: the
|
|
2109
|
+
// same face cameras, linear output and prefilter as any probe.
|
|
2110
|
+
let probe = makeProbe({ position: [0, 0, 0], size, label: (opts?.label ?? "scene") + "-sky" }, 0)
|
|
2111
|
+
probe.update()
|
|
2112
|
+
return probe.finish()
|
|
2324
2113
|
},
|
|
2325
2114
|
dispose() {
|
|
2326
2115
|
if (disposed) return
|
|
2327
2116
|
disposed = true
|
|
2117
|
+
listeners.clear()
|
|
2328
2118
|
// Full tree-side teardown, not just the target: every node leaves
|
|
2329
2119
|
// the scene (entries' geometry-buffer references and pick leaves
|
|
2330
2120
|
// dropped, core nodes freed), so a disposed scene leaves no
|
|
@@ -2335,32 +2125,101 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
2335
2125
|
spatial.destroyNode(root._node)
|
|
2336
2126
|
root._node = null
|
|
2337
2127
|
}
|
|
2128
|
+
spatial.setFrustum(texture, null)
|
|
2338
2129
|
// Drain the zeroed direction slots the teardown queued while the
|
|
2339
2130
|
// targets still exist; afterwards their groups are gone.
|
|
2340
2131
|
spatial.flush()
|
|
2341
2132
|
destroyTexture(texture)
|
|
2342
|
-
shadows.clear()
|
|
2343
2133
|
for (let v of views.slice()) disposeView(v)
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
shadowAtlas = null
|
|
2347
|
-
}
|
|
2134
|
+
shadowSys.dispose()
|
|
2135
|
+
destroyTexture(envPlaceholder)
|
|
2348
2136
|
if (background !== null) {
|
|
2349
|
-
// The
|
|
2350
|
-
// scene's own (unlike shared material pipelines), so they go too.
|
|
2137
|
+
// The entries died with the targets; the pipeline and program are
|
|
2138
|
+
// the scene's own (unlike shared material pipelines), so they go too.
|
|
2351
2139
|
destroyRenderPipeline(background.pipeline)
|
|
2352
2140
|
destroyProgram(background.program)
|
|
2353
2141
|
background = null
|
|
2354
2142
|
}
|
|
2355
2143
|
},
|
|
2356
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
|
+
})
|
|
2154
|
+
// A reflection probe over layer mask `mask` (see createReflectionProbe):
|
|
2155
|
+
// the public object plus `finish`, which keeps the prefiltered chain and
|
|
2156
|
+
// drops everything else - the bake's one-shot use.
|
|
2157
|
+
let makeProbe = (popts: ReflectionProbeOptions, mask: number): ReflectionProbe & { finish(): TextureId } => {
|
|
2158
|
+
let size = popts.size ?? PROBE_SIZE
|
|
2159
|
+
if (!Number.isInteger(size) || size < 1) throw new Error("createReflectionProbe: size must be a positive integer, got " + size)
|
|
2160
|
+
let prefilter = popts.prefilter !== false
|
|
2161
|
+
let v = makeView(
|
|
2162
|
+
{ width: size, height: size, layers: mask, clearColor: popts.clearColor, label: popts.label },
|
|
2163
|
+
null,
|
|
2164
|
+
size,
|
|
2165
|
+
prefilter,
|
|
2166
|
+
true,
|
|
2167
|
+
)
|
|
2168
|
+
v.camera.mirror = true
|
|
2169
|
+
updateCamera(v.camera, { fov: PROBE_FOV, near: popts.near, far: popts.far })
|
|
2170
|
+
// The faces hold LINEAR radiance, untouched by the scene's output
|
|
2171
|
+
// stage: no sRGB encode, no tone mapping, unit exposure - the
|
|
2172
|
+
// probe's own names, so the scene's fan-out leaves them alone.
|
|
2173
|
+
for (let k of Object.keys(LINEAR_OUTPUT)) v.ownNames.add(k)
|
|
2174
|
+
setTargetParams(v.texture, LINEAR_OUTPUT)
|
|
2175
|
+
let chain = prefilter ? createPrefilter(size, v.texture, probeFormat(), (popts.label ?? (opts?.label ?? "scene") + "-probe") + "-chain") : null
|
|
2176
|
+
v.probeCube = chain?.cube ?? v.texture
|
|
2177
|
+
let position: Vec3 = [popts.position[0], popts.position[1], popts.position[2]]
|
|
2178
|
+
return {
|
|
2179
|
+
cube: v.probeCube,
|
|
2180
|
+
setPosition(p) {
|
|
2181
|
+
position = [p[0], p[1], p[2]]
|
|
2182
|
+
},
|
|
2183
|
+
update() {
|
|
2184
|
+
if (v.disposed) return
|
|
2185
|
+
// The scene's pending state (lights, params, the fan-out to this
|
|
2186
|
+
// target) lands before the faces read it.
|
|
2187
|
+
sync()
|
|
2188
|
+
renderCubeFaces(v.texture, v.camera, size, position, chain !== null, face => {
|
|
2189
|
+
// Opaque order is a hint (front to back); only transparency
|
|
2190
|
+
// needs the per-face sort.
|
|
2191
|
+
if (face === 0 || transparentCount > 1) sortView(v)
|
|
2192
|
+
})
|
|
2193
|
+
chain?.run()
|
|
2194
|
+
},
|
|
2195
|
+
dispose() {
|
|
2196
|
+
chain?.dispose()
|
|
2197
|
+
disposeView(v)
|
|
2198
|
+
},
|
|
2199
|
+
finish() {
|
|
2200
|
+
if (chain === null) throw new Error("finish: a sharp probe has no chain to keep")
|
|
2201
|
+
let cube = chain.finish()
|
|
2202
|
+
disposeView(v)
|
|
2203
|
+
return cube
|
|
2204
|
+
},
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2357
2207
|
// The fog set starts at "none" (uFogInv and uFogDensity 0 are factor 0,
|
|
2358
2208
|
// uFogHeightFalloff 0 is no attenuation) so every material that declares
|
|
2359
2209
|
// it has coverage from the first frame; a target tolerates the names
|
|
2360
2210
|
// when nothing declares them.
|
|
2361
2211
|
scene.setParams({ uFogColor: [0, 0, 0], uFogNear: 0, uFogInv: 0, uFogDensity: 0, uFogHeight: 0, uFogHeightFalloff: 0 })
|
|
2212
|
+
// Likewise the environment set starts at "none" (uEnvOn 0), so a
|
|
2213
|
+
// reflective material has coverage before setEnvironment.
|
|
2214
|
+
scene.setParams(environmentParams(null))
|
|
2215
|
+
// And the output stage at its defaults (exposure 1, no tone mapping),
|
|
2216
|
+
// which every library fragment declares.
|
|
2217
|
+
scene.setParams({ uExposure: 1, uToneMapping: TONE_MAPPING_CODE.none, uOutputEncode: 1 })
|
|
2362
2218
|
if (opts?.fog !== undefined) scene.setFog(opts.fog)
|
|
2219
|
+
if (opts?.toneMapping !== undefined) scene.setToneMapping(opts.toneMapping)
|
|
2220
|
+
if (opts?.exposure !== undefined) scene.setExposure(opts.exposure)
|
|
2363
2221
|
if (opts?.background !== undefined) scene.setBackground(opts.background)
|
|
2222
|
+
if (opts?.environment !== undefined) scene.setEnvironment(opts.environment)
|
|
2364
2223
|
if (opts?.autoFree !== false && getOwner()) onCleanup(() => scene.dispose())
|
|
2365
2224
|
return scene
|
|
2366
|
-
}
|
|
2225
|
+
}
|