@solidrt/3d 0.0.65 → 0.0.67
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 +29 -29
- package/examples/README.md +4 -4
- package/examples/cascades.tsx +1 -1
- package/examples/collision.tsx +1 -1
- package/examples/exits.tsx +1 -1
- package/examples/first-person.tsx +1 -1
- package/examples/fleet.tsx +1 -1
- package/examples/fog.tsx +1 -1
- package/examples/lod.tsx +1 -1
- package/examples/model-load.tsx +2 -2
- package/examples/model.tsx +1 -1
- package/examples/morph.tsx +1 -1
- package/examples/normals.tsx +1 -1
- package/examples/pick.tsx +1 -1
- package/examples/wireframe.tsx +1 -1
- package/package.json +3 -3
- package/src/components/node-props.ts +6 -1
- package/src/components/scene.tsx +3 -0
- package/src/components/view3d.tsx +2 -0
- package/src/environment-bake.ts +8 -8
- package/src/environment.ts +6 -6
- package/src/glsl.ts +1 -1
- package/src/mixer.ts +1 -1
- package/src/model-data.ts +3 -3
- package/src/model-file.ts +9 -9
- package/src/model.ts +4 -4
- package/src/node.ts +4 -2
- package/src/scene-pointer.ts +14 -1
- package/src/scene.ts +2 -1
- package/src/splat-data.ts +13 -13
- package/src/splat.ts +3 -3
- package/tools/environment.ts +4 -4
- package/tools/model.flux.ts +4 -4
- package/tools/node-env.d.ts +1 -1
- package/tools/splat.ts +4 -4
package/AGENTS.md
CHANGED
|
@@ -217,7 +217,7 @@ displacement - and `cullMargin` (world units, Godot's
|
|
|
217
217
|
`extra_cull_margin`) for bounded displacement such as wind. Sprites
|
|
218
218
|
cull by their quad's reach at any facing. A SKINNED part is culled by
|
|
219
219
|
the union of its joints' boxes (the bake computes each joint's
|
|
220
|
-
influence box in joint space, `ModelSkin.jointBounds`, .
|
|
220
|
+
influence box in joint space, `ModelSkin.jointBounds`, .sol3m VERSION
|
|
221
221
|
5; the joint nodes carry them as culling-only bounds, outside the
|
|
222
222
|
picking index), so the box follows the pose with no per-frame JS -
|
|
223
223
|
Unity's bone bounds, Godot's per-bone AABBs; no `updateWhenOffscreen`
|
|
@@ -409,7 +409,7 @@ that pop in and shrink away on tap, the shelf cascading out and back
|
|
|
409
409
|
through `<Show>`. Each natural settle calls the node's `onTransitionEnd`
|
|
410
410
|
(plain field like the pointer handlers) with `{ component }` - never on
|
|
411
411
|
a cancel, snap, leave or exit; the raw "spatialTransitionEnd" engine
|
|
412
|
-
event (
|
|
412
|
+
event (sol:events, carrying the CORE node id `_node`) stays for
|
|
413
413
|
flux:spatial consumers.
|
|
414
414
|
|
|
415
415
|
### Geometry layout, indices and topology
|
|
@@ -495,7 +495,7 @@ so the mesh picks and culls where it is now drawn (the box follows at
|
|
|
495
495
|
the next flush, the triangle index is rebuilt by the next query - a
|
|
496
496
|
deforming mesh needs no re-attach). transformGeometry and
|
|
497
497
|
the edge builders share extra streams with their source; merge
|
|
498
|
-
concatenates every stream; the `.
|
|
498
|
+
concatenates every stream; the `.sol3m` container refuses streams (run
|
|
499
499
|
time data). `setDrawRange(mesh, first, count?)` draws indices `[first,
|
|
500
500
|
first + count)` of the mesh's geometry (Three's `setDrawRange`, on the
|
|
501
501
|
mesh because the scene owns the entries), applied to every entry of the
|
|
@@ -550,12 +550,12 @@ are Solid-free and
|
|
|
550
550
|
GPU-free BY DESIGN so they can be checked headless and run under bun:
|
|
551
551
|
`src/model-data.ts` re-exports exactly this set as the published
|
|
552
552
|
`@solidrt/3d/model` entry (the bake tool and app bake scripts), and
|
|
553
|
-
`tests/model-data.test.ts` imports it under `
|
|
554
|
-
binary, so a gui or `
|
|
553
|
+
`tests/model-data.test.ts` imports it under `sol test`, on the bare flux
|
|
554
|
+
binary, so a gui or `sol:` import creeping into any of them fails there
|
|
555
555
|
(a headless `flux:` import would not); keep them that way.
|
|
556
556
|
`src/splat-data.ts` is its own published entry the same way
|
|
557
557
|
(`@solidrt/3d/splat`, guarded by `tests/splat-data.test.ts`).
|
|
558
|
-
The tests under `tests/` run with `bun run
|
|
558
|
+
The tests under `tests/` run with `bun run sol test packages/3d` from the
|
|
559
559
|
repo root, or one file, or `--filter <text>` for the tests whose name
|
|
560
560
|
contains it: the pure modules on the bare flux binary, and the files
|
|
561
561
|
that need a GPU (`.test.tsx`, importing `@solidrt/test`) on the
|
|
@@ -1664,10 +1664,10 @@ specular, `envIrradiance` - the fully rough sample along the normal, as
|
|
|
1664
1664
|
Three's getIBLIrradiance and Godot read it - added to the hemisphere
|
|
1665
1665
|
for the diffuse) and every `phong({ reflectivity })` material mirrors,
|
|
1666
1666
|
typically the skybox's own cube turned with it. The cube to use is a
|
|
1667
|
-
BAKED one: `bun run
|
|
1667
|
+
BAKED one: `bun run sol tool 3d/environment sky.hdr -o assets/sky.sol3e`
|
|
1668
1668
|
turns an equirectangular Radiance .hdr (Poly Haven's are CC0) into the
|
|
1669
1669
|
six faces plus the GGX-prefiltered mip chain in linear float, and
|
|
1670
|
-
`await loadEnvironment("assets/sky.
|
|
1670
|
+
`await loadEnvironment("assets/sky.sol3e")` uploads it as an explicit
|
|
1671
1671
|
"rgba16f" chain (createCubeTexture's array-of-levels form: no generated
|
|
1672
1672
|
mipmaps, so no half-float render support needed - it works on every
|
|
1673
1673
|
device; created after an await, so not auto-freed). Unity convolves at
|
|
@@ -1682,7 +1682,7 @@ is a coarse average - fine for a sky gradient, wrong for a photograph.
|
|
|
1682
1682
|
A 128 environment (2 MiB, the default) lights any surface; for a
|
|
1683
1683
|
mirror-finish showpiece bake at 256, and for a crisp backdrop pair it
|
|
1684
1684
|
with a separate hi-res LDR skybox (a 2k panorama through equirectToCube)
|
|
1685
|
-
as `background` while the .
|
|
1685
|
+
as `background` while the .sol3e stays the `environment`. Scene-level like Three's
|
|
1686
1686
|
`scene.environment`, Unity's environment reflections and Godot's
|
|
1687
1687
|
sky-lit reflections: ONE `uEnv` samplerCube bound on every target the
|
|
1688
1688
|
scene draws into (a 1x1 black placeholder while unset) and one
|
|
@@ -1750,7 +1750,7 @@ one), then the PREFILTER: the faces convolved on the GPU into a second,
|
|
|
1750
1750
|
`mipmap: true` cube target level by level (`renderTarget(chain, face,
|
|
1751
1751
|
level)`, one small pass each - 48 at 128 - the bake tool's GGX
|
|
1752
1752
|
importance sampling as a fragment, `createPrefilter` in environment.ts,
|
|
1753
|
-
with the same roughness-to-level rule as a .
|
|
1753
|
+
with the same roughness-to-level rule as a .sol3e chain), so `standard`
|
|
1754
1754
|
blurs a probe by roughness exactly like a baked environment; `prefilter:
|
|
1755
1755
|
false` skips it and hands out the sharp faces (Three's CubeCamera: a
|
|
1756
1756
|
mirror at every roughness). COST: the chain's passes are tiny, but a
|
|
@@ -1808,7 +1808,7 @@ clamp (`repeat` also wraps vertically and bleeds the poles). An HDR
|
|
|
1808
1808
|
panorama uploaded as rgba16f converts into a half-float cube (sharp:
|
|
1809
1809
|
a skybox, or `mipmap: true` for the box chain); its PREFILTERED form is
|
|
1810
1810
|
the bake tool above, whose CPU pipeline (`src/environment-bake.ts`:
|
|
1811
|
-
decodeHdr, panoramaToCube, prefilterCube, the .
|
|
1811
|
+
decodeHdr, panoramaToCube, prefilterCube, the .sol3e encode/decode) is
|
|
1812
1812
|
pure TypeScript and bun-tested - there is no runtime .hdr decoder.
|
|
1813
1813
|
|
|
1814
1814
|
### Fog
|
|
@@ -2324,8 +2324,8 @@ for the loaders that scope is the caller's at the call, captured before
|
|
|
2324
2324
|
the await, so a model loaded inside a memo goes with the memo's rerun
|
|
2325
2325
|
and one loaded in a component with its unmount. A load that settles
|
|
2326
2326
|
after its scope is gone (unmounted mid-load) is freed on arrival and
|
|
2327
|
-
resolves already disposed - nothing owns it any more. `loadModel` reads the baked `.
|
|
2328
|
-
3d/model <in.gltf|glb> -o assets/<name>.
|
|
2327
|
+
resolves already disposed - nothing owns it any more. `loadModel` reads the baked `.sol3m` written by `sol tool
|
|
2328
|
+
3d/model <in.gltf|glb> -o assets/<name>.sol3m`: the same parse run once
|
|
2329
2329
|
at build time, stored in the GPU layout, so loading is views onto the
|
|
2330
2330
|
file's bytes plus the image decodes. Numbers from a 32k-vertex, 6-texture model
|
|
2331
2331
|
on a release client: `parseGltf` 124 ms on flux (22 ms under bun) against
|
|
@@ -2336,7 +2336,7 @@ models and a binary import (`import bytes from "./x.glb" with { type:
|
|
|
2336
2336
|
|
|
2337
2337
|
#### Compressed textures
|
|
2338
2338
|
|
|
2339
|
-
`
|
|
2339
|
+
`sol tool 3d/model <in> -o assets/<name>.sol3m` bakes the images
|
|
2340
2340
|
too: each becomes a KTX2 compressed texture with its mip chain. The file is the
|
|
2341
2341
|
same for every platform; at load each device turns the images into the
|
|
2342
2342
|
block format its GPU samples (BC7 on desktops, ETC2 elsewhere), which
|
|
@@ -2456,12 +2456,12 @@ import type { ModelData } from "@solidrt/3d/model"
|
|
|
2456
2456
|
// takes `placements`), materials as glTF-shaped records, app data in
|
|
2457
2457
|
// `extras` (JSON) and `blobs` (named bytes, each an aligned block).
|
|
2458
2458
|
let data: ModelData = { nodes, parts, skins: [], clips: [], materials, images: [], bounds, extras: { spawn }, blobs: { grid } }
|
|
2459
|
-
await Bun.write("assets/level.
|
|
2459
|
+
await Bun.write("assets/level.sol3m", encodeModel(data))
|
|
2460
2460
|
```
|
|
2461
2461
|
|
|
2462
|
-
`loadModel("assets/level.
|
|
2462
|
+
`loadModel("assets/level.sol3m")` then builds it like any authored model;
|
|
2463
2463
|
`model.extras` and `model.blobs` hand the app data back (blobs are views
|
|
2464
|
-
onto the loaded bytes). `
|
|
2464
|
+
onto the loaded bytes). `sol tool 3d/model` is the same entry applied
|
|
2465
2465
|
to a glTF, so the two bakes share one code path.
|
|
2466
2466
|
|
|
2467
2467
|
### Async loading
|
|
@@ -2479,7 +2479,7 @@ PENDING_ASYNC_UNTRACKED_READ, and any element the component builds before
|
|
|
2479
2479
|
the suspending read is orphaned on the boundary's retry and never freed
|
|
2480
2480
|
(the dev leak sentinel reports it) - so the suspending component creates
|
|
2481
2481
|
no elements of its own. Async here means the file read: the parse and
|
|
2482
|
-
createModel run synchronously on main. Bake anything big to .
|
|
2482
|
+
createModel run synchronously on main. Bake anything big to .sol3m; when
|
|
2483
2483
|
a source glTF must be parsed at runtime, do the parse in an isolate
|
|
2484
2484
|
(parseGltf's result is plain data and copies across) and keep
|
|
2485
2485
|
createModel on main.
|
|
@@ -2552,10 +2552,10 @@ rigged one; linear and premultiplied as glTF stores it) and the default
|
|
|
2552
2552
|
material takes `vertexColors: true` for such parts - the callback's
|
|
2553
2553
|
fourth argument says so, and a material shared by painted and unpainted
|
|
2554
2554
|
parts is made once per variant, like the skinned split.
|
|
2555
|
-
A `.
|
|
2555
|
+
A `.sol3m` baked before the material records carried the PBR fields
|
|
2556
2556
|
(file version 3), or before the vertex formats took the WebGPU spelling
|
|
2557
2557
|
and byte counts (file version 5), is rejected by loadModel - re-bake
|
|
2558
|
-
with `
|
|
2558
|
+
with `sol tool 3d/model`. A baked part keeps a quantized export's
|
|
2559
2559
|
bytes: u8 colors, u8/u16 joints and normalized weights, u16 uvs land in
|
|
2560
2560
|
their own formats, positions and normals as floats.
|
|
2561
2561
|
|
|
@@ -2695,10 +2695,10 @@ mesh (JS-written records, no nodes) cannot morph: rejected at add().
|
|
|
2695
2695
|
|
|
2696
2696
|
Not in the subset, dropped: tangents and further UV sets; samplers are
|
|
2697
2697
|
ignored (every texture repeats); additive blending draws as base color.
|
|
2698
|
-
The follow-ups are filed in okf/backlog/3d-model-loader.md. The `.
|
|
2698
|
+
The follow-ups are filed in okf/backlog/3d-model-loader.md. The `.sol3m`
|
|
2699
2699
|
container is VERSION 11 (node table in the header, node-local vertices,
|
|
2700
2700
|
skins, clips, packed morph targets, app data, images under their
|
|
2701
|
-
names); older bakes are rejected - re-bake with `
|
|
2701
|
+
names); older bakes are rejected - re-bake with `sol tool 3d/model`.
|
|
2702
2702
|
|
|
2703
2703
|
## Splats
|
|
2704
2704
|
|
|
@@ -2715,12 +2715,12 @@ successors) as ordinary content, the model-loading split repeated:
|
|
|
2715
2715
|
times opacity, so the first n are the scene at n), y-down captures
|
|
2716
2716
|
(.ply, .splat) stood up to y-up (positions and covariances rotated
|
|
2717
2717
|
together; `keepOrientation` opts out, `.spz` is y-up already), bounds
|
|
2718
|
-
measured. `encodeSplat`/`decodeSplat` round-trip the `.
|
|
2718
|
+
measured. `encodeSplat`/`decodeSplat` round-trip the `.sol3s` container
|
|
2719
2719
|
(the record and SH blocks, next bullet); decode is a header parse
|
|
2720
|
-
plus byte VIEWS, nothing per-splat. `
|
|
2721
|
-
out.
|
|
2720
|
+
plus byte VIEWS, nothing per-splat. `sol tool 3d/splat <in> [-o
|
|
2721
|
+
out.sol3s] [--sh 0..3] [--keep-orientation]` is the parse and bake
|
|
2722
2722
|
under bun (tools/splat.ts); an app bake script uses the same entry.
|
|
2723
|
-
- The `.
|
|
2723
|
+
- The `.sol3s` record (version 2) is 32 bytes = two rgba32ui texels:
|
|
2724
2724
|
center float32x3 + sRGB color and opacity unorm8x4, then the
|
|
2725
2725
|
covariance upper triangle as six float16 + a spare word
|
|
2726
2726
|
(SPLAT_ATTRIBUTES as a byte layout, SPLAT_RECORD_TEXELS). `--sh 1..3`
|
|
@@ -2731,7 +2731,7 @@ successors) as ordinary content, the model-loading split repeated:
|
|
|
2731
2731
|
rgb interleaved), 32/48/96 bytes per splat of texture, so the
|
|
2732
2732
|
default is 0 and a bake opts in. The y-up flip negates the
|
|
2733
2733
|
coefficients whose basis is odd in (y, z).
|
|
2734
|
-
- On the runtime, `loadSplat(path)` reads a `.
|
|
2734
|
+
- On the runtime, `loadSplat(path)` reads a `.sol3s` (fetch + decode,
|
|
2735
2735
|
the loadModel shape) and `createSplatMesh(data, { count?, material?,
|
|
2736
2736
|
label? })` / `<SplatMesh>` shows it: a RecordMesh in the INDEXED form
|
|
2737
2737
|
(see "Instance order") - the records handed to the engine as the
|
|
@@ -2828,7 +2828,7 @@ successors) as ordinary content, the model-loading split repeated:
|
|
|
2828
2828
|
ambient probe, this does not). glTF's default metallic factor is 1,
|
|
2829
2829
|
so an untextured asset is all metal, and createModel's default is
|
|
2830
2830
|
`standard`: give a model scene an `environment` (loadEnvironment's
|
|
2831
|
-
baked .
|
|
2831
|
+
baked .sol3e, or the skybox's cube), or pass `phong` as the material.
|
|
2832
2832
|
- Light intensities are the same numbers for `phong` and `standard`: 1
|
|
2833
2833
|
lights a white matte surface to 1 face-on. A Three scene's intensities
|
|
2834
2834
|
are a factor pi larger for the same look; divide when porting.
|
|
@@ -2901,7 +2901,7 @@ successors) as ordinary content, the model-loading split repeated:
|
|
|
2901
2901
|
- A generated cube chain (`mipmap: true` from six faces) is a box
|
|
2902
2902
|
filter, not the GGX convolution the roughness-to-level rule assumes:
|
|
2903
2903
|
rough reflections read too sharp and the diffuse `envIrradiance` is a
|
|
2904
|
-
4x4 average. Bake with `
|
|
2904
|
+
4x4 average. Bake with `sol tool 3d/environment` for anything
|
|
2905
2905
|
photographed; the JS sky gradients in the examples get away with it.
|
|
2906
2906
|
- The background covers the whole target with depth off, drawn first: it
|
|
2907
2907
|
REPLACES the clearColor visually (the clear still runs; you just never
|
package/examples/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @solidrt/3d examples
|
|
2
2
|
|
|
3
|
-
One concept per file; run with `bun run
|
|
3
|
+
One concept per file; run with `bun run sol run <file>` from an app that
|
|
4
4
|
depends on `@solidrt/3d` (or in-repo from the package directory).
|
|
5
5
|
|
|
6
6
|
- `scene-basic.tsx` - the whole v1 surface: a `<Scene>` composited as a
|
|
@@ -57,8 +57,8 @@ depends on `@solidrt/3d` (or in-repo from the package directory).
|
|
|
57
57
|
the environment as tinted reflections, the dielectric row the same
|
|
58
58
|
sky as a faint face-on gloss. Drag to look around.
|
|
59
59
|
- `environment.tsx` - a baked HDR environment lighting the scene ALONE
|
|
60
|
-
(no lights, no hemisphere): `loadEnvironment` reads the .
|
|
61
|
-
`bun run
|
|
60
|
+
(no lights, no hemisphere): `loadEnvironment` reads the .sol3e that
|
|
61
|
+
`bun run sol tool 3d/environment <panorama>.hdr -o assets/environment.sol3e`
|
|
62
62
|
bakes from any equirectangular .hdr (Poly Haven's are CC0; the asset
|
|
63
63
|
is not committed, bake one first), used as background and environment
|
|
64
64
|
with ACES tone mapping. A metal row and a red dielectric row, roughness
|
|
@@ -200,4 +200,4 @@ depends on `@solidrt/3d` (or in-repo from the package directory).
|
|
|
200
200
|
stays above the boundary - the two rules that avoid
|
|
201
201
|
PENDING_ASYNC_UNTRACKED_READ and the suspend-retry element leak. Needs
|
|
202
202
|
the asset baked into the running app:
|
|
203
|
-
`bun run
|
|
203
|
+
`bun run sol tool 3d/model examples/model.glb -o assets/model.sol3m`.
|
package/examples/cascades.tsx
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
// plane, so the far pillars sink into the horizon instead of clipping
|
|
23
23
|
// (`examples/fog.tsx` is the fog tour).
|
|
24
24
|
import { createSignal, flush, onFrame, pct, render } from "@solidrt/core"
|
|
25
|
-
import { registerDebug } from "
|
|
25
|
+
import { registerDebug } from "sol:dev"
|
|
26
26
|
import { box, DirectionalLight, HemisphereLight, phong, Mesh, PerspectiveCamera, plane, Scene, sphere } from "@solidrt/3d"
|
|
27
27
|
import type { Geometry, Vec3 } from "@solidrt/3d"
|
|
28
28
|
|
package/examples/collision.tsx
CHANGED
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
sphere,
|
|
31
31
|
} from "@solidrt/3d"
|
|
32
32
|
import type { FirstPersonCameraHandle, MeshNode, MoveResult, SceneHandle, Vec3, Volume } from "@solidrt/3d"
|
|
33
|
-
import { registerDebug } from "
|
|
33
|
+
import { registerDebug } from "sol:dev"
|
|
34
34
|
|
|
35
35
|
// Layer bit the level geometry adds to its drawn layer: what every
|
|
36
36
|
// collision query selects, so pickups (drawn only) never block the walker.
|
package/examples/exits.tsx
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import { createSignal, For, pct, render, Show } from "@solidrt/core"
|
|
15
15
|
import { box, Group, Mesh, PerspectiveCamera, plane, Scene, unlit } from "@solidrt/3d"
|
|
16
16
|
import type { NodeTransition, SceneHandle, SceneTapEvent, Vec3 } from "@solidrt/3d"
|
|
17
|
-
import { registerDebug } from "
|
|
17
|
+
import { registerDebug } from "sol:dev"
|
|
18
18
|
|
|
19
19
|
const COUNT = 5
|
|
20
20
|
// Crate spacing along x, and the pause before a tapped-away crate is
|
|
@@ -13,7 +13,7 @@ import { createInputMap, createPointerFeed, createSignal, gamepad, keyboard, loc
|
|
|
13
13
|
import type { PointerEvent } from "@solidrt/core"
|
|
14
14
|
import { box, cylinder, DirectionalLight, FirstPersonCamera, firstPersonActions, firstPersonBindings, HemisphereLight, phong, Mesh, PerspectiveCamera, plane, Scene } from "@solidrt/3d"
|
|
15
15
|
import type { FirstPersonCameraHandle, Vec3 } from "@solidrt/3d"
|
|
16
|
-
import { registerDebug } from "
|
|
16
|
+
import { registerDebug } from "sol:dev"
|
|
17
17
|
|
|
18
18
|
// Half extent of the walkable courtyard in world units; the walls stand
|
|
19
19
|
// just outside it.
|
package/examples/fleet.tsx
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import { createInputMap, createPointerFeed, createSignal, flush, For, gamepad, pct, render } from "@solidrt/core"
|
|
19
19
|
import { box, DirectionalLight, HemisphereLight, Instance, InstancedMesh, phong, Mesh, OrbitCamera, orbitActions, orbitBindings, plane, Scene, worldPosition } from "@solidrt/3d"
|
|
20
20
|
import type { InstanceNode, NodeTransition, SceneHandle, Vec3 } from "@solidrt/3d"
|
|
21
|
-
import { registerDebug } from "
|
|
21
|
+
import { registerDebug } from "sol:dev"
|
|
22
22
|
|
|
23
23
|
type Tint = [number, number, number, number]
|
|
24
24
|
|
package/examples/fog.tsx
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
// repeats; `suns` is the two suns' world positions and `project`
|
|
20
20
|
// (`{ point }`) is where a world point is on screen, for a pixel probe.
|
|
21
21
|
import { createSignal, flush, onFrame, pct, render } from "@solidrt/core"
|
|
22
|
-
import { registerDebug } from "
|
|
22
|
+
import { registerDebug } from "sol:dev"
|
|
23
23
|
import { cone, cylinder, DirectionalLight, HemisphereLight, phong, Mesh, PerspectiveCamera, plane, Scene, sphere, unlit } from "@solidrt/3d"
|
|
24
24
|
import type { FogOptions, SceneHandle, Vec3 } from "@solidrt/3d"
|
|
25
25
|
|
package/examples/lod.tsx
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
import { onFrame, pct, render } from "@solidrt/core"
|
|
22
22
|
import { addInstance, cone, cylinder, DirectionalLight, HemisphereLight, icosahedron, InstancedLod, phong, Lod, Mesh, mergeGeometries, plane, Scene, transformGeometry, useScene } from "@solidrt/3d"
|
|
23
23
|
import type { InstancedMeshNode } from "@solidrt/3d"
|
|
24
|
-
import { registerDebug } from "
|
|
24
|
+
import { registerDebug } from "sol:dev"
|
|
25
25
|
|
|
26
26
|
const TREES = 1000
|
|
27
27
|
// Half the side of the square field the trees scatter over.
|
package/examples/model-load.tsx
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// bundle - the shape every app with real content ends up needing. Bake
|
|
4
4
|
// the asset once and drop it in your app's assets tree:
|
|
5
5
|
//
|
|
6
|
-
// bun run
|
|
6
|
+
// bun run sol tool 3d/model examples/model.glb -o assets/model.sol3m
|
|
7
7
|
//
|
|
8
8
|
// (loadGltf("assets/model.glb") has the exact same async shape; the bake
|
|
9
9
|
// just removes the runtime parse.)
|
|
@@ -51,7 +51,7 @@ function Rover(props: { turn: () => number }) {
|
|
|
51
51
|
// The async value: a memo returning a Promise. Its read below suspends
|
|
52
52
|
// until the file is read and the model built, then resumes under the
|
|
53
53
|
// <Loading> boundary in App.
|
|
54
|
-
let loaded = createMemo(() => loadModel("assets/model.
|
|
54
|
+
let loaded = createMemo(() => loadModel("assets/model.sol3m", { label: "rover" }))
|
|
55
55
|
|
|
56
56
|
// Everything derived from the model - framing, mounting - happens in
|
|
57
57
|
// this memo, AFTER the suspending read, so nothing here runs or builds
|
package/examples/model.tsx
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// with parseGltf and turned into a Group of meshes by createModel. The
|
|
5
5
|
// glb rides in the bundle as a binary import; a model under assets/ goes
|
|
6
6
|
// through loadGltf (same parser, read with flux:fs) or, baked once with
|
|
7
|
-
// `
|
|
7
|
+
// `sol tool 3d/model`, through loadModel (no parsing at all). Every part
|
|
8
8
|
// keeps its node name and is an ordinary mesh: pointer events, transforms
|
|
9
9
|
// and materials work per part. The materials are createModel's default
|
|
10
10
|
// `standard` from the file's PBR factors, so the scene carries an
|
package/examples/morph.tsx
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
// shadows morph too.
|
|
21
21
|
|
|
22
22
|
import { createSignal, onCleanup, onFrame, pct, render } from "@solidrt/core"
|
|
23
|
-
import { registerDebug } from "
|
|
23
|
+
import { registerDebug } from "sol:dev"
|
|
24
24
|
import { DirectionalLight, geometryAttribute, geometryVertexCount, getMorphNames, getMorphWeights, HemisphereLight, Instance, InstancedMesh, phong, Mesh, PerspectiveCamera, plane, Scene, setMorphWeights, sphere, withMorphTargets } from "@solidrt/3d"
|
|
25
25
|
import type { InstanceNode, MeshNode } from "@solidrt/3d"
|
|
26
26
|
|
package/examples/normals.tsx
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
// `helpers` debug command ({ on?: boolean }) toggles them.
|
|
18
18
|
|
|
19
19
|
import { createSignal, onFrame, pct, render } from "@solidrt/core"
|
|
20
|
-
import { registerDebug } from "
|
|
20
|
+
import { registerDebug } from "sol:dev"
|
|
21
21
|
import { computeVertexNormals, cylinder, DirectionalLight, fillAttribute, geometryAttribute, geometryVertexCount, Group, HemisphereLight, phong, Mesh, normalsHelper, PerspectiveCamera, Scene, sphere, toNonIndexed, unlit, updateVertices, withNormals } from "@solidrt/3d"
|
|
22
22
|
|
|
23
23
|
// The crease angles in degrees: flat, the default, everything smoothed.
|
package/examples/pick.tsx
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
import { createInputMap, createPointerFeed, createSignal, displayScale, pct, render } from "@solidrt/core"
|
|
25
25
|
import { box, cone, getRotation, Group, Mesh, OrbitCamera, orbitActions, orbitBindings, PerspectiveCamera, plane, Scene, setMeshParams, setTransform, sphere, torus, unlit, useScene } from "@solidrt/3d"
|
|
26
26
|
import type { Geometry, MeshNode, NodePointerEvent, NodeTapEvent, NodeWheelEvent, OrbitCameraHandle, SceneHandle, Vec3 } from "@solidrt/3d"
|
|
27
|
-
import { registerDebug } from "
|
|
27
|
+
import { registerDebug } from "sol:dev"
|
|
28
28
|
|
|
29
29
|
type Color = [number, number, number]
|
|
30
30
|
|
package/examples/wireframe.tsx
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import { createSignal, onFrame, pct, render } from "@solidrt/core"
|
|
19
19
|
import { createTexture } from "@solidrt/core/gpu"
|
|
20
20
|
import type { TextureId } from "@solidrt/core/gpu"
|
|
21
|
-
import { registerDebug } from "
|
|
21
|
+
import { registerDebug } from "sol:dev"
|
|
22
22
|
import { add, axesHelper, box3Helper, createModel, DirectionalLight, edgesGeometry, equirectToCube, gridHelper, Group, Mesh, parseGltf, PerspectiveCamera, Scene, setGeometry, setMaterial, unlit, wireframeGeometry } from "@solidrt/3d"
|
|
23
23
|
import type { SceneNode } from "@solidrt/3d"
|
|
24
24
|
import modelBytes from "./model.glb" with { type: "binary" }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidrt/3d",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.67",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"funding": "https://github.com/sponsors/wellawaretech",
|
|
6
6
|
"author": "Antoine van Wel",
|
|
@@ -20,10 +20,10 @@
|
|
|
20
20
|
"AGENTS.md"
|
|
21
21
|
],
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@solidrt/test": "0.0.
|
|
23
|
+
"@solidrt/test": "0.0.67"
|
|
24
24
|
},
|
|
25
25
|
"peerDependencies": {
|
|
26
26
|
"@solidjs/signals": "2.0.0-rc.13",
|
|
27
|
-
"@solidrt/core": "0.0.
|
|
27
|
+
"@solidrt/core": "0.0.67"
|
|
28
28
|
}
|
|
29
29
|
}
|
|
@@ -50,6 +50,9 @@ export type BubblingPointerEventProps = {
|
|
|
50
50
|
onPointerDown?: (event: NodePointerEvent) => void
|
|
51
51
|
onPointerMove?: (event: NodePointerEvent) => void
|
|
52
52
|
onPointerUp?: (event: NodePointerEvent) => void
|
|
53
|
+
/** The pointer's other end: the system took it away (see core's
|
|
54
|
+
* onPointerCancel); bubbles like up, and the press it ends never taps. */
|
|
55
|
+
onPointerCancel?: (event: NodePointerEvent) => void
|
|
53
56
|
/** The wheel over the node: `deltaX`/`deltaY`. */
|
|
54
57
|
onWheel?: (event: NodeWheelEvent) => void
|
|
55
58
|
/** A press released on the node within the slop, alone for its whole
|
|
@@ -73,18 +76,20 @@ export function syncNode(node: SceneNode, props: TransformProps & PointerEventPr
|
|
|
73
76
|
props.onPointerDown,
|
|
74
77
|
props.onPointerMove,
|
|
75
78
|
props.onPointerUp,
|
|
79
|
+
props.onPointerCancel,
|
|
76
80
|
props.onPointerEnter,
|
|
77
81
|
props.onPointerLeave,
|
|
78
82
|
props.onWheel,
|
|
79
83
|
props.onTap,
|
|
80
84
|
props.onTransitionEnd,
|
|
81
85
|
] as const,
|
|
82
|
-
([position, rotation, quaternion, scale, visible, down, move, up, enter, leave, wheel, tap, end]) => {
|
|
86
|
+
([position, rotation, quaternion, scale, visible, down, move, up, cancel, enter, leave, wheel, tap, end]) => {
|
|
83
87
|
setTransform(node, { position, rotation, quaternion, scale })
|
|
84
88
|
setVisible(node, visible !== false)
|
|
85
89
|
node.onPointerDown = down
|
|
86
90
|
node.onPointerMove = move
|
|
87
91
|
node.onPointerUp = up
|
|
92
|
+
node.onPointerCancel = cancel
|
|
88
93
|
node.onPointerEnter = enter
|
|
89
94
|
node.onPointerLeave = leave
|
|
90
95
|
node.onWheel = wheel
|
package/src/components/scene.tsx
CHANGED
|
@@ -18,6 +18,7 @@ export type ScenePointerProps = {
|
|
|
18
18
|
onPointerDown?: (event: ScenePointerEvent) => void
|
|
19
19
|
onPointerMove?: (event: ScenePointerEvent) => void
|
|
20
20
|
onPointerUp?: (event: ScenePointerEvent) => void
|
|
21
|
+
onPointerCancel?: (event: ScenePointerEvent) => void
|
|
21
22
|
onWheel?: (event: SceneWheelEvent) => void
|
|
22
23
|
onTap?: (event: SceneTapEvent) => void
|
|
23
24
|
}
|
|
@@ -268,6 +269,7 @@ export let Scene: ParentComponent<SceneProps> = props => {
|
|
|
268
269
|
onPointerDown: e => props.onPointerDown?.(e),
|
|
269
270
|
onPointerMove: e => props.onPointerMove?.(e),
|
|
270
271
|
onPointerUp: e => props.onPointerUp?.(e),
|
|
272
|
+
onPointerCancel: e => props.onPointerCancel?.(e),
|
|
271
273
|
onWheel: e => props.onWheel?.(e),
|
|
272
274
|
onTap: e => props.onTap?.(e),
|
|
273
275
|
}),
|
|
@@ -331,6 +333,7 @@ export let Scene: ParentComponent<SceneProps> = props => {
|
|
|
331
333
|
onPointerDown={events ? sceneHandlers.onPointerDown : undefined}
|
|
332
334
|
onPointerMove={events ? sceneHandlers.onPointerMove : undefined}
|
|
333
335
|
onPointerUp={events ? sceneHandlers.onPointerUp : undefined}
|
|
336
|
+
onPointerCancel={events ? sceneHandlers.onPointerCancel : undefined}
|
|
334
337
|
onPointerLeave={events ? sceneHandlers.onPointerLeave : undefined}
|
|
335
338
|
onWheel={events ? sceneHandlers.onWheel : undefined}
|
|
336
339
|
/>
|
|
@@ -129,6 +129,7 @@ export let View3d: ParentComponent<View3dProps> = props => {
|
|
|
129
129
|
onPointerDown: e => props.onPointerDown?.(e),
|
|
130
130
|
onPointerMove: e => props.onPointerMove?.(e),
|
|
131
131
|
onPointerUp: e => props.onPointerUp?.(e),
|
|
132
|
+
onPointerCancel: e => props.onPointerCancel?.(e),
|
|
132
133
|
onWheel: e => props.onWheel?.(e),
|
|
133
134
|
onTap: e => props.onTap?.(e),
|
|
134
135
|
}),
|
|
@@ -159,6 +160,7 @@ export let View3d: ParentComponent<View3dProps> = props => {
|
|
|
159
160
|
onPointerDown={events ? view.handlers.onPointerDown : undefined}
|
|
160
161
|
onPointerMove={events ? view.handlers.onPointerMove : undefined}
|
|
161
162
|
onPointerUp={events ? view.handlers.onPointerUp : undefined}
|
|
163
|
+
onPointerCancel={events ? view.handlers.onPointerCancel : undefined}
|
|
162
164
|
onPointerLeave={events ? view.handlers.onPointerLeave : undefined}
|
|
163
165
|
onWheel={events ? view.handlers.onWheel : undefined}
|
|
164
166
|
/>
|
package/src/environment-bake.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
// Environment baking: the pure pipeline behind `
|
|
1
|
+
// Environment baking: the pure pipeline behind `sol tool 3d/environment`
|
|
2
2
|
// and its tests. A Radiance .hdr panorama is decoded to linear float, laid
|
|
3
3
|
// onto the six faces of a cube, convolved level by level with the GGX lobe
|
|
4
4
|
// `standard`'s split sum samples (Karis's importance sampling, the source
|
|
5
5
|
// lod picked from each sample's solid angle - the Filament and Unity
|
|
6
6
|
// method; Godot and Three convolve the same way at runtime), and packed
|
|
7
|
-
// into the .
|
|
7
|
+
// into the .sol3e container loadEnvironment uploads as an explicit rgba16f
|
|
8
8
|
// chain. Runs under bun and on the runtime alike: no GPU, no flux imports.
|
|
9
9
|
//
|
|
10
10
|
// Cube faces hold what a GL lookup of a world direction returns (GL's own
|
|
@@ -428,15 +428,15 @@ export function prefilterCube(base: CubeFaces, size: number): CubeFaces[] {
|
|
|
428
428
|
return levels
|
|
429
429
|
}
|
|
430
430
|
|
|
431
|
-
// The .
|
|
431
|
+
// The .sol3e container: "SOLE" u32 | version u32 | size u32 | levels u32,
|
|
432
432
|
// then the faces as float32 rgba, level-major, face-major - the exact
|
|
433
433
|
// buffers createCubeTexture's explicit chain takes, viewed in place.
|
|
434
|
-
/** "
|
|
435
|
-
const MAGIC =
|
|
434
|
+
/** "SOLE" read as a little-endian u32. */
|
|
435
|
+
const MAGIC = 0x454c4f53
|
|
436
436
|
const VERSION = 1
|
|
437
437
|
const HEADER_BYTES = 16
|
|
438
438
|
|
|
439
|
-
/** Serialize a prefiltered chain (prefilterCube's result) into the .
|
|
439
|
+
/** Serialize a prefiltered chain (prefilterCube's result) into the .sol3e container. */
|
|
440
440
|
export function encodeEnvironment(levels: CubeFaces[], size: number): Uint8Array {
|
|
441
441
|
if (levels.length !== mipLevels(size)) {
|
|
442
442
|
throw new Error("encodeEnvironment: a " + size + " chain has " + mipLevels(size) + " levels, got " + levels.length)
|
|
@@ -459,14 +459,14 @@ export function encodeEnvironment(levels: CubeFaces[], size: number): Uint8Array
|
|
|
459
459
|
return out
|
|
460
460
|
}
|
|
461
461
|
|
|
462
|
-
/** Read a .
|
|
462
|
+
/** Read a .sol3e container back into its size and chain, the faces viewing
|
|
463
463
|
* the bytes in place (copied only if the buffer is not float-aligned). */
|
|
464
464
|
export function decodeEnvironment(bytes: Uint8Array): { size: number; levels: CubeFaces[] } {
|
|
465
465
|
if (bytes.byteLength < HEADER_BYTES) throw new Error("decodeEnvironment: not an environment file (too short)")
|
|
466
466
|
let view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
|
|
467
467
|
if (view.getUint32(0, true) !== MAGIC) throw new Error("decodeEnvironment: not an environment file (bad magic)")
|
|
468
468
|
let version = view.getUint32(4, true)
|
|
469
|
-
if (version !== VERSION) throw new Error("decodeEnvironment: version " + version + ", expected " + VERSION + " - re-bake with
|
|
469
|
+
if (version !== VERSION) throw new Error("decodeEnvironment: version " + version + ", expected " + VERSION + " - re-bake with sol tool 3d/environment")
|
|
470
470
|
let size = view.getUint32(8, true)
|
|
471
471
|
let count = view.getUint32(12, true)
|
|
472
472
|
if (size < 1 || count !== mipLevels(size)) throw new Error("decodeEnvironment: a " + size + " chain has " + mipLevels(size) + " levels, file says " + count)
|
package/src/environment.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// Environment helpers: the baked HDR environment loader (the .
|
|
2
|
-
// `
|
|
1
|
+
// Environment helpers: the baked HDR environment loader (the .sol3e chain
|
|
2
|
+
// `sol tool 3d/environment` writes), the equirectangular-to-cube
|
|
3
3
|
// conversion a fetched LDR panorama needs at runtime, the GPU prefilter
|
|
4
4
|
// that turns a rendered cube (a reflection probe's, a baked sky's) into
|
|
5
5
|
// the roughness chain, and the placeholder cube a scene binds as uEnv
|
|
@@ -28,8 +28,8 @@ import type { CreateOptions, DrawId, ProgramId, RenderPipelineId, SamplerOptions
|
|
|
28
28
|
import { decodeEnvironment, levelRoughness, mipLevels } from "./environment-bake.ts"
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
|
-
* Read a baked environment (`
|
|
32
|
-
* assets/sky.
|
|
31
|
+
* Read a baked environment (`sol tool 3d/environment sky.hdr -o
|
|
32
|
+
* assets/sky.sol3e`) and upload it: the HDR cube with its GGX-prefiltered
|
|
33
33
|
* mip chain, as an explicit "rgba16f" chain - no generated mipmaps, so it
|
|
34
34
|
* works on every device. The id is what `environment={{ cube }}` and
|
|
35
35
|
* `background={{ cube }}` take (Three's `scene.environment` from an
|
|
@@ -147,7 +147,7 @@ const EQUIRECT_FACE = glsl`
|
|
|
147
147
|
* photographed sky uploaded as such, "rgba8" (default) for data,
|
|
148
148
|
* "rgba16f" for an HDR panorama (the cube keeps the range; renderable on
|
|
149
149
|
* every device with half-float rendering, else it throws - the prefiltered
|
|
150
|
-
* form of that panorama is `
|
|
150
|
+
* form of that panorama is `sol tool 3d/environment` plus
|
|
151
151
|
* loadEnvironment). The panorama's center column faces -Z and its top
|
|
152
152
|
* row is +Y. Leave its wrap at the default clamp: `repeat` would also
|
|
153
153
|
* wrap vertically and bleed the poles across the top and bottom rows,
|
|
@@ -280,7 +280,7 @@ export function bufferFormat(): BufferFormat {
|
|
|
280
280
|
* `source`, a `mipmap: true` cube of the same size and format (a cube
|
|
281
281
|
* draw target rendered sharp), one pass per face per level - 48 small
|
|
282
282
|
* passes at 128. Three's PMREMGenerator, Unity's and Godot's probe
|
|
283
|
-
* convolution, on the same roughness-to-level rule as the .
|
|
283
|
+
* convolution, on the same roughness-to-level rule as the .sol3e chain
|
|
284
284
|
* (levelRoughness), so `standard` reads both alike.
|
|
285
285
|
*/
|
|
286
286
|
export function createPrefilter(size: number, source: TextureId, format: BufferFormat, label: string): Prefilter {
|
package/src/glsl.ts
CHANGED
|
@@ -815,7 +815,7 @@ export function resolveFragment(source: string = DEFAULT_RESOLVE): string {
|
|
|
815
815
|
* roughness - 0 the sharp base level, 1 the level whose faces are
|
|
816
816
|
* ENV_ROUGH_FACE (4) texels wide, linear between (Godot's
|
|
817
817
|
* `roughness * MAX_ROUGHNESS_LOD`; Unity's six LOD steps; Three maps
|
|
818
|
-
* onto a 16-texel floor): the level a baked chain (`
|
|
818
|
+
* onto a 16-texel floor): the level a baked chain (`sol tool
|
|
819
819
|
* 3d/environment`, loadEnvironment) convolves at that roughness, and a
|
|
820
820
|
* generated chain merely box-filters - scaled by the intensity: the
|
|
821
821
|
* prefiltered radiance of the split sum (`standard` multiplies it by PBR's
|
package/src/mixer.ts
CHANGED
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
|
|
45
45
|
import * as spatial from "flux:spatial"
|
|
46
46
|
import type { NodeId } from "flux:spatial"
|
|
47
|
-
import { on } from "
|
|
47
|
+
import { on } from "sol:events"
|
|
48
48
|
import type { ModelChannel, ModelClip } from "./gltf.ts"
|
|
49
49
|
import { channelElements, sampleChannel } from "./clip.ts"
|
|
50
50
|
import type { Vec3 } from "./math.ts"
|
package/src/model-data.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @solidrt/3d/model - the model DATA side of the package without the
|
|
2
|
-
// runtime: the glTF parser, the .
|
|
2
|
+
// runtime: the glTF parser, the .sol3m container and the pure geometry kit,
|
|
3
3
|
// for a script that bakes an app's own geometry under bun (or any host
|
|
4
4
|
// with no flux). The package root imports `flux:*` and so loads only on
|
|
5
5
|
// the runtime; this entry imports nothing that does. tools/model.flux.ts
|
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
// app's bake script and the tool share one path:
|
|
8
8
|
//
|
|
9
9
|
// import { box, encodeModel, transformGeometry } from "@solidrt/3d/model"
|
|
10
|
-
// writeFileSync("assets/terrain.
|
|
10
|
+
// writeFileSync("assets/terrain.sol3m", encodeModel({ nodes, parts, ... }))
|
|
11
11
|
//
|
|
12
12
|
// createModel / loadModel on the runtime read what it writes. Keep this
|
|
13
13
|
// entry pure. tests/model-data.test.ts imports it on the bare flux binary
|
|
14
|
-
// and fails the moment a gui or
|
|
14
|
+
// and fails the moment a gui or sol: import creeps into the chain; a
|
|
15
15
|
// headless flux: import it would not notice, and a bake script under bun
|
|
16
16
|
// would.
|
|
17
17
|
|
package/src/model-file.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The baked model container (.
|
|
1
|
+
// The baked model container (.sol3m): ModelData as one file whose payload IS
|
|
2
2
|
// the GPU layout, so loading it is a header parse plus typed-array views -
|
|
3
3
|
// no per-vertex work. Written by tools/model.flux.ts (from parseGltf),
|
|
4
4
|
// read by loadModel on flux; both ends are this pure module. An image
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// the image's name.
|
|
7
7
|
//
|
|
8
8
|
// Layout, all little-endian:
|
|
9
|
-
// "
|
|
9
|
+
// "SOLM" u32 | version u32 | jsonLength u32 | json (padded to 4) | payload
|
|
10
10
|
// The JSON header describes each block's byte range into the payload; every
|
|
11
11
|
// block starts 4-aligned so Float32Array/Uint32Array views sit on it
|
|
12
12
|
// directly. Images travel as their encoded files (PNG/JPEG bytes), app
|
|
@@ -23,14 +23,14 @@ import { layoutStride, vertexView, VERTEX_FORMATS } from "./geometry.ts"
|
|
|
23
23
|
import type { VertexAttribute } from "@solidrt/core/gpu"
|
|
24
24
|
import type { Geometry, VertexLayout } from "./geometry.ts"
|
|
25
25
|
|
|
26
|
-
/** "
|
|
27
|
-
const MAGIC =
|
|
26
|
+
/** "SOLM" read as a little-endian u32. */
|
|
27
|
+
const MAGIC = 0x4d4c4f53
|
|
28
28
|
// Version 3 retained the node hierarchy - a node table (name/parent/TRS)
|
|
29
29
|
// in the header, parts referencing their node, vertices NODE-LOCAL
|
|
30
30
|
// instead of world-baked - and carries skins (joints + inverse binds,
|
|
31
31
|
// parts in the "skinned" layout) and animation clips (channel
|
|
32
32
|
// times/values as payload blocks). Version-2 files world-baked and have
|
|
33
|
-
// none of it, so they are rejected - re-bake with `
|
|
33
|
+
// none of it, so they are rejected - re-bake with `sol tool 3d/model`.
|
|
34
34
|
// Version 4 adds the metalness/roughness fields to the material records
|
|
35
35
|
// (metalness, roughness, metalnessRoughnessMap); a version-3 file lacks
|
|
36
36
|
// them, so it is rejected the same way rather than read as all-metal.
|
|
@@ -39,7 +39,7 @@ const MAGIC = 0x4d545253
|
|
|
39
39
|
// carry packed channels; a version-5 file's "vec3" words do not parse.
|
|
40
40
|
// Version 7 changes the named "skinned" layout's joints from float32x4 to
|
|
41
41
|
// uint8x4 (an integer shader input), so a version-6 skinned part would
|
|
42
|
-
// decode at the wrong stride; it is rejected. Re-bake with `
|
|
42
|
+
// decode at the wrong stride; it is rejected. Re-bake with `sol tool
|
|
43
43
|
// 3d/model`.
|
|
44
44
|
// Version 8 carries morph targets: a part's packed targets block (names,
|
|
45
45
|
// the sparse-by-vertex texels, the extent - see packMorphTargets), the
|
|
@@ -55,7 +55,7 @@ const MAGIC = 0x4d545253
|
|
|
55
55
|
// shared blocks, see the header). A version-9 file has none of these and is rejected like
|
|
56
56
|
// every earlier version rather than read as a file without them, so a
|
|
57
57
|
// stale bake never silently drops the data an app relies on. Re-bake
|
|
58
|
-
// with `
|
|
58
|
+
// with `sol tool 3d/model`.
|
|
59
59
|
// Version 11 names the images: an image record is its block and the
|
|
60
60
|
// image's name (ModelImage.name), which labels its texture. A
|
|
61
61
|
// version-10 file has blocks alone and is rejected the same way.
|
|
@@ -138,7 +138,7 @@ type Header = {
|
|
|
138
138
|
blobs?: Record<string, Block>
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
/** Serialize a model into the .
|
|
141
|
+
/** Serialize a model into the .sol3m container. */
|
|
142
142
|
export function encodeModel(data: ModelData): Uint8Array {
|
|
143
143
|
let blocks: Uint8Array[] = []
|
|
144
144
|
let offset = 0
|
|
@@ -237,7 +237,7 @@ export function encodeModel(data: ModelData): Uint8Array {
|
|
|
237
237
|
}
|
|
238
238
|
|
|
239
239
|
/**
|
|
240
|
-
* Read a .
|
|
240
|
+
* Read a .sol3m container back into ModelData. The geometry arrays are
|
|
241
241
|
* VIEWS onto `bytes` (copied once only when the input is not 4-aligned),
|
|
242
242
|
* so the bytes must outlive the model.
|
|
243
243
|
*/
|
package/src/model.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Models in a scene: ModelData (parsed glTF or a decoded .
|
|
1
|
+
// Models in a scene: ModelData (parsed glTF or a decoded .sol3m) becomes a
|
|
2
2
|
// Group carrying the file's node hierarchy - nested Groups with the file's
|
|
3
3
|
// local TRS, each part's mesh under its node - with the images uploaded as
|
|
4
4
|
// textures and a material per glTF material: Three's `gltf.scene`, an
|
|
@@ -13,7 +13,7 @@
|
|
|
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). Compressed images (KTX2, what `
|
|
16
|
+
// way (a binary import, a fetch). Compressed images (KTX2, what `sol tool
|
|
17
17
|
// 3d/model` bakes) are turned into this device's block format by
|
|
18
18
|
// transcodeModelImages, the one asynchronous step between the two.
|
|
19
19
|
|
|
@@ -439,7 +439,7 @@ function buildOwned(owner: Owner | null, opts: ModelOptions | undefined, build:
|
|
|
439
439
|
* Read a .glb or .gltf (with its external .bin and image files, resolved
|
|
440
440
|
* next to it) and build the model. The parse runs on the runtime - fine
|
|
441
441
|
* for models of tens of thousands of vertices; bake bigger ones with
|
|
442
|
-
* `
|
|
442
|
+
* `sol tool 3d/model` and use loadModel.
|
|
443
443
|
*/
|
|
444
444
|
export async function loadGltf(path: string, opts?: ModelOptions): Promise<Model> {
|
|
445
445
|
let owner = getOwner()
|
|
@@ -456,7 +456,7 @@ export async function loadGltf(path: string, opts?: ModelOptions): Promise<Model
|
|
|
456
456
|
return buildOwned(owner, opts, () => createModel(data, opts))
|
|
457
457
|
}
|
|
458
458
|
|
|
459
|
-
/** Read a baked .
|
|
459
|
+
/** Read a baked .sol3m model (`sol tool 3d/model`) and build it: no parsing,
|
|
460
460
|
* the geometry views the file's bytes directly. */
|
|
461
461
|
export async function loadModel(path: string, opts?: ModelOptions): Promise<Model> {
|
|
462
462
|
let owner = getOwner()
|
package/src/node.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import * as spatial from "flux:spatial"
|
|
10
10
|
import type { NodeId, NodeMotionSpec, NodeTransition } from "flux:spatial"
|
|
11
|
-
import { on } from "
|
|
11
|
+
import { on } from "sol:events"
|
|
12
12
|
import { createMutableTexture, destroyTexture } from "@solidrt/core/gpu"
|
|
13
13
|
import type { ShaderParams, TextureId } from "@solidrt/core/gpu"
|
|
14
14
|
import type { PointerEvent as ElementPointerEvent, WheelEvent as ElementWheelEvent } from "@solidrt/core"
|
|
@@ -203,6 +203,7 @@ export type SceneNode = {
|
|
|
203
203
|
onPointerDown?: (event: NodePointerEvent) => void
|
|
204
204
|
onPointerMove?: (event: NodePointerEvent) => void
|
|
205
205
|
onPointerUp?: (event: NodePointerEvent) => void
|
|
206
|
+
onPointerCancel?: (event: NodePointerEvent) => void
|
|
206
207
|
onPointerEnter?: (event: NodePointerEvent) => void
|
|
207
208
|
onPointerLeave?: (event: NodePointerEvent) => void
|
|
208
209
|
/** The wheel over the node (NodeWheelEvent: `deltaX`/`deltaY`), bubbling
|
|
@@ -352,6 +353,7 @@ export type ScenePointerListener = {
|
|
|
352
353
|
onPointerDown?: (event: ScenePointerEvent) => void
|
|
353
354
|
onPointerMove?: (event: ScenePointerEvent) => void
|
|
354
355
|
onPointerUp?: (event: ScenePointerEvent) => void
|
|
356
|
+
onPointerCancel?: (event: ScenePointerEvent) => void
|
|
355
357
|
onWheel?: (event: SceneWheelEvent) => void
|
|
356
358
|
onTap?: (event: SceneTapEvent) => void
|
|
357
359
|
}
|
|
@@ -776,7 +778,7 @@ export type { TransformUpdate } from "./math.ts"
|
|
|
776
778
|
* orchestrator is always an ancestor). Clearing cancels running tracks in
|
|
777
779
|
* place (the node keeps its mid-flight transform) and later writes snap. Each natural settle calls the node's
|
|
778
780
|
* `onTransitionEnd` with the component (the raw "spatialTransitionEnd"
|
|
779
|
-
* engine event on
|
|
781
|
+
* engine event on sol:events stays for flux:spatial consumers; it carries
|
|
780
782
|
* the core id, `_node`).
|
|
781
783
|
*/
|
|
782
784
|
export function setTransition(node: SceneNode, transition: NodeTransition | string | null): void {
|
package/src/scene-pointer.ts
CHANGED
|
@@ -41,7 +41,7 @@ const TAP_INTERVAL_MS = 300
|
|
|
41
41
|
// neighbouring meshes.
|
|
42
42
|
const TAP_REPEAT_SLOP = 20
|
|
43
43
|
|
|
44
|
-
type HandlerName = "onPointerDown" | "onPointerMove" | "onPointerUp" | "onWheel" | "onTap"
|
|
44
|
+
type HandlerName = "onPointerDown" | "onPointerMove" | "onPointerUp" | "onPointerCancel" | "onWheel" | "onTap"
|
|
45
45
|
|
|
46
46
|
// What a press captures, a hover tracks and a tap compares: the struck
|
|
47
47
|
// mesh, and the instance when the mesh is instanced.
|
|
@@ -254,6 +254,18 @@ export function makePointerInput(deps: PointerInputDeps): PointerInput {
|
|
|
254
254
|
tap.tapCount = count
|
|
255
255
|
walk("onTap", tap, !press.claimed)
|
|
256
256
|
},
|
|
257
|
+
onPointerCancel(e) {
|
|
258
|
+
let [x, y] = toScene(e)
|
|
259
|
+
let press = presses.get(e.pointerId)
|
|
260
|
+
if (!press) {
|
|
261
|
+
let hit = nearest(x, y)
|
|
262
|
+
walk("onPointerCancel", makeEvent(e, hit === null ? null : targetOf(hit), x, y, hit), true)
|
|
263
|
+
return
|
|
264
|
+
}
|
|
265
|
+
// The press ends where it was: the walk it captured, never a tap.
|
|
266
|
+
presses.delete(e.pointerId)
|
|
267
|
+
walk("onPointerCancel", makeEvent(e, press.target, x, y, hitOn(press.target, x, y)), !press.claimed)
|
|
268
|
+
},
|
|
257
269
|
onWheel(e) {
|
|
258
270
|
let [x, y] = toScene(e)
|
|
259
271
|
let hit = nearest(x, y)
|
|
@@ -290,6 +302,7 @@ export function feedPointer(root: { listen(listener: ScenePointerListener): () =
|
|
|
290
302
|
onPointerDown: e => feed.handlers.onPointerDown(e.native),
|
|
291
303
|
onPointerMove: e => feed.handlers.onPointerMove(e.native),
|
|
292
304
|
onPointerUp: e => feed.handlers.onPointerUp(e.native),
|
|
305
|
+
onPointerCancel: e => feed.handlers.onPointerCancel(e.native),
|
|
293
306
|
onWheel: e => feed.handlers.onWheel(e.native),
|
|
294
307
|
})
|
|
295
308
|
}
|
package/src/scene.ts
CHANGED
|
@@ -227,6 +227,7 @@ export type SceneHandlers = {
|
|
|
227
227
|
onPointerDown(event: ElementPointerEvent): void
|
|
228
228
|
onPointerMove(event: ElementPointerEvent): void
|
|
229
229
|
onPointerUp(event: ElementPointerEvent): void
|
|
230
|
+
onPointerCancel(event: ElementPointerEvent): void
|
|
230
231
|
onPointerLeave(event: ElementPointerEvent): void
|
|
231
232
|
onWheel(event: ElementWheelEvent): void
|
|
232
233
|
}
|
|
@@ -635,7 +636,7 @@ export type ReflectionProbeOptions = {
|
|
|
635
636
|
export type ReflectionProbe = {
|
|
636
637
|
/** The cube map: what `environment={{ cube }}` and `background={{
|
|
637
638
|
* cube }}` take. The prefiltered chain (roughness 0 sharp at level 0,
|
|
638
|
-
* blurred below, the same rule as a baked .
|
|
639
|
+
* blurred below, the same rule as a baked .sol3e), or with `prefilter:
|
|
639
640
|
* false` the sharp faces alone - then `standard` reflects it at its
|
|
640
641
|
* roughness 0 look whatever its roughness. */
|
|
641
642
|
cube: TextureId
|
package/src/splat-data.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
// @solidrt/3d/splat - the splat DATA side of the package without the
|
|
2
2
|
// runtime: the capture parsers (.ply, .splat, gunzipped .spz), the
|
|
3
|
-
// covariance bake and the .
|
|
3
|
+
// covariance bake and the .sol3s container, for `sol tool 3d/splat` and any
|
|
4
4
|
// app bake script under bun. The package root imports `flux:*` and so
|
|
5
5
|
// loads only on the runtime; this entry imports nothing that does (the
|
|
6
6
|
// model-data rule; tests/splat-data.test.ts imports it on the bare flux
|
|
7
|
-
// binary and fails the moment a gui or
|
|
7
|
+
// binary and fails the moment a gui or sol: import creeps into the chain).
|
|
8
8
|
//
|
|
9
9
|
// import { encodeSplat, parseSplat } from "@solidrt/3d/splat"
|
|
10
|
-
// writeFileSync("assets/scan.
|
|
10
|
+
// writeFileSync("assets/scan.sol3s", encodeSplat(parseSplat(bytes)))
|
|
11
11
|
//
|
|
12
|
-
// loadSplat / createSplatMesh on the runtime read what it writes: a .
|
|
12
|
+
// loadSplat / createSplatMesh on the runtime read what it writes: a .sol3s
|
|
13
13
|
// record IS the texel data the splat shader fetches (two rgba32ui texels
|
|
14
14
|
// per splat, SPLAT_ATTRIBUTES as a byte layout) and the record the core
|
|
15
15
|
// sorts by depth, so runtime loading is a header parse plus byte views -
|
|
@@ -113,8 +113,8 @@ const SPZ_COLOR_SCALE = 0.15
|
|
|
113
113
|
const SPZ_SCALE_OFFSET = 10
|
|
114
114
|
const SPZ_SCALE_STEP = 16
|
|
115
115
|
|
|
116
|
-
/** "
|
|
117
|
-
const MAGIC =
|
|
116
|
+
/** "SOLS" read as a little-endian u32. */
|
|
117
|
+
const MAGIC = 0x534c4f53
|
|
118
118
|
// Version 2: the 32-byte two-texel record, importance-sorted, y-up unless
|
|
119
119
|
// the bake kept orientation, plus the optional SH texel block; the header
|
|
120
120
|
// carries count, shDegree, bounds and both block ranges. (Version 1 was
|
|
@@ -164,10 +164,10 @@ let colorByte = (v: number) => Math.round(Math.min(1, Math.max(0, v)) * 255)
|
|
|
164
164
|
* Parse a captured splat cloud - a trainer's .ply, the de-facto .splat
|
|
165
165
|
* interchange, or a gunzipped .spz (Niantic; the container is
|
|
166
166
|
* gzip-compressed, and this entry stays decompression-free, so hand it
|
|
167
|
-
* the inflated bytes - `
|
|
167
|
+
* the inflated bytes - `sol tool 3d/splat` does) - into baked SplatData:
|
|
168
168
|
* covariance precomputed, y-up by default, importance-sorted (size times
|
|
169
169
|
* opacity), bounds measured. Runs anywhere (bun, the runtime); for a
|
|
170
|
-
* capture of real size run it at pack time and ship encodeSplat's .
|
|
170
|
+
* capture of real size run it at pack time and ship encodeSplat's .sol3s.
|
|
171
171
|
*/
|
|
172
172
|
export function parseSplat(bytes: Uint8Array, opts?: ParseSplatOptions): SplatData {
|
|
173
173
|
let format = opts?.format ?? sniffFormat(bytes)
|
|
@@ -185,7 +185,7 @@ function sniffFormat(bytes: Uint8Array): SplatFormat {
|
|
|
185
185
|
if (dv.getUint32(0, true) === SPZ_MAGIC) return "spz"
|
|
186
186
|
}
|
|
187
187
|
if (bytes.length >= 2 && bytes[0] === 0x1f && bytes[1] === 0x8b) {
|
|
188
|
-
throw new Error("parseSplat: gzip-compressed input (.spz files are); gunzip the bytes first -
|
|
188
|
+
throw new Error("parseSplat: gzip-compressed input (.spz files are); gunzip the bytes first - sol tool 3d/splat does")
|
|
189
189
|
}
|
|
190
190
|
if (bytes.length > 0 && bytes.length % SPLAT_IN_BYTES === 0) return "splat"
|
|
191
191
|
throw new Error("parseSplat: unrecognized input - not a .ply, not a gunzipped .spz, and not whole 32-byte .splat records")
|
|
@@ -537,8 +537,8 @@ function shBytesOf(count: number, shDegree: number): number {
|
|
|
537
537
|
}
|
|
538
538
|
|
|
539
539
|
/**
|
|
540
|
-
* Serialize a splat cloud into the .
|
|
541
|
-
* "
|
|
540
|
+
* Serialize a splat cloud into the .sol3s container, all little-endian:
|
|
541
|
+
* "SOLS" u32 | version u32 | jsonLength u32 | json (padded to 4) |
|
|
542
542
|
* payload. The JSON header carries count, shDegree, bounds and the byte
|
|
543
543
|
* range of the record block and (above degree 0) the SH block; the
|
|
544
544
|
* payload is both blocks verbatim.
|
|
@@ -574,7 +574,7 @@ export function encodeSplat(data: SplatData): Uint8Array {
|
|
|
574
574
|
}
|
|
575
575
|
|
|
576
576
|
/**
|
|
577
|
-
* Read a .
|
|
577
|
+
* Read a .sol3s container back into SplatData. The records and the SH
|
|
578
578
|
* block are VIEWS onto `bytes` (copied once only when the input is not
|
|
579
579
|
* 4-aligned), so the bytes must outlive the data - which is the point:
|
|
580
580
|
* loading a bake is a header parse, and the views upload as-is.
|
|
@@ -585,7 +585,7 @@ export function decodeSplat(bytes: Uint8Array): SplatData {
|
|
|
585
585
|
let dv = new DataView(bytes.buffer, bytes.byteOffset, 12)
|
|
586
586
|
if (dv.getUint32(0, true) !== MAGIC) throw new Error("decodeSplat: not a splat file (bad magic)")
|
|
587
587
|
let version = dv.getUint32(4, true)
|
|
588
|
-
if (version !== VERSION) throw new Error("decodeSplat: version " + version + ", expected " + VERSION + " - re-bake with `
|
|
588
|
+
if (version !== VERSION) throw new Error("decodeSplat: version " + version + ", expected " + VERSION + " - re-bake with `sol tool 3d/splat`")
|
|
589
589
|
let jsonLength = dv.getUint32(8, true)
|
|
590
590
|
let header: Header = JSON.parse(new TextDecoder().decode(bytes.subarray(12, 12 + jsonLength)))
|
|
591
591
|
let payload = 12 + jsonLength + ((4 - (jsonLength % 4)) % 4)
|
package/src/splat.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// The splat RUNTIME side: the stock splat material and createSplatMesh
|
|
2
2
|
// over a baked SplatData (parse and bake live in ./splat-data.ts, the
|
|
3
|
-
// runtime-free `@solidrt/3d/splat` entry; `
|
|
4
|
-
// .
|
|
3
|
+
// runtime-free `@solidrt/3d/splat` entry; `sol tool 3d/splat` writes the
|
|
4
|
+
// .sol3s this loads). A splat cloud here is an ordinary record mesh in the
|
|
5
5
|
// INDEXED form - the records live in an rgba32ui data texture the vertex
|
|
6
6
|
// stage fetches by id, SPLAT_GROUP splats share one drawn instance (a
|
|
7
7
|
// merged-quad geometry; the instancing frontend was the measured wall),
|
|
@@ -393,7 +393,7 @@ export function createSplatMesh(data: SplatData, opts?: SplatMeshOptions): Splat
|
|
|
393
393
|
return mesh
|
|
394
394
|
}
|
|
395
395
|
|
|
396
|
-
/** Read a baked .
|
|
396
|
+
/** Read a baked .sol3s splat cloud (`sol tool 3d/splat`): no parsing, the
|
|
397
397
|
* records and the SH block view the file's bytes and upload as-is at
|
|
398
398
|
* createSplatMesh. */
|
|
399
399
|
export async function loadSplat(path: string): Promise<SplatData> {
|
package/tools/environment.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
/// <reference path="./node-env.d.ts" />
|
|
3
3
|
|
|
4
|
-
//
|
|
4
|
+
// sol tool 3d/environment: bake a Radiance .hdr panorama into a .sol3e
|
|
5
5
|
// environment file - the six cube faces and their GGX-prefiltered mip
|
|
6
6
|
// chain in linear float, the exact chain loadEnvironment uploads as an
|
|
7
7
|
// explicit rgba16f cube (no runtime conversion, no generated mipmaps, no
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// at runtime every time the sky changes. Put the output under assets/ so
|
|
11
11
|
// it ships with the app.
|
|
12
12
|
//
|
|
13
|
-
//
|
|
13
|
+
// sol tool 3d/environment <in.hdr> [-o <out.sol3e>] [--size <edge>]
|
|
14
14
|
//
|
|
15
15
|
// --size is the cube's face edge: 128 (default) is Unity's probe default
|
|
16
16
|
// and enough for every surface short of a mirror; 256 (Godot's radiance
|
|
@@ -28,7 +28,7 @@ const MAX_SIZE = 1024
|
|
|
28
28
|
|
|
29
29
|
function usage(error?: string): never {
|
|
30
30
|
if (error) console.error(error)
|
|
31
|
-
console.log("Usage:
|
|
31
|
+
console.log("Usage: sol tool 3d/environment <in.hdr> [-o <out.sol3e>] [--size <edge>]")
|
|
32
32
|
process.exit(error ? 1 : 0)
|
|
33
33
|
}
|
|
34
34
|
|
|
@@ -54,7 +54,7 @@ for (let i = 0; i < args.length; i++) {
|
|
|
54
54
|
else usage("Unexpected argument " + arg)
|
|
55
55
|
}
|
|
56
56
|
if (input === undefined) usage("Missing input file")
|
|
57
|
-
if (output === undefined) output = basename(input, extname(input)) + ".
|
|
57
|
+
if (output === undefined) output = basename(input, extname(input)) + ".sol3e"
|
|
58
58
|
|
|
59
59
|
let started = performance.now()
|
|
60
60
|
let panorama = decodeHdr(new Uint8Array(readFileSync(input)))
|
package/tools/model.flux.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// sol tool 3d/model: bake a glTF into a .sol3m model file - the parse
|
|
2
2
|
// (src/gltf.ts) run once here, the result written in the exact layout
|
|
3
3
|
// loadModel views without any per-vertex work at runtime. Same parser,
|
|
4
4
|
// same subset, same result as loadGltf; this only moves the cost to build
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
// `@solidrt/3d/model` entry (src/model-data.ts), the same surface an
|
|
21
21
|
// app's own bake script uses under bun.
|
|
22
22
|
//
|
|
23
|
-
//
|
|
23
|
+
// sol tool 3d/model <in.gltf|in.glb> [-o <out.sol3m>] [--compress|--no-compress]
|
|
24
24
|
|
|
25
25
|
import { file, glob } from "flux:fs"
|
|
26
26
|
import { decodeImage, encodeTexture } from "flux:image"
|
|
@@ -42,7 +42,7 @@ const IMAGES_IN_FLIGHT = 4
|
|
|
42
42
|
// of the texture settings are written from.
|
|
43
43
|
const PROJECT_ROOT = "."
|
|
44
44
|
|
|
45
|
-
const USAGE = "Usage:
|
|
45
|
+
const USAGE = "Usage: sol tool 3d/model <in.gltf|in.glb> [-o <out.sol3m>] [--compress|--no-compress]"
|
|
46
46
|
|
|
47
47
|
function usage(error?: string): never {
|
|
48
48
|
if (error) console.error(error)
|
|
@@ -149,7 +149,7 @@ for (let i = 0; i < argv.length; i++) {
|
|
|
149
149
|
else usage("Unexpected argument " + arg)
|
|
150
150
|
}
|
|
151
151
|
if (input === undefined) usage("Missing input file")
|
|
152
|
-
if (output === undefined) output = basename(input, extname(input)) + ".
|
|
152
|
+
if (output === undefined) output = basename(input, extname(input)) + ".sol3m"
|
|
153
153
|
|
|
154
154
|
// The parse is synchronous, so the external files it will open are read
|
|
155
155
|
// ahead of it (the same prefetch loadGltf does).
|
package/tools/node-env.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The node surface the tools use, declared here so `
|
|
1
|
+
// The node surface the tools use, declared here so `sol check
|
|
2
2
|
// packages/3d/tools/<name>.ts` typechecks them on the package's flux-typed
|
|
3
3
|
// program (the nearest tsconfig is the repo root's). The tools run under
|
|
4
4
|
// bun, but bun's own types cannot join this program: their globals
|
package/tools/splat.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
/// <reference path="./node-env.d.ts" />
|
|
3
3
|
|
|
4
|
-
//
|
|
4
|
+
// sol tool 3d/splat: bake a captured splat cloud into a .sol3s file - the
|
|
5
5
|
// parse, covariance bake and SH packing (src/splat-data.ts) run once here
|
|
6
6
|
// under bun, the result written in the exact texel layout createSplatMesh
|
|
7
7
|
// uploads and the engine sorts, without any per-splat work at runtime.
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// Everything here comes from the runtime-free `@solidrt/3d/splat` entry,
|
|
10
10
|
// the same surface an app's own bake script uses.
|
|
11
11
|
//
|
|
12
|
-
//
|
|
12
|
+
// sol tool 3d/splat <in.ply|in.splat|in.spz> [-o <out.sol3s>] [--sh <0..3>] [--keep-orientation]
|
|
13
13
|
//
|
|
14
14
|
// Inputs: a trainer's .ply, the de-facto .splat interchange, or Niantic's
|
|
15
15
|
// .spz (gunzipped here; version 2 or 3). A y-down capture (.ply, .splat)
|
|
@@ -25,7 +25,7 @@ import { encodeSplat, parseSplat } from "../src/splat-data.ts"
|
|
|
25
25
|
|
|
26
26
|
function usage(error?: string): never {
|
|
27
27
|
if (error) console.error(error)
|
|
28
|
-
console.log("Usage:
|
|
28
|
+
console.log("Usage: sol tool 3d/splat <in.ply|in.splat|in.spz> [-o <out.sol3s>] [--sh <0..3>] [--keep-orientation]")
|
|
29
29
|
process.exit(error ? 1 : 0)
|
|
30
30
|
}
|
|
31
31
|
|
|
@@ -51,7 +51,7 @@ for (let i = 0; i < args.length; i++) {
|
|
|
51
51
|
else usage("Unexpected argument " + arg)
|
|
52
52
|
}
|
|
53
53
|
if (input === undefined) usage("Missing input file")
|
|
54
|
-
if (output === undefined) output = basename(input, extname(input)) + ".
|
|
54
|
+
if (output === undefined) output = basename(input, extname(input)) + ".sol3s"
|
|
55
55
|
|
|
56
56
|
let bytes = new Uint8Array(readFileSync(input))
|
|
57
57
|
// .spz is gzip-compressed; the parser takes the inflated bytes.
|