@solidrt/3d 0.0.63 → 0.0.64
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 +198 -17
- package/package.json +3 -2
- package/src/camera.ts +6 -2
- package/src/components/directional-light.tsx +6 -2
- package/src/components/hemisphere-light.tsx +12 -4
- package/src/components/index.ts +2 -0
- package/src/components/instanced-mesh.tsx +6 -2
- package/src/components/mesh.tsx +12 -1
- package/src/components/point-light.tsx +6 -2
- package/src/components/record-mesh.tsx +14 -2
- package/src/components/scene.tsx +8 -1
- package/src/components/splat-mesh.tsx +50 -0
- package/src/components/spot-light.tsx +7 -3
- package/src/environment.ts +55 -0
- package/src/glsl.ts +68 -14
- package/src/index.ts +9 -4
- package/src/light.ts +56 -4
- package/src/material.ts +26 -1
- package/src/mesh.ts +361 -6
- package/src/node.ts +3 -0
- package/src/scene-shadows.ts +8 -7
- package/src/scene.ts +225 -104
- package/src/splat-data.ts +617 -0
- package/src/splat.ts +401 -0
- package/tools/splat.ts +70 -0
package/src/mesh.ts
CHANGED
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
// setter write paths that keep a live scene's draw entries in step. The
|
|
4
4
|
// scene side is reached through the node's SceneHooks (node.ts).
|
|
5
5
|
|
|
6
|
-
import { createBuffer, destroyBuffer, destroyTexture, writeBuffer } from "@solidrt/core/gpu"
|
|
6
|
+
import { beginBufferWrite, createBuffer, destroyBuffer, destroyTexture, endBufferWrite, transferRecords, writeBuffer } from "@solidrt/core/gpu"
|
|
7
7
|
import type { BufferId, DrawId, ShaderParams, TextureBindings, TextureId, VertexAttribute, VertexBufferLayout } from "@solidrt/core/gpu"
|
|
8
|
+
import { checkCubeKnobs } from "./environment.ts"
|
|
9
|
+
import type { EnvironmentOptions } from "./environment.ts"
|
|
8
10
|
import { geometryBounds, isFloatLayout, layoutKey, layoutStride, plane, vertexBytes, vertexView, VERTEX_FORMATS } from "./geometry.ts"
|
|
9
11
|
import type { AttributeAccess, Geometry } from "./geometry.ts"
|
|
10
12
|
import { INSTANCE_MATRIX_ATTRIBUTES } from "./glsl.ts"
|
|
@@ -32,6 +34,15 @@ export type Mesh = SceneNode & {
|
|
|
32
34
|
* target draws the mesh when its mask intersects this. Not inherited
|
|
33
35
|
* from ancestor Groups (Three's and Godot's rule). Set with setLayers. */
|
|
34
36
|
layers: number
|
|
37
|
+
/** This mesh's environment instead of the scene's (resolved; set with
|
|
38
|
+
* setEnvironment): the cube its image-lit and reflective terms sample,
|
|
39
|
+
* `null` for NONE (a sealed interior under an outdoor sky reflects and
|
|
40
|
+
* receives no sky), `undefined` (the default) to follow the scene's,
|
|
41
|
+
* live through scene.setEnvironment. Applied where the material
|
|
42
|
+
* reflects at all (Material.env - `standard`, `phong` with
|
|
43
|
+
* `reflectivity`, a custom source composing ENVIRONMENT); on any other
|
|
44
|
+
* material it is a no-op, so an interior can be masked wholesale. */
|
|
45
|
+
environment: { cube: TextureId; intensity: number; rotation: number } | null | undefined
|
|
35
46
|
/** Whether every target's frustum gates the mesh (default true, Three's
|
|
36
47
|
* `frustumCulled`): outside it the entry draws nothing. Off for geometry
|
|
37
48
|
* a vertex stage moves beyond its box (a fullscreen quad, a custom
|
|
@@ -130,6 +141,31 @@ export type MeshInstances = {
|
|
|
130
141
|
* shader reads at row gl_InstanceID. Replaced on growth (the entries
|
|
131
142
|
* re-point), freed by disposeInstances. */
|
|
132
143
|
morph: { texture: TextureId; rows: number } | null
|
|
144
|
+
/** The mesh's instanceOrder resolved against its material's instance
|
|
145
|
+
* layouts (see InstanceOrderOptions), or null for buffer order. Fixed
|
|
146
|
+
* at creation; slots as the app writes them never move. */
|
|
147
|
+
order: ResolvedInstanceOrder | null
|
|
148
|
+
/** Whether the records were transferred to the engine at creation
|
|
149
|
+
* (RecordMeshOptions.transfer, or an indexed instanceOrder): no JS-side
|
|
150
|
+
* mirror exists, the population is write-once, and rewrites/growth/
|
|
151
|
+
* attribute access throw. Fixed at creation; always false on an
|
|
152
|
+
* instanced mesh. */
|
|
153
|
+
transfer: boolean
|
|
154
|
+
/** Records per drawn instance: 1, or on an indexed instanceOrder the
|
|
155
|
+
* ids per id-stream record (the entry's instanceCount is
|
|
156
|
+
* ceil(count / group), see drawCount). Fixed at creation. */
|
|
157
|
+
group: number
|
|
158
|
+
/** Data textures the population owns (an indexed population's record
|
|
159
|
+
* texture and the like, created by whoever built the mesh), freed by
|
|
160
|
+
* disposeInstances with the buffers. */
|
|
161
|
+
textures: TextureId[]
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** The draw's instance count for a population: its records over the
|
|
165
|
+
* records per instance, rounded up (an indexed population's last group
|
|
166
|
+
* pads with the engine's sentinel ids). */
|
|
167
|
+
export function drawCount(inst: MeshInstances): number {
|
|
168
|
+
return Math.ceil(inst.count / inst.group)
|
|
133
169
|
}
|
|
134
170
|
|
|
135
171
|
export type InstanceSlots = { slots: (InstanceNode | null)[]; free: number[] }
|
|
@@ -213,6 +249,7 @@ export function createMesh(geometry: Geometry, material: Material): Mesh {
|
|
|
213
249
|
mesh.renderOrder = 0
|
|
214
250
|
mesh.castShadow = false
|
|
215
251
|
mesh.layers = 1
|
|
252
|
+
mesh.environment = undefined
|
|
216
253
|
mesh.frustumCulled = true
|
|
217
254
|
mesh.cullMargin = 0
|
|
218
255
|
mesh._cullJoints = null
|
|
@@ -341,6 +378,55 @@ function makeStream(layout: VertexAttribute[], capacity: number, blank: Uint8Arr
|
|
|
341
378
|
return stream
|
|
342
379
|
}
|
|
343
380
|
|
|
381
|
+
// The indexed order's stream: the id buffer the core writes whole with the
|
|
382
|
+
// sorted record indices (ceil(capacity / group) instance records of
|
|
383
|
+
// `group` u32 ids each; it starts zeroed and the entry's attach seeds the
|
|
384
|
+
// first sort), the records handed to the core's order mirror as the key
|
|
385
|
+
// source. No JS mirror exists, as under transfer.
|
|
386
|
+
function makeIndexStream(layout: VertexAttribute[], capacity: number, group: number, records: Uint8Array, label: string | undefined): InstanceStream {
|
|
387
|
+
let stride = layoutStride(layout)
|
|
388
|
+
let stream: InstanceStream = {
|
|
389
|
+
layout,
|
|
390
|
+
stride,
|
|
391
|
+
buffer: createBuffer(Math.ceil(capacity / group) * stride, { autoFree: false, label }),
|
|
392
|
+
data: new Uint8Array(0),
|
|
393
|
+
blank: new Uint8Array(stride),
|
|
394
|
+
dirty: null,
|
|
395
|
+
_view: new DataView(new ArrayBuffer(0)),
|
|
396
|
+
_floats: false,
|
|
397
|
+
_components: layoutComponents(layout),
|
|
398
|
+
_fields: [],
|
|
399
|
+
}
|
|
400
|
+
transferRecords(stream.buffer, records)
|
|
401
|
+
return stream
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// The transfer form's stream: records go to the engine at creation and no
|
|
405
|
+
// JS mirror exists (data stays a zero-length view; the operations that
|
|
406
|
+
// would read or write it throw on the transfer flag first). An ordered
|
|
407
|
+
// stream hands the records to the core's order mirror - the entry's
|
|
408
|
+
// attach publishes the first gather, so the buffer starts zeroed; an
|
|
409
|
+
// unordered one is its own upload, the buffer created with the records.
|
|
410
|
+
function makeTransferredStream(layout: VertexAttribute[], records: Uint8Array, ordered: boolean, label: string | undefined): InstanceStream {
|
|
411
|
+
let stride = layoutStride(layout)
|
|
412
|
+
let stream: InstanceStream = {
|
|
413
|
+
layout,
|
|
414
|
+
stride,
|
|
415
|
+
buffer: ordered
|
|
416
|
+
? createBuffer(records.byteLength, { autoFree: false, label })
|
|
417
|
+
: createBuffer(records, { autoFree: false, label }),
|
|
418
|
+
data: new Uint8Array(0),
|
|
419
|
+
blank: new Uint8Array(stride),
|
|
420
|
+
dirty: null,
|
|
421
|
+
_view: new DataView(new ArrayBuffer(0)),
|
|
422
|
+
_floats: isFloatLayout(layout),
|
|
423
|
+
_components: layoutComponents(layout),
|
|
424
|
+
_fields: [],
|
|
425
|
+
}
|
|
426
|
+
if (ordered) transferRecords(stream.buffer, records)
|
|
427
|
+
return stream
|
|
428
|
+
}
|
|
429
|
+
|
|
344
430
|
// Encode `values` (one per component, in layout order, as the shader
|
|
345
431
|
// sees them) into record `i` of a stream: one typed-array set over an
|
|
346
432
|
// all-float layout (the values ARE the bytes), the codecs otherwise.
|
|
@@ -380,6 +466,155 @@ function markRecords(mesh: Mesh, stream: InstanceStream, lo: number, hi: number)
|
|
|
380
466
|
mesh._scene?._setRecords(mesh)
|
|
381
467
|
}
|
|
382
468
|
|
|
469
|
+
/**
|
|
470
|
+
* Draw a population's records in key order, decided core-side - the
|
|
471
|
+
* engine's instance order riding the mesh's record streams
|
|
472
|
+
* (createInstancedMesh and createRecordMesh both take it). Record slots
|
|
473
|
+
* never move: the app keeps writing slot i forever, and the engine
|
|
474
|
+
* gathers the records into key order as they publish.
|
|
475
|
+
*
|
|
476
|
+
* `position` names an instance attribute (float32x3 or float32x4) whose
|
|
477
|
+
* first three floats are a position in the mesh's local space: records
|
|
478
|
+
* draw by their depth along the scene camera's view direction, which the
|
|
479
|
+
* core feeds itself whenever the camera or the mesh turns meaningfully -
|
|
480
|
+
* zero per-frame JS, and a parked camera costs nothing. `descending:
|
|
481
|
+
* true` draws farthest first, the transparent blend order. The order
|
|
482
|
+
* follows the scene's OWN camera: one buffer holds one order, so views
|
|
483
|
+
* draw the same records in that order, and a mesh masked out of the
|
|
484
|
+
* scene target keeps its last order. `retain: true` opts a write-once
|
|
485
|
+
* population (a splat cloud) into the engine's retained copy: a camera
|
|
486
|
+
* turn re-sorts and republishes core-side with no publish from here, and
|
|
487
|
+
* an order-preserving turn uploads nothing.
|
|
488
|
+
*
|
|
489
|
+
* `field` names a float32 instance attribute: records draw by that value
|
|
490
|
+
* as the app wrote it (an age, an explicit sort key), re-ordered when
|
|
491
|
+
* the records publish.
|
|
492
|
+
*
|
|
493
|
+
* An ordered mesh gives up partial record publishes: any write sends the
|
|
494
|
+
* live record set whole, gathered (a byte range has no stable position
|
|
495
|
+
* under a permutation).
|
|
496
|
+
*
|
|
497
|
+
* The INDEXED form (`records`) is for a population whose records live in
|
|
498
|
+
* a data texture the shader fetches by record index (a splat cloud):
|
|
499
|
+
* `records` is the layout of the records createRecordMesh received -
|
|
500
|
+
* `position` names a float32x3/x4 attribute IN THAT LAYOUT - and the
|
|
501
|
+
* material's one instance buffer is the ID STREAM, uint32/uint32x4
|
|
502
|
+
* attributes the engine fills with the sorted record indices, K ids per
|
|
503
|
+
* instance record so one drawn instance covers K records (the
|
|
504
|
+
* geometry repeats K times; `gl_VertexID` picks the id). The records go
|
|
505
|
+
* to the engine at creation (transfer implied, retain by nature) as the
|
|
506
|
+
* sort's key source; the app uploads the same bytes as its texture. A
|
|
507
|
+
* re-sort then uploads 4 bytes per record instead of the record, and
|
|
508
|
+
* the last instance's group past `count` carries 0xFFFFFFFF, which the
|
|
509
|
+
* shader culls before fetching.
|
|
510
|
+
*/
|
|
511
|
+
export type InstanceOrderOptions =
|
|
512
|
+
| (({ position: string; retain?: boolean } | { field: string }) & {
|
|
513
|
+
/** Largest key first - back-to-front for a position key. Default false. */
|
|
514
|
+
descending?: boolean
|
|
515
|
+
})
|
|
516
|
+
| {
|
|
517
|
+
/** The position attribute of the record layout below. */
|
|
518
|
+
position: string
|
|
519
|
+
/** The layout of the handed-off records the shader fetches by
|
|
520
|
+
* index; the material's instance buffer is the id stream. */
|
|
521
|
+
records: VertexAttribute[]
|
|
522
|
+
/** Largest key first - back-to-front. Default false. */
|
|
523
|
+
descending?: boolean
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/** The instanceOrder resolved against the material's instance layouts at
|
|
527
|
+
* creation: the key stream's layout key (the entry's pipeline buffer is
|
|
528
|
+
* found by it at attach, exactly as buffer bindings are), the key's
|
|
529
|
+
* float offset inside one record, and the declared knobs. */
|
|
530
|
+
export type ResolvedInstanceOrder = {
|
|
531
|
+
key: string
|
|
532
|
+
floats: number
|
|
533
|
+
position: boolean
|
|
534
|
+
descending: boolean
|
|
535
|
+
retain: boolean
|
|
536
|
+
/** The index materialization (InstanceOrderOptions' `records` form):
|
|
537
|
+
* the handed-off record's byte stride; null for gathered records. */
|
|
538
|
+
indices: { stride: number } | null
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Resolve an instanceOrder option against the material's instance
|
|
542
|
+
// layouts: the named attribute must exist in one of them, hold floats of
|
|
543
|
+
// the key's shape, and start on a 4-byte boundary of its record. The
|
|
544
|
+
// indexed form resolves the key in the given record layout instead, and
|
|
545
|
+
// the material's one instance buffer must be whole u32 ids.
|
|
546
|
+
function resolveInstanceOrder(order: InstanceOrderOptions | undefined, buffers: VertexBufferLayout[], site: string): ResolvedInstanceOrder | null {
|
|
547
|
+
if (order === undefined) return null
|
|
548
|
+
if ("records" in order) {
|
|
549
|
+
if (buffers.length !== 1) {
|
|
550
|
+
throw new Error(site + ": an indexed instanceOrder takes a material with one instance buffer (the id stream); this one declares " + buffers.length)
|
|
551
|
+
}
|
|
552
|
+
let ids = buffers[0]!.attributes
|
|
553
|
+
for (let a of ids) {
|
|
554
|
+
if (!a.format.startsWith("uint32")) throw new Error(site + ": the id stream's attribute '" + a.name + "' must be uint32, uint32x2, uint32x3 or uint32x4 (whole record ids), got " + a.format)
|
|
555
|
+
}
|
|
556
|
+
let stride = layoutStride(order.records)
|
|
557
|
+
if (stride === 0 || stride % 4 !== 0) throw new Error(site + ": the indexed record layout is " + stride + " bytes; records must be whole floats")
|
|
558
|
+
let at = 0
|
|
559
|
+
for (let a of order.records) {
|
|
560
|
+
if (a.name === order.position) {
|
|
561
|
+
if (a.format !== "float32x3" && a.format !== "float32x4") {
|
|
562
|
+
throw new Error(site + ": instanceOrder position attribute '" + a.name + "' must be float32x3 or float32x4, got " + a.format)
|
|
563
|
+
}
|
|
564
|
+
if (at % 4 !== 0) throw new Error(site + ": instanceOrder key attribute '" + a.name + "' starts at byte " + at + " of its record; the key must start on a 4-byte boundary")
|
|
565
|
+
return { key: layoutKey(ids), floats: at / 4, position: true, descending: order.descending === true, retain: true, indices: { stride } }
|
|
566
|
+
}
|
|
567
|
+
at += VERTEX_FORMATS[a.format].bytes
|
|
568
|
+
}
|
|
569
|
+
throw new Error(site + ": instanceOrder names attribute '" + order.position + "', which the record layout does not declare")
|
|
570
|
+
}
|
|
571
|
+
let position = "position" in order
|
|
572
|
+
let name = position ? (order as { position: string }).position : (order as { field: string }).field
|
|
573
|
+
if (!position && (order as { retain?: boolean }).retain !== undefined) {
|
|
574
|
+
throw new Error(site + ": instanceOrder retain applies to a position key; a field key re-orders when its records publish")
|
|
575
|
+
}
|
|
576
|
+
for (let b of buffers) {
|
|
577
|
+
let at = 0
|
|
578
|
+
for (let a of b.attributes) {
|
|
579
|
+
if (a.name === name) {
|
|
580
|
+
if (position && a.format !== "float32x3" && a.format !== "float32x4") {
|
|
581
|
+
throw new Error(site + ": instanceOrder position attribute '" + name + "' must be float32x3 or float32x4, got " + a.format)
|
|
582
|
+
}
|
|
583
|
+
if (!position && a.format !== "float32") {
|
|
584
|
+
throw new Error(site + ": instanceOrder field attribute '" + name + "' must be float32, got " + a.format)
|
|
585
|
+
}
|
|
586
|
+
if (at % 4 !== 0) {
|
|
587
|
+
throw new Error(site + ": instanceOrder key attribute '" + name + "' starts at byte " + at + " of its record; the key must start on a 4-byte boundary")
|
|
588
|
+
}
|
|
589
|
+
return {
|
|
590
|
+
key: layoutKey(b.attributes),
|
|
591
|
+
floats: at / 4,
|
|
592
|
+
position,
|
|
593
|
+
descending: order.descending === true,
|
|
594
|
+
retain: position && (order as { retain?: boolean }).retain === true,
|
|
595
|
+
indices: null,
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
at += VERTEX_FORMATS[a.format].bytes
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
throw new Error(site + ": instanceOrder names attribute '" + name + "', which no instance buffer of the material declares")
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Whether a material still carries the layout an ordered mesh keys on
|
|
606
|
+
* (the key stream must be one of the buffers the entry binds). Throws
|
|
607
|
+
* naming `site`; checked beside the instance pairing at add().
|
|
608
|
+
*/
|
|
609
|
+
export function checkOrderPairing(material: Material, inst: MeshInstances, site: string): void {
|
|
610
|
+
if (inst.order === null) return
|
|
611
|
+
let buffers = material.instanceBuffers ?? []
|
|
612
|
+
let key = inst.order.key
|
|
613
|
+
if (!buffers.some(b => layoutKey(b.attributes) === key)) {
|
|
614
|
+
throw new Error(site + ": the material declares no instance buffer with the mesh's instanceOrder key layout (" + key + ")")
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
383
618
|
/** What both population kinds take. */
|
|
384
619
|
type PopulationOptions = {
|
|
385
620
|
/** LOCAL bounds covering the population ([minX, minY, minZ, maxX, maxY,
|
|
@@ -392,6 +627,10 @@ type PopulationOptions = {
|
|
|
392
627
|
* follows them) and sorts by its node position; a record mesh is never
|
|
393
628
|
* culled and never picked. */
|
|
394
629
|
bounds?: ArrayLike<number>
|
|
630
|
+
/** Draw the records in key order, decided core-side (see
|
|
631
|
+
* InstanceOrderOptions): back-to-front for a transparent population, an
|
|
632
|
+
* explicit sort field for a JS-stepped one. Fixed at creation. */
|
|
633
|
+
instanceOrder?: InstanceOrderOptions
|
|
395
634
|
/** Debug label for the record buffer. */
|
|
396
635
|
label?: string
|
|
397
636
|
}
|
|
@@ -421,7 +660,20 @@ export type InstancedMeshOptions = PopulationOptions & {
|
|
|
421
660
|
/** A record mesh's bounds are also its ONLY picking leaf: records are
|
|
422
661
|
* opaque data, so without them the mesh never picks and pointer events
|
|
423
662
|
* never target it. */
|
|
424
|
-
export type RecordMeshOptions = PopulationOptions
|
|
663
|
+
export type RecordMeshOptions = PopulationOptions & {
|
|
664
|
+
/** Transfer the records to the engine at creation - the write-once
|
|
665
|
+
* form for a large population (a baked splat cloud): the engine keeps
|
|
666
|
+
* the one CPU-side copy, and the mesh keeps NO mirror, so the records
|
|
667
|
+
* are resident once instead of twice and the creation bytes are free
|
|
668
|
+
* to drop the moment this returns. The population is write-once by
|
|
669
|
+
* contract: setRecords, updateRecords, instanceAttribute and growth
|
|
670
|
+
* throw; setRecordCount (an ordered mesh republishes the prefix
|
|
671
|
+
* core-side), setVisible, transforms, re-attach and disposeInstances
|
|
672
|
+
* all still work. Takes a single-stream material; with an
|
|
673
|
+
* instanceOrder the engine's retained copy is implied (`retain` is
|
|
674
|
+
* forced on). Default false. */
|
|
675
|
+
transfer?: boolean
|
|
676
|
+
}
|
|
425
677
|
|
|
426
678
|
function copyBounds(bounds: ArrayLike<number> | undefined, site: string): Float32Array | null {
|
|
427
679
|
if (bounds === undefined) return null
|
|
@@ -522,6 +774,10 @@ export function createInstancedMesh(geometry: Geometry, material: Material, opts
|
|
|
522
774
|
anchor: opts?.anchor ?? null,
|
|
523
775
|
levels: null,
|
|
524
776
|
morph: populationMorph(geometry, capacity, opts?.label),
|
|
777
|
+
order: resolveInstanceOrder(opts?.instanceOrder, buffers, "createInstancedMesh"),
|
|
778
|
+
transfer: false,
|
|
779
|
+
group: 1,
|
|
780
|
+
textures: [],
|
|
525
781
|
}
|
|
526
782
|
return mesh
|
|
527
783
|
}
|
|
@@ -602,6 +858,7 @@ export function setInstanceStyle(instance: InstanceNode, values: ArrayLike<numbe
|
|
|
602
858
|
export function instanceAttribute(mesh: InstancedMesh | RecordMesh, name: string): AttributeAccess | null {
|
|
603
859
|
let inst: MeshInstances | null = mesh._instances
|
|
604
860
|
if (inst === null) throw new Error("instanceAttribute: the mesh's instances are disposed")
|
|
861
|
+
if (inst.transfer) throw new Error("instanceAttribute: the records were transferred at creation (transfer: true) - there is no mirror to read or write")
|
|
605
862
|
for (let s of inst.streams) {
|
|
606
863
|
let i = s.layout.findIndex(a => a.name === name)
|
|
607
864
|
if (i >= 0) return s._fields[i]!
|
|
@@ -625,6 +882,7 @@ export type UpdateRecordsOptions = { stream?: number; first?: number; count?: nu
|
|
|
625
882
|
export function updateRecords(mesh: InstancedMesh | RecordMesh, options: UpdateRecordsOptions = {}): void {
|
|
626
883
|
let inst: MeshInstances | null = mesh._instances
|
|
627
884
|
if (inst === null) throw new Error("updateRecords: the mesh's instances are disposed")
|
|
885
|
+
if (inst.transfer) throw new Error("updateRecords: the records were transferred at creation (transfer: true) - there is no mirror to publish from")
|
|
628
886
|
let index = options.stream ?? 0
|
|
629
887
|
let stream = inst.streams[index]
|
|
630
888
|
if (!Number.isInteger(index) || stream === undefined) {
|
|
@@ -639,10 +897,25 @@ export function updateRecords(mesh: InstancedMesh | RecordMesh, options: UpdateR
|
|
|
639
897
|
}
|
|
640
898
|
|
|
641
899
|
/** Publish a mesh's pending record writes, one buffer write per dirty
|
|
642
|
-
* stream (the scene calls it from its sync).
|
|
900
|
+
* stream (the scene calls it from its sync). An ordered mesh publishes
|
|
901
|
+
* the LIVE record set whole through the write lease - the engine gathers
|
|
902
|
+
* it into key order during the copy, and a byte range has no stable
|
|
903
|
+
* position under a permutation (writeBuffer throws on an ordered buffer). */
|
|
643
904
|
export function publishRecords(mesh: Mesh): void {
|
|
644
905
|
let inst = mesh._instances
|
|
645
906
|
if (inst === null) return
|
|
907
|
+
if (inst.order !== null) {
|
|
908
|
+
for (let s of inst.streams) {
|
|
909
|
+
if (s.dirty === null) continue
|
|
910
|
+
s.dirty = null
|
|
911
|
+
let bytes = inst.count * s.stride
|
|
912
|
+
if (bytes === 0) continue
|
|
913
|
+
let block = beginBufferWrite(s.buffer)
|
|
914
|
+
new Uint8Array(block.buffer, 0, bytes).set(vertexBytes(s.data).subarray(0, bytes))
|
|
915
|
+
endBufferWrite(s.buffer, bytes)
|
|
916
|
+
}
|
|
917
|
+
return
|
|
918
|
+
}
|
|
646
919
|
for (let s of inst.streams) {
|
|
647
920
|
if (s.dirty === null) continue
|
|
648
921
|
let [lo, hi] = s.dirty
|
|
@@ -651,6 +924,18 @@ export function publishRecords(mesh: Mesh): void {
|
|
|
651
924
|
}
|
|
652
925
|
}
|
|
653
926
|
|
|
927
|
+
/** @internal Mark every stream's live records dirty, so the next sync
|
|
928
|
+
* republishes the population whole: an ordered mesh (re-)entering a
|
|
929
|
+
* scene seeds the entry's freshly built engine-side order registry from
|
|
930
|
+
* the JS mirrors. */
|
|
931
|
+
export function markLiveRecords(mesh: Mesh): void {
|
|
932
|
+
let inst = mesh._instances
|
|
933
|
+
// A transferred mesh has no mirror; the engine's own (buffer-lifetime)
|
|
934
|
+
// mirror seeds a re-attached entry instead.
|
|
935
|
+
if (inst === null || inst.count === 0 || inst.transfer) return
|
|
936
|
+
for (let s of inst.streams) markRecords(mesh, s, 0, inst.count * s.stride)
|
|
937
|
+
}
|
|
938
|
+
|
|
654
939
|
/**
|
|
655
940
|
* Add an instance to an instanced mesh: a scene node placed by `update`
|
|
656
941
|
* (position/rotation/quaternion/scale; absent keys at their identity)
|
|
@@ -739,6 +1024,7 @@ export function reparentInstance(instance: InstanceNode, parent: SceneNode): voi
|
|
|
739
1024
|
// are freed.
|
|
740
1025
|
function growInstances(mesh: InstancedMesh | RecordMesh, next: number): void {
|
|
741
1026
|
let inst: MeshInstances = mesh._instances
|
|
1027
|
+
if (inst.transfer) throw new Error("growInstances: a transferred population is fixed at its creation records")
|
|
742
1028
|
let previous = inst.capacity
|
|
743
1029
|
inst.capacity = next
|
|
744
1030
|
let freed: BufferId[] = []
|
|
@@ -785,11 +1071,42 @@ export function createRecordMesh(
|
|
|
785
1071
|
opts?: RecordMeshOptions,
|
|
786
1072
|
): RecordMesh {
|
|
787
1073
|
let buffers = instancedBuffers(material, "createRecordMesh")
|
|
788
|
-
let
|
|
1074
|
+
let order = resolveInstanceOrder(opts?.instanceOrder, buffers, "createRecordMesh")
|
|
1075
|
+
let indices = order?.indices ?? null
|
|
1076
|
+
if (indices !== null && opts?.transfer === false) {
|
|
1077
|
+
throw new Error("createRecordMesh: an indexed instanceOrder hands the records to the engine by nature; transfer cannot be false")
|
|
1078
|
+
}
|
|
1079
|
+
let transfer = opts?.transfer === true || indices !== null
|
|
1080
|
+
if (transfer && buffers.length !== 1) {
|
|
1081
|
+
throw new Error(
|
|
1082
|
+
"createRecordMesh: transfer takes a single-stream material; this one declares " + buffers.length + " instance buffers (further streams need the partial writes transfer gives up)",
|
|
1083
|
+
)
|
|
1084
|
+
}
|
|
1085
|
+
// Under an indexed order the records are laid out by the order's record
|
|
1086
|
+
// layout, not the material's (whose one buffer is the id stream).
|
|
1087
|
+
let capacity: number
|
|
1088
|
+
if (indices !== null) {
|
|
1089
|
+
if (records.byteLength === 0 || records.byteLength % indices.stride !== 0) {
|
|
1090
|
+
throw new Error("createRecordMesh: records hold " + records.byteLength + " bytes, not a positive whole number of " + indices.stride + "-byte records")
|
|
1091
|
+
}
|
|
1092
|
+
capacity = records.byteLength / indices.stride
|
|
1093
|
+
} else {
|
|
1094
|
+
capacity = recordCount(records, buffers[0]!.attributes, "createRecordMesh")
|
|
1095
|
+
}
|
|
1096
|
+
// A transferred order always retains: the engine's mirror is the ONE
|
|
1097
|
+
// record copy, what re-attaches seed from and count changes republish
|
|
1098
|
+
// (a field key gets the same retention, engine-side only).
|
|
1099
|
+
if (transfer && order !== null && !order.retain) order = { ...order, retain: true }
|
|
789
1100
|
let mesh = createMesh(geometry, material) as RecordMesh
|
|
1101
|
+
let group = indices !== null ? layoutStride(buffers[0]!.attributes) / 4 : 1
|
|
790
1102
|
mesh._instances = {
|
|
791
1103
|
matrix: null,
|
|
792
|
-
streams:
|
|
1104
|
+
streams:
|
|
1105
|
+
indices !== null
|
|
1106
|
+
? [makeIndexStream(buffers[0]!.attributes, capacity, group, vertexBytes(records), streamLabel(opts?.label, 0))]
|
|
1107
|
+
: transfer
|
|
1108
|
+
? [makeTransferredStream(buffers[0]!.attributes, vertexBytes(records), order !== null, streamLabel(opts?.label, 0))]
|
|
1109
|
+
: buffers.map((b, i) => makeStream(b.attributes, capacity, null, streamLabel(opts?.label, i))),
|
|
793
1110
|
capacity,
|
|
794
1111
|
label: opts?.label,
|
|
795
1112
|
count: Math.max(0, Math.min(Math.floor(count ?? capacity), capacity)),
|
|
@@ -798,8 +1115,12 @@ export function createRecordMesh(
|
|
|
798
1115
|
anchor: null,
|
|
799
1116
|
levels: null,
|
|
800
1117
|
morph: null,
|
|
1118
|
+
order,
|
|
1119
|
+
transfer,
|
|
1120
|
+
group,
|
|
1121
|
+
textures: [],
|
|
801
1122
|
}
|
|
802
|
-
copyRecords(mesh, records, capacity)
|
|
1123
|
+
if (!transfer) copyRecords(mesh, records, capacity)
|
|
803
1124
|
return mesh
|
|
804
1125
|
}
|
|
805
1126
|
|
|
@@ -829,6 +1150,7 @@ function copyRecords(mesh: RecordMesh, records: ArrayBufferView, written: number
|
|
|
829
1150
|
*/
|
|
830
1151
|
export function setRecords(mesh: RecordMesh, records: ArrayBufferView, count?: number): void {
|
|
831
1152
|
let inst = mesh._instances
|
|
1153
|
+
if (inst.transfer) throw new Error("setRecords: the records were transferred at creation (transfer: true) - the population is write-once; mount a new mesh for new records")
|
|
832
1154
|
let written = recordCount(records, inst.streams[0]!.layout, "setRecords")
|
|
833
1155
|
if (written > inst.capacity) growInstances(mesh, Math.max(written, inst.capacity * 2))
|
|
834
1156
|
copyRecords(mesh, records, written)
|
|
@@ -842,6 +1164,12 @@ export function setRecordCount(mesh: RecordMesh, count: number): void {
|
|
|
842
1164
|
let n = Math.max(0, Math.min(Math.floor(count), inst.capacity))
|
|
843
1165
|
if (n === inst.count) return
|
|
844
1166
|
inst.count = n
|
|
1167
|
+
// Ordered records draw the first n GATHERED records, so a count change
|
|
1168
|
+
// republishes the live set - a shrink would otherwise keep drawing the
|
|
1169
|
+
// old population's nearest records, a growth its stale tail. A
|
|
1170
|
+
// transferred mesh has no mirror to publish from; the engine follows
|
|
1171
|
+
// the entry's instance count into its own mirror's prefix instead.
|
|
1172
|
+
if (inst.order !== null && !inst.transfer && n > 0) for (let s of inst.streams) markRecords(mesh, s, 0, n * s.stride)
|
|
845
1173
|
mesh._scene?._setCount(mesh)
|
|
846
1174
|
}
|
|
847
1175
|
|
|
@@ -882,6 +1210,7 @@ export function disposeInstances(mesh: InstancedMesh | RecordMesh): void {
|
|
|
882
1210
|
if (inst.matrix !== null) destroyBuffer(inst.matrix)
|
|
883
1211
|
for (let s of inst.streams) destroyBuffer(s.buffer)
|
|
884
1212
|
if (inst.morph !== null) destroyTexture(inst.morph.texture)
|
|
1213
|
+
for (let t of inst.textures) destroyTexture(t)
|
|
885
1214
|
;(mesh as Mesh)._instances = null
|
|
886
1215
|
}
|
|
887
1216
|
|
|
@@ -919,6 +1248,32 @@ export function setCastShadow(mesh: Mesh, cast: boolean): void {
|
|
|
919
1248
|
for (let l of mesh._instances?.levels ?? []) if (l !== mesh) setCastShadow(l, cast)
|
|
920
1249
|
}
|
|
921
1250
|
|
|
1251
|
+
/**
|
|
1252
|
+
* Give the mesh an environment of its own, or none: `null` removes the
|
|
1253
|
+
* environment term entirely - a sealed interior under an outdoor sky,
|
|
1254
|
+
* where light `layers` mask the sun but cannot mask image lighting -
|
|
1255
|
+
* `{ cube, intensity?, rotation? }` samples that cube instead of the
|
|
1256
|
+
* scene's (an interior probe's chain, a baked room), and `undefined`
|
|
1257
|
+
* (the default) follows the scene's, live through scene.setEnvironment.
|
|
1258
|
+
* Unity's reflectionProbeUsage Off and Three's `material.envMap`, per
|
|
1259
|
+
* MESH, because materials are shared and interiors are mesh-grained
|
|
1260
|
+
* here. Applied where the material reflects at all (Material.env); a
|
|
1261
|
+
* no-op on any other material. The mesh's entries are rebuilt, so this
|
|
1262
|
+
* is a state change (a camera crossing a doorway), not a frame-rate
|
|
1263
|
+
* dial. A reflection probe drawing this mesh never samples its own cube
|
|
1264
|
+
* through it (the black placeholder stands in, the same-pass rule).
|
|
1265
|
+
*/
|
|
1266
|
+
export function setEnvironment(mesh: Mesh, environment: EnvironmentOptions | null | undefined): void {
|
|
1267
|
+
let next = environment == null ? environment : { cube: environment.cube, ...checkCubeKnobs(environment, "setEnvironment") }
|
|
1268
|
+
let a = mesh.environment
|
|
1269
|
+
let same = next == null || a == null ? next === a : next.cube === a.cube && next.intensity === a.intensity && next.rotation === a.rotation
|
|
1270
|
+
if (same) return
|
|
1271
|
+
mesh.environment = next
|
|
1272
|
+
rebuildEntry(mesh)
|
|
1273
|
+
// An instanced LOD's levels reflect what the population reflects.
|
|
1274
|
+
for (let l of mesh._instances?.levels ?? []) if (l !== mesh) setEnvironment(l, environment)
|
|
1275
|
+
}
|
|
1276
|
+
|
|
922
1277
|
/** Frustum culling per mesh: `frustumCulled` (default true) switches the
|
|
923
1278
|
* gate, `cullMargin` (world units, default 0) grows the tested box. */
|
|
924
1279
|
export function setCulling(mesh: Mesh, options: { frustumCulled?: boolean; cullMargin?: number }): void {
|
package/src/node.ts
CHANGED
|
@@ -163,6 +163,9 @@ export type SceneHooks = {
|
|
|
163
163
|
_setLayers(mesh: Mesh): void
|
|
164
164
|
/** A light's castShadow/shadow options changed. */
|
|
165
165
|
_shadowChanged(light: CastingLight): void
|
|
166
|
+
/** A light's layers mask changed: re-filter its shadow views' casters
|
|
167
|
+
* (the uniform rewrite rides the _lightChanged that follows). */
|
|
168
|
+
_lightLayersChanged(light: Light): void
|
|
166
169
|
_reorder(mesh: Mesh): void
|
|
167
170
|
/** The node's transform changed (for the sort and light bookkeeping). */
|
|
168
171
|
_moved(node: SceneNode): void
|
package/src/scene-shadows.ts
CHANGED
|
@@ -80,8 +80,9 @@ export type ShadowSystemDeps<V extends ShadowView> = {
|
|
|
80
80
|
targetSize(): { width: number; height: number }
|
|
81
81
|
/** Base debug label ("scene" by default). */
|
|
82
82
|
label: string
|
|
83
|
-
/** Make one shadow view
|
|
84
|
-
* with the depth override (the
|
|
83
|
+
/** Make one shadow view for `light`: a tile of `into` drawing the
|
|
84
|
+
* casting meshes the light's layers admit with the depth override (the
|
|
85
|
+
* scene's makeView derives the caster filter and the layer mask). */
|
|
85
86
|
makeView(
|
|
86
87
|
vopts: {
|
|
87
88
|
width: number
|
|
@@ -93,7 +94,7 @@ export type ShadowSystemDeps<V extends ShadowView> = {
|
|
|
93
94
|
clearColor: [number, number, number, number]
|
|
94
95
|
label: string
|
|
95
96
|
},
|
|
96
|
-
|
|
97
|
+
light: CastingLight,
|
|
97
98
|
): V
|
|
98
99
|
disposeView(view: V): void
|
|
99
100
|
/** The light set changed shape: the scene owes a writeLights. */
|
|
@@ -203,9 +204,9 @@ export function makeShadowSystem<V extends ShadowView>(deps: ShadowSystemDeps<V>
|
|
|
203
204
|
deps.schedule()
|
|
204
205
|
}
|
|
205
206
|
// A caster's views: one square tile of the shadow atlas per map,
|
|
206
|
-
// drawing the casting meshes
|
|
207
|
-
// frustum. The light rewrite writes the rects
|
|
208
|
-
// every receiving target.
|
|
207
|
+
// drawing the casting meshes the light's layers admit with the depth
|
|
208
|
+
// override from that map's frustum. The light rewrite writes the rects
|
|
209
|
+
// in the light's slots on every receiving target.
|
|
209
210
|
let createShadow = (light: CastingLight, rects: ShadowRect[]) => {
|
|
210
211
|
let atlas = shadowAtlas!
|
|
211
212
|
let views = rects.map(rect =>
|
|
@@ -220,7 +221,7 @@ export function makeShadowSystem<V extends ShadowView>(deps: ShadowSystemDeps<V>
|
|
|
220
221
|
clearColor: [1, 1, 1, 1],
|
|
221
222
|
label: deps.label + "-shadow",
|
|
222
223
|
},
|
|
223
|
-
|
|
224
|
+
light,
|
|
224
225
|
),
|
|
225
226
|
)
|
|
226
227
|
shadows.set(light, { light, views, lastWorld: mat4(), dirty: true, rects })
|