@solidrt/3d 0.0.62 → 0.0.64
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +198 -17
- package/package.json +3 -2
- package/src/camera.ts +6 -2
- package/src/components/directional-light.tsx +6 -2
- package/src/components/hemisphere-light.tsx +12 -4
- package/src/components/index.ts +2 -0
- package/src/components/instanced-mesh.tsx +6 -2
- package/src/components/mesh.tsx +12 -1
- package/src/components/point-light.tsx +6 -2
- package/src/components/record-mesh.tsx +14 -2
- package/src/components/scene.tsx +8 -1
- package/src/components/splat-mesh.tsx +50 -0
- package/src/components/spot-light.tsx +7 -3
- package/src/environment.ts +55 -0
- package/src/glsl.ts +68 -14
- package/src/index.ts +9 -4
- package/src/light.ts +56 -4
- package/src/material.ts +26 -1
- package/src/mesh.ts +361 -6
- package/src/node.ts +3 -0
- package/src/scene-shadows.ts +8 -7
- package/src/scene.ts +225 -104
- package/src/splat-data.ts +617 -0
- package/src/splat.ts +401 -0
- package/tools/splat.ts +70 -0
package/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,12 +545,15 @@ 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
553
|
`tests/model-data.test.ts` imports it under `bun test`, so a runtime
|
|
546
554
|
import creeping into any of them fails there; keep them that way.
|
|
555
|
+
`src/splat-data.ts` is its own published entry the same way
|
|
556
|
+
(`@solidrt/3d/splat`, guarded by `tests/splat-data.test.ts`).
|
|
547
557
|
The rigs under `checks/`
|
|
548
558
|
(`geometry-check`, `sweep-check`, `pick-check`, `dispatch-check`,
|
|
549
559
|
`gltf-check`) run on
|
|
@@ -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)` |
|
|
@@ -1333,7 +1344,10 @@ with uViewProj - the specular/fresnel view vector is
|
|
|
1333
1344
|
uViewProj rows, that carries the clip flip), `uniform mat4
|
|
1334
1345
|
uInvViewProj` (the camera's inverse view-projection, shared likewise -
|
|
1335
1346
|
a clip position back to world, the world-space ray through a pixel
|
|
1336
|
-
without knowing the projection)
|
|
1347
|
+
without knowing the projection), `uniform vec2 uViewport` (the target's
|
|
1348
|
+
pixel size, shared likewise and per target - each view feeds its own -
|
|
1349
|
+
so a pixel-space footprint like the splat material's needs no app
|
|
1350
|
+
write) and `uniform mat4 uNormal` (the world
|
|
1337
1351
|
inverse-transpose, written beside uModel for this material's meshes;
|
|
1338
1352
|
take `mat3(uNormal)` - correct under non-uniform scale, where
|
|
1339
1353
|
mat3(uModel) bends normals off the surface). Attributes come from the
|
|
@@ -1491,7 +1505,8 @@ or in five figures, belong to the function face.
|
|
|
1491
1505
|
#### createRecordMesh
|
|
1492
1506
|
|
|
1493
1507
|
`createRecordMesh(geometry, material, records, count?, { bounds?,
|
|
1494
|
-
label? })` is the raw form: `records` is the
|
|
1508
|
+
instanceOrder?, label? })` is the raw form: `records` is the
|
|
1509
|
+
per-instance data laid out
|
|
1495
1510
|
in the material's first instance layout - a Float32Array over an
|
|
1496
1511
|
all-float layout, otherwise bytes built with `vertexView(layout, buffer)`
|
|
1497
1512
|
and `attributeAccess` like a geometry stream (a length that is not whole
|
|
@@ -1523,6 +1538,63 @@ unhide, renderOrder/params/geometry/material swaps apply, and
|
|
|
1523
1538
|
explicit free, geometry-buffer rule. `examples/fleet.tsx` (instances,
|
|
1524
1539
|
components) and `examples/instanced.tsx` (records) are the live proofs.
|
|
1525
1540
|
|
|
1541
|
+
#### Instance order
|
|
1542
|
+
|
|
1543
|
+
`instanceOrder` on either create (and the components) draws a
|
|
1544
|
+
population's records in key order, decided core-side; slots as you write
|
|
1545
|
+
them never move. `{ position: "iCenter", retain: true, descending: true
|
|
1546
|
+
}` names a float32x3/x4 attribute holding a local-space position: records
|
|
1547
|
+
draw by their depth along the scene camera's view direction, which the
|
|
1548
|
+
SCENE feeds the core whenever the camera or the mesh turns meaningfully
|
|
1549
|
+
(mapped through the mesh's rotation; an epsilon gate meters the
|
|
1550
|
+
re-sorts) - back-to-front transparency over 100k+ records with zero
|
|
1551
|
+
per-frame JS, and with `retain` a camera turn re-sorts and republishes
|
|
1552
|
+
core-side while a parked camera uploads nothing. `{ field: "age" }`
|
|
1553
|
+
names a float32 attribute and draws by its value, re-ordered when the
|
|
1554
|
+
records publish. The costs, by design: an ordered mesh publishes the
|
|
1555
|
+
LIVE record set whole on any write (a byte range has no stable position
|
|
1556
|
+
under a permutation; `count` defines the sorted population), and one
|
|
1557
|
+
buffer holds ONE order - the scene target's entry owns it, views and
|
|
1558
|
+
shadow passes draw the same gathered buffers, and a mesh layered out of
|
|
1559
|
+
the scene target keeps its last order. Where the key sits decides
|
|
1560
|
+
nothing else: raw instancing stays buffer-order everywhere (Three,
|
|
1561
|
+
Unity, Godot), so this knob has no engine counterpart to compare
|
|
1562
|
+
against; the core primitive is okf/done/gpu-instance-order.md.
|
|
1563
|
+
|
|
1564
|
+
`transfer: true` (createRecordMesh only) is the write-once form for a
|
|
1565
|
+
large population: the records go to the ENGINE at creation and the mesh
|
|
1566
|
+
keeps no JS mirror, so the record bytes are resident once CPU-side
|
|
1567
|
+
(engine mirror) plus the GPU buffer, instead of twice more (JS mirror,
|
|
1568
|
+
plus the creation bytes the app would keep). The contract is loud:
|
|
1569
|
+
setRecords, updateRecords, instanceAttribute and growth throw naming
|
|
1570
|
+
the transfer; setRecordCount still works (with an instanceOrder the
|
|
1571
|
+
engine re-sorts and republishes the first n records of ITS copy),
|
|
1572
|
+
and so do setVisible, transforms, re-attach and disposeInstances (which
|
|
1573
|
+
frees the engine copy with the buffer). Takes a single-stream material;
|
|
1574
|
+
with an instanceOrder the engine's retained copy is implied. The engine
|
|
1575
|
+
mirror is BUFFER-lifetime, so a detached-and-re-attached ordered entry
|
|
1576
|
+
(setLayers re-admission included) seeds its order from it with no
|
|
1577
|
+
publish from JS - true for every retained ordered mesh since this
|
|
1578
|
+
landed, transfer or not.
|
|
1579
|
+
|
|
1580
|
+
The INDEXED form, `instanceOrder: { position: "iCenter", records:
|
|
1581
|
+
LAYOUT, descending: true }` (createRecordMesh only), is for a population
|
|
1582
|
+
whose records live in a DATA TEXTURE the vertex stage fetches by record
|
|
1583
|
+
index - a splat cloud, any large write-once set whose per-instance
|
|
1584
|
+
frontend cost matters. `records` is the layout of the record bytes you
|
|
1585
|
+
pass (the key attribute lives there), and the material's ONE instance
|
|
1586
|
+
buffer is the id stream: uint32/uint32x4 attributes the engine fills
|
|
1587
|
+
with the sorted record indices, K ids per instance record, so one drawn
|
|
1588
|
+
instance covers K records - repeat the geometry K times and pick the
|
|
1589
|
+
record with `gl_VertexID` (quad `gl_VertexID / 4`), fetch it by id
|
|
1590
|
+
(`texelFetch` on an `rgba32ui` texture the app built from the same
|
|
1591
|
+
bytes and bound per mesh), and cull the id 0xFFFFFFFF that pads the
|
|
1592
|
+
last group. Transfer is implied (the records are the engine's sort
|
|
1593
|
+
key source), a re-sort uploads 4 bytes per record instead of the
|
|
1594
|
+
record, and `count` stays in records while the entry draws
|
|
1595
|
+
ceil(count / K) instances (`drawCount`). `MeshInstances.textures` holds
|
|
1596
|
+
data textures the population owns, freed by disposeInstances.
|
|
1597
|
+
|
|
1526
1598
|
### Background
|
|
1527
1599
|
|
|
1528
1600
|
Background: `scene.setBackground(source | null)`, the `background` option
|
|
@@ -1614,8 +1686,27 @@ sky-lit reflections: ONE `uEnv` samplerCube bound on every target the
|
|
|
1614
1686
|
scene draws into (a 1x1 black placeholder while unset) and one
|
|
1615
1687
|
shared-params write (`uEnvIntensity`, `uEnvRotation`, `uEnvOn`), however
|
|
1616
1688
|
many meshes reflect; no per-material envMap (Three's Basic/Phong
|
|
1617
|
-
`envMap`
|
|
1618
|
-
`@solidrt/3d/glsl
|
|
1689
|
+
`envMap` sits on the MATERIAL, but materials are shared here) - a
|
|
1690
|
+
custom material composes ENVIRONMENT from `@solidrt/3d/glsl`, and a
|
|
1691
|
+
single object's exception is PER MESH: `Mesh.environment`
|
|
1692
|
+
(`setEnvironment(mesh, env)`, the `environment` prop on every mesh
|
|
1693
|
+
component). `null` removes the environment term from that mesh - the
|
|
1694
|
+
sealed-interior pair to masked lights, since light `layers` cannot mask
|
|
1695
|
+
image lighting; Unity's reflectionProbeUsage Off, Godot's
|
|
1696
|
+
disable_ambient_light in spirit - an `{ cube, intensity?, rotation? }`
|
|
1697
|
+
samples that cube instead of the scene's (an interior probe's cube, a
|
|
1698
|
+
baked room - Unity assigns probes by volume with an anchor override,
|
|
1699
|
+
Godot by probe box and cull_mask; here the app assigns explicitly),
|
|
1700
|
+
and absent follows the scene. The same uEnv* names written per ENTRY,
|
|
1701
|
+
which beat the target's shared values, so it costs nothing per frame;
|
|
1702
|
+
the change rebuilds the mesh's entries, a state switch (a doorway
|
|
1703
|
+
crossing), not a per-frame dial. Applied where the material reflects at
|
|
1704
|
+
all (`standard`, `phong({ reflectivity })`, a custom ENVIRONMENT
|
|
1705
|
+
composer - `Material.env`); on any other material it is a no-op, so an
|
|
1706
|
+
interior can be masked wholesale. A reflection probe drawing such a
|
|
1707
|
+
mesh never samples its own cube through it (the black placeholder
|
|
1708
|
+
stands in, the same rule the scene's own environment follows on probe
|
|
1709
|
+
faces). `reflectivity` 0..1 is the
|
|
1619
1710
|
face-on weight, rising to 1 at grazing angles (Schlick), mixed in as
|
|
1620
1711
|
`rgb = mix(rgb, reflection, weight)`: 1 is chrome, ~0.05 a glossy
|
|
1621
1712
|
dielectric with rim reflections; Three's Phong `reflectivity` under its
|
|
@@ -1915,7 +2006,7 @@ resolve - at one of three tiers, top first:
|
|
|
1915
2006
|
maps and encodes it). A custom LIGHT MODEL
|
|
1916
2007
|
loops `sceneLight(i, position, normal)` to `uLightCount` instead of a
|
|
1917
2008
|
shade function: light i's direction and its color already attenuated,
|
|
1918
|
-
cone-faded and shadowed (zero when it cannot reach). The stock
|
|
2009
|
+
cone-faded, layer-masked and shadowed (zero when it cannot reach). The stock
|
|
1919
2010
|
materials are built from this same set, so the tiers cannot drift.
|
|
1920
2011
|
The demo `the-third-dimension.tsx` has tier 2 (the ground) and tier 3
|
|
1921
2012
|
(the knot's rim term).
|
|
@@ -1939,7 +2030,7 @@ instance or every caster move warns about the skipped write.
|
|
|
1939
2030
|
### Lights
|
|
1940
2031
|
|
|
1941
2032
|
Lights and `phong`: lights are graph NODES, like Three. `createDirectionalLight({
|
|
1942
|
-
direction?, color?, intensity? })` / `<DirectionalLight>` is parallel light
|
|
2033
|
+
direction?, color?, intensity?, layers? })` / `<DirectionalLight>` is parallel light
|
|
1943
2034
|
travelling along `direction` in the node's LOCAL space (default `[0, -1,
|
|
1944
2035
|
0]`, a sun overhead; length ignored), so a parent Group's rotation turns it
|
|
1945
2036
|
and position/scale do not matter - deliberately a direction, not Three's
|
|
@@ -1958,7 +2049,16 @@ decay? })` / `<PointLight>` is the omnidirectional version: position
|
|
|
1958
2049
|
only, same falloff, no cone. `createHemisphereLight({ sky?, ground?, intensity?
|
|
1959
2050
|
})` / `<HemisphereLight>` is the ambient term, a gradient by the WORLD
|
|
1960
2051
|
normal's tilt (fixed to world up, the node's transform is ignored); one per
|
|
1961
|
-
scene, the last attached wins.
|
|
2052
|
+
scene, the last attached wins. EVERY light (the hemisphere included)
|
|
2053
|
+
takes `layers?: number`, a filter bitmask defaulting to all bits set:
|
|
2054
|
+
the light lights, and a casting light shadows, only the meshes whose
|
|
2055
|
+
`layers` intersect it (Unity's cullingMask, Godot's light_cull_mask;
|
|
2056
|
+
Three has none) - the sealed-interiors pattern's light half (see Views
|
|
2057
|
+
and layers), one integer AND per light per fragment on two uniforms,
|
|
2058
|
+
so the branch is coherent per draw. A mesh a casting light's mask
|
|
2059
|
+
excludes also draws into none of its shadow maps. The environment term
|
|
2060
|
+
is not a light and not masked: its per-mesh knob is `Mesh.environment`
|
|
2061
|
+
(see Environment). Placement goes through setTransform, the
|
|
1962
2062
|
light's own fields through `setLight(light, { ... })` (frame-rate-safe,
|
|
1963
2063
|
like setMeshParams). At most `MAX_LIGHTS` (8, exported from the root and `/glsl`)
|
|
1964
2064
|
lights per scene, directional, spot and point together (the hemisphere
|
|
@@ -1971,13 +2071,21 @@ node's world rotation (direction, negated so the shader reads the
|
|
|
1971
2071
|
vector TOWARD the light) and world position, so a MOVING light costs no
|
|
1972
2072
|
JS. The sync rewrites the rest whenever a light attaches, detaches or
|
|
1973
2073
|
changes a field - `uHemiSky`/`uHemiGround` (vec3, intensity folded in),
|
|
1974
|
-
`
|
|
2074
|
+
`uHemiMask` (int, the hemisphere's layers), `uLightCount` (int),
|
|
2075
|
+
`uLightType[N]` (LIGHT_DIRECTIONAL | LIGHT_SPOT |
|
|
1975
2076
|
LIGHT_POINT), `uLightDir[N]`/`uLightPos[N]`/`uLightColor[N]` (intensity
|
|
1976
|
-
folded into the color)
|
|
1977
|
-
distance, decay)
|
|
2077
|
+
folded into the color), `uLightParams[N]` (cosInner, cosOuter,
|
|
2078
|
+
distance, decay) and `uLightMask[N]` (each light's layers; masks cross
|
|
2079
|
+
as SIGNED 32-bit ints, all bits = -1, the shader only ANDs the bits) -
|
|
2080
|
+
so a custom fragment composing `LIGHT_SLOTS` +
|
|
1978
2081
|
`LIGHT_LOOKUP` from `/glsl` reads the same list through `lightVector(i,
|
|
1979
2082
|
worldPos, out l)` (returns the attenuation, 0 = skip the light; `phong`
|
|
1980
2083
|
is the shape), and a light change costs one write however many meshes.
|
|
2084
|
+
The mesh side of the mask is `uniform int uLayers` (declared by
|
|
2085
|
+
LIGHT_LOOKUP, tested first in lightVector): a per-entry param the scene
|
|
2086
|
+
seeds from `mesh.layers` and follows through setLayers on every
|
|
2087
|
+
material whose source mentions it - never per frame, and never
|
|
2088
|
+
something a fragment declares itself.
|
|
1981
2089
|
A custom fragment that declares only the old directional subset
|
|
1982
2090
|
(`uLightCount`/`uLightDir`/`uLightColor`) still works - it just shades
|
|
1983
2091
|
every light as directional, so keep such materials to directional-only
|
|
@@ -2483,6 +2591,79 @@ container is VERSION 8 (node table in the header, node-local vertices,
|
|
|
2483
2591
|
skins, clips, packed morph targets); older bakes are rejected - re-bake
|
|
2484
2592
|
with `srt tool 3d/model`.
|
|
2485
2593
|
|
|
2594
|
+
## Splats
|
|
2595
|
+
|
|
2596
|
+
Captured 3D Gaussian splat scenes (phone scans, photogrammetry
|
|
2597
|
+
successors) as ordinary content, the model-loading split repeated:
|
|
2598
|
+
|
|
2599
|
+
- `@solidrt/3d/splat` (src/splat-data.ts) is the runtime-free data side,
|
|
2600
|
+
the model-data rule: `parseSplat(bytes, { format?, keepOrientation? })`
|
|
2601
|
+
reads a trainer's `.ply`, the de-facto `.splat` interchange or a
|
|
2602
|
+
GUNZIPPED `.spz` (Niantic, version 2 or 3; the entry stays
|
|
2603
|
+
decompression-free, so inflate first) and bakes it - the 3D covariance
|
|
2604
|
+
precomputed per splat (R S St Rt, the vertex-stage wall every
|
|
2605
|
+
bake-less viewer pays at each corner), records importance-sorted (size
|
|
2606
|
+
times opacity, so the first n are the scene at n), y-down captures
|
|
2607
|
+
(.ply, .splat) stood up to y-up (positions and covariances rotated
|
|
2608
|
+
together; `keepOrientation` opts out, `.spz` is y-up already), bounds
|
|
2609
|
+
measured. `encodeSplat`/`decodeSplat` round-trip the `.srts` container
|
|
2610
|
+
(the record and SH blocks, next bullet); decode is a header parse
|
|
2611
|
+
plus byte VIEWS, nothing per-splat. `srt tool 3d/splat <in> [-o
|
|
2612
|
+
out.srts] [--sh 0..3] [--keep-orientation]` is the parse and bake
|
|
2613
|
+
under bun (tools/splat.ts); an app bake script uses the same entry.
|
|
2614
|
+
- The `.srts` record (version 2) is 32 bytes = two rgba32ui texels:
|
|
2615
|
+
center float32x3 + sRGB color and opacity unorm8x4, then the
|
|
2616
|
+
covariance upper triangle as six float16 + a spare word
|
|
2617
|
+
(SPLAT_ATTRIBUTES as a byte layout, SPLAT_RECORD_TEXELS). `--sh 1..3`
|
|
2618
|
+
bakes that many spherical-harmonic bands beyond the base color from a
|
|
2619
|
+
trainer's `.ply` or an `.spz` (`.splat` has none), capped at the
|
|
2620
|
+
capture's: a second texel block of 2/3/6 texels per splat
|
|
2621
|
+
(SPLAT_SH_TEXELS; the halves packed pairwise, coefficient-major with
|
|
2622
|
+
rgb interleaved), 32/48/96 bytes per splat of texture, so the
|
|
2623
|
+
default is 0 and a bake opts in. The y-up flip negates the
|
|
2624
|
+
coefficients whose basis is odd in (y, z).
|
|
2625
|
+
- On the runtime, `loadSplat(path)` reads a `.srts` (fetch + decode,
|
|
2626
|
+
the loadModel shape) and `createSplatMesh(data, { count?, material?,
|
|
2627
|
+
label? })` / `<SplatMesh>` shows it: a RecordMesh in the INDEXED form
|
|
2628
|
+
(see "Instance order") - the records handed to the engine as the
|
|
2629
|
+
depth sort's key source AND uploaded once as the `rgba32ui` record
|
|
2630
|
+
texture the vertex stage fetches by id (`uSplatRecords`, plus
|
|
2631
|
+
`uSplatSh` when baked; bound per mesh, so a forked material keeps
|
|
2632
|
+
them), SPLAT_GROUP (16) splats per drawn instance of a merged-quad
|
|
2633
|
+
geometry, the header bounds as the cull and picking box, and the
|
|
2634
|
+
id stream sorted back to front core-side - the cloud composes as a
|
|
2635
|
+
mesh (depth-tested against opaque geometry, placed by the transparent
|
|
2636
|
+
entry sort, transformed by its node), a camera turn re-sorts and
|
|
2637
|
+
republishes 4 bytes per splat with zero per-frame JS, and a parked
|
|
2638
|
+
camera uploads nothing. Write-once: treat the SplatData as consumed
|
|
2639
|
+
and drop it; at 1M splats the cloud is resident as the engine's 32 MB
|
|
2640
|
+
key copy plus 32 MB of texture plus a 4 MB id buffer, and
|
|
2641
|
+
disposeInstances frees all three. `splatMaterialClass(shDegree)` is
|
|
2642
|
+
the stock class per degree (a shared instance each), and
|
|
2643
|
+
`splatVertexSource(shDegree)` / SPLAT_FRAGMENT / SPLAT_ID_ATTRIBUTES
|
|
2644
|
+
are exported for forks; `count` rounds up to whole groups.
|
|
2645
|
+
- The vertex stage projects the baked covariance through the Jacobian
|
|
2646
|
+
of `uViewProj * uModel` (no focal/view uniforms; any node transform
|
|
2647
|
+
shapes the footprint with the same matrix that moves the center),
|
|
2648
|
+
reads the target size from the standard `uViewport`, and evaluates
|
|
2649
|
+
the SH bands per corner from the view direction (`uCamPos` through
|
|
2650
|
+
the transposed model rotation - exact under rotation and uniform
|
|
2651
|
+
scale). The data textures' row width is the device's (up to 4096),
|
|
2652
|
+
compiled into the source.
|
|
2653
|
+
- Why this shape: on the Pixel 7 at 1M splats the one-quad-per-instance
|
|
2654
|
+
form spent two thirds of its vertex side on the instancing frontend;
|
|
2655
|
+
the indexed form takes the full-res frame from 45 to 31 ms and the
|
|
2656
|
+
vertex side from 24 to 8 ms, the per-vertex fetch and random id order
|
|
2657
|
+
cost under a millisecond, and SH1-3 are near free (the bands cost
|
|
2658
|
+
memory, not time). What remains at 1M is fill (overdraw). The table
|
|
2659
|
+
is in okf/done/gaussian-splats.md.
|
|
2660
|
+
- Two capture caveats worth knowing at the app level: captures are
|
|
2661
|
+
TRAINED against sRGB blending, so show them in a display-space scene
|
|
2662
|
+
(`blendSpace: "display"`) - a linear scene lays a milky veil over
|
|
2663
|
+
stacked translucents - and a capture is only clean near the camera
|
|
2664
|
+
path it was taken from (elsewhere it is floaters), so keep the camera
|
|
2665
|
+
near that path.
|
|
2666
|
+
|
|
2486
2667
|
## Traps
|
|
2487
2668
|
|
|
2488
2669
|
### Models and skinning
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidrt/3d",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.64",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"funding": "https://github.com/sponsors/wellawaretech",
|
|
6
6
|
"author": "Antoine van Wel",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"exports": {
|
|
10
10
|
".": "./src/index.ts",
|
|
11
11
|
"./model": "./src/model-data.ts",
|
|
12
|
+
"./splat": "./src/splat-data.ts",
|
|
12
13
|
"./math": "./src/math.ts",
|
|
13
14
|
"./glsl": "./src/glsl.ts"
|
|
14
15
|
},
|
|
@@ -20,6 +21,6 @@
|
|
|
20
21
|
],
|
|
21
22
|
"peerDependencies": {
|
|
22
23
|
"@solidjs/signals": "2.0.0-rc.9",
|
|
23
|
-
"@solidrt/core": "0.0.
|
|
24
|
+
"@solidrt/core": "0.0.64"
|
|
24
25
|
}
|
|
25
26
|
}
|
package/src/camera.ts
CHANGED
|
@@ -135,8 +135,11 @@ export function ensureCamera(cam: Camera, width: number, height: number): void {
|
|
|
135
135
|
// billboard needs no reconstruction from uViewProj. The inverse rides too:
|
|
136
136
|
// uInvViewProj carries a clip position back to world, which is how the
|
|
137
137
|
// background slot (and any shader declaring it) gets a world-space ray
|
|
138
|
-
// per pixel without knowing the projection.
|
|
139
|
-
|
|
138
|
+
// per pixel without knowing the projection. The target's pixel size rides
|
|
139
|
+
// as uViewport (the size the camera's aspect came from), so a pixel-space
|
|
140
|
+
// footprint (the splat material) needs no app write and is per target -
|
|
141
|
+
// each view feeds its own.
|
|
142
|
+
export function cameraParams(cam: Camera, width: number, height: number): ShaderParams {
|
|
140
143
|
let v = cam.view
|
|
141
144
|
return {
|
|
142
145
|
uViewProj: cam.viewProj,
|
|
@@ -144,5 +147,6 @@ export function cameraParams(cam: Camera): ShaderParams {
|
|
|
144
147
|
uCamPos: cam.eye,
|
|
145
148
|
uCamRight: [v[0], v[4], v[8]],
|
|
146
149
|
uCamUp: [v[1], v[5], v[9]],
|
|
150
|
+
uViewport: [width, height],
|
|
147
151
|
}
|
|
148
152
|
}
|
|
@@ -13,6 +13,9 @@ export type DirectionalLightProps = TransformProps & {
|
|
|
13
13
|
direction?: Vec3
|
|
14
14
|
color?: Vec3
|
|
15
15
|
intensity?: number
|
|
16
|
+
/** Layer filter bitmask, default all bits set: the light lights and
|
|
17
|
+
* shadows only the meshes whose `layers` intersect it. */
|
|
18
|
+
layers?: number
|
|
16
19
|
/** Render a shadow map from this light (any directional light may;
|
|
17
20
|
* each is a pass). Its shadow camera sits at the light's WORLD
|
|
18
21
|
* position, so give a casting light a `position` above the scene. */
|
|
@@ -32,6 +35,7 @@ export let DirectionalLight: VoidComponent<DirectionalLightProps> = props => {
|
|
|
32
35
|
direction: props.direction,
|
|
33
36
|
color: props.color,
|
|
34
37
|
intensity: props.intensity,
|
|
38
|
+
layers: props.layers,
|
|
35
39
|
castShadow: props.castShadow,
|
|
36
40
|
shadow: props.shadow,
|
|
37
41
|
}),
|
|
@@ -39,8 +43,8 @@ export let DirectionalLight: VoidComponent<DirectionalLightProps> = props => {
|
|
|
39
43
|
add(ctx.parent, light)
|
|
40
44
|
syncNode(light, props)
|
|
41
45
|
createEffect(
|
|
42
|
-
() => [props.direction, props.color, props.intensity, props.castShadow, props.shadow] as const,
|
|
43
|
-
([direction, color, intensity, castShadow, shadow]) => setLight(light, { direction, color, intensity, castShadow, shadow }),
|
|
46
|
+
() => [props.direction, props.color, props.intensity, props.layers, props.castShadow, props.shadow] as const,
|
|
47
|
+
([direction, color, intensity, layers, castShadow, shadow]) => setLight(light, { direction, color, intensity, layers, castShadow, shadow }),
|
|
44
48
|
)
|
|
45
49
|
untrack(() => props.ref)?.(light)
|
|
46
50
|
onCleanup(() => destroy(light))
|
|
@@ -6,17 +6,25 @@ import { createHemisphereLight, setLight } from "../light.ts"
|
|
|
6
6
|
import type { HemisphereLight as HemisphereLightNode } from "../light.ts"
|
|
7
7
|
import type { Vec3 } from "../math.ts"
|
|
8
8
|
|
|
9
|
-
export type HemisphereLightProps = {
|
|
9
|
+
export type HemisphereLightProps = {
|
|
10
|
+
sky?: Vec3
|
|
11
|
+
ground?: Vec3
|
|
12
|
+
intensity?: number
|
|
13
|
+
/** Layer filter bitmask, default all bits set: a mesh outside it gets
|
|
14
|
+
* no hemisphere term. */
|
|
15
|
+
layers?: number
|
|
16
|
+
ref?: (light: HemisphereLightNode) => void
|
|
17
|
+
}
|
|
10
18
|
|
|
11
19
|
/** The scene's ambient term as a node (createHemisphereLight); one per
|
|
12
20
|
* scene, the last mounted wins. */
|
|
13
21
|
export let HemisphereLight: VoidComponent<HemisphereLightProps> = props => {
|
|
14
22
|
let ctx = useContext(SceneContext)
|
|
15
|
-
let light = untrack(() => createHemisphereLight({ sky: props.sky, ground: props.ground, intensity: props.intensity }))
|
|
23
|
+
let light = untrack(() => createHemisphereLight({ sky: props.sky, ground: props.ground, intensity: props.intensity, layers: props.layers }))
|
|
16
24
|
add(ctx.parent, light)
|
|
17
25
|
createEffect(
|
|
18
|
-
() => [props.sky, props.ground, props.intensity] as const,
|
|
19
|
-
([sky, ground, intensity]) => setLight(light, { sky, ground, intensity }),
|
|
26
|
+
() => [props.sky, props.ground, props.intensity, props.layers] as const,
|
|
27
|
+
([sky, ground, intensity, layers]) => setLight(light, { sky, ground, intensity, layers }),
|
|
20
28
|
)
|
|
21
29
|
untrack(() => props.ref)?.(light)
|
|
22
30
|
onCleanup(() => destroy(light))
|
package/src/components/index.ts
CHANGED
|
@@ -33,6 +33,8 @@ export { InstancedLod, Lod } from "./lod.tsx"
|
|
|
33
33
|
export type { InstancedLodProps, LodProps } from "./lod.tsx"
|
|
34
34
|
export { RecordMesh } from "./record-mesh.tsx"
|
|
35
35
|
export type { RecordMeshProps } from "./record-mesh.tsx"
|
|
36
|
+
export { SplatMesh } from "./splat-mesh.tsx"
|
|
37
|
+
export type { SplatMeshProps } from "./splat-mesh.tsx"
|
|
36
38
|
export { PerspectiveCamera } from "./perspective-camera.tsx"
|
|
37
39
|
export type { PerspectiveCameraProps } from "./perspective-camera.tsx"
|
|
38
40
|
export { OrbitCamera } from "./orbit-camera.tsx"
|
|
@@ -8,7 +8,7 @@ import type { PopulatedMeshProps } from "./mesh.tsx"
|
|
|
8
8
|
import { add, destroy, setMorphWeights } from "../node.ts"
|
|
9
9
|
import type { MorphWeights, SceneNode } from "../node.ts"
|
|
10
10
|
import { addInstance, createInstancedMesh, disposeInstances, setCastShadow, setGeometry, setInstanceStyle } from "../mesh.ts"
|
|
11
|
-
import type { InstancedMesh as InstancedMeshNode, InstanceNode } from "../mesh.ts"
|
|
11
|
+
import type { InstancedMesh as InstancedMeshNode, InstanceNode, InstanceOrderOptions } from "../mesh.ts"
|
|
12
12
|
import type { Geometry } from "../geometry.ts"
|
|
13
13
|
import type { Material } from "../material.ts"
|
|
14
14
|
|
|
@@ -30,6 +30,10 @@ export type InstancedMeshProps = Omit<PopulatedMeshProps, "onPointerEnter" | "on
|
|
|
30
30
|
* transparent sort. Optional here - instances pick by themselves, and
|
|
31
31
|
* without it the mesh culls by the union of their boxes. */
|
|
32
32
|
bounds?: ArrayLike<number>
|
|
33
|
+
/** Draw the records in key order, decided core-side (fixed at
|
|
34
|
+
* creation; see InstanceOrderOptions): back-to-front for a transparent
|
|
35
|
+
* population, an explicit sort field for a stepped one. */
|
|
36
|
+
instanceOrder?: InstanceOrderOptions
|
|
33
37
|
/** Debug label for the record buffers. */
|
|
34
38
|
label?: string
|
|
35
39
|
/** The node the instance records are relative to (default the mesh;
|
|
@@ -63,7 +67,7 @@ export let InstancedMeshContext = createContext<InstancedMeshNode | null>(null)
|
|
|
63
67
|
export let InstancedMesh: ParentComponent<InstancedMeshProps> = props => {
|
|
64
68
|
let ctx = useContext(SceneContext)
|
|
65
69
|
let mesh = untrack(() =>
|
|
66
|
-
createInstancedMesh(props.geometry, props.material, { capacity: props.capacity, bounds: props.bounds, anchor: props.anchor, label: props.label }),
|
|
70
|
+
createInstancedMesh(props.geometry, props.material, { capacity: props.capacity, bounds: props.bounds, anchor: props.anchor, instanceOrder: props.instanceOrder, label: props.label }),
|
|
67
71
|
)
|
|
68
72
|
add(ctx.parent, mesh)
|
|
69
73
|
createEffect(
|
package/src/components/mesh.tsx
CHANGED
|
@@ -5,8 +5,9 @@ import { syncNode } from "./node-props.ts"
|
|
|
5
5
|
import type { TransformProps, PointerEventProps } from "./node-props.ts"
|
|
6
6
|
import { add, destroy, setMorphWeights } from "../node.ts"
|
|
7
7
|
import type { MorphWeights } from "../node.ts"
|
|
8
|
-
import { createMesh, setCastShadow, setCulling, setGeometry, setLayers, setMaterial, setMeshParams, setRenderOrder } from "../mesh.ts"
|
|
8
|
+
import { createMesh, setCastShadow, setCulling, setEnvironment, setGeometry, setLayers, setMaterial, setMeshParams, setRenderOrder } from "../mesh.ts"
|
|
9
9
|
import type { Mesh as MeshNode } from "../mesh.ts"
|
|
10
|
+
import type { EnvironmentOptions } from "../environment.ts"
|
|
10
11
|
import type { ShaderParams } from "@solidrt/core/gpu"
|
|
11
12
|
import type { Geometry } from "../geometry.ts"
|
|
12
13
|
import type { Material } from "../material.ts"
|
|
@@ -29,6 +30,12 @@ export type MeshProps = TransformProps & PointerEventProps & {
|
|
|
29
30
|
* draws the mesh when its mask intersects this. Not inherited from
|
|
30
31
|
* ancestor Groups. */
|
|
31
32
|
layers?: number
|
|
33
|
+
/** This mesh's environment instead of the scene's (setEnvironment as a
|
|
34
|
+
* prop): `null` removes the environment term - the sealed-interior
|
|
35
|
+
* pair to masked lights - a `{ cube, intensity?, rotation? }` samples
|
|
36
|
+
* that cube, absent follows the scene. Rebuilds the mesh's entries on
|
|
37
|
+
* change: a state switch, not a per-frame dial. */
|
|
38
|
+
environment?: EnvironmentOptions | null
|
|
32
39
|
/** Frustum culling switch (setCulling as a prop; default true). */
|
|
33
40
|
frustumCulled?: boolean
|
|
34
41
|
/** World units the culled box grows by (setCulling as a prop; default 0). */
|
|
@@ -71,6 +78,10 @@ export function syncMesh(mesh: MeshNode, props: Omit<MeshProps, "geometry" | "ma
|
|
|
71
78
|
() => props.layers,
|
|
72
79
|
l => setLayers(mesh, l ?? 1),
|
|
73
80
|
)
|
|
81
|
+
createEffect(
|
|
82
|
+
() => props.environment,
|
|
83
|
+
e => setEnvironment(mesh, e),
|
|
84
|
+
)
|
|
74
85
|
createEffect(
|
|
75
86
|
() => [props.frustumCulled, props.cullMargin] as const,
|
|
76
87
|
([culled, margin]) => setCulling(mesh, { frustumCulled: culled !== false, cullMargin: margin ?? 0 }),
|
|
@@ -11,6 +11,9 @@ import type { Vec3 } from "../math.ts"
|
|
|
11
11
|
export type PointLightProps = TransformProps & {
|
|
12
12
|
color?: Vec3
|
|
13
13
|
intensity?: number
|
|
14
|
+
/** Layer filter bitmask, default all bits set: the light lights and
|
|
15
|
+
* shadows only the meshes whose `layers` intersect it. */
|
|
16
|
+
layers?: number
|
|
14
17
|
/** Falloff cutoff in world units (0 = no cutoff). */
|
|
15
18
|
distance?: number
|
|
16
19
|
/** Falloff exponent; default 2 (inverse square). */
|
|
@@ -34,6 +37,7 @@ export let PointLight: VoidComponent<PointLightProps> = props => {
|
|
|
34
37
|
createPointLight({
|
|
35
38
|
color: props.color,
|
|
36
39
|
intensity: props.intensity,
|
|
40
|
+
layers: props.layers,
|
|
37
41
|
distance: props.distance,
|
|
38
42
|
decay: props.decay,
|
|
39
43
|
castShadow: props.castShadow,
|
|
@@ -43,8 +47,8 @@ export let PointLight: VoidComponent<PointLightProps> = props => {
|
|
|
43
47
|
add(ctx.parent, light)
|
|
44
48
|
syncNode(light, props)
|
|
45
49
|
createEffect(
|
|
46
|
-
() => [props.color, props.intensity, props.distance, props.decay, props.castShadow, props.shadow] as const,
|
|
47
|
-
([color, intensity, distance, decay, castShadow, shadow]) => setLight(light, { color, intensity, distance, decay, castShadow, shadow }),
|
|
50
|
+
() => [props.color, props.intensity, props.layers, props.distance, props.decay, props.castShadow, props.shadow] as const,
|
|
51
|
+
([color, intensity, layers, distance, decay, castShadow, shadow]) => setLight(light, { color, intensity, layers, distance, decay, castShadow, shadow }),
|
|
48
52
|
)
|
|
49
53
|
untrack(() => props.ref)?.(light)
|
|
50
54
|
onCleanup(() => destroy(light))
|
|
@@ -5,7 +5,7 @@ import { syncMesh } from "./mesh.tsx"
|
|
|
5
5
|
import type { PopulatedMeshProps } from "./mesh.tsx"
|
|
6
6
|
import { add, destroy } from "../node.ts"
|
|
7
7
|
import { createRecordMesh, disposeInstances, setCastShadow, setGeometry, setRecordCount, setRecords } from "../mesh.ts"
|
|
8
|
-
import type { RecordMesh as RecordMeshNode } from "../mesh.ts"
|
|
8
|
+
import type { InstanceOrderOptions, RecordMesh as RecordMeshNode } from "../mesh.ts"
|
|
9
9
|
import type { Geometry } from "../geometry.ts"
|
|
10
10
|
import type { Material } from "../material.ts"
|
|
11
11
|
|
|
@@ -25,6 +25,14 @@ export type RecordMeshProps = PopulatedMeshProps & {
|
|
|
25
25
|
* creation. Without them the mesh has no picking leaf, so pointer events
|
|
26
26
|
* never target it. */
|
|
27
27
|
bounds?: ArrayLike<number>
|
|
28
|
+
/** Draw the records in key order, decided core-side (fixed at
|
|
29
|
+
* creation; see InstanceOrderOptions): back-to-front for a transparent
|
|
30
|
+
* population, an explicit sort field for a JS-stepped one. */
|
|
31
|
+
instanceOrder?: InstanceOrderOptions
|
|
32
|
+
/** Transfer the records to the engine at creation (fixed; see
|
|
33
|
+
* RecordMeshOptions.transfer): the write-once form - no JS mirror, so
|
|
34
|
+
* a later `records` value throws instead of rewriting. */
|
|
35
|
+
transfer?: boolean
|
|
28
36
|
/** Draw into the scene's shadow map (setCastShadow as a prop); default
|
|
29
37
|
* false. Needs a `castShadow` light AND a material class declaring
|
|
30
38
|
* `shadowVertex` (the depth pass with the instance placement) - the
|
|
@@ -39,7 +47,11 @@ export type RecordMeshProps = PopulatedMeshProps & {
|
|
|
39
47
|
export let RecordMesh: VoidComponent<RecordMeshProps> = props => {
|
|
40
48
|
let ctx = useContext(SceneContext)
|
|
41
49
|
let mesh = untrack(() =>
|
|
42
|
-
createRecordMesh(props.geometry, props.material, props.records, props.count, {
|
|
50
|
+
createRecordMesh(props.geometry, props.material, props.records, props.count, {
|
|
51
|
+
bounds: props.bounds,
|
|
52
|
+
instanceOrder: props.instanceOrder,
|
|
53
|
+
transfer: props.transfer,
|
|
54
|
+
}),
|
|
43
55
|
)
|
|
44
56
|
add(ctx.parent, mesh)
|
|
45
57
|
createEffect(
|
package/src/components/scene.tsx
CHANGED
|
@@ -3,7 +3,7 @@ import type { Element, ParentComponent, PointerFeed, TextureId } from "@solidrt/
|
|
|
3
3
|
import { SceneContext } from "./context.tsx"
|
|
4
4
|
import { createScene } from "../scene.ts"
|
|
5
5
|
import { feedPointer } from "../scene-pointer.ts"
|
|
6
|
-
import type { BloomOptions, EnvironmentOptions, FogOptions, ResolveInput, Scene as SceneHandle, SkyboxOptions, ToneMapping } from "../scene.ts"
|
|
6
|
+
import type { BlendSpace, BloomOptions, EnvironmentOptions, FogOptions, ResolveInput, Scene as SceneHandle, SkyboxOptions, ToneMapping } from "../scene.ts"
|
|
7
7
|
import type { ScenePointerEvent, SceneTapEvent, SceneWheelEvent } from "../node.ts"
|
|
8
8
|
import type { CameraUpdate } from "../camera.ts"
|
|
9
9
|
|
|
@@ -75,6 +75,12 @@ export type SceneProps = ScenePointerProps & {
|
|
|
75
75
|
toneMapping?: ToneMapping
|
|
76
76
|
/** Output exposure (scene.setExposure), default 1. Reactive. */
|
|
77
77
|
exposure?: number
|
|
78
|
+
/** The buffer's blend space (SceneOptions.blendSpace), default
|
|
79
|
+
* "linear"; fixed at creation. "display" blends sRGB-encoded values
|
|
80
|
+
* in an rgba8 buffer that is itself the displayed image - the mode a
|
|
81
|
+
* splat capture is faithful in - and rejects resolve, bloom,
|
|
82
|
+
* toneMapping and exposure. */
|
|
83
|
+
blendSpace?: BlendSpace
|
|
78
84
|
/** The scene target's layer mask (scene.setLayers as a prop; default 1):
|
|
79
85
|
* the scene draws the meshes whose `layers` intersect it. Reactive. */
|
|
80
86
|
layers?: number
|
|
@@ -175,6 +181,7 @@ export let Scene: ParentComponent<SceneProps> = props => {
|
|
|
175
181
|
fog: props.fog,
|
|
176
182
|
toneMapping: props.toneMapping,
|
|
177
183
|
exposure: props.exposure,
|
|
184
|
+
blendSpace: props.blendSpace,
|
|
178
185
|
resolve: props.resolve,
|
|
179
186
|
bloom: props.bloom,
|
|
180
187
|
layers: props.layers,
|