@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/AGENTS.md
CHANGED
|
@@ -161,8 +161,11 @@ Groups), and each target carries a mask (default 1) - `layers` on
|
|
|
161
161
|
createScene/createView, live via `setLayers` on the scene handle and
|
|
162
162
|
each view. A mesh draws where mask & layers is non-zero, so a minimap's
|
|
163
163
|
marker meshes live on bit 2: invisible in the main render, drawn by
|
|
164
|
-
the map view whose mask admits them.
|
|
165
|
-
|
|
164
|
+
the map view whose mask admits them. LIGHTS carry a `layers` mask too
|
|
165
|
+
(all bits by default, see Lights below): a light lights and shadows
|
|
166
|
+
only the meshes its mask intersects. Shadow views follow the SCENE's
|
|
167
|
+
mask (what the scene cannot see must not darken it) narrowed by their
|
|
168
|
+
light's own `layers`, and
|
|
166
169
|
pick()/raycast()/overlap()/sweep() skip scene-masked-out meshes like
|
|
167
170
|
invisible ones - unless the query passes its own `{ layers }`, which is
|
|
168
171
|
how a low-poly collision mesh lives undrawn in the scene yet answers
|
|
@@ -172,8 +175,12 @@ hides its whole interior, but frustum culling cannot know that and
|
|
|
172
175
|
there is no occlusion or portal culling. Put the interior, the shell
|
|
173
176
|
and an opening "plug" on their own layers and switch the scene mask by
|
|
174
177
|
the camera's zone (inside, outside, in the doorway) - the reactive
|
|
175
|
-
`<Scene layers>` makes it one signal
|
|
176
|
-
the same way
|
|
178
|
+
`<Scene layers>` makes it one signal - and mask each light (and the
|
|
179
|
+
hemisphere) to its side the same way, so the sun does not light the
|
|
180
|
+
room through its shell nor the room's bulb the street. Light `layers`
|
|
181
|
+
cannot mask the ENVIRONMENT term (image lighting is per target, not
|
|
182
|
+
per light): the interior meshes take `environment={null}` - or an
|
|
183
|
+
interior probe's cube - instead (Mesh.environment, see Environment).
|
|
177
184
|
Per-view fog: `fog: FogOptions | null` on createView overrides the
|
|
178
185
|
scene's fog for that view (null = unfogged - the clear minimap over a
|
|
179
186
|
fogged scene); absent follows the scene. `overrideMaterial` (Three's
|
|
@@ -538,27 +545,30 @@ faces); an instance is just per-entry uniforms (`uColor`) and bindings
|
|
|
538
545
|
### Pure pieces and checks
|
|
539
546
|
|
|
540
547
|
The pure pieces (`math.ts`, `color.ts`, `geometry.ts`,
|
|
541
|
-
`profile.ts`, `sweep.ts`, `gltf.ts`, `model-file.ts`)
|
|
548
|
+
`profile.ts`, `sweep.ts`, `gltf.ts`, `model-file.ts`, `splat-data.ts`)
|
|
549
|
+
are Solid-free and
|
|
542
550
|
GPU-free BY DESIGN so they can be checked headless and run under bun:
|
|
543
551
|
`src/model-data.ts` re-exports exactly this set as the published
|
|
544
552
|
`@solidrt/3d/model` entry (the bake tool and app bake scripts), and
|
|
545
|
-
`tests/model-data.test.ts` imports it under `
|
|
546
|
-
import creeping into any of them fails there
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
`
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
meshes
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
553
|
+
`tests/model-data.test.ts` imports it under `srt test`, on the bare flux
|
|
554
|
+
binary, so a gui or `srt:` import creeping into any of them fails there
|
|
555
|
+
(a headless `flux:` import would not); keep them that way.
|
|
556
|
+
`src/splat-data.ts` is its own published entry the same way
|
|
557
|
+
(`@solidrt/3d/splat`, guarded by `tests/splat-data.test.ts`).
|
|
558
|
+
The tests under `tests/` run with `bun run srt test packages/3d` from the
|
|
559
|
+
repo root, or one file, or `--filter <text>` for the tests whose name
|
|
560
|
+
contains it: the pure modules on the bare flux binary, and the files
|
|
561
|
+
that need a GPU (`.test.tsx`, importing `@solidrt/test`) on the
|
|
562
|
+
headless dev client. Run them after a change. `raycast.test.tsx` and
|
|
563
|
+
`collision.test.tsx` assert the documented picking contract (triangle
|
|
564
|
+
accuracy, the box tier, pick/raycast parity, layer masks, the
|
|
565
|
+
`{ meshes }` filter) and collision contract (exact sweep times, the
|
|
566
|
+
surface rule, the slide filter, layers and meshes on overlap/sweep,
|
|
567
|
+
moveAndSlide's landing) against a real scene; run them whenever a doc
|
|
568
|
+
edit touches picking or collision claims - two copies of this contract
|
|
569
|
+
have drifted before. `fog.test.tsx` reads the fog example's pixels
|
|
570
|
+
through its debug commands, and `camera-components.test.tsx` drives
|
|
571
|
+
`<OrbitCamera>` and `<FirstPersonCamera>` with a real drag and key.
|
|
562
572
|
|
|
563
573
|
## Components
|
|
564
574
|
|
|
@@ -573,9 +583,10 @@ collision claims - two copies of this contract have drifted before.
|
|
|
573
583
|
| `InstancedLod` | as InstancedMesh minus `material`, plus `levels` (`[{ geometry, material, size }]` nearest first, fixed at creation; instanced materials as InstancedMesh's), `castShadow?` (every level); `<Instance>` children populate it as under `InstancedMesh`, each drawing the level its own projected size picks |
|
|
574
584
|
| `Mesh` | `geometry`, `material`, transforms as Group, `params?` (per-mesh uniforms, merge semantics - no unset), `renderOrder?`, `castShadow?`, `layers?` (membership bitmask, default 1), `frustumCulled?` (default true; false for geometry a vertex stage moves beyond the node's box - a billboard, a fullscreen quad; see Culling), `cullMargin?` (world units of slack around the box for bounded displacement), pointer events (below), `ref?(mesh)` |
|
|
575
585
|
| `Sprite` | as Mesh minus `geometry`: a camera-facing unit quad, `scale` is its world size, rotation is ignored; pair with a `sprite()` material |
|
|
576
|
-
| `InstancedMesh` | as Mesh (an instanced `material`: a stock one with `instanced`/`instanceColors`, or a class declaring INSTANCE_MATRIX_ATTRIBUTES), plus `capacity?` (instance slots, default 64; the buffers double past it), `bounds?` (an optional cull box, never a hit: instances pick by themselves, and without it the mesh culls by their union), `label?`; a PARENT: `<Instance>` children populate it, `<Group>` children are squads; the record buffers are component-owned and freed on unmount, and `anchor?` (a SceneNode, fixed at creation: the ancestor the records are relative to, so `<Instance mesh>` children may sit anywhere under it - see createInstancedMesh) |
|
|
586
|
+
| `InstancedMesh` | as Mesh (an instanced `material`: a stock one with `instanced`/`instanceColors`, or a class declaring INSTANCE_MATRIX_ATTRIBUTES), plus `capacity?` (instance slots, default 64; the buffers double past it), `bounds?` (an optional cull box, never a hit: instances pick by themselves, and without it the mesh culls by their union), `instanceOrder?` (draw the records in key order, fixed at creation - see "Instance order"), `label?`; a PARENT: `<Instance>` children populate it, `<Group>` children are squads; the record buffers are component-owned and freed on unmount, and `anchor?` (a SceneNode, fixed at creation: the ancestor the records are relative to, so `<Instance mesh>` children may sit anywhere under it - see createInstancedMesh) |
|
|
577
587
|
| `Instance` | one instance of the enclosing `InstancedMesh`: transforms, `transition`, pointer events as Group, plus `style?` (the material's style record, one value per component - `[r, g, b, a]` under `instanceColors`), `ref?(instance)`; a parent too (a `<Mesh>` under an instance rides with it), and `mesh?` (the population, fixed at creation, when the instance is placed outside its `<InstancedMesh>` - inside that mesh's `anchor` subtree) |
|
|
578
|
-
| `RecordMesh` | as Mesh, plus `records` (the per-instance records in the material's first instance layout, an ArrayBufferView; buffer capacity starts at the first value and grows on larger rewrites), `count?` (records drawn, default all), `bounds?` (local [minX..maxZ] over the population - without it the mesh never picks); the record buffers are component-owned and freed on unmount |
|
|
588
|
+
| `RecordMesh` | as Mesh, plus `records` (the per-instance records in the material's first instance layout, an ArrayBufferView; buffer capacity starts at the first value and grows on larger rewrites), `count?` (records drawn, default all), `bounds?` (local [minX..maxZ] over the population - without it the mesh never picks), `instanceOrder?` (draw the records in key order, fixed at creation - see "Instance order"), `transfer?` (hand the records to the engine at creation, the write-once form - a later `records` value then throws; see "Instance order"); the record buffers are component-owned and freed on unmount |
|
|
589
|
+
| `SplatMesh` | as Mesh, plus `data` (a baked SplatData from loadSplat; write-once under the default transfer - swap clouds by remounting), `count?` (splats drawn, default all; records are importance-sorted at bake, so a prefix is the scene at that size), `material?` (a fork of the stock splat class) and `transfer?` (default true: records go to the engine, halving residency; see "Splats"); bounds, the back-to-front instance order and the stock material come from the bake |
|
|
579
590
|
| `PerspectiveCamera` | `fov?` (vertical DEGREES, default 60), `near?`, `far?`, `position?`, `lookAt?`, `up?` - or the Scene `camera` prop, the same state (last write wins) |
|
|
580
591
|
| `SpotLight` | transforms as Group, `direction?` (local aim, default [0, -1, 0]), `color?`, `intensity?`, `distance?` (falloff cutoff, 0 = none), `angle?` (cone half-angle DEGREES, default 60), `penumbra?` (0..1 rim fade, default 0), `decay?` (falloff exponent, default 2), `castShadow?`, `shadow?` (mapSize, bias, normalBias, near), `ref?(light)` |
|
|
581
592
|
| `PointLight` | transforms as Group (position is what matters), `color?`, `intensity?`, `distance?`, `decay?`, `castShadow?` (six face maps, six shadow slots), `shadow?` (mapSize, bias, normalBias, near), `ref?(light)` |
|
|
@@ -799,7 +810,7 @@ driving - the predicate every camera control shares) are reactive
|
|
|
799
810
|
(HUD-safe); the pose is plain state via `pose()`/`set()` (also the
|
|
800
811
|
debug-command shape). It drives position and target only; fov/near/far
|
|
801
812
|
stay on scene.setCamera (or the Scene `camera` prop). The auto-orbit
|
|
802
|
-
pauses while a gesture is open.
|
|
813
|
+
pauses while a gesture is open. tests/orbit.test.ts pins every stage
|
|
803
814
|
headless.
|
|
804
815
|
|
|
805
816
|
In a component tree, skip the wiring: `<OrbitCamera input={input}
|
|
@@ -857,7 +868,7 @@ the shot's recording target, so an `<OrbitCamera>` or
|
|
|
857
868
|
it, the container runs the blend on frames while one is in flight. A
|
|
858
869
|
shot's picks and raycasts (`anchor`, `occlusion`) go through the
|
|
859
870
|
owner's actual camera, which is the shot's own only while it is live
|
|
860
|
-
and at rest. Pinned in
|
|
871
|
+
and at rest. Pinned in tests/orbit.test.ts; probes/shots-3d-probe.tsx
|
|
861
872
|
is the live guard.
|
|
862
873
|
|
|
863
874
|
### First-person camera
|
|
@@ -920,8 +931,8 @@ swap with it), `moveSpeed`/`lookSpeed` follow their props, and a pitch
|
|
|
920
931
|
clamp change re-clamps at once. Like the orbit control it drives
|
|
921
932
|
position and yaw/pitch only: fov/near/far stay on scene.setCamera (or
|
|
922
933
|
the Scene `camera` prop), so a walker in a scene bigger than the default
|
|
923
|
-
`far` of 100 sets one there.
|
|
924
|
-
|
|
934
|
+
`far` of 100 sets one there. tests/orbit.test.ts and
|
|
935
|
+
tests/first-person.test.ts pin both controls headless (they import
|
|
925
936
|
`@solidrt/core/input` only).
|
|
926
937
|
|
|
927
938
|
### Overlay projection
|
|
@@ -1073,7 +1084,9 @@ repeats within 300 ms and 20 px on the same target (a double tap is
|
|
|
1073
1084
|
tap. Wheel walks like a move with `deltaX/deltaY` (a node stopping it
|
|
1074
1085
|
keeps the zoom out). Every event carries `native`, the leaf's element
|
|
1075
1086
|
event, for core's recognizers, and the element fields (pointerId,
|
|
1076
|
-
pointerType, button, modifiers
|
|
1087
|
+
pointerType, button, modifiers, `timeStamp`, and `predicted`: a move to
|
|
1088
|
+
draw with, not to measure with - core's AGENTS.md has both) plus `x`/`y`
|
|
1089
|
+
in scene pixels
|
|
1077
1090
|
(screenRay's input - a drag plane is one intersection away). Root
|
|
1078
1091
|
listeners all run, in registration order (the root is the last stop,
|
|
1079
1092
|
nothing is left to claim). Wiring: the built-in `<Scene>` leaf carries
|
|
@@ -1087,7 +1100,7 @@ natural size, under any ancestor transforms or design-size fits (the hit
|
|
|
1087
1100
|
test undoes them; localX/localY arrive in the leaf's layout frame). A
|
|
1088
1101
|
leaf laid out at a different size (the supersampling pattern) uses
|
|
1089
1102
|
`scene.handlersFor(() => ({ width, height }))` with its layout size.
|
|
1090
|
-
|
|
1103
|
+
tests/dispatch.test.ts pins the walk, claiming, capture, hover, wheel
|
|
1091
1104
|
and tap rules headless; examples/pick.tsx is the live guard.
|
|
1092
1105
|
|
|
1093
1106
|
### Geometry generators
|
|
@@ -1333,7 +1346,10 @@ with uViewProj - the specular/fresnel view vector is
|
|
|
1333
1346
|
uViewProj rows, that carries the clip flip), `uniform mat4
|
|
1334
1347
|
uInvViewProj` (the camera's inverse view-projection, shared likewise -
|
|
1335
1348
|
a clip position back to world, the world-space ray through a pixel
|
|
1336
|
-
without knowing the projection)
|
|
1349
|
+
without knowing the projection), `uniform vec2 uViewport` (the target's
|
|
1350
|
+
pixel size, shared likewise and per target - each view feeds its own -
|
|
1351
|
+
so a pixel-space footprint like the splat material's needs no app
|
|
1352
|
+
write) and `uniform mat4 uNormal` (the world
|
|
1337
1353
|
inverse-transpose, written beside uModel for this material's meshes;
|
|
1338
1354
|
take `mat3(uNormal)` - correct under non-uniform scale, where
|
|
1339
1355
|
mat3(uModel) bends normals off the surface). Attributes come from the
|
|
@@ -1491,7 +1507,8 @@ or in five figures, belong to the function face.
|
|
|
1491
1507
|
#### createRecordMesh
|
|
1492
1508
|
|
|
1493
1509
|
`createRecordMesh(geometry, material, records, count?, { bounds?,
|
|
1494
|
-
label? })` is the raw form: `records` is the
|
|
1510
|
+
instanceOrder?, label? })` is the raw form: `records` is the
|
|
1511
|
+
per-instance data laid out
|
|
1495
1512
|
in the material's first instance layout - a Float32Array over an
|
|
1496
1513
|
all-float layout, otherwise bytes built with `vertexView(layout, buffer)`
|
|
1497
1514
|
and `attributeAccess` like a geometry stream (a length that is not whole
|
|
@@ -1523,6 +1540,63 @@ unhide, renderOrder/params/geometry/material swaps apply, and
|
|
|
1523
1540
|
explicit free, geometry-buffer rule. `examples/fleet.tsx` (instances,
|
|
1524
1541
|
components) and `examples/instanced.tsx` (records) are the live proofs.
|
|
1525
1542
|
|
|
1543
|
+
#### Instance order
|
|
1544
|
+
|
|
1545
|
+
`instanceOrder` on either create (and the components) draws a
|
|
1546
|
+
population's records in key order, decided core-side; slots as you write
|
|
1547
|
+
them never move. `{ position: "iCenter", retain: true, descending: true
|
|
1548
|
+
}` names a float32x3/x4 attribute holding a local-space position: records
|
|
1549
|
+
draw by their depth along the scene camera's view direction, which the
|
|
1550
|
+
SCENE feeds the core whenever the camera or the mesh turns meaningfully
|
|
1551
|
+
(mapped through the mesh's rotation; an epsilon gate meters the
|
|
1552
|
+
re-sorts) - back-to-front transparency over 100k+ records with zero
|
|
1553
|
+
per-frame JS, and with `retain` a camera turn re-sorts and republishes
|
|
1554
|
+
core-side while a parked camera uploads nothing. `{ field: "age" }`
|
|
1555
|
+
names a float32 attribute and draws by its value, re-ordered when the
|
|
1556
|
+
records publish. The costs, by design: an ordered mesh publishes the
|
|
1557
|
+
LIVE record set whole on any write (a byte range has no stable position
|
|
1558
|
+
under a permutation; `count` defines the sorted population), and one
|
|
1559
|
+
buffer holds ONE order - the scene target's entry owns it, views and
|
|
1560
|
+
shadow passes draw the same gathered buffers, and a mesh layered out of
|
|
1561
|
+
the scene target keeps its last order. Where the key sits decides
|
|
1562
|
+
nothing else: raw instancing stays buffer-order everywhere (Three,
|
|
1563
|
+
Unity, Godot), so this knob has no engine counterpart to compare
|
|
1564
|
+
against; the core primitive is okf/done/gpu-instance-order.md.
|
|
1565
|
+
|
|
1566
|
+
`transfer: true` (createRecordMesh only) is the write-once form for a
|
|
1567
|
+
large population: the records go to the ENGINE at creation and the mesh
|
|
1568
|
+
keeps no JS mirror, so the record bytes are resident once CPU-side
|
|
1569
|
+
(engine mirror) plus the GPU buffer, instead of twice more (JS mirror,
|
|
1570
|
+
plus the creation bytes the app would keep). The contract is loud:
|
|
1571
|
+
setRecords, updateRecords, instanceAttribute and growth throw naming
|
|
1572
|
+
the transfer; setRecordCount still works (with an instanceOrder the
|
|
1573
|
+
engine re-sorts and republishes the first n records of ITS copy),
|
|
1574
|
+
and so do setVisible, transforms, re-attach and disposeInstances (which
|
|
1575
|
+
frees the engine copy with the buffer). Takes a single-stream material;
|
|
1576
|
+
with an instanceOrder the engine's retained copy is implied. The engine
|
|
1577
|
+
mirror is BUFFER-lifetime, so a detached-and-re-attached ordered entry
|
|
1578
|
+
(setLayers re-admission included) seeds its order from it with no
|
|
1579
|
+
publish from JS - true for every retained ordered mesh since this
|
|
1580
|
+
landed, transfer or not.
|
|
1581
|
+
|
|
1582
|
+
The INDEXED form, `instanceOrder: { position: "iCenter", records:
|
|
1583
|
+
LAYOUT, descending: true }` (createRecordMesh only), is for a population
|
|
1584
|
+
whose records live in a DATA TEXTURE the vertex stage fetches by record
|
|
1585
|
+
index - a splat cloud, any large write-once set whose per-instance
|
|
1586
|
+
frontend cost matters. `records` is the layout of the record bytes you
|
|
1587
|
+
pass (the key attribute lives there), and the material's ONE instance
|
|
1588
|
+
buffer is the id stream: uint32/uint32x4 attributes the engine fills
|
|
1589
|
+
with the sorted record indices, K ids per instance record, so one drawn
|
|
1590
|
+
instance covers K records - repeat the geometry K times and pick the
|
|
1591
|
+
record with `gl_VertexID` (quad `gl_VertexID / 4`), fetch it by id
|
|
1592
|
+
(`texelFetch` on an `rgba32ui` texture the app built from the same
|
|
1593
|
+
bytes and bound per mesh), and cull the id 0xFFFFFFFF that pads the
|
|
1594
|
+
last group. Transfer is implied (the records are the engine's sort
|
|
1595
|
+
key source), a re-sort uploads 4 bytes per record instead of the
|
|
1596
|
+
record, and `count` stays in records while the entry draws
|
|
1597
|
+
ceil(count / K) instances (`drawCount`). `MeshInstances.textures` holds
|
|
1598
|
+
data textures the population owns, freed by disposeInstances.
|
|
1599
|
+
|
|
1526
1600
|
### Background
|
|
1527
1601
|
|
|
1528
1602
|
Background: `scene.setBackground(source | null)`, the `background` option
|
|
@@ -1590,7 +1664,7 @@ specular, `envIrradiance` - the fully rough sample along the normal, as
|
|
|
1590
1664
|
Three's getIBLIrradiance and Godot read it - added to the hemisphere
|
|
1591
1665
|
for the diffuse) and every `phong({ reflectivity })` material mirrors,
|
|
1592
1666
|
typically the skybox's own cube turned with it. The cube to use is a
|
|
1593
|
-
BAKED one: `
|
|
1667
|
+
BAKED one: `bun run srt tool 3d/environment sky.hdr -o assets/sky.srte`
|
|
1594
1668
|
turns an equirectangular Radiance .hdr (Poly Haven's are CC0) into the
|
|
1595
1669
|
six faces plus the GGX-prefiltered mip chain in linear float, and
|
|
1596
1670
|
`await loadEnvironment("assets/sky.srte")` uploads it as an explicit
|
|
@@ -1614,8 +1688,27 @@ sky-lit reflections: ONE `uEnv` samplerCube bound on every target the
|
|
|
1614
1688
|
scene draws into (a 1x1 black placeholder while unset) and one
|
|
1615
1689
|
shared-params write (`uEnvIntensity`, `uEnvRotation`, `uEnvOn`), however
|
|
1616
1690
|
many meshes reflect; no per-material envMap (Three's Basic/Phong
|
|
1617
|
-
`envMap`
|
|
1618
|
-
`@solidrt/3d/glsl
|
|
1691
|
+
`envMap` sits on the MATERIAL, but materials are shared here) - a
|
|
1692
|
+
custom material composes ENVIRONMENT from `@solidrt/3d/glsl`, and a
|
|
1693
|
+
single object's exception is PER MESH: `Mesh.environment`
|
|
1694
|
+
(`setEnvironment(mesh, env)`, the `environment` prop on every mesh
|
|
1695
|
+
component). `null` removes the environment term from that mesh - the
|
|
1696
|
+
sealed-interior pair to masked lights, since light `layers` cannot mask
|
|
1697
|
+
image lighting; Unity's reflectionProbeUsage Off, Godot's
|
|
1698
|
+
disable_ambient_light in spirit - an `{ cube, intensity?, rotation? }`
|
|
1699
|
+
samples that cube instead of the scene's (an interior probe's cube, a
|
|
1700
|
+
baked room - Unity assigns probes by volume with an anchor override,
|
|
1701
|
+
Godot by probe box and cull_mask; here the app assigns explicitly),
|
|
1702
|
+
and absent follows the scene. The same uEnv* names written per ENTRY,
|
|
1703
|
+
which beat the target's shared values, so it costs nothing per frame;
|
|
1704
|
+
the change rebuilds the mesh's entries, a state switch (a doorway
|
|
1705
|
+
crossing), not a per-frame dial. Applied where the material reflects at
|
|
1706
|
+
all (`standard`, `phong({ reflectivity })`, a custom ENVIRONMENT
|
|
1707
|
+
composer - `Material.env`); on any other material it is a no-op, so an
|
|
1708
|
+
interior can be masked wholesale. A reflection probe drawing such a
|
|
1709
|
+
mesh never samples its own cube through it (the black placeholder
|
|
1710
|
+
stands in, the same rule the scene's own environment follows on probe
|
|
1711
|
+
faces). `reflectivity` 0..1 is the
|
|
1619
1712
|
face-on weight, rising to 1 at grazing angles (Schlick), mixed in as
|
|
1620
1713
|
`rgb = mix(rgb, reflection, weight)`: 1 is chrome, ~0.05 a glossy
|
|
1621
1714
|
dielectric with rim reflections; Three's Phong `reflectivity` under its
|
|
@@ -1915,7 +2008,7 @@ resolve - at one of three tiers, top first:
|
|
|
1915
2008
|
maps and encodes it). A custom LIGHT MODEL
|
|
1916
2009
|
loops `sceneLight(i, position, normal)` to `uLightCount` instead of a
|
|
1917
2010
|
shade function: light i's direction and its color already attenuated,
|
|
1918
|
-
cone-faded and shadowed (zero when it cannot reach). The stock
|
|
2011
|
+
cone-faded, layer-masked and shadowed (zero when it cannot reach). The stock
|
|
1919
2012
|
materials are built from this same set, so the tiers cannot drift.
|
|
1920
2013
|
The demo `the-third-dimension.tsx` has tier 2 (the ground) and tier 3
|
|
1921
2014
|
(the knot's rim term).
|
|
@@ -1939,7 +2032,7 @@ instance or every caster move warns about the skipped write.
|
|
|
1939
2032
|
### Lights
|
|
1940
2033
|
|
|
1941
2034
|
Lights and `phong`: lights are graph NODES, like Three. `createDirectionalLight({
|
|
1942
|
-
direction?, color?, intensity? })` / `<DirectionalLight>` is parallel light
|
|
2035
|
+
direction?, color?, intensity?, layers? })` / `<DirectionalLight>` is parallel light
|
|
1943
2036
|
travelling along `direction` in the node's LOCAL space (default `[0, -1,
|
|
1944
2037
|
0]`, a sun overhead; length ignored), so a parent Group's rotation turns it
|
|
1945
2038
|
and position/scale do not matter - deliberately a direction, not Three's
|
|
@@ -1958,7 +2051,16 @@ decay? })` / `<PointLight>` is the omnidirectional version: position
|
|
|
1958
2051
|
only, same falloff, no cone. `createHemisphereLight({ sky?, ground?, intensity?
|
|
1959
2052
|
})` / `<HemisphereLight>` is the ambient term, a gradient by the WORLD
|
|
1960
2053
|
normal's tilt (fixed to world up, the node's transform is ignored); one per
|
|
1961
|
-
scene, the last attached wins.
|
|
2054
|
+
scene, the last attached wins. EVERY light (the hemisphere included)
|
|
2055
|
+
takes `layers?: number`, a filter bitmask defaulting to all bits set:
|
|
2056
|
+
the light lights, and a casting light shadows, only the meshes whose
|
|
2057
|
+
`layers` intersect it (Unity's cullingMask, Godot's light_cull_mask;
|
|
2058
|
+
Three has none) - the sealed-interiors pattern's light half (see Views
|
|
2059
|
+
and layers), one integer AND per light per fragment on two uniforms,
|
|
2060
|
+
so the branch is coherent per draw. A mesh a casting light's mask
|
|
2061
|
+
excludes also draws into none of its shadow maps. The environment term
|
|
2062
|
+
is not a light and not masked: its per-mesh knob is `Mesh.environment`
|
|
2063
|
+
(see Environment). Placement goes through setTransform, the
|
|
1962
2064
|
light's own fields through `setLight(light, { ... })` (frame-rate-safe,
|
|
1963
2065
|
like setMeshParams). At most `MAX_LIGHTS` (8, exported from the root and `/glsl`)
|
|
1964
2066
|
lights per scene, directional, spot and point together (the hemisphere
|
|
@@ -1971,13 +2073,21 @@ node's world rotation (direction, negated so the shader reads the
|
|
|
1971
2073
|
vector TOWARD the light) and world position, so a MOVING light costs no
|
|
1972
2074
|
JS. The sync rewrites the rest whenever a light attaches, detaches or
|
|
1973
2075
|
changes a field - `uHemiSky`/`uHemiGround` (vec3, intensity folded in),
|
|
1974
|
-
`
|
|
2076
|
+
`uHemiMask` (int, the hemisphere's layers), `uLightCount` (int),
|
|
2077
|
+
`uLightType[N]` (LIGHT_DIRECTIONAL | LIGHT_SPOT |
|
|
1975
2078
|
LIGHT_POINT), `uLightDir[N]`/`uLightPos[N]`/`uLightColor[N]` (intensity
|
|
1976
|
-
folded into the color)
|
|
1977
|
-
distance, decay)
|
|
2079
|
+
folded into the color), `uLightParams[N]` (cosInner, cosOuter,
|
|
2080
|
+
distance, decay) and `uLightMask[N]` (each light's layers; masks cross
|
|
2081
|
+
as SIGNED 32-bit ints, all bits = -1, the shader only ANDs the bits) -
|
|
2082
|
+
so a custom fragment composing `LIGHT_SLOTS` +
|
|
1978
2083
|
`LIGHT_LOOKUP` from `/glsl` reads the same list through `lightVector(i,
|
|
1979
2084
|
worldPos, out l)` (returns the attenuation, 0 = skip the light; `phong`
|
|
1980
2085
|
is the shape), and a light change costs one write however many meshes.
|
|
2086
|
+
The mesh side of the mask is `uniform int uLayers` (declared by
|
|
2087
|
+
LIGHT_LOOKUP, tested first in lightVector): a per-entry param the scene
|
|
2088
|
+
seeds from `mesh.layers` and follows through setLayers on every
|
|
2089
|
+
material whose source mentions it - never per frame, and never
|
|
2090
|
+
something a fragment declares itself.
|
|
1981
2091
|
A custom fragment that declares only the old directional subset
|
|
1982
2092
|
(`uLightCount`/`uLightDir`/`uLightColor`) still works - it just shades
|
|
1983
2093
|
every light as directional, so keep such materials to directional-only
|
|
@@ -2117,7 +2227,9 @@ index into `images`, `doubleSided`, `transparent` = alphaMode BLEND,
|
|
|
2117
2227
|
`alphaMode` as written and `alphaCutoff`, spec default 0.5, the
|
|
2118
2228
|
normal and emissive slots, `metalness`/`roughness` factors and the
|
|
2119
2229
|
packed `metalnessRoughnessMap` - standard's inputs), `images`
|
|
2120
|
-
(the encoded PNG
|
|
2230
|
+
(each `{ bytes, name, uri? }`: the encoded PNG, JPEG or KTX2 file,
|
|
2231
|
+
undecoded; its name in the document, else the uri of its file, else
|
|
2232
|
+
"image<i>"; the `uri` when it is a file of its own) and `bounds` (world-space rest
|
|
2121
2233
|
pose: a part's box through its node, a skinned part's per-joint boxes
|
|
2122
2234
|
through the joints' rest transforms - where the skin places it, armature
|
|
2123
2235
|
scale included; conservative under rotation). External
|
|
@@ -2165,7 +2277,8 @@ its true roots; a hierarchy reaching a node twice (a cycle) throws.
|
|
|
2165
2277
|
#### createModel
|
|
2166
2278
|
|
|
2167
2279
|
`createModel(data, { material?, label?, autoFree? })` - uploads the images (repeat
|
|
2168
|
-
wrap, mipmapped, 4x anisotropic
|
|
2280
|
+
wrap, mipmapped, 4x anisotropic; each texture is labeled `<label>-<image name>`, so the
|
|
2281
|
+
`/gpu` inventory names the image), makes one material per glTF material (default `standard`
|
|
2169
2282
|
with the file's color, maps, normal scale, metalness/roughness and
|
|
2170
2283
|
packed map, emissive and transparency - the glTF material model, so a
|
|
2171
2284
|
scene showing a model wants an `environment` (a glTF metal in a scene
|
|
@@ -2213,14 +2326,118 @@ and one loaded in a component with its unmount. A load that settles
|
|
|
2213
2326
|
after its scope is gone (unmounted mid-load) is freed on arrival and
|
|
2214
2327
|
resolves already disposed - nothing owns it any more. `loadModel` reads the baked `.srtm` written by `srt tool
|
|
2215
2328
|
3d/model <in.gltf|glb> -o assets/<name>.srtm`: the same parse run once
|
|
2216
|
-
|
|
2217
|
-
bytes plus the image decodes. Numbers from a 32k-vertex, 6-texture model
|
|
2329
|
+
at build time, stored in the GPU layout, so loading is views onto the
|
|
2330
|
+
file's bytes plus the image decodes. Numbers from a 32k-vertex, 6-texture model
|
|
2218
2331
|
on a release client: `parseGltf` 124 ms on flux (22 ms under bun) against
|
|
2219
2332
|
40 ms for the whole baked load - the runtime parse is fine for small
|
|
2220
2333
|
models and a binary import (`import bytes from "./x.glb" with { type:
|
|
2221
2334
|
"binary" }` then `createModel(parseGltf(bytes))`, see
|
|
2222
2335
|
`examples/model.tsx`); bake anything big.
|
|
2223
2336
|
|
|
2337
|
+
#### Compressed textures
|
|
2338
|
+
|
|
2339
|
+
`srt tool 3d/model <in> -o assets/<name>.srtm` bakes the images
|
|
2340
|
+
too: each becomes a KTX2 compressed texture with its mip chain. The file is the
|
|
2341
|
+
same for every platform; at load each device turns the images into the
|
|
2342
|
+
block format its GPU samples (BC7 on desktops, ETC2 elsewhere), which
|
|
2343
|
+
holds them in a quarter of the memory of the decoded image. Measured on
|
|
2344
|
+
the Khronos Sponza sample (69 images): 363 MB of textures as PNG/JPEG,
|
|
2345
|
+
91 MB compressed. The encode is slow (a minute for that
|
|
2346
|
+
model) and runs once, at bake time. Block compression is lossy: judge
|
|
2347
|
+
the result on your own textures. What it saves is GPU memory; the file
|
|
2348
|
+
shrinks for color maps and can grow for data maps.
|
|
2349
|
+
|
|
2350
|
+
How the images are compressed, and whether, is the app's to say, in the
|
|
2351
|
+
`textures` group of the `solidrt` key in its package.json (the project
|
|
2352
|
+
the tool runs in, whose root is where the tool is run). These are the
|
|
2353
|
+
defaults; name only what differs:
|
|
2354
|
+
|
|
2355
|
+
```json
|
|
2356
|
+
"solidrt": {
|
|
2357
|
+
"textures": {
|
|
2358
|
+
"compress": true,
|
|
2359
|
+
"color": { "codec": "etc1s", "quality": 0.75 },
|
|
2360
|
+
"normal": { "codec": "uastc", "quality": 0.9 },
|
|
2361
|
+
"data": { "codec": "uastc", "quality": 0.9 },
|
|
2362
|
+
"files": []
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
```
|
|
2366
|
+
|
|
2367
|
+
Three levels, the narrower one winning: the app (`compress`), a kind
|
|
2368
|
+
of map (`compress`, `codec`, `quality`), and single files:
|
|
2369
|
+
|
|
2370
|
+
```json
|
|
2371
|
+
"files": [
|
|
2372
|
+
{ "match": "assets/sponza/textures/lion_*.png", "codec": "uastc", "quality": 0.95 },
|
|
2373
|
+
{ "match": ["assets/ui/**", "assets/hero.glb#lut*"], "compress": false }
|
|
2374
|
+
]
|
|
2375
|
+
```
|
|
2376
|
+
|
|
2377
|
+
- `match` is a glob pattern, or a list of them, against the image's
|
|
2378
|
+
path FROM THE PROJECT ROOT, written with "/": `*` inside a segment,
|
|
2379
|
+
`**` across segments, `?`, `[a-z]`, `[!a-z]` (`matchesGlob` of
|
|
2380
|
+
flux:path; no `{a,b}`, list the patterns). Not the uri the glTF
|
|
2381
|
+
writes: two models with a `textures/base.png` each are two files.
|
|
2382
|
+
- An image embedded in the model (a .glb) has no file of its own: the
|
|
2383
|
+
model's path names every image in it, and `#name` after it picks
|
|
2384
|
+
the ones of that name (the glTF image's `name`, "image<i>" without
|
|
2385
|
+
one).
|
|
2386
|
+
- An entry sets `compress`, `codec`, `quality` or several, over what
|
|
2387
|
+
the image's kind says. Where several entries name one image, a
|
|
2388
|
+
later entry wins, field by field.
|
|
2389
|
+
- `"compress": false` keeps the image as its PNG or JPEG. That is for
|
|
2390
|
+
what must come back exact (a lookup table, a distance field) and
|
|
2391
|
+
for sharp art and text, which block compression damages.
|
|
2392
|
+
- A pattern that names nothing FAILS the bake, before anything is
|
|
2393
|
+
encoded: no file of the project matches it, or the baked model has
|
|
2394
|
+
no embedded image of that name. A misspelled pattern would
|
|
2395
|
+
otherwise do nothing and say nothing.
|
|
2396
|
+
|
|
2397
|
+
`--compress` and `--no-compress` stand in for the app's `compress` for
|
|
2398
|
+
one run (a quick bake while the model is still changing); what a kind
|
|
2399
|
+
or an entry says about itself still holds.
|
|
2400
|
+
|
|
2401
|
+
The kinds are core's and say what the texels are (`@solidrt/core/textures`).
|
|
2402
|
+
This package maps a material's slots onto them: base color and emissive
|
|
2403
|
+
maps are `color`, normal maps are `normal`, metallic-roughness maps
|
|
2404
|
+
(occlusion rides in the same image) are `data`.
|
|
2405
|
+
An image several slots share takes the most demanding kind, normal over
|
|
2406
|
+
color over data, and stays sRGB when one of them is a color slot;
|
|
2407
|
+
`modelImageUses(data)` returns that per image, the rule the loader and
|
|
2408
|
+
the bake both follow, for a bake script of your own.
|
|
2409
|
+
`codec` and `quality` are `encodeTexture`'s: "etc1s" is small (a
|
|
2410
|
+
quarter of a JPEG), "uastc" is accurate and large (often larger than
|
|
2411
|
+
the JPEG it replaces), `quality` is 0..1. A normal map steers the
|
|
2412
|
+
lighting and shows its errors, and a metallic-roughness map packs
|
|
2413
|
+
unrelated values into its channels, which the small codec handles
|
|
2414
|
+
badly, so both take the accurate one (what the Khronos KTX guide
|
|
2415
|
+
recommends). Set `data` to "etc1s" when the download matters more than
|
|
2416
|
+
roughness detail: on the Khronos Sponza sample that is 9 MB less for
|
|
2417
|
+
about 2 dB in the views that glossy surfaces fill. An unknown key, codec or field fails the bake naming the key,
|
|
2418
|
+
before anything is encoded, and the bake prints how many images it
|
|
2419
|
+
compressed with which codec and quality, and how many it kept. The color space (sRGB for `color`), the mip chain and the
|
|
2420
|
+
wrap are not settings: they follow how `createModel` samples. The
|
|
2421
|
+
settings are the app's, not this package's: an app's own bake script,
|
|
2422
|
+
or another extension's, reads the same group with
|
|
2423
|
+
`textureSettings(packageJson)` from `@solidrt/core/textures` and asks
|
|
2424
|
+
`textureSettingFor(settings, kind, { file, name }, matchesGlob)` per
|
|
2425
|
+
texture (core's AGENTS.md has the rest).
|
|
2426
|
+
|
|
2427
|
+
`loadModel` and `loadGltf` handle such a model as they are, and
|
|
2428
|
+
`loadGltf` reads a glTF that carries KTX2 images itself
|
|
2429
|
+
(`KHR_texture_basisu`). Only the primitives show the step: transcoding
|
|
2430
|
+
is asynchronous and `createModel` is not, so bytes obtained another way
|
|
2431
|
+
go through `transcodeModelImages` between the two:
|
|
2432
|
+
|
|
2433
|
+
```ts
|
|
2434
|
+
let data = await transcodeModelImages(decodeModel(bytes))
|
|
2435
|
+
let model = createModel(data)
|
|
2436
|
+
```
|
|
2437
|
+
|
|
2438
|
+
`createModel` on data whose KTX2 images were not transcoded throws and
|
|
2439
|
+
names the function. Data without KTX2 images needs no such step.
|
|
2440
|
+
|
|
2224
2441
|
#### Baking your own geometry
|
|
2225
2442
|
|
|
2226
2443
|
Geometry an app GENERATES (a terrain, a city block, a hundred thousand
|
|
@@ -2479,9 +2696,82 @@ mesh (JS-written records, no nodes) cannot morph: rejected at add().
|
|
|
2479
2696
|
Not in the subset, dropped: tangents and further UV sets; samplers are
|
|
2480
2697
|
ignored (every texture repeats); additive blending draws as base color.
|
|
2481
2698
|
The follow-ups are filed in okf/backlog/3d-model-loader.md. The `.srtm`
|
|
2482
|
-
container is VERSION
|
|
2483
|
-
skins, clips, packed morph targets
|
|
2484
|
-
with `srt tool 3d/model`.
|
|
2699
|
+
container is VERSION 11 (node table in the header, node-local vertices,
|
|
2700
|
+
skins, clips, packed morph targets, app data, images under their
|
|
2701
|
+
names); older bakes are rejected - re-bake with `srt tool 3d/model`.
|
|
2702
|
+
|
|
2703
|
+
## Splats
|
|
2704
|
+
|
|
2705
|
+
Captured 3D Gaussian splat scenes (phone scans, photogrammetry
|
|
2706
|
+
successors) as ordinary content, the model-loading split repeated:
|
|
2707
|
+
|
|
2708
|
+
- `@solidrt/3d/splat` (src/splat-data.ts) is the runtime-free data side,
|
|
2709
|
+
the model-data rule: `parseSplat(bytes, { format?, keepOrientation? })`
|
|
2710
|
+
reads a trainer's `.ply`, the de-facto `.splat` interchange or a
|
|
2711
|
+
GUNZIPPED `.spz` (Niantic, version 2 or 3; the entry stays
|
|
2712
|
+
decompression-free, so inflate first) and bakes it - the 3D covariance
|
|
2713
|
+
precomputed per splat (R S St Rt, the vertex-stage wall every
|
|
2714
|
+
bake-less viewer pays at each corner), records importance-sorted (size
|
|
2715
|
+
times opacity, so the first n are the scene at n), y-down captures
|
|
2716
|
+
(.ply, .splat) stood up to y-up (positions and covariances rotated
|
|
2717
|
+
together; `keepOrientation` opts out, `.spz` is y-up already), bounds
|
|
2718
|
+
measured. `encodeSplat`/`decodeSplat` round-trip the `.srts` container
|
|
2719
|
+
(the record and SH blocks, next bullet); decode is a header parse
|
|
2720
|
+
plus byte VIEWS, nothing per-splat. `srt tool 3d/splat <in> [-o
|
|
2721
|
+
out.srts] [--sh 0..3] [--keep-orientation]` is the parse and bake
|
|
2722
|
+
under bun (tools/splat.ts); an app bake script uses the same entry.
|
|
2723
|
+
- The `.srts` record (version 2) is 32 bytes = two rgba32ui texels:
|
|
2724
|
+
center float32x3 + sRGB color and opacity unorm8x4, then the
|
|
2725
|
+
covariance upper triangle as six float16 + a spare word
|
|
2726
|
+
(SPLAT_ATTRIBUTES as a byte layout, SPLAT_RECORD_TEXELS). `--sh 1..3`
|
|
2727
|
+
bakes that many spherical-harmonic bands beyond the base color from a
|
|
2728
|
+
trainer's `.ply` or an `.spz` (`.splat` has none), capped at the
|
|
2729
|
+
capture's: a second texel block of 2/3/6 texels per splat
|
|
2730
|
+
(SPLAT_SH_TEXELS; the halves packed pairwise, coefficient-major with
|
|
2731
|
+
rgb interleaved), 32/48/96 bytes per splat of texture, so the
|
|
2732
|
+
default is 0 and a bake opts in. The y-up flip negates the
|
|
2733
|
+
coefficients whose basis is odd in (y, z).
|
|
2734
|
+
- On the runtime, `loadSplat(path)` reads a `.srts` (fetch + decode,
|
|
2735
|
+
the loadModel shape) and `createSplatMesh(data, { count?, material?,
|
|
2736
|
+
label? })` / `<SplatMesh>` shows it: a RecordMesh in the INDEXED form
|
|
2737
|
+
(see "Instance order") - the records handed to the engine as the
|
|
2738
|
+
depth sort's key source AND uploaded once as the `rgba32ui` record
|
|
2739
|
+
texture the vertex stage fetches by id (`uSplatRecords`, plus
|
|
2740
|
+
`uSplatSh` when baked; bound per mesh, so a forked material keeps
|
|
2741
|
+
them), SPLAT_GROUP (16) splats per drawn instance of a merged-quad
|
|
2742
|
+
geometry, the header bounds as the cull and picking box, and the
|
|
2743
|
+
id stream sorted back to front core-side - the cloud composes as a
|
|
2744
|
+
mesh (depth-tested against opaque geometry, placed by the transparent
|
|
2745
|
+
entry sort, transformed by its node), a camera turn re-sorts and
|
|
2746
|
+
republishes 4 bytes per splat with zero per-frame JS, and a parked
|
|
2747
|
+
camera uploads nothing. Write-once: treat the SplatData as consumed
|
|
2748
|
+
and drop it; at 1M splats the cloud is resident as the engine's 32 MB
|
|
2749
|
+
key copy plus 32 MB of texture plus a 4 MB id buffer, and
|
|
2750
|
+
disposeInstances frees all three. `splatMaterialClass(shDegree)` is
|
|
2751
|
+
the stock class per degree (a shared instance each), and
|
|
2752
|
+
`splatVertexSource(shDegree)` / SPLAT_FRAGMENT / SPLAT_ID_ATTRIBUTES
|
|
2753
|
+
are exported for forks; `count` rounds up to whole groups.
|
|
2754
|
+
- The vertex stage projects the baked covariance through the Jacobian
|
|
2755
|
+
of `uViewProj * uModel` (no focal/view uniforms; any node transform
|
|
2756
|
+
shapes the footprint with the same matrix that moves the center),
|
|
2757
|
+
reads the target size from the standard `uViewport`, and evaluates
|
|
2758
|
+
the SH bands per corner from the view direction (`uCamPos` through
|
|
2759
|
+
the transposed model rotation - exact under rotation and uniform
|
|
2760
|
+
scale). The data textures' row width is the device's (up to 4096),
|
|
2761
|
+
compiled into the source.
|
|
2762
|
+
- Why this shape: on the Pixel 7 at 1M splats the one-quad-per-instance
|
|
2763
|
+
form spent two thirds of its vertex side on the instancing frontend;
|
|
2764
|
+
the indexed form takes the full-res frame from 45 to 31 ms and the
|
|
2765
|
+
vertex side from 24 to 8 ms, the per-vertex fetch and random id order
|
|
2766
|
+
cost under a millisecond, and SH1-3 are near free (the bands cost
|
|
2767
|
+
memory, not time). What remains at 1M is fill (overdraw). The table
|
|
2768
|
+
is in okf/done/gaussian-splats.md.
|
|
2769
|
+
- Two capture caveats worth knowing at the app level: captures are
|
|
2770
|
+
TRAINED against sRGB blending, so show them in a display-space scene
|
|
2771
|
+
(`blendSpace: "display"`) - a linear scene lays a milky veil over
|
|
2772
|
+
stacked translucents - and a capture is only clean near the camera
|
|
2773
|
+
path it was taken from (elsewhere it is floaters), so keep the camera
|
|
2774
|
+
near that path.
|
|
2485
2775
|
|
|
2486
2776
|
## Traps
|
|
2487
2777
|
|
package/examples/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @solidrt/3d examples
|
|
2
2
|
|
|
3
|
-
One concept per file; run with `
|
|
3
|
+
One concept per file; run with `bun run srt 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
|
|
@@ -58,7 +58,7 @@ depends on `@solidrt/3d` (or in-repo from the package directory).
|
|
|
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
60
|
(no lights, no hemisphere): `loadEnvironment` reads the .srte that
|
|
61
|
-
`
|
|
61
|
+
`bun run srt tool 3d/environment <panorama>.hdr -o assets/environment.srte`
|
|
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
|
-
`
|
|
203
|
+
`bun run srt tool 3d/model examples/model.glb -o assets/model.srtm`.
|