@solidrt/3d 0.0.70 → 0.0.72
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 +75 -16
- package/package.json +3 -3
- package/src/components/directional-light.tsx +2 -2
- package/src/components/first-person-camera.tsx +2 -2
- package/src/components/group.tsx +2 -2
- package/src/components/hemisphere-light.tsx +2 -2
- package/src/components/instanced-mesh.tsx +3 -3
- package/src/components/lod.tsx +3 -3
- package/src/components/mesh.tsx +2 -2
- package/src/components/node-props.ts +13 -1
- package/src/components/orbit-camera.tsx +2 -2
- package/src/components/point-light.tsx +2 -2
- package/src/components/record-mesh.tsx +2 -2
- package/src/components/scene.tsx +13 -2
- package/src/components/shots.tsx +2 -2
- package/src/components/splat-mesh.tsx +2 -2
- package/src/components/spot-light.tsx +2 -2
- package/src/components/sprite.tsx +2 -2
- package/src/components/view3d.tsx +2 -2
- package/src/geometry.ts +23 -188
- package/src/glsl.ts +13 -3
- package/src/index.ts +1 -1
- package/src/material.ts +74 -50
- package/src/mesh.ts +61 -109
- package/src/mixer.ts +6 -1
- package/src/node.ts +47 -3
- package/src/scene-pointer.ts +11 -8
- package/src/scene.ts +49 -5
package/src/mesh.ts
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
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 {
|
|
7
|
-
import type { BufferId, DrawId, ShaderParams, TextureBindings, TextureId, VertexAttribute, VertexBufferLayout } from "@solidrt/core/gpu"
|
|
6
|
+
import { createBuffer, createRecordStream, destroyBuffer, destroyTexture, encodeRecord, layoutComponents, layoutFields, transferRecords, writeRecord as writeLayoutRecord, VERTEX_FORMATS } from "@solidrt/core/gpu"
|
|
7
|
+
import type { AttributeAccess, BufferId, DrawId, RecordStream, ShaderParams, TextureBindings, TextureId, VertexAttribute, VertexBufferLayout } from "@solidrt/core/gpu"
|
|
8
8
|
import { checkCubeKnobs } from "./environment.ts"
|
|
9
9
|
import type { EnvironmentOptions } from "./environment.ts"
|
|
10
|
-
import { geometryBounds, isFloatLayout, layoutKey, layoutStride, plane, vertexBytes, vertexView
|
|
11
|
-
import type {
|
|
10
|
+
import { geometryBounds, isFloatLayout, layoutKey, layoutStride, plane, vertexBytes, vertexView } from "./geometry.ts"
|
|
11
|
+
import type { Geometry } from "./geometry.ts"
|
|
12
12
|
import { INSTANCE_MATRIX_ATTRIBUTES } from "./glsl.ts"
|
|
13
13
|
import type { GeometryBuffers } from "./geometry-gpu.ts"
|
|
14
14
|
import type { Material } from "./material.ts"
|
|
@@ -189,15 +189,22 @@ export type InstanceStream = {
|
|
|
189
189
|
layout: VertexAttribute[]
|
|
190
190
|
/** Bytes per record: the layout's stride. */
|
|
191
191
|
stride: number
|
|
192
|
+
/** The GPU buffer the entry binds; `stream.buffer` where a stream
|
|
193
|
+
* exists (growth re-points both), the engine-held buffer otherwise. */
|
|
192
194
|
buffer: BufferId
|
|
195
|
+
/** The core record stream behind the mirror (createRecordStream: the
|
|
196
|
+
* buffer, the byte mirror, the dirty range and the publish), or null
|
|
197
|
+
* where no JS mirror exists: the transfer form hands the records to
|
|
198
|
+
* the engine, the indexed order's id stream is the engine's to fill. */
|
|
199
|
+
stream: RecordStream | null
|
|
200
|
+
/** The mirror as the layout's view (a Float32Array over an all-float
|
|
201
|
+
* layout, bytes otherwise): `stream.bytes` viewed, re-derived on growth;
|
|
202
|
+
* zero-length without a stream. */
|
|
193
203
|
data: ArrayBufferView
|
|
194
204
|
/** The record a fresh (or recycled) instance slot starts with: the
|
|
195
205
|
* material's instanceStyle encoded, on the first stream of an
|
|
196
206
|
* instanced mesh; zeros otherwise. */
|
|
197
207
|
blank: Uint8Array
|
|
198
|
-
/** The byte range [lo, hi) of `data` written since the last publish,
|
|
199
|
-
* or null. */
|
|
200
|
-
dirty: [number, number] | null
|
|
201
208
|
/** The DataView the accessors read through; follows `data`. */
|
|
202
209
|
_view: DataView
|
|
203
210
|
/** All-float32 layout: `data` is a Float32Array and a record write is
|
|
@@ -337,13 +344,6 @@ const MATRIX_KEY = layoutKey(INSTANCE_MATRIX_ATTRIBUTES)
|
|
|
337
344
|
// doubles past it.
|
|
338
345
|
const DEFAULT_CAPACITY = 64
|
|
339
346
|
|
|
340
|
-
// Values per record of a layout: its attributes' components summed - what
|
|
341
|
-
// setInstanceStyle and a material's instanceStyle count in.
|
|
342
|
-
function layoutComponents(layout: VertexAttribute[]): number {
|
|
343
|
-
let n = 0
|
|
344
|
-
for (let a of layout) n += VERTEX_FORMATS[a.format].components
|
|
345
|
-
return n
|
|
346
|
-
}
|
|
347
347
|
|
|
348
348
|
// Records in a byte view of `layout`: 4-aligned and a whole number of
|
|
349
349
|
// strides, or a throw naming `site`.
|
|
@@ -356,22 +356,6 @@ function recordCount(records: ArrayBufferView, layout: VertexAttribute[], site:
|
|
|
356
356
|
return records.byteLength / stride
|
|
357
357
|
}
|
|
358
358
|
|
|
359
|
-
// The accessors of a stream's attributes, reading through the stream's
|
|
360
|
-
// current view so they survive growth.
|
|
361
|
-
function streamFields(stream: InstanceStream): AttributeAccess[] {
|
|
362
|
-
let offset = 0
|
|
363
|
-
return stream.layout.map(attr => {
|
|
364
|
-
let codec = VERTEX_FORMATS[attr.format]
|
|
365
|
-
let at = offset
|
|
366
|
-
offset += codec.bytes
|
|
367
|
-
return {
|
|
368
|
-
format: attr.format,
|
|
369
|
-
components: codec.components,
|
|
370
|
-
get: (i, k) => codec.get(stream._view, i * stream.stride + at, k),
|
|
371
|
-
set: (i, k, v) => codec.set(stream._view, i * stream.stride + at, k, v),
|
|
372
|
-
}
|
|
373
|
-
})
|
|
374
|
-
}
|
|
375
359
|
|
|
376
360
|
// Point a stream's mirror at `bytes` (a fresh ArrayBuffer of whole
|
|
377
361
|
// records): the handed-out view and the accessors' view.
|
|
@@ -384,24 +368,26 @@ function streamLabel(label: string | undefined, index: number): string | undefin
|
|
|
384
368
|
return label === undefined ? undefined : label + "-" + index
|
|
385
369
|
}
|
|
386
370
|
|
|
387
|
-
// A stream of `capacity` zeroed records over `layout
|
|
388
|
-
// sized and unwritten (the mirror is the truth,
|
|
371
|
+
// A stream of `capacity` zeroed records over `layout` on a core record
|
|
372
|
+
// stream; its GPU buffer is sized and unwritten (the mirror is the truth,
|
|
373
|
+
// publishes follow the dirty range).
|
|
389
374
|
function makeStream(layout: VertexAttribute[], capacity: number, blank: Uint8Array | null, label: string | undefined): InstanceStream {
|
|
390
375
|
let stride = layoutStride(layout)
|
|
376
|
+
let records = createRecordStream(stride, capacity, { autoFree: false, label })
|
|
391
377
|
let stream: InstanceStream = {
|
|
392
378
|
layout,
|
|
393
379
|
stride,
|
|
394
|
-
buffer:
|
|
380
|
+
buffer: records.buffer,
|
|
381
|
+
stream: records,
|
|
395
382
|
data: new Uint8Array(0),
|
|
396
383
|
blank: blank ?? new Uint8Array(stride),
|
|
397
|
-
dirty: null,
|
|
398
384
|
_view: new DataView(new ArrayBuffer(0)),
|
|
399
385
|
_floats: isFloatLayout(layout),
|
|
400
386
|
_components: layoutComponents(layout),
|
|
401
387
|
_fields: [],
|
|
402
388
|
}
|
|
403
|
-
viewStream(stream,
|
|
404
|
-
stream._fields =
|
|
389
|
+
viewStream(stream, records.bytes.buffer)
|
|
390
|
+
stream._fields = layoutFields(stream.layout, () => stream._view)
|
|
405
391
|
return stream
|
|
406
392
|
}
|
|
407
393
|
|
|
@@ -416,9 +402,9 @@ function makeIndexStream(layout: VertexAttribute[], capacity: number, group: num
|
|
|
416
402
|
layout,
|
|
417
403
|
stride,
|
|
418
404
|
buffer: createBuffer(Math.ceil(capacity / group) * stride, { autoFree: false, label }),
|
|
405
|
+
stream: null,
|
|
419
406
|
data: new Uint8Array(0),
|
|
420
407
|
blank: new Uint8Array(stride),
|
|
421
|
-
dirty: null,
|
|
422
408
|
_view: new DataView(new ArrayBuffer(0)),
|
|
423
409
|
_floats: false,
|
|
424
410
|
_components: layoutComponents(layout),
|
|
@@ -442,9 +428,9 @@ function makeTransferredStream(layout: VertexAttribute[], records: Uint8Array, o
|
|
|
442
428
|
buffer: ordered
|
|
443
429
|
? createBuffer(records.byteLength, { autoFree: false, label })
|
|
444
430
|
: createBuffer(records, { autoFree: false, label }),
|
|
431
|
+
stream: null,
|
|
445
432
|
data: new Uint8Array(0),
|
|
446
433
|
blank: new Uint8Array(stride),
|
|
447
|
-
dirty: null,
|
|
448
434
|
_view: new DataView(new ArrayBuffer(0)),
|
|
449
435
|
_floats: isFloatLayout(layout),
|
|
450
436
|
_components: layoutComponents(layout),
|
|
@@ -455,41 +441,18 @@ function makeTransferredStream(layout: VertexAttribute[], records: Uint8Array, o
|
|
|
455
441
|
}
|
|
456
442
|
|
|
457
443
|
// Encode `values` (one per component, in layout order, as the shader
|
|
458
|
-
// sees them) into record `i` of a stream:
|
|
459
|
-
//
|
|
444
|
+
// sees them) into record `i` of a stream: core's writeRecord over the
|
|
445
|
+
// stream's layout, accessors and float view.
|
|
460
446
|
function writeRecord(stream: InstanceStream, i: number, values: ArrayLike<number>, site: string): void {
|
|
461
|
-
|
|
462
|
-
if (values.length !== n) throw new Error(site + ": a " + layoutKey(stream.layout) + " record is " + n + " values, got " + values.length)
|
|
463
|
-
if (stream._floats) {
|
|
464
|
-
// Indexed stores beat TypedArray.set over a plain array under QuickJS,
|
|
465
|
-
// which walks the array-like generically.
|
|
466
|
-
let data = stream.data as Float32Array
|
|
467
|
-
let at = i * n
|
|
468
|
-
for (let k = 0; k < n; k++) data[at + k] = values[k]!
|
|
469
|
-
return
|
|
470
|
-
}
|
|
471
|
-
let j = 0
|
|
472
|
-
for (let f of stream._fields) for (let k = 0; k < f.components; k++) f.set(i, k, values[j++]!)
|
|
447
|
+
writeLayoutRecord(stream.layout, stream._fields, stream._floats ? (stream.data as Float32Array) : null, i, values, site)
|
|
473
448
|
}
|
|
474
449
|
|
|
475
|
-
// One record of `layout` encoded from `values`, the bytes a blank slot copies.
|
|
476
|
-
function encodeRecord(layout: VertexAttribute[], values: ArrayLike<number>, site: string): Uint8Array {
|
|
477
|
-
let stride = layoutStride(layout)
|
|
478
|
-
let one: InstanceStream = { layout, stride, buffer: 0 as BufferId, data: new Uint8Array(0), blank: new Uint8Array(0), dirty: null, _view: new DataView(new ArrayBuffer(0)), _floats: isFloatLayout(layout), _components: layoutComponents(layout), _fields: [] }
|
|
479
|
-
viewStream(one, new ArrayBuffer(stride))
|
|
480
|
-
one._fields = streamFields(one)
|
|
481
|
-
writeRecord(one, 0, values, site)
|
|
482
|
-
return vertexBytes(one.data)
|
|
483
|
-
}
|
|
484
450
|
|
|
485
|
-
//
|
|
486
|
-
//
|
|
451
|
+
// Mark records [lo, hi) of a stream's mirror and ask the scene to publish
|
|
452
|
+
// them at the next sync.
|
|
487
453
|
function markRecords(mesh: Mesh, stream: InstanceStream, lo: number, hi: number): void {
|
|
488
|
-
if (stream.
|
|
489
|
-
|
|
490
|
-
if (lo < stream.dirty[0]) stream.dirty[0] = lo
|
|
491
|
-
if (hi > stream.dirty[1]) stream.dirty[1] = hi
|
|
492
|
-
}
|
|
454
|
+
if (stream.stream === null) throw new Error("markRecords: the stream has no mirror (its records were transferred to the engine)")
|
|
455
|
+
stream.stream.mark(lo, hi)
|
|
493
456
|
mesh._scene?._setRecords(mesh)
|
|
494
457
|
}
|
|
495
458
|
|
|
@@ -869,7 +832,7 @@ export function setInstanceStyle(instance: InstanceNode, values: ArrayLike<numbe
|
|
|
869
832
|
let stream = l._instances.streams[0]
|
|
870
833
|
if (stream === undefined) continue
|
|
871
834
|
writeRecord(stream, instance._slot, values, "setInstanceStyle")
|
|
872
|
-
markRecords(l, stream, instance._slot
|
|
835
|
+
markRecords(l, stream, instance._slot, instance._slot + 1)
|
|
873
836
|
}
|
|
874
837
|
}
|
|
875
838
|
|
|
@@ -944,34 +907,23 @@ export function updateRecords(mesh: InstancedMesh | RecordMesh, options: UpdateR
|
|
|
944
907
|
if (!Number.isInteger(first) || !Number.isInteger(count) || first < 0 || count < 0 || first + count > inst.capacity) {
|
|
945
908
|
throw new Error("updateRecords: range [" + first + ", " + (first + count) + ") is outside the mesh's " + inst.capacity + " records")
|
|
946
909
|
}
|
|
947
|
-
if (count > 0) markRecords(mesh, stream, first
|
|
910
|
+
if (count > 0) markRecords(mesh, stream, first, first + count)
|
|
948
911
|
}
|
|
949
912
|
|
|
950
913
|
/** Publish a mesh's pending record writes, one buffer write per dirty
|
|
951
|
-
* stream (the scene calls it from its sync)
|
|
952
|
-
* the
|
|
953
|
-
*
|
|
954
|
-
*
|
|
914
|
+
* stream (the scene calls it from its sync): the dirty record range, up
|
|
915
|
+
* to the capacity (a record mesh writes ahead of its count). An ordered
|
|
916
|
+
* mesh publishes the LIVE record set whole through the write lease - the
|
|
917
|
+
* engine gathers it into key order during the copy, and a byte range has
|
|
918
|
+
* no stable position under a permutation (writeBuffer throws on an
|
|
919
|
+
* ordered buffer). */
|
|
955
920
|
export function publishRecords(mesh: Mesh): void {
|
|
956
921
|
let inst = mesh._instances
|
|
957
922
|
if (inst === null) return
|
|
958
|
-
|
|
959
|
-
for (let s of inst.streams) {
|
|
960
|
-
if (s.dirty === null) continue
|
|
961
|
-
s.dirty = null
|
|
962
|
-
let bytes = inst.count * s.stride
|
|
963
|
-
if (bytes === 0) continue
|
|
964
|
-
let block = beginBufferWrite(s.buffer)
|
|
965
|
-
new Uint8Array(block.buffer, 0, bytes).set(vertexBytes(s.data).subarray(0, bytes))
|
|
966
|
-
endBufferWrite(s.buffer, bytes)
|
|
967
|
-
}
|
|
968
|
-
return
|
|
969
|
-
}
|
|
923
|
+
let ordered = inst.order !== null
|
|
970
924
|
for (let s of inst.streams) {
|
|
971
|
-
if (s.
|
|
972
|
-
|
|
973
|
-
s.dirty = null
|
|
974
|
-
writeBuffer(s.buffer, vertexBytes(s.data).subarray(lo, hi), lo)
|
|
925
|
+
if (s.stream === null || !s.stream.dirty) continue
|
|
926
|
+
s.stream.publish(ordered, ordered ? inst.count : inst.capacity)
|
|
975
927
|
}
|
|
976
928
|
}
|
|
977
929
|
|
|
@@ -984,7 +936,7 @@ export function markLiveRecords(mesh: Mesh): void {
|
|
|
984
936
|
// A transferred mesh has no mirror; the engine's own (buffer-lifetime)
|
|
985
937
|
// mirror seeds a re-attached entry instead.
|
|
986
938
|
if (inst === null || inst.count === 0 || inst.transfer) return
|
|
987
|
-
for (let s of inst.streams) markRecords(mesh, s, 0, inst.count
|
|
939
|
+
for (let s of inst.streams) markRecords(mesh, s, 0, inst.count)
|
|
988
940
|
}
|
|
989
941
|
|
|
990
942
|
/**
|
|
@@ -1033,7 +985,7 @@ export function addInstance(mesh: InstancedMesh, update?: TransformUpdate, paren
|
|
|
1033
985
|
// A recycled slot must not wear its last occupant's records.
|
|
1034
986
|
for (let s of li.streams) {
|
|
1035
987
|
vertexBytes(s.data).set(s.blank, slot * s.stride)
|
|
1036
|
-
markRecords(l, s, slot
|
|
988
|
+
markRecords(l, s, slot, slot + 1)
|
|
1037
989
|
}
|
|
1038
990
|
}
|
|
1039
991
|
if (parent._scene) enterScene(instance, parent._scene)
|
|
@@ -1069,14 +1021,13 @@ export function reparentInstance(instance: InstanceNode, parent: SceneNode): voi
|
|
|
1069
1021
|
|
|
1070
1022
|
// Grow the population's buffers to `next` records: replacements (never a
|
|
1071
1023
|
// resize). An instanced mesh's live matrix records move to the new buffer
|
|
1072
|
-
// in one core call and republish at the next flush; every stream
|
|
1073
|
-
//
|
|
1074
|
-
// re-points, then the old buffers, which
|
|
1075
|
-
// are freed.
|
|
1024
|
+
// in one core call and republish at the next flush; every stream grows
|
|
1025
|
+
// into a replacement with its mirror copied over and marked whole, so the
|
|
1026
|
+
// scene republishes it; the entry re-points, then the old buffers, which
|
|
1027
|
+
// the entry held alive until now, are freed.
|
|
1076
1028
|
function growInstances(mesh: InstancedMesh | RecordMesh, next: number): void {
|
|
1077
1029
|
let inst: MeshInstances = mesh._instances
|
|
1078
1030
|
if (inst.transfer) throw new Error("growInstances: a transferred population is fixed at its creation records")
|
|
1079
|
-
let previous = inst.capacity
|
|
1080
1031
|
inst.capacity = next
|
|
1081
1032
|
let freed: BufferId[] = []
|
|
1082
1033
|
if (inst.matrix !== null) {
|
|
@@ -1085,15 +1036,13 @@ function growInstances(mesh: InstancedMesh | RecordMesh, next: number): void {
|
|
|
1085
1036
|
// Out of a scene nothing is bound, and retargeting an empty source throws.
|
|
1086
1037
|
if (inst.nodes !== null && inst.nodes.slots.some(n => n !== null && n._node !== null)) spatial.retargetRecords(freed[0]!, inst.matrix)
|
|
1087
1038
|
}
|
|
1088
|
-
inst.streams
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
viewStream(s,
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
markRecords(mesh, s, 0, previous * s.stride)
|
|
1096
|
-
})
|
|
1039
|
+
for (let s of inst.streams) {
|
|
1040
|
+
if (s.stream === null) throw new Error("growInstances: a stream without a mirror cannot grow")
|
|
1041
|
+
freed.push(s.stream.grow(next))
|
|
1042
|
+
s.buffer = s.stream.buffer
|
|
1043
|
+
viewStream(s, s.stream.bytes.buffer)
|
|
1044
|
+
mesh._scene?._setRecords(mesh)
|
|
1045
|
+
}
|
|
1097
1046
|
mesh._scene?._setBuffer(mesh)
|
|
1098
1047
|
for (let b of freed) destroyBuffer(b)
|
|
1099
1048
|
if (inst.morph !== null) replacePopulationMorph(mesh, populationMorph(mesh.geometry, next, inst.label), true)
|
|
@@ -1180,7 +1129,7 @@ export function createRecordMesh(
|
|
|
1180
1129
|
function copyRecords(mesh: RecordMesh, records: ArrayBufferView, written: number): void {
|
|
1181
1130
|
let stream = mesh._instances.streams[0]!
|
|
1182
1131
|
vertexBytes(stream.data).set(vertexBytes(records))
|
|
1183
|
-
if (written > 0) markRecords(mesh, stream, 0, written
|
|
1132
|
+
if (written > 0) markRecords(mesh, stream, 0, written)
|
|
1184
1133
|
}
|
|
1185
1134
|
|
|
1186
1135
|
/**
|
|
@@ -1220,7 +1169,7 @@ export function setRecordCount(mesh: RecordMesh, count: number): void {
|
|
|
1220
1169
|
// old population's nearest records, a growth its stale tail. A
|
|
1221
1170
|
// transferred mesh has no mirror to publish from; the engine follows
|
|
1222
1171
|
// the entry's instance count into its own mirror's prefix instead.
|
|
1223
|
-
if (inst.order !== null && !inst.transfer && n > 0) for (let s of inst.streams) markRecords(mesh, s, 0, n
|
|
1172
|
+
if (inst.order !== null && !inst.transfer && n > 0) for (let s of inst.streams) markRecords(mesh, s, 0, n)
|
|
1224
1173
|
mesh._scene?._setCount(mesh)
|
|
1225
1174
|
}
|
|
1226
1175
|
|
|
@@ -1259,7 +1208,10 @@ export function disposeInstances(mesh: InstancedMesh | RecordMesh): void {
|
|
|
1259
1208
|
spatial.flush()
|
|
1260
1209
|
}
|
|
1261
1210
|
if (inst.matrix !== null) destroyBuffer(inst.matrix)
|
|
1262
|
-
for (let s of inst.streams)
|
|
1211
|
+
for (let s of inst.streams) {
|
|
1212
|
+
if (s.stream !== null) s.stream.destroy()
|
|
1213
|
+
else destroyBuffer(s.buffer)
|
|
1214
|
+
}
|
|
1263
1215
|
if (inst.morph !== null) destroyTexture(inst.morph.texture)
|
|
1264
1216
|
for (let t of inst.textures) destroyTexture(t)
|
|
1265
1217
|
;(mesh as Mesh)._instances = null
|
package/src/mixer.ts
CHANGED
|
@@ -401,7 +401,12 @@ export function createMixer(model: Model, mixerOpts: MixerOptions = {}): Mixer {
|
|
|
401
401
|
if (i >= 0) actions.splice(i, 1)
|
|
402
402
|
}
|
|
403
403
|
let start = (clip: ModelClip, opts: MixerPlayOptions, weight: number, fade: number): void => {
|
|
404
|
-
let
|
|
404
|
+
let targets = targetsFor(clip)
|
|
405
|
+
// The player's time runs at the model node's rate (setTimeScale on
|
|
406
|
+
// it or an ancestor): targetsFor threw if the model is not in a scene.
|
|
407
|
+
let clock = model._node
|
|
408
|
+
if (clock === null) throw new Error("play: the model must be in a scene (add() it first) before clips can play")
|
|
409
|
+
let player = spatial.createPlayer(clipFor(clip, opts), targets, clock, { speed: opts.speed, loop: opts.loop, weight, fade })
|
|
405
410
|
let action: Action = { name: clip.name, player, fadingOut: false, finished: false }
|
|
406
411
|
PLAYER_ENDS.set(player, (reason) => {
|
|
407
412
|
if (reason === "finished") {
|
package/src/node.ts
CHANGED
|
@@ -217,8 +217,9 @@ export type SceneNode = {
|
|
|
217
217
|
/** The wheel over the node (NodeWheelEvent: `deltaX`/`deltaY`), bubbling
|
|
218
218
|
* like down/move/up. */
|
|
219
219
|
onWheel?: (event: NodeWheelEvent) => void
|
|
220
|
-
/** A press
|
|
221
|
-
*
|
|
220
|
+
/** A primary-button press (a right click never taps) released on this
|
|
221
|
+
* node within the slop, alone for its whole press (NodeTapEvent:
|
|
222
|
+
* `tapCount`); bubbles like down/move/up. */
|
|
222
223
|
onTap?: (event: NodeTapEvent) => void
|
|
223
224
|
/** A declared transition (setTransition) settled naturally on one
|
|
224
225
|
* component; a cancel, snap, scene leave or exit never fires. */
|
|
@@ -239,6 +240,9 @@ export type SceneNode = {
|
|
|
239
240
|
_scene: SceneHooks | null
|
|
240
241
|
/** The declared transition, re-applied on every scene enter. */
|
|
241
242
|
_transition: NodeTransition | string | null
|
|
243
|
+
/** The declared time scale (setTimeScale), re-applied on every scene
|
|
244
|
+
* enter; null inherits. */
|
|
245
|
+
_timeScale: number | null
|
|
242
246
|
/** Skin palette rows this node feeds (a model joint carries one per
|
|
243
247
|
* skin): bound to the core at every scene enter, so the flush writes
|
|
244
248
|
* `inverse(anchorWorld) * world * post` - the model-local bone matrix -
|
|
@@ -289,6 +293,9 @@ export type SceneEventBase = {
|
|
|
289
293
|
predicted: boolean
|
|
290
294
|
pointerId: number
|
|
291
295
|
pointerType: string
|
|
296
|
+
/** The pressed button on a down, up or tap (0 = primary, 2 = secondary;
|
|
297
|
+
* touch is always 0), as the element event carries it; absent on a
|
|
298
|
+
* move, cancel or wheel. */
|
|
292
299
|
button?: number
|
|
293
300
|
shiftKey: boolean
|
|
294
301
|
ctrlKey: boolean
|
|
@@ -407,6 +414,7 @@ export function makeNode(kind: SceneNode["kind"]): SceneNode {
|
|
|
407
414
|
_moved: false,
|
|
408
415
|
_scene: null,
|
|
409
416
|
_transition: null,
|
|
417
|
+
_timeScale: null,
|
|
410
418
|
_palettes: null,
|
|
411
419
|
_morph: null,
|
|
412
420
|
_cullBounds: null,
|
|
@@ -548,6 +556,7 @@ export function enterScene(node: SceneNode, scene: SceneHooks): void {
|
|
|
548
556
|
spatial.setTransition(node._node, node._transition)
|
|
549
557
|
declareTransition(node._node, node)
|
|
550
558
|
}
|
|
559
|
+
if (node._timeScale !== null) spatial.setTimeScale(node._node, node._timeScale)
|
|
551
560
|
// The parent is in the scene already (add() enters the child only then),
|
|
552
561
|
// and the scene root is the one node without a parent.
|
|
553
562
|
if (node.parent !== null && node.parent._node !== null) spatial.setParent(node._node, node.parent._node)
|
|
@@ -799,6 +808,37 @@ export function setTransition(node: SceneNode, transition: NodeTransition | stri
|
|
|
799
808
|
}
|
|
800
809
|
}
|
|
801
810
|
|
|
811
|
+
/**
|
|
812
|
+
* Declare (null clears) the rate the native motion in the node's subtree
|
|
813
|
+
* runs at: transitions with their delays, enters, exits and stagger,
|
|
814
|
+
* morph weight tracks, and the clip players of the models under it
|
|
815
|
+
* (a mixer's players are clocked on their model's node, so this is also
|
|
816
|
+
* Three's `mixer.timeScale`). 0 freezes, 1 is app time, other values
|
|
817
|
+
* are slow or fast motion. The nearest declaring ancestor wins, never a
|
|
818
|
+
* product: a world at 0 with a group inside it declaring 1 keeps that
|
|
819
|
+
* group running (a preview on a pause screen). A frozen subtree asks for
|
|
820
|
+
* no frames; this write brings the one that resumes it. The declaration
|
|
821
|
+
* lives on the SceneNode and re-applies on every scene enter. What it
|
|
822
|
+
* does not cover: `onFrame` logic, timers, video and audio stay on app
|
|
823
|
+
* time - multiply your own step by `timeRate(node)`. For the whole scene,
|
|
824
|
+
* `scene.setTimeScale` / `<Scene timeScale>` declare it on `scene.root`.
|
|
825
|
+
*/
|
|
826
|
+
export function setTimeScale(node: SceneNode, scale: number | null): void {
|
|
827
|
+
if (node._destroyed) return
|
|
828
|
+
node._timeScale = scale
|
|
829
|
+
if (node._node !== null) spatial.setTimeScale(node._node, scale)
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* The rate the node's motion runs at (setTimeScale): its own scale, else
|
|
834
|
+
* the nearest declaring ancestor's, else 1. Null outside a scene, where
|
|
835
|
+
* nothing runs and no ancestry resolves.
|
|
836
|
+
*/
|
|
837
|
+
export function timeRate(node: SceneNode): number | null {
|
|
838
|
+
if (node._node === null) return null
|
|
839
|
+
return spatial.timeRate(node._node)
|
|
840
|
+
}
|
|
841
|
+
|
|
802
842
|
/**
|
|
803
843
|
* The one write path for node transforms (so the scene knows to sync).
|
|
804
844
|
* Values are copied in; absent keys keep their current value. This is also
|
|
@@ -941,7 +981,11 @@ export function getTransform(node: SceneNode): { position: Vec3; quaternion: Qua
|
|
|
941
981
|
/**
|
|
942
982
|
* A node's position in world space, copied into `out` (or a fresh Vec3) -
|
|
943
983
|
* Three's `getWorldPosition`. Brings the ancestor chain up to date first,
|
|
944
|
-
* so it is exact before the pending sync has run
|
|
984
|
+
* so it is exact before the pending sync has run, and exact against
|
|
985
|
+
* native motion: a transition or a clip player has stepped to this
|
|
986
|
+
* frame's pose before any of the frame's JS runs, so a read in onFrame
|
|
987
|
+
* or the late pass of onBeforeRender is where the node is drawn this
|
|
988
|
+
* frame.
|
|
945
989
|
*/
|
|
946
990
|
export function worldPosition(node: SceneNode, out: Vec3 = [0, 0, 0]): Vec3 {
|
|
947
991
|
let world = worldInto(worldScratch, node)
|
package/src/scene-pointer.ts
CHANGED
|
@@ -12,11 +12,12 @@
|
|
|
12
12
|
// turns. Capture is per pointer to the press target, the root included: a
|
|
13
13
|
// drag from empty space keeps delivering to the root as it crosses meshes,
|
|
14
14
|
// exactly as a drag from a mesh keeps naming that mesh. Enter/leave pair
|
|
15
|
-
// on the struck node alone. Taps are synthesized here (DOM's click,
|
|
16
|
-
// Unity's IPointerClickHandler
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
15
|
+
// on the struck node alone. Taps are synthesized here (DOM's click, which
|
|
16
|
+
// is primary-button only; Unity's IPointerClickHandler fires for every
|
|
17
|
+
// button and reports it, we do not): a primary-button press that releases
|
|
18
|
+
// on the same target within the slop, the only pointer down for its whole
|
|
19
|
+
// press, counted up for repeats (DOM's detail, Unity's clickCount).
|
|
20
|
+
// @solidrt/2d's dispatch.ts is the same model one dimension down.
|
|
20
21
|
//
|
|
21
22
|
// Pure BY DESIGN (types only, nothing with GPU imports) so
|
|
22
23
|
// tests/dispatch.test.ts drives it headless with a fake pick.
|
|
@@ -240,9 +241,11 @@ export function makePointerInput(deps: PointerInputDeps): PointerInput {
|
|
|
240
241
|
presses.delete(e.pointerId)
|
|
241
242
|
let under = hitOn(press.target, x, y)
|
|
242
243
|
walk("onPointerUp", makeEvent(e, press.target, x, y, under), !press.claimed)
|
|
243
|
-
// The tap rule:
|
|
244
|
-
// press
|
|
245
|
-
//
|
|
244
|
+
// The tap rule: the primary button (DOM click; core's press and
|
|
245
|
+
// long-press gate the same way), no travel past the slop, alone
|
|
246
|
+
// for the whole press, released over the target it pressed (empty
|
|
247
|
+
// space for the root itself).
|
|
248
|
+
if (e.button != null && e.button !== 0) return
|
|
246
249
|
if (press.moved || !press.alone) return
|
|
247
250
|
let release = nearest(x, y)
|
|
248
251
|
if (press.target === null ? release !== null : release === null || !sameHit(release, press.target)) return
|
package/src/scene.ts
CHANGED
|
@@ -63,7 +63,7 @@ import type { EnvironmentOptions, Prefilter } from "./environment.ts"
|
|
|
63
63
|
|
|
64
64
|
export type { EnvironmentOptions } from "./environment.ts"
|
|
65
65
|
import type { Material } from "./material.ts"
|
|
66
|
-
import { activateMorph, fillTransform, freeLeaving, leaveScene, makeNode, setTransition, worldInto } from "./node.ts"
|
|
66
|
+
import { activateMorph, fillTransform, freeLeaving, leaveScene, makeNode, setTimeScale, setTransition, timeRate, worldInto } from "./node.ts"
|
|
67
67
|
import type { SceneHooks, SceneNode, ScenePointerListener } from "./node.ts"
|
|
68
68
|
import { checkInstancePairing, checkMask, checkOrderPairing, drawCount, instanceBinding, localBounds, markLiveRecords, publishRecords, spriteScreenPx } from "./mesh.ts"
|
|
69
69
|
import type { InstancedMesh, InstanceNode, Mesh, MeshInstances, ResolvedInstanceOrder } from "./mesh.ts"
|
|
@@ -474,6 +474,10 @@ export type SceneOptions = {
|
|
|
474
474
|
* the whole-scene form of a Group's stagger (nested groups declaring
|
|
475
475
|
* their own win for what is under them). See setStagger. */
|
|
476
476
|
stagger?: number
|
|
477
|
+
/** The rate the scene's native motion runs at - `setTimeScale(scene.root,
|
|
478
|
+
* scale)` at creation: 0 freezes the world, 1 is app time (nodes
|
|
479
|
+
* declaring their own win for what is under them). See setTimeScale. */
|
|
480
|
+
timeScale?: number
|
|
477
481
|
/** `autoFree: false` opts out of owner-scoped auto-dispose (then call dispose yourself). */
|
|
478
482
|
autoFree?: boolean
|
|
479
483
|
filter?: FilterMode
|
|
@@ -833,6 +837,16 @@ export type Scene = {
|
|
|
833
837
|
/** Stagger (ms) on the scene's root node, live (SceneOptions.stagger
|
|
834
838
|
* is its creation form): null removes it. The 2d layer's setStagger. */
|
|
835
839
|
setStagger(ms: number | null): void
|
|
840
|
+
/** The time scale on the scene's root node, live (SceneOptions.timeScale
|
|
841
|
+
* is its creation form): the rate every transition, enter, exit and
|
|
842
|
+
* clip player in the scene runs at, 0 a pause that leaves the app's own
|
|
843
|
+
* UI moving; null removes it (app time). A node declaring its own keeps
|
|
844
|
+
* its subtree at that rate (see node setTimeScale). The 2d layer's
|
|
845
|
+
* setTimeScale. */
|
|
846
|
+
setTimeScale(scale: number | null): void
|
|
847
|
+
/** The rate the scene's root runs at (setTimeScale): what the app's own
|
|
848
|
+
* per-frame logic in this scene multiplies its step by. */
|
|
849
|
+
timeRate(): number
|
|
836
850
|
/** The level a LOD group draws in the scene's own render (the thing to
|
|
837
851
|
* read while tuning thresholds): the index into its levels, nearest
|
|
838
852
|
* first, `levels.length` when culled past the last one, null before
|
|
@@ -2071,7 +2085,7 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
2071
2085
|
for (let v of views) attachView(v, mesh)
|
|
2072
2086
|
// Record writes made while the mesh was out of a scene kept their
|
|
2073
2087
|
// dirty ranges: publish them now.
|
|
2074
|
-
if (inst !== null && inst.streams.some(s => s.
|
|
2088
|
+
if (inst !== null && inst.streams.some(s => s.stream !== null && s.stream.dirty)) recordsDirty.add(mesh)
|
|
2075
2089
|
// Picking: an ordinary mesh's node carries its geometry's shape,
|
|
2076
2090
|
// which is its box in the core index (following every
|
|
2077
2091
|
// updateVertices) and its triangle narrowphase. An instanced mesh's
|
|
@@ -2272,22 +2286,45 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
2272
2286
|
root._scene = hooks
|
|
2273
2287
|
root._node = spatial.createNode(fillTransform(root), true)
|
|
2274
2288
|
if (opts?.stagger !== undefined) setTransition(root, { stagger: opts.stagger })
|
|
2289
|
+
if (opts?.timeScale !== undefined) setTimeScale(root, opts.timeScale)
|
|
2275
2290
|
// The first light rewrite seeds the (empty) light set and the shadow
|
|
2276
2291
|
// slots - placeholders, no casts - so receivers draw plain from the
|
|
2277
2292
|
// first frame.
|
|
2278
2293
|
lightsDirty = true
|
|
2279
2294
|
hooks._schedule()
|
|
2295
|
+
// What the sync derives from poses the core moves on its own, placed
|
|
2296
|
+
// every frame: a casting light's shadow cameras follow a light under a
|
|
2297
|
+
// transition or a clip player in the frame the lighting moves, which
|
|
2298
|
+
// the core writes itself. The compare in placeShadowCamera makes this
|
|
2299
|
+
// one world read per caster and no write when nothing moved; the
|
|
2300
|
+
// records, the lights and the budgets wait for a write to schedule the
|
|
2301
|
+
// sync, which places the cameras itself.
|
|
2302
|
+
let follow = () => {
|
|
2303
|
+
if (disposed) return
|
|
2304
|
+
shadowSys.placeCameras(false)
|
|
2305
|
+
for (let v of views) {
|
|
2306
|
+
ensureCamera(v.camera, v.width, v.height)
|
|
2307
|
+
if (!v.camera.pending) continue
|
|
2308
|
+
v.camera.pending = false
|
|
2309
|
+
setTargetParams(v.texture, cameraParams(v.camera, v.width, v.height))
|
|
2310
|
+
setView(v.texture, v.camera)
|
|
2311
|
+
if (v.shadowFilter !== null) shadowSys.markMatricesDirty()
|
|
2312
|
+
}
|
|
2313
|
+
shadowSys.flushMatrices(params => receivingTargets(t => setTargetParams(t, params)))
|
|
2314
|
+
}
|
|
2280
2315
|
// The publish pass of every frame runs the pending sync ahead of the
|
|
2281
2316
|
// paint, so a write made anywhere in the frame's JS is in that frame's
|
|
2282
2317
|
// picture: the count, the buffers and the bytes of a record write land
|
|
2283
|
-
// together, and nothing shows a frame late.
|
|
2284
|
-
//
|
|
2285
|
-
//
|
|
2318
|
+
// together, and nothing shows a frame late. With nothing scheduled it
|
|
2319
|
+
// follows native motion instead. The microtask stays for writes made
|
|
2320
|
+
// outside a frame. Registered outside any owner: the scene's own
|
|
2321
|
+
// dispose unhooks it (an autoFree: false scene outlives the owner it
|
|
2286
2322
|
// was created in).
|
|
2287
2323
|
let unhook = runWithOwner(null, () =>
|
|
2288
2324
|
onBeforeRender(
|
|
2289
2325
|
() => {
|
|
2290
2326
|
if (scheduled) sync()
|
|
2327
|
+
else follow()
|
|
2291
2328
|
},
|
|
2292
2329
|
{ publish: true },
|
|
2293
2330
|
),
|
|
@@ -2444,6 +2481,13 @@ export function createScene(width: number, height: number, opts?: SceneOptions):
|
|
|
2444
2481
|
if (disposed) return
|
|
2445
2482
|
setTransition(root, ms === null ? null : { stagger: ms })
|
|
2446
2483
|
},
|
|
2484
|
+
setTimeScale(scale) {
|
|
2485
|
+
if (disposed) return
|
|
2486
|
+
setTimeScale(root, scale)
|
|
2487
|
+
},
|
|
2488
|
+
timeRate() {
|
|
2489
|
+
return timeRate(root) ?? 1
|
|
2490
|
+
},
|
|
2447
2491
|
setParams(params) {
|
|
2448
2492
|
if (disposed) return
|
|
2449
2493
|
Object.assign(sceneParams, params)
|