lecodes-sdk 2.4.7 → 2.4.8

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/package.json CHANGED
@@ -43,7 +43,7 @@
43
43
  "marcidb-embedded": "^0.15.0",
44
44
  "sharp": "^0.35.2"
45
45
  },
46
- "version": "2.4.7",
46
+ "version": "2.4.8",
47
47
  "files": [
48
48
  "src",
49
49
  "dist",
@@ -184,6 +184,10 @@ declare global {
184
184
  * not the roughness-prefiltered `_ibl.ktx` beside it. Optional: hosts that predate it keep the
185
185
  * flat skybox. */
186
186
  setSkyboxTexture?(sceneId: u32, fetchId: bytes): void
187
+ /** Optional. Light the scene from a CUBEMAP texture (a render texture's cube, creator.h setEnvironmentTexture): the
188
+ * IBL reflects it, its irradiance and intensity stay; no roughness prefiltering, so only mirror-like reflections are
189
+ * right. The engine holds the texture while it lights; setDefaultIbl replaces it. */
190
+ setEnvironmentTexture?(sceneId: u32, textureId: u32): void
187
191
  /** Temporal anti-aliasing (Filament's TAA) for the scene and its overlays: on, MSAA and FXAA are off for the
188
192
  * view; `feedback` = the history's share (0.12 default, higher = less ghosting, less smoothing), `sharpness` =
189
193
  * the post-TAA sharpen (0–1), `upscale` = the fraction the 3D is rendered at and TAA-upscaled from (0.5–1,
@@ -325,6 +329,30 @@ declare global {
325
329
  * re-uploads a sub-rectangle with the creation channel count. Optional (Texture.fromPixels throws). @args data width height channels srgb @count pixelBytes=data @owned GlTexture */
326
330
  createTexturePixels?(width: u32, height: u32, channels: u32, data: Uint8Array, srgb: boolean): Handle
327
331
  updateTexturePixels?(textureId: u32, x: u32, y: u32, width: u32, height: u32, data: Uint8Array): void
332
+ /** Optional. A RENDER TEXTURE, what a render view draws into (creator.h createRenderTexture): `flags` bit 1 = CUBEMAP
333
+ * (`width` = the edge, `height` ignored), bit 2 = HDR (R11F_G11F_B10F linear light, for lighting); neither = an LDR
334
+ * display texture (SRGB8_A8, the tone-mapped picture); bit 4 = PREFILTER, with both others only (flags 7, a
335
+ * power-of-two edge of 16 at least): a probe filtered for roughness — the id is the 5-level filtered cube, its views
336
+ * draw a raw one behind it, filtered on the GPU at the frame after each completed cube with `samples` samples
337
+ * (0 = 256; read for PREFILTER only). Otherwise an ordinary texture id (setUniformTexture, setSceneBackground); its
338
+ * texel rows run as OpenGL renders, on every backend (a 2D texture's first row = the picture's bottom). 0xFFFFFFFF
339
+ * on failure. @owned GlTexture */
340
+ createRenderTexture?(width: u32, height: u32, flags: u32, samples: u32): Handle
341
+ /** Optional. A RENDER VIEW of the scene into a render texture, from node `cameraEntityId` (its world pose, read every
342
+ * frame; a cube takes its position only): `layerMask` = the layers it sees (setLayer), `fov` vertical degrees (a cube
343
+ * is 90), `flags` bit 1 = no shadows. Drawn each frame its scene is presented, before the scene's own view; freeing the
344
+ * texture takes its views with it. 0 on failure. @owned GlRenderView */
345
+ createRenderView?(sceneId: u32, textureId: u32, cameraEntityId: u32, layerMask: u8, fov: f32, near: f32, far: f32, flags: u32): Handle
346
+ /** Optional. `everyNth` 1 = every frame, N = one render per N frames drawn, counted afresh from this call (the next
347
+ * frame renders); `facesPerFrame` 1..6 for a cube (round-robin). */
348
+ setRenderViewOptions?(renderViewId: u32, enabled: boolean, everyNth: u32, facesPerFrame: u32): void
349
+ /** Optional. The view's projection, live (createRenderView's fov / near / far were the initial values): a 2D view's
350
+ * vertical fov in degrees and clip planes; a cube ignores `fov` (90) and takes the planes. Unknown id = no-op. */
351
+ setRenderViewProjection?(renderViewId: u32, fov: f32, near: f32, far: f32): void
352
+ /** Optional. RGBA8 bytes of a rectangle of a render texture as the next frame leaves it — rows in the texture's order
353
+ * (the first is texel row `y`), `face` 0..5 = +X -X +Y -Y +Z -Z of a cube (0 for 2D), an HDR one clamped to 0..1
354
+ * (linear); null when refused. A GPU readback, for checks and tools. @custom */
355
+ readRenderTexturePixels?(textureId: u32, face: u32, x: u32, y: u32, width: u32, height: u32, onDone: (pixels: Uint8Array | null) => void): void
328
356
 
329
357
  /** A material INSTANCE of these .filamat bytes (an app's own shader, fetched); the material itself is deduplicated by content in the engine. @c createMaterialInstanceFromBytes @owned GlMaterial @throw createMaterial: no .filamat bytes behind this fetch id */
330
358
  createMaterial(systemId: bytes): Handle
@@ -355,6 +383,12 @@ declare global {
355
383
 
356
384
  setVisible(entityId: u32, visible: boolean): void
357
385
  isVisible(entityId: u32): boolean
386
+ /** Optional. Visibility layers: the entity's explicit layer bits (u8), inherited by its subtree down to a
387
+ * descendant with its own; 0 clears them (inherit again). Nothing set = 0x01. A view draws a renderable
388
+ * when their layers share a bit (the scene's own view sees all). A host without it has no layers. @c setEntityLayer */
389
+ setLayer?(entityId: u32, mask: u8): void
390
+ /** Optional. The entity's effective layer bits: its own, else the nearest ancestor's, else 0x01. @c getEntityLayer */
391
+ getLayer?(entityId: u32): u8
358
392
 
359
393
  /** @custom */
360
394
  createGlb(systemId: bytes, onComplete: (root: Handle | null) => void, onReject: () => void): void
package/src/gl/Camera.ts CHANGED
@@ -1,20 +1,49 @@
1
- // The scene camera — a Node (so position / lookAt / transform all work) plus projection reads and
2
- // screen↔world ray helpers. Created and attached by Scene. Ported from worker/src/components/Camera.ts.
1
+ // A camera — a Node (so position / lookAt / transform all work) plus its lens. The scene camera is created and attached
2
+ // by Scene (`_attach`: its projection, exposure and the screen↔world ray helpers are the screen's); a SECONDARY camera is
3
+ // `new Camera(options)`, a plain node until `renderToTexture` gives it a render view of its scene (RenderTexture.ts,
4
+ // creator-gl renderview.cpp), started once the node is in a scene (Node.ts awaitScene). Ported from
5
+ // worker/src/components/Camera.ts.
3
6
 
4
7
  import { Vec3 } from "../math/vec"
5
8
  import { Aspect } from "../core/Aspect"
6
- import { Node, type NodeTweenProps } from "./Node"
9
+ import { Node, awaitScene, type NodeTweenProps } from "./Node"
7
10
  import { Ray } from "./Ray"
11
+ import { RenderTexture, makeRenderTexture, startRenderView } from "./RenderTexture"
8
12
  import { DOM_GL_CAMERA, floatValue, type TweenChannel, type TweenMeta } from "../animate/tween/spec"
9
13
  import type { Animation } from "../animate/tween/Animation"
10
14
 
11
- /** Animatable camera props on top of the node transform. */
15
+ /** Animatable camera props on top of the node transform. `fov` / `near` / `far` are the SCENE camera's: on a secondary
16
+ * camera they throw (set them directly — they are live — or from `animate({ onUpdate })`). */
12
17
  export type CameraTweenProps = NodeTweenProps & {
13
18
  fov?: number | number[],
14
19
  near?: number | number[],
15
20
  far?: number | number[],
16
21
  }
17
22
 
23
+ /** `new Camera(options)`: a SECONDARY camera's lens and what its view draws (the scene's own camera is `scene.camera`). */
24
+ export type CameraOptions = {
25
+ /** Vertical field of view in degrees (default 60). Live: `camera.fov`. */
26
+ fov?: number
27
+ /** Near clip distance in metres (default 0.01). Live: `camera.near`. */
28
+ near?: number
29
+ /** Far clip distance in metres (default 1000). Live: `camera.far`. */
30
+ far?: number
31
+ /** The layers its view draws, a bit mask over `Node.layer` (default 0xff = everything): a renderable is drawn when its
32
+ * layers share a bit with these — the player's own body left out of a mirror. Fixed when the view starts. */
33
+ layers?: number
34
+ /** Shadows in its view (default `true`): the view renders shadow maps of its own each time it draws. Fixed when the
35
+ * view starts. */
36
+ shadows?: boolean
37
+ }
38
+
39
+ /** `camera.renderToTexture(options)`: the picture's size. */
40
+ export type RenderToTextureOptions = {
41
+ /** Texels wide, 1..4096 (default 256). */
42
+ width?: number
43
+ /** Texels tall, 1..4096 (default 256). */
44
+ height?: number
45
+ }
46
+
18
47
  /** @internal `camera.follow(node)`: copies the node's world pose onto the camera every LATE frame
19
48
  * (order 1000 — after every other aspect has moved things). One per camera; lives on the target. */
20
49
  export class CameraFollowRig extends Aspect<"__cameraFollow"> {
@@ -31,16 +60,33 @@ export class CameraFollowRig extends Aspect<"__cameraFollow"> {
31
60
  /** Projection every host starts a scene with (creator-gl scene.cpp / the lite core agree on these). */
32
61
  export const CAMERA_DEFAULTS = { fov: 60, near: 0.01, far: 1000 }
33
62
 
63
+ /** A texture side: a whole number, 1 at least (the host refuses more than 4096). */
64
+ const side = (n: number): number => Math.max(1, Math.floor(n))
65
+
66
+ /**
67
+ * A camera: a Node (position, `lookAt`, parenting — it looks down its −Z) with a lens, `fov` / `near` / `far`. Two kinds:
68
+ * - THE SCENE CAMERA, `scene.camera`: made by the Scene, it draws the screen. Its exposure, `projectionMatrix` and the
69
+ * ray helpers (`getRay`, `getViewDirection`) are the screen's, and its projection tweens (`animateTo({ fov })`).
70
+ * - A SECONDARY camera, `new Camera({ fov, layers, shadows })`: a plain node until `renderToTexture({ width, height })`
71
+ * gives it a second view of its scene, drawn into a texture every frame the scene is on show — a rear-view mirror, a
72
+ * monitor, a security camera. Place it like any node (`car.add(mirrorCam)`); its lens is live (`mirrorCam.fov = 50`),
73
+ * its view's schedule is `enabled` / `everyNth`. A cube of light for reflections is a `ReflectionProbe`.
74
+ */
34
75
  export class Camera extends Node {
76
+ /** The scene this camera draws the screen of (Scene `_attach`); -1 = a secondary camera. */
35
77
  private _sceneId = -1
36
78
 
37
- /** Tween `fov` / `near` / `far` (a zoom, a dolly) and the transform — see {@link Node.animateTo}. */
79
+ /** Tween `fov` / `near` / `far` (a zoom, a dolly — the scene camera's) and the transform — see {@link Node.animateTo}. */
38
80
  override animateTo(props: CameraTweenProps & TweenMeta): Animation { return super.animateTo(props) }
39
81
  override animateFrom(props: CameraTweenProps & TweenMeta): Animation { return super.animateFrom(props) }
40
- /** @internal The projection channels address the SCENE (one camera per scene on the host). */
82
+ /** @internal The projection channels address the SCENE (one camera per scene on the host): a render view's
83
+ * projection has no tween writer, so a secondary camera's throw. */
41
84
  override _tweenChannel(prop: string): TweenChannel | null {
42
85
  if (prop === "fov" || prop === "near" || prop === "far") {
43
- const id = () => (this._sceneId < 0 ? 0 : this._sceneId + 1) // scene ids start at 0; 0 = unaddressable
86
+ if (this._sceneId < 0) {
87
+ throw new Error(`Camera: a secondary camera's "${prop}" is not animatable (the tween core drives the scene camera's projection only) — set camera.${prop} directly, it is live: per frame, or animate({ from, to, onUpdate: (v) => { camera.${prop} = v } })`)
88
+ }
89
+ const id = () => this._sceneId + 1 // scene ids start at 0; 0 = unaddressable
44
90
  const commit = (v: unknown) => {
45
91
  if (prop === "fov") this._fov = v as number
46
92
  else if (prop === "near") this._near = v as number
@@ -51,11 +97,12 @@ export class Camera extends Node {
51
97
  }
52
98
  return super._tweenChannel(prop)
53
99
  }
54
- /** Live projection matrix (updated by the host when the viewport/fov changes). */
100
+ /** The scene camera's live projection matrix (updated by the host when the viewport / fov changes). A secondary
101
+ * camera's stays zero: its projection is `fov` / `near` / `far` at its texture's aspect. */
55
102
  readonly projectionMatrix = new Float32Array(16)
56
103
 
57
- // Projection the app asked for (host defaults until it does). The host owns the aspect and
58
- // re-applies these on every viewport change, so they are set, not maintained.
104
+ // Projection the app asked for (host defaults until it does). The scene camera's host owns the aspect and re-applies
105
+ // these on every viewport change, so they are set, not maintained; a secondary camera's are its view's.
59
106
  private _fov = CAMERA_DEFAULTS.fov
60
107
  private _near = CAMERA_DEFAULTS.near
61
108
  private _far = CAMERA_DEFAULTS.far
@@ -69,7 +116,29 @@ export class Camera extends Node {
69
116
  private _ev = 0
70
117
  private _exposureSet = false
71
118
 
72
- /** @internal — Scene wires the camera to its native scene id + projection callback. */
119
+ // A secondary camera's view (renderToTexture): what it draws with, fixed when it starts, and its schedule.
120
+ private readonly _layers: number
121
+ private readonly _shadows: boolean
122
+ private _enabled = true
123
+ private _everyNth = 1
124
+ /** @internal The texture `renderToTexture` made (held: it lives as long as the camera does). */
125
+ _texture: RenderTexture | null = null
126
+ /** @internal The render view's owned handle (`_view.id` = the engine's view id), null until the view starts: it lives
127
+ * exactly as long as this camera holds it. */
128
+ _view: Handle | null = null
129
+
130
+ /** A SECONDARY camera (`scene.camera` is the scene's own): a plain node until `renderToTexture`. `options`: its lens
131
+ * (live after, through `fov` / `near` / `far`) and what its view draws (`layers`, `shadows` — fixed). */
132
+ constructor(options: CameraOptions = {}) {
133
+ super()
134
+ if (options.fov !== undefined) this._fov = options.fov
135
+ if (options.near !== undefined) this._near = options.near
136
+ if (options.far !== undefined) this._far = options.far
137
+ this._layers = (options.layers ?? 0xff) & 0xff
138
+ this._shadows = options.shadows !== false
139
+ }
140
+
141
+ /** @internal — Scene wires the camera to its native scene id + projection callback: the SCENE camera. */
73
142
  _attach(sceneId: number): void {
74
143
  this._sceneId = sceneId
75
144
  _creator.attachCameraToAR(sceneId, this.id, (mat) => {
@@ -79,11 +148,90 @@ export class Camera extends Node {
79
148
  if (this._exposureSet) this._applyExposure()
80
149
  }
81
150
 
151
+ /** @internal What only the scene camera has (the screen's ray helpers): a clear throw on a secondary camera. */
152
+ private _sceneOnly(what: string): void {
153
+ if (this._sceneId < 0) throw new Error(`camera.${what}: the scene camera's (scene.camera) — a secondary camera has no screen to pick through`)
154
+ }
155
+ /** @internal What only a secondary camera has (a render view): a clear throw on the scene camera. */
156
+ private _secondaryOnly(what: string): void {
157
+ if (this._sceneId >= 0) throw new Error(`camera.${what}: the scene camera draws the screen, every frame its scene is on show — a second view is a new Camera() with renderToTexture`)
158
+ }
159
+
160
+ /**
161
+ * Draw this camera's view of its scene into a new 2D texture of `width × height` texels (default 256 × 256): the
162
+ * tone-mapped picture as the screen would show it (LDR) — `screen.material.map = cam.renderToTexture({ ... })`. The
163
+ * camera holds it (`camera.texture`).
164
+ *
165
+ * THE VIEW starts once the camera is in a scene (added to one, or under a node that is — at once if it already is)
166
+ * and draws every frame that scene is on show, after the frame's update passes (aspects, physics, animation: this
167
+ * frame's poses) and before the scene's own picture. It sees the scene's renderables, lights, environment and sky (not
168
+ * its overlays) from the camera's WORLD pose (scale ignored) through `fov` / `near` / `far` at the texture's aspect,
169
+ * with the `layers` and `shadows` the camera was made with, at the scene camera's exposure; its post-processing is the
170
+ * engine's default tone mapping (the scene's grading, bloom and the rest are not mirrored). Moving the camera to
171
+ * another scene later does not move the view.
172
+ *
173
+ * COST: every render is another pass over the scene — with `shadows`, its own shadow maps too. Keep the texture small,
174
+ * draw less often (`everyNth`), leave out what the view does not need (`layers`), and turn it off out of sight
175
+ * (`enabled = false`).
176
+ *
177
+ * Calling it again REPLACES the texture and the view: the previous view stops at once, its texture keeps its last
178
+ * picture while something still shows it. Throws on the scene camera, and on a host without render textures (the web
179
+ * hosts, for now).
180
+ */
181
+ renderToTexture(options: RenderToTextureOptions = {}): RenderTexture {
182
+ this._secondaryOnly("renderToTexture")
183
+ const texture = makeRenderTexture("camera.renderToTexture", side(options.width ?? 256), side(options.height ?? 256), 0)
184
+ if (this._view) { // replaced: the old view stops now; its handle goes with this reference
185
+ _creator.setRenderViewOptions?.(this._view.id, false, 1, 6)
186
+ this._view = null
187
+ }
188
+ this._texture = texture
189
+ awaitScene(this)
190
+ return texture
191
+ }
192
+
193
+ /** @internal Node.ts awaitScene: the camera is in scene `sceneId` — start its view into its texture. */
194
+ _bindView(sceneId: number): void {
195
+ const texture = this._texture
196
+ if (this._view || !texture) return
197
+ this._view = startRenderView("camera", sceneId, texture, this.id, this._layers, this._fov, this._near, this._far, this._shadows)
198
+ if (this._view && (!this._enabled || this._everyNth !== 1)) this._sendSchedule()
199
+ }
200
+
201
+ /** The texture `renderToTexture` made — the latest, null before one. */
202
+ get texture(): RenderTexture | null { return this._texture }
203
+
204
+ /** Whether this secondary camera's view draws (default `true`). Off keeps the last picture in its texture and costs
205
+ * nothing — a mirror out of sight; back on, it renders on the next frame drawn. The scene camera always draws (reads
206
+ * `true`; setting it throws). */
207
+ get enabled(): boolean { return this._enabled }
208
+ set enabled(on: boolean) {
209
+ this._secondaryOnly("enabled")
210
+ this._enabled = on
211
+ this._sendSchedule()
212
+ }
213
+ /** Render this secondary camera's view once every N frames DRAWN (default 1 = every frame). The count restarts with
214
+ * each change: the next frame drawn renders, then one in every N. The scene camera draws every frame (reads 1;
215
+ * setting it throws). */
216
+ get everyNth(): number { return this._everyNth }
217
+ set everyNth(n: number) {
218
+ this._secondaryOnly("everyNth")
219
+ this._everyNth = Math.max(1, Math.floor(n))
220
+ this._sendSchedule()
221
+ }
222
+
223
+ /** @internal Send the whole schedule (`setRenderViewOptions` restarts the count, so the next frame drawn renders). */
224
+ private _sendSchedule(): void {
225
+ if (this._view) _creator.setRenderViewOptions?.(this._view.id, this._enabled, this._everyNth, 6)
226
+ }
227
+
82
228
  /** Exposure compensation in stops (default 0): +1 doubles how bright the scene renders, −2
83
229
  * quarters it. The one exposure knob most games need — a dark interior, a flash of white, or eye
84
230
  * adaptation when the sun swings into view (drop it, and the sun stays at the display's peak
85
231
  * while everything else darkens). Scene light follows it; particle `emissive` is post-exposure
86
- * and does not, so a fireball keeps its on-screen brightness. Cheap to animate every frame. */
232
+ * and does not, so a fireball keeps its on-screen brightness. Cheap to animate every frame. The
233
+ * scene camera's: a secondary camera's view renders at its scene camera's exposure (what is set on
234
+ * a secondary camera is kept, unused). */
87
235
  /** The camera's EV100 with the compensation in: log2(aperture² / shutterSpeed x 100 / iso) - compensation. Sunny 16 is
88
236
  * ~15; a lit surface of luminance L lands on screen at L / (1.2 x 2^ev100). */
89
237
  get ev100(): number {
@@ -99,7 +247,7 @@ export class Camera extends Node {
99
247
 
100
248
  /** The physical camera: `aperture` in f-stops, `shutterSpeed` in seconds, `iso`, plus the same
101
249
  * `compensation` in stops — any subset, one call. `camera.setExposure({ iso: 400 })` is two stops
102
- * brighter than the default f/16 · 1/125 · ISO 100. */
250
+ * brighter than the default f/16 · 1/125 · ISO 100. The scene camera's (as `exposureCompensation`). */
103
251
  setExposure(exposure: { aperture?: number, shutterSpeed?: number, iso?: number, compensation?: number }): this {
104
252
  if (exposure.aperture !== undefined) this._exposure.aperture = exposure.aperture
105
253
  if (exposure.shutterSpeed !== undefined) this._exposure.shutterSpeed = exposure.shutterSpeed
@@ -117,17 +265,25 @@ export class Camera extends Node {
117
265
  _creator.setCameraExposure?.(this._sceneId, e.aperture, e.shutterSpeed, e.iso * 2 ** this._ev)
118
266
  }
119
267
 
120
- /** Vertical field of view in degrees (default 60) — a smaller angle is a longer lens. */
121
- get fov(): number { return _creator.getCameraFov(this._sceneId, 0) }
268
+ /** Vertical field of view in degrees (default 60) — a smaller angle is a longer lens. Live on both kinds: a
269
+ * secondary camera's reaches its view at the next render (before `renderToTexture`, the view's starting value). */
270
+ get fov(): number { return this._sceneId >= 0 ? _creator.getCameraFov(this._sceneId, 0) : this._fov }
122
271
  set fov(degrees: number) { this._fov = degrees; this._projSet = true; this._applyProjection() }
123
- get horizontalFov(): number { return _creator.getCameraFov(this._sceneId, 1) }
272
+ /** The horizontal field of view in degrees: the scene camera's at the screen's aspect, a secondary camera's at its
273
+ * texture's (square before `renderToTexture`). */
274
+ get horizontalFov(): number {
275
+ if (this._sceneId >= 0) return _creator.getCameraFov(this._sceneId, 1)
276
+ const t = this._texture
277
+ const aspect = t ? t.width / t.height : 1
278
+ return Math.atan(Math.tan(this._fov * Math.PI / 360) * aspect) * 360 / Math.PI
279
+ }
124
280
 
125
- /** Near clip distance (default 0.01) — nothing closer than this draws. */
281
+ /** Near clip distance (default 0.01) — nothing closer than this draws. Live, as `fov`. */
126
282
  get near(): number { return this._near }
127
283
  set near(distance: number) { this._near = distance; this._projSet = true; this._applyProjection() }
128
284
 
129
285
  /** Far clip distance / view range (default 1000) — geometry past it is culled. Shadows keep
130
- * their own (shorter) range, so a long view distance doesn't cost shadow sharpness. */
286
+ * their own (shorter) range, so a long view distance doesn't cost shadow sharpness. Live, as `fov`. */
131
287
  get far(): number { return this._far }
132
288
  set far(distance: number) { this._far = distance; this._projSet = true; this._applyProjection() }
133
289
 
@@ -141,11 +297,22 @@ export class Camera extends Node {
141
297
  return this
142
298
  }
143
299
 
144
- /** Hosts that predate the call keep the fixed defaults (older wasm / native builds). */
300
+ /** Hosts that predate the call keep the fixed defaults (older wasm / native builds); a secondary camera's view keeps
301
+ * the projection it started with (warned once). */
145
302
  private _applyProjection(): void {
146
- if (this._sceneId < 0 || typeof _creator.setCameraProjection !== "function") return
303
+ if (this._sceneId < 0) {
304
+ if (!this._view) return // remembered: the view starts with it
305
+ if (_creator.setRenderViewProjection) _creator.setRenderViewProjection(this._view.id, this._fov, this._near, this._far)
306
+ else if (!Camera._warnedViewProjection) {
307
+ Camera._warnedViewProjection = true
308
+ console.warn("[camera] this host has no setRenderViewProjection — a secondary camera keeps the projection its view started with")
309
+ }
310
+ return
311
+ }
312
+ if (typeof _creator.setCameraProjection !== "function") return
147
313
  _creator.setCameraProjection(this._sceneId, this._fov, this._near, this._far)
148
314
  }
315
+ private static _warnedViewProjection = false
149
316
  private _following: Node | null = null
150
317
  /** The node the camera currently follows (`follow()`), or null. */
151
318
  get following(): Node | null { return this._following }
@@ -155,7 +322,9 @@ export class Camera extends Node {
155
322
  * view direction), so movement aspects / animation on that node are camera moves — dolly shots,
156
323
  * a cutscene path, a `CameraPlace` in a scene file. A node carrying a `CameraPlace` also hands
157
324
  * over its projection (fov / near / far). `follow(null)` releases the camera where it is.
158
- * One camera per scene: following a new node stops following the previous one.
325
+ * Following a new node stops following the previous one, and a node leads one camera at a time.
326
+ * Either kind: the pose is written as the camera's own transform, so a following secondary
327
+ * camera is a scene root, not a child.
159
328
  */
160
329
  follow(target: Node | { node: Node, applyTo(camera: Camera): void } | null): this {
161
330
  const node = target === null ? null : target instanceof Node ? target : target.node
@@ -175,22 +344,26 @@ export class Camera extends Node {
175
344
 
176
345
  /** The scene view's [width, height] in logical units — the units of the scene's touches
177
346
  * (`clientX` / `clientY`) and of `getRay`, on every host; × `device.pixelRatio` for device px.
178
- * [0, 0] before the scene has a viewport. */
347
+ * [0, 0] before the scene has a viewport. The screen's, whichever camera reads it. */
179
348
  get displaySize(): [number, number] {
180
349
  const s = _creator.getDisplaySize()
181
350
  return [ s[0], s[1] ]
182
351
  }
183
352
 
184
- /** World-space ray direction through a point of the scene view (logical units, as a touch's). */
353
+ /** World-space ray direction through a point of the scene view (logical units, as a touch's). The scene camera's
354
+ * (a secondary camera throws). */
185
355
  getViewDirection(screenX: number, screenY: number): Vec3 {
186
- if (this._sceneId < 0) throw new Error("Camera is not attached to a scene")
356
+ this._sceneOnly("getViewDirection")
187
357
  const v = new Float32Array(3)
188
358
  _creator.getCameraViewDirection(this._sceneId, screenX, screenY, v)
189
359
  return new Vec3(v[0], v[1], v[2])
190
360
  }
191
361
 
192
- /** A pick ray from the camera through a point of the scene view (logical units, as a touch's). */
362
+ /** A pick ray from the camera through a point of the scene view (logical units, as a touch's). The scene camera's
363
+ * (a secondary camera throws). */
193
364
  getRay(screenX: number, screenY: number): Ray {
365
+ this._sceneOnly("getRay")
194
366
  return new Ray(this.worldPosition, this.getViewDirection(screenX, screenY))
195
367
  }
196
368
  }
369
+
package/src/gl/Node.ts CHANGED
@@ -94,6 +94,43 @@ const findNodeByWalk = (rootId: number, name: string): number => {
94
94
  // Scratch for the world-position read-back in _setOwnedPosition (one per module, never handed out).
95
95
  const worldScratch = new Float32Array(3)
96
96
 
97
+ /** @internal The scene a node is in: the one `scene.add` put it or its nearest such ancestor in (`_inScene`), -1 = none
98
+ * yet. A scene's nodes are pinned, so the walk meets the very wrappers `scene.add` stamped. */
99
+ export const sceneIdOf = (node: Node): number => {
100
+ if (node._inScene >= 0) return node._inScene
101
+ for (let id = _creator.getParent(node.id); id !== 0; id = _creator.getParent(id)) {
102
+ const n = nodes.get(id)
103
+ if (n && n._inScene >= 0) return n._inScene
104
+ }
105
+ return -1
106
+ }
107
+
108
+ /** @internal A node whose render view starts once the node is in a scene (a secondary Camera's `renderToTexture`, a
109
+ * ReflectionProbe): `_bindView(sceneId)` starts it if it has none. */
110
+ export type ViewBinder = Node & { _bindView(sceneId: number): void }
111
+ // The binders waiting for a scene, WEAKLY: a view never placed must not pin its node. Tried whenever the scene graph
112
+ // grows (bindPendingViews) — an empty set costs that call one size check.
113
+ const pendingViews = new Set<WeakRef<ViewBinder>>()
114
+ /** @internal Start `binder`'s view now if its node is in a scene, else at the scene.add / node.add that puts it in one.
115
+ * One try per placement: a view the host refuses (an overlay scene) warns and is not retried. */
116
+ export const awaitScene = (binder: ViewBinder): void => {
117
+ const sceneId = sceneIdOf(binder)
118
+ if (sceneId >= 0) binder._bindView(sceneId)
119
+ else pendingViews.add(new WeakRef(binder))
120
+ }
121
+ /** @internal The scene graph grew (scene.add, node.add, setParent): start the waiting views whose node is in a scene now. */
122
+ export const bindPendingViews = (): void => {
123
+ if (pendingViews.size === 0) return
124
+ for (const ref of pendingViews) {
125
+ const binder = ref.deref()
126
+ if (!binder) { pendingViews.delete(ref); continue }
127
+ const sceneId = sceneIdOf(binder)
128
+ if (sceneId < 0) continue
129
+ pendingViews.delete(ref)
130
+ binder._bindView(sceneId)
131
+ }
132
+ }
133
+
97
134
  export class Node extends AspectHost<NodeEvents> implements CompWriter {
98
135
  /** @internal The entity's owned handle (`_h.id` = the entity id; DEAD_HANDLE once destroyed). An
99
136
  * engine-created entity (a glTF part, an anchor) is wrapped from its number and pinned as the
@@ -101,6 +138,9 @@ export class Node extends AspectHost<NodeEvents> implements CompWriter {
101
138
  _h: Handle
102
139
  /** Native entity id. */
103
140
  get id(): number { return this._h.id }
141
+ /** @internal The id of the scene `scene.add` put this node in (-1 = none; `scene.remove` clears it) — how a render
142
+ * view finds its scene (sceneIdOf). */
143
+ _inScene = -1
104
144
 
105
145
  private _matrix?: Float32Array
106
146
  private _worldMatrix?: Float32Array
@@ -132,6 +172,25 @@ export class Node extends AspectHost<NodeEvents> implements CompWriter {
132
172
  set name(value: string) { _creator.setName(this.id, value) }
133
173
  get visible(): boolean { return _creator.isVisible(this.id) }
134
174
  set visible(value: boolean) { _creator.setVisible(this.id, value) }
175
+ /**
176
+ * The node's visibility LAYERS, a bit mask 1..255. The scene's own view draws every layer; a render
177
+ * view (a secondary `Camera`'s or a `ReflectionProbe`'s, their `layers` option) draws only the
178
+ * renderables whose layers share a bit with its mask — which is what layers are for: leaving a subtree
179
+ * out of a second camera (the car out of the reflection probe riding on it, the player's own body out
180
+ * of a mirror). A node without a layer
181
+ * of its own takes its nearest ancestor's, else 1, so a car's layer covers everything parented under
182
+ * it; `0` clears the node's own (inherit again). `visible = false` wins over any layer. Reads the
183
+ * effective bits. A host without layers draws everything everywhere (reads 1, a write warns once).
184
+ */
185
+ get layer(): number { return _creator.getLayer ? _creator.getLayer(this.id) : 1 }
186
+ set layer(mask: number) {
187
+ if (_creator.setLayer) _creator.setLayer(this.id, mask & 0xff)
188
+ else if (!Node._warnedLayer) {
189
+ Node._warnedLayer = true
190
+ console.warn("[node] this host has no setLayer — node layers are ignored, every view draws everything")
191
+ }
192
+ }
193
+ private static _warnedLayer = false
135
194
 
136
195
  // --- local transform ---
137
196
  // Native owns the authoritative matrix. `_sync()` reads it into a per-node scratch buffer on EVERY
@@ -317,6 +376,7 @@ export class Node extends AspectHost<NodeEvents> implements CompWriter {
317
376
  /** Parent the given nodes under this one. */
318
377
  add(...children: Node[]): this {
319
378
  for (const c of children) { _creator.setParent(c.id, this.id, false); nodes.pin(c) }
379
+ bindPendingViews()
320
380
  return this
321
381
  }
322
382
  setParent(parent: Node | null, worldPositionStays = false): this {
@@ -326,6 +386,7 @@ export class Node extends AspectHost<NodeEvents> implements CompWriter {
326
386
  } else {
327
387
  _creator.setParent(this.id, parent.id, worldPositionStays)
328
388
  nodes.pin(this)
389
+ bindPendingViews()
329
390
  }
330
391
  return this
331
392
  }
@@ -0,0 +1,141 @@
1
+ // A reflection probe: a cube of the scene's light drawn around a point, live (creator-gl renderview.cpp: a render view of
2
+ // six faces into an HDR cube render texture; with PREFILTER, Filament's libs/iblprefilter filter after each completed
3
+ // cube). A Node, placed and parented like any; its view starts once the node is in a scene (Node.ts awaitScene) — or at
4
+ // `scene.environment = probe` while it is in none. RenderTexture.ts holds the half it shares with a secondary Camera.
5
+
6
+ import { Node, awaitScene } from "./Node"
7
+ import { CAMERA_DEFAULTS } from "./Camera"
8
+ import {
9
+ RenderTexture, RT_FLAG_CUBEMAP, RT_FLAG_HDR, RT_FLAG_PREFILTER, RT_MAX_SIDE, makeRenderTexture, requireRenderTextures, startRenderView,
10
+ } from "./RenderTexture"
11
+
12
+ /** `new ReflectionProbe(options)`: the cube, what its view draws, and how often. */
13
+ export type ReflectionProbeOptions = {
14
+ /** The cube's edge, texels per face side (default 128): a power of two, 16..4096, when prefiltered; 1..4096 otherwise.
15
+ * A probe is light, not a picture — 64..128 is plenty for a car's reflections. */
16
+ size?: number
17
+ /** The layers its view draws, a bit mask over `Node.layer` (default 0xff = everything): leave out the probe's own
18
+ * carrier, or the cube is the inside of the car it rides on. Fixed. */
19
+ layers?: number
20
+ /** Shadows in its view (default `false`): with them, a full cube renders its own shadow maps six times. Fixed. */
21
+ shadows?: boolean
22
+ /** Faces drawn per render, 1..6, round-robin (default 6 = the whole cube) — `probe.facesPerFrame`. */
23
+ facesPerFrame?: number
24
+ /** Render once every N frames drawn (default 1 = every frame) — `probe.everyNth`. */
25
+ everyNth?: number
26
+ /** Filter the cube for ROUGHNESS (default `true`; `size` a power of two then). Without it the cube has one sharp level:
27
+ * under `scene.environment` only mirror-like surfaces (roughness near 0) are right, and the diffuse ambient stays the
28
+ * scene's own probe's. With it the GPU filters every completed cube into five roughness levels — a brushed or painted
29
+ * body shows its surroundings blurred as much as its roughness says — and the roughest level becomes the diffuse
30
+ * ambient: what the probe sees lights matte surfaces too (a red wall tints them red).
31
+ *
32
+ * COST: one filtering per completed cube (every render at `facesPerFrame` 6, every third at 2), on the frame after:
33
+ * the cube's mip chain plus a pass per level whose taps are the level's texels × `samples` — for a 64 cube at the
34
+ * default 256 samples about 2 million cube taps — and ten small driver submissions on the CPU. `everyNth` /
35
+ * `facesPerFrame` space the filterings out. Fixed. */
36
+ prefilter?: boolean
37
+ /** The filter's samples per texel, 1..2048 (default 256 — a quarter of Filament's offline default: each sample reads
38
+ * the mip that matches its footprint, which keeps a low count smooth; raise it if a mid-roughness surface reads
39
+ * blotchy). Smoothness for time; read with `prefilter` only. */
40
+ samples?: number
41
+ /** Near clip plane in metres (default 0.01). Fixed. */
42
+ near?: number
43
+ /** Far clip plane in metres (default 1000). Fixed. */
44
+ far?: number
45
+ }
46
+
47
+ /**
48
+ * A REFLECTION PROBE: a cube of the scene's light drawn around a point, live — chrome and glass show what is actually
49
+ * around them, moving objects included. A Node: place it, or parent it (`car.add(probe)` — it rides along); the six faces
50
+ * look along the world axes from its WORLD position (its rotation and scale are ignored). It draws once it is in a scene
51
+ * (added to one, or under a node that is), every frame that scene is on show — after the frame's update passes, before
52
+ * the scene's own picture — in linear HDR light (no tone mapping) at the scene camera's exposure. It sees the scene's
53
+ * renderables, lights, environment and sky, not its overlays.
54
+ *
55
+ * Two ways to use it:
56
+ * - `scene.environment = probe`: the scene's reflections come from the cube instead of the scene's own probe
57
+ * (`SceneOptions.ibl`) — and, prefiltered (the default), its diffuse ambient too. A probe in no scene yet starts in
58
+ * that one.
59
+ * - `probe.texture` in a material that samples a cube itself (a custom shader's samplerCubemap): the same cube as a
60
+ * `RenderTexture` — when prefiltered, the filtered one, five roughness levels.
61
+ *
62
+ * COST: a full render is six passes over the scene, plus (prefiltered) a GPU filtering per completed cube (`prefilter`
63
+ * says what one costs). Keep `size` small (128 is a lot), spread the cube over frames (`facesPerFrame: 2` = a whole cube
64
+ * every 3 frames, a third of the cost each), render less often (`everyNth`), leave out what it need not see (`layers` —
65
+ * its carrier first), keep `shadows` off, and turn it off once a static surrounding is drawn (`enabled = false` keeps the
66
+ * last cube).
67
+ *
68
+ * LIFETIME: the cube and its view live as long as the probe — keep a reference (`scene.environment` holds it too).
69
+ * Destroying the probe's node stops the view (the cube keeps its last light); moving it to another scene later does not
70
+ * move the view. Throws on a host without render textures (`createRenderTexture` — the web hosts, for now).
71
+ */
72
+ export class ReflectionProbe extends Node {
73
+ /** The cube the probe draws (`cubemap`, linear HDR light) — the filtered one, five roughness levels, when `prefilter`. */
74
+ readonly texture: RenderTexture
75
+ /** Filtered for roughness (the `prefilter` option). */
76
+ readonly prefilter: boolean
77
+ /** @internal The render view's owned handle (`_view.id` = the engine's view id), null until the view starts: it lives
78
+ * exactly as long as this probe holds it. */
79
+ _view: Handle | null = null
80
+ private readonly _layers: number
81
+ private readonly _shadows: boolean
82
+ private readonly _near: number
83
+ private readonly _far: number
84
+ private _enabled = true
85
+ private _everyNth: number
86
+ private _facesPerFrame: number
87
+
88
+ constructor(options: ReflectionProbeOptions = {}) {
89
+ requireRenderTextures("new ReflectionProbe")
90
+ const size = Math.max(1, Math.floor(options.size ?? 128))
91
+ const prefilter = options.prefilter !== false
92
+ if (prefilter && (size < 16 || size > RT_MAX_SIDE || (size & (size - 1)) !== 0)) {
93
+ throw new Error(`new ReflectionProbe: a prefiltered probe's size is a power of two, 16..${RT_MAX_SIDE} (got ${size}) — or prefilter: false`)
94
+ }
95
+ super()
96
+ const samples = prefilter ? Math.max(0, Math.floor(options.samples ?? 0)) : 0
97
+ this.texture = makeRenderTexture("new ReflectionProbe", size, size, RT_FLAG_CUBEMAP | RT_FLAG_HDR | (prefilter ? RT_FLAG_PREFILTER : 0), samples)
98
+ this.prefilter = prefilter
99
+ this._layers = (options.layers ?? 0xff) & 0xff
100
+ this._shadows = options.shadows === true
101
+ this._near = options.near ?? CAMERA_DEFAULTS.near
102
+ this._far = options.far ?? CAMERA_DEFAULTS.far
103
+ this._everyNth = Math.max(1, Math.floor(options.everyNth ?? 1))
104
+ this._facesPerFrame = Math.min(6, Math.max(1, Math.floor(options.facesPerFrame ?? 6)))
105
+ awaitScene(this)
106
+ }
107
+
108
+ /** @internal Node.ts awaitScene / Scene.environment: the probe is in scene `sceneId` — start its view into its cube. */
109
+ _bindView(sceneId: number): void {
110
+ if (this._view) return
111
+ this._view = startRenderView("ReflectionProbe", sceneId, this.texture, this.id, this._layers, 90, this._near, this._far, this._shadows)
112
+ if (this._view && (!this._enabled || this._everyNth !== 1 || this._facesPerFrame !== 6)) this._sendSchedule()
113
+ }
114
+
115
+ /** Whether the probe draws (default `true`). Off keeps the last cube and costs nothing — a probe that has drawn its
116
+ * static surroundings once; back on, it renders on the next frame drawn. */
117
+ get enabled(): boolean { return this._enabled }
118
+ set enabled(on: boolean) {
119
+ this._enabled = on
120
+ this._sendSchedule()
121
+ }
122
+ /** Render once every N frames DRAWN (default 1 = every frame). The count restarts with each change: the next frame
123
+ * drawn renders, then one in every N. */
124
+ get everyNth(): number { return this._everyNth }
125
+ set everyNth(n: number) {
126
+ this._everyNth = Math.max(1, Math.floor(n))
127
+ this._sendSchedule()
128
+ }
129
+ /** The cube's faces drawn per render, 1..6 (default 6), round-robin from where the last render stopped: 2 spreads a
130
+ * whole cube over 3 renders at a third of the cost each. A prefiltered cube is filtered each time it completes. */
131
+ get facesPerFrame(): number { return this._facesPerFrame }
132
+ set facesPerFrame(n: number) {
133
+ this._facesPerFrame = Math.min(6, Math.max(1, Math.floor(n)))
134
+ this._sendSchedule()
135
+ }
136
+
137
+ /** @internal Send the whole schedule (`setRenderViewOptions` restarts the count, so the next frame drawn renders). */
138
+ private _sendSchedule(): void {
139
+ if (this._view) _creator.setRenderViewOptions?.(this._view.id, this._enabled, this._everyNth, this._facesPerFrame)
140
+ }
141
+ }