@solidrt/3d 0.0.63 → 0.0.65
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 +338 -48
- package/examples/README.md +3 -3
- package/examples/fog.tsx +18 -5
- package/examples/model-load.tsx +1 -1
- package/package.json +7 -3
- 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/geometry.ts +1 -1
- package/src/glsl.ts +69 -15
- package/src/gltf.ts +98 -15
- package/src/index.ts +12 -7
- package/src/light.ts +56 -4
- package/src/material.ts +26 -1
- package/src/mesh.ts +361 -6
- package/src/model-data.ts +9 -7
- package/src/model-file.ts +16 -7
- package/src/model.ts +40 -20
- package/src/node.ts +8 -0
- package/src/scene-pointer.ts +6 -5
- 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/model.flux.ts +206 -0
- package/tools/splat.ts +70 -0
- package/tools/model.ts +0 -56
package/src/model.ts
CHANGED
|
@@ -13,15 +13,17 @@
|
|
|
13
13
|
// them and detaches the group. loadGltf / loadModel are the
|
|
14
14
|
// read-then-create conveniences over flux:fs; parseGltf / decodeModel +
|
|
15
15
|
// createModel are the primitives under them, for bytes obtained any other
|
|
16
|
-
// way (a binary import, a fetch).
|
|
16
|
+
// way (a binary import, a fetch). Compressed images (KTX2, what `srt tool
|
|
17
|
+
// 3d/model` bakes) are turned into this device's block format by
|
|
18
|
+
// transcodeModelImages, the one asynchronous step between the two.
|
|
17
19
|
|
|
18
20
|
import { file } from "flux:fs"
|
|
19
21
|
import * as spatial from "flux:spatial"
|
|
20
|
-
import { decodeImage, getOwner, isDisposed, onCleanup, runWithOwner } from "@solidrt/core"
|
|
22
|
+
import { decodeImage, getOwner, isDisposed, isKtx2, onCleanup, runWithOwner, transcodeTexture } from "@solidrt/core"
|
|
21
23
|
import type { Owner } from "@solidrt/core"
|
|
22
24
|
import { createMutableTexture, createTexture, destroyTexture } from "@solidrt/core/gpu"
|
|
23
25
|
import type { TextureId } from "@solidrt/core/gpu"
|
|
24
|
-
import { gltfExternalUris, parseGltf } from "./gltf.ts"
|
|
26
|
+
import { gltfExternalUris, modelImageUses, parseGltf } from "./gltf.ts"
|
|
25
27
|
import type { ModelClip, ModelData, ModelExtras, ModelMaterial } from "./gltf.ts"
|
|
26
28
|
import { decodeModel } from "./model-file.ts"
|
|
27
29
|
import { disposeGeometry } from "./geometry-gpu.ts"
|
|
@@ -144,6 +146,20 @@ export type ModelSkinNodes = {
|
|
|
144
146
|
jointBounds: Float32Array
|
|
145
147
|
}
|
|
146
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Transcode the model's compressed images (KTX2) into this device's block
|
|
151
|
+
* format, off the JS thread (all are started at once; the runtime runs a
|
|
152
|
+
* few at a time), and return the data with them as `textures`; the images that are PNG or JPEG are left for
|
|
153
|
+
* createModel to decode. The step between a parse (or decodeModel) and
|
|
154
|
+
* createModel for data that may hold KTX2 images; loadModel and loadGltf
|
|
155
|
+
* run it. Data with no KTX2 image comes back as it is.
|
|
156
|
+
*/
|
|
157
|
+
export async function transcodeModelImages(data: ModelData): Promise<ModelData> {
|
|
158
|
+
if (!data.images.some((image) => isKtx2(image.bytes))) return data
|
|
159
|
+
let textures = await Promise.all(data.images.map((image) => (isKtx2(image.bytes) ? transcodeTexture(image.bytes) : undefined)))
|
|
160
|
+
return { ...data, textures }
|
|
161
|
+
}
|
|
162
|
+
|
|
147
163
|
/**
|
|
148
164
|
* Build the scene object for parsed model data: upload its images (repeat
|
|
149
165
|
* wrap, mipmapped, MODEL_ANISOTROPY; the base color and emissive images
|
|
@@ -151,24 +167,26 @@ export type ModelSkinNodes = {
|
|
|
151
167
|
* rgba8), make a material per glTF material, the file's node hierarchy as
|
|
152
168
|
* nested Groups, and a mesh per part under its node. Synchronous - the
|
|
153
169
|
* data is already in memory.
|
|
170
|
+
*
|
|
171
|
+
* A compressed image uploads as transcodeModelImages left it: at the
|
|
172
|
+
* format and color space its file names, with the mip chain the file
|
|
173
|
+
* carries (none can be generated for compressed storage). Data holding a
|
|
174
|
+
* KTX2 image that was not transcoded throws.
|
|
154
175
|
*/
|
|
155
176
|
export function createModel(data: ModelData, opts: ModelOptions = {}): Model {
|
|
156
177
|
let label = opts.label
|
|
157
|
-
let
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
autoFree: false,
|
|
170
|
-
label: label ? label + "-image" + i : undefined,
|
|
171
|
-
})
|
|
178
|
+
let uses = modelImageUses(data)
|
|
179
|
+
let textures: TextureId[] = data.images.map((image, i) => {
|
|
180
|
+
let sampling = { wrap: "repeat" as const, anisotropy: MODEL_ANISOTROPY, autoFree: false, label: label ? label + "-" + image.name : image.name }
|
|
181
|
+
let ready = data.textures?.[i]
|
|
182
|
+
if (ready !== undefined) {
|
|
183
|
+
return createTexture(ready.data, ready.width, ready.height, { ...sampling, format: ready.format, mipmap: ready.mipmap })
|
|
184
|
+
}
|
|
185
|
+
if (isKtx2(image.bytes)) {
|
|
186
|
+
throw new Error("createModel: image '" + image.name + "' is a KTX2 compressed texture, which is transcoded before the model is created: await transcodeModelImages(data) first (loadModel and loadGltf do)")
|
|
187
|
+
}
|
|
188
|
+
let decoded = decodeImage(image.bytes)
|
|
189
|
+
return createTexture(decoded.data, decoded.width, decoded.height, { ...sampling, format: uses[i]!.srgb ? "rgba8-srgb" : "rgba8", mipmap: true })
|
|
172
190
|
})
|
|
173
191
|
let make = opts.material ?? ((m: ModelMaterial, maps: ModelMaps, skinned: boolean, vertexColors: boolean, morphed: boolean, instanced: boolean): Material => {
|
|
174
192
|
// An emissive factor of zero is emission OFF (the glTF product rule:
|
|
@@ -434,7 +452,8 @@ export async function loadGltf(path: string, opts?: ModelOptions): Promise<Model
|
|
|
434
452
|
for (let uri of gltfExternalUris(bytes)) {
|
|
435
453
|
if (!files.has(uri)) files.set(uri, await file(dir + decodeURIComponent(uri)).bytes())
|
|
436
454
|
}
|
|
437
|
-
|
|
455
|
+
let data = await transcodeModelImages(parseGltf(bytes, (uri) => files.get(uri)!))
|
|
456
|
+
return buildOwned(owner, opts, () => createModel(data, opts))
|
|
438
457
|
}
|
|
439
458
|
|
|
440
459
|
/** Read a baked .srtm model (`srt tool 3d/model`) and build it: no parsing,
|
|
@@ -442,5 +461,6 @@ export async function loadGltf(path: string, opts?: ModelOptions): Promise<Model
|
|
|
442
461
|
export async function loadModel(path: string, opts?: ModelOptions): Promise<Model> {
|
|
443
462
|
let owner = getOwner()
|
|
444
463
|
let bytes = await file(path).bytes()
|
|
445
|
-
|
|
464
|
+
let data = await transcodeModelImages(decodeModel(bytes))
|
|
465
|
+
return buildOwned(owner, opts, () => createModel(data, opts))
|
|
446
466
|
}
|
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
|
|
@@ -270,6 +273,11 @@ export type SceneEventBase = {
|
|
|
270
273
|
* (a view's own pixels under a view leaf). */
|
|
271
274
|
x: number
|
|
272
275
|
y: number
|
|
276
|
+
/** When the event happened, ms: the element event's `timeStamp`. */
|
|
277
|
+
timeStamp: number
|
|
278
|
+
/** The element event's `predicted`: a move the pointer was never
|
|
279
|
+
* reported at. Draw with it, do not measure with it. */
|
|
280
|
+
predicted: boolean
|
|
273
281
|
pointerId: number
|
|
274
282
|
pointerType: string
|
|
275
283
|
button?: number
|
package/src/scene-pointer.ts
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
// dispatch.ts is the same model one dimension down.
|
|
20
20
|
//
|
|
21
21
|
// Pure BY DESIGN (types only, nothing with GPU imports) so
|
|
22
|
-
//
|
|
22
|
+
// tests/dispatch.test.ts drives it headless with a fake pick.
|
|
23
23
|
|
|
24
24
|
import type { PointerEvent as ElementPointerEvent, PointerFeed } from "@solidrt/core"
|
|
25
25
|
import type { NodePointerEvent, SceneNode, ScenePointerListener } from "./node.ts"
|
|
@@ -58,6 +58,8 @@ type InternalEvent = {
|
|
|
58
58
|
distance: number | null
|
|
59
59
|
x: number
|
|
60
60
|
y: number
|
|
61
|
+
timeStamp: number
|
|
62
|
+
predicted: boolean
|
|
61
63
|
pointerId: number
|
|
62
64
|
pointerType: string
|
|
63
65
|
button?: number
|
|
@@ -95,8 +97,6 @@ export type PointerInputDeps = {
|
|
|
95
97
|
root: Scene | ViewHandle
|
|
96
98
|
/** The root's listeners, in registration order. */
|
|
97
99
|
listeners: Set<ScenePointerListener>
|
|
98
|
-
/** The clock for tap repeats (default performance.now; checks inject). */
|
|
99
|
-
now?: () => number
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
export type PointerInput = {
|
|
@@ -108,7 +108,6 @@ export type PointerInput = {
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
export function makePointerInput(deps: PointerInputDeps): PointerInput {
|
|
111
|
-
let now = deps.now ?? (() => performance.now())
|
|
112
111
|
let presses = new Map<number, Press>()
|
|
113
112
|
let hover = new Map<number, Target>()
|
|
114
113
|
let lastTap: { time: number; cx: number; cy: number; target: Target | null; count: number } | null = null
|
|
@@ -170,6 +169,8 @@ export function makePointerInput(deps: PointerInputDeps): PointerInput {
|
|
|
170
169
|
distance: hit === null ? null : hit.distance,
|
|
171
170
|
x,
|
|
172
171
|
y,
|
|
172
|
+
timeStamp: e.timeStamp,
|
|
173
|
+
predicted: e.predicted,
|
|
173
174
|
pointerId: e.pointerId,
|
|
174
175
|
pointerType: e.pointerType,
|
|
175
176
|
button: e.button,
|
|
@@ -245,7 +246,7 @@ export function makePointerInput(deps: PointerInputDeps): PointerInput {
|
|
|
245
246
|
if (press.moved || !press.alone) return
|
|
246
247
|
let release = nearest(x, y)
|
|
247
248
|
if (press.target === null ? release !== null : release === null || !sameHit(release, press.target)) return
|
|
248
|
-
let time =
|
|
249
|
+
let time = e.timeStamp
|
|
249
250
|
let repeat = lastTap !== null && time - lastTap.time <= TAP_INTERVAL_MS && sameTarget(lastTap.target, press.target) && Math.hypot(e.clientX - lastTap.cx, e.clientY - lastTap.cy) <= TAP_REPEAT_SLOP
|
|
250
251
|
let count = repeat ? lastTap!.count + 1 : 1
|
|
251
252
|
lastTap = { time, cx: e.clientX, cy: e.clientY, target: press.target, count }
|
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 })
|