@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/examples/fog.tsx
CHANGED
|
@@ -16,11 +16,12 @@
|
|
|
16
16
|
// command sets the mode and its knobs (`{ mode: "linear" | "exp2" |
|
|
17
17
|
// "height" | "off", near, far, density, height, falloff }`) and returns
|
|
18
18
|
// the state; `pan` parks the camera (`{ t: seconds }`), so a capture
|
|
19
|
-
// repeats
|
|
19
|
+
// repeats; `suns` is the two suns' world positions and `project`
|
|
20
|
+
// (`{ point }`) is where a world point is on screen, for a pixel probe.
|
|
20
21
|
import { createSignal, flush, onFrame, pct, render } from "@solidrt/core"
|
|
21
22
|
import { registerDebug } from "srt:dev"
|
|
22
23
|
import { cone, cylinder, DirectionalLight, HemisphereLight, phong, Mesh, PerspectiveCamera, plane, Scene, sphere, unlit } from "@solidrt/3d"
|
|
23
|
-
import type { FogOptions, Vec3 } from "@solidrt/3d"
|
|
24
|
+
import type { FogOptions, SceneHandle, Vec3 } from "@solidrt/3d"
|
|
24
25
|
|
|
25
26
|
const FAR = 400
|
|
26
27
|
// The sky, shared by the clear and the fog so the horizon has no band.
|
|
@@ -36,6 +37,10 @@ const LINEAR_FAR = FAR
|
|
|
36
37
|
const DENSITY = 0.006
|
|
37
38
|
const HEIGHT = 10
|
|
38
39
|
const HEIGHT_FALLOFF = 0.12
|
|
40
|
+
// The two suns, side by side on the far ridge: the left one opts out of
|
|
41
|
+
// the fog, the right one fogs like everything else.
|
|
42
|
+
const SUN_LIT: Vec3 = [-30, 100, -220]
|
|
43
|
+
const SUN_FOGGED: Vec3 = [30, 100, -220]
|
|
39
44
|
|
|
40
45
|
type Mode = "linear" | "exp2" | "height" | "off"
|
|
41
46
|
const MODES: Mode[] = ["linear", "exp2", "height", "off"]
|
|
@@ -48,6 +53,7 @@ let [height, setHeight] = createSignal(HEIGHT)
|
|
|
48
53
|
let [falloff, setFalloff] = createSignal(HEIGHT_FALLOFF)
|
|
49
54
|
let [time, setTime] = createSignal(0)
|
|
50
55
|
let parked: number | null = null
|
|
56
|
+
let scene: SceneHandle | null = null
|
|
51
57
|
|
|
52
58
|
let num = (v: unknown, set: (n: number) => void) => {
|
|
53
59
|
if (typeof v === "number") set(v)
|
|
@@ -73,6 +79,13 @@ registerDebug("pan", (args?: Record<string, unknown>) => {
|
|
|
73
79
|
flush()
|
|
74
80
|
return { t: time(), parked: parked !== null }
|
|
75
81
|
})
|
|
82
|
+
registerDebug("suns", () => ({ lit: SUN_LIT, fogged: SUN_FOGGED }))
|
|
83
|
+
registerDebug("project", (args?: Record<string, unknown>) => {
|
|
84
|
+
if (scene === null) throw new Error("project: the scene is not mounted")
|
|
85
|
+
let point = args?.point
|
|
86
|
+
if (!Array.isArray(point) || point.length !== 3 || !point.every(v => typeof v === "number")) throw new Error("project: pass { point: [x, y, z] }")
|
|
87
|
+
return scene.project(point as Vec3)
|
|
88
|
+
})
|
|
76
89
|
|
|
77
90
|
function fog(): FogOptions | undefined {
|
|
78
91
|
switch (mode()) {
|
|
@@ -148,7 +161,7 @@ function App() {
|
|
|
148
161
|
return (
|
|
149
162
|
<window>
|
|
150
163
|
<view width={pct(100)} height={pct(100)} onPointerDown={() => setMode(m => MODES[(MODES.indexOf(m) + 1) % MODES.length] ?? "linear")}>
|
|
151
|
-
<Scene clearColor={[SKY[0], SKY[1], SKY[2], 1]} fog={fog()} samples={4} label="fog">
|
|
164
|
+
<Scene ref={s => (scene = s)} clearColor={[SKY[0], SKY[1], SKY[2], 1]} fog={fog()} samples={4} label="fog">
|
|
152
165
|
<PerspectiveCamera fov={55} near={0.5} far={FAR} position={EYE} lookAt={lookAt()} />
|
|
153
166
|
<HemisphereLight sky={[0.55, 0.62, 0.75]} ground={[0.28, 0.24, 0.2]} />
|
|
154
167
|
<DirectionalLight color={[0.9, 0.85, 0.75]} rotation={[-0.9, 0.6, 0]} />
|
|
@@ -162,8 +175,8 @@ function App() {
|
|
|
162
175
|
<Mesh geometry={crown} material={needles} position={[p.position[0], p.position[1] + p.size * 0.3 + p.size * 0.5, p.position[2]]} scale={[p.size * 0.9, p.size, p.size * 0.9]} />
|
|
163
176
|
</>
|
|
164
177
|
))}
|
|
165
|
-
<Mesh geometry={sun} material={sunLit} position={
|
|
166
|
-
<Mesh geometry={sun} material={sunFogged} position={
|
|
178
|
+
<Mesh geometry={sun} material={sunLit} position={SUN_LIT} />
|
|
179
|
+
<Mesh geometry={sun} material={sunFogged} position={SUN_FOGGED} />
|
|
167
180
|
</Scene>
|
|
168
181
|
</view>
|
|
169
182
|
</window>
|
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
|
-
//
|
|
6
|
+
// bun run srt tool 3d/model examples/model.glb -o assets/model.srtm
|
|
7
7
|
//
|
|
8
8
|
// (loadGltf("assets/model.glb") has the exact same async shape; the bake
|
|
9
9
|
// just removes the runtime parse.)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidrt/3d",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.65",
|
|
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
|
},
|
|
@@ -18,8 +19,11 @@
|
|
|
18
19
|
"examples/",
|
|
19
20
|
"AGENTS.md"
|
|
20
21
|
],
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@solidrt/test": "0.0.65"
|
|
24
|
+
},
|
|
21
25
|
"peerDependencies": {
|
|
22
|
-
"@solidjs/signals": "2.0.0-rc.
|
|
23
|
-
"@solidrt/core": "0.0.
|
|
26
|
+
"@solidjs/signals": "2.0.0-rc.13",
|
|
27
|
+
"@solidrt/core": "0.0.65"
|
|
24
28
|
}
|
|
25
29
|
}
|
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,
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { createEffect, onCleanup, untrack, useContext } from "@solidrt/core"
|
|
2
|
+
import type { VoidComponent } from "@solidrt/core"
|
|
3
|
+
import { SceneContext } from "./context.tsx"
|
|
4
|
+
import { syncMesh } from "./mesh.tsx"
|
|
5
|
+
import type { PopulatedMeshProps } from "./mesh.tsx"
|
|
6
|
+
import { add, destroy } from "../node.ts"
|
|
7
|
+
import { disposeInstances, setRecordCount } from "../mesh.ts"
|
|
8
|
+
import { createSplatMesh } from "../splat.ts"
|
|
9
|
+
import type { SplatMesh as SplatMeshNode, SplatMeshOptions } from "../splat.ts"
|
|
10
|
+
import type { SplatData } from "../splat-data.ts"
|
|
11
|
+
|
|
12
|
+
export type SplatMeshProps = PopulatedMeshProps & {
|
|
13
|
+
/** The baked cloud (loadSplat). Write-once: the records went to the
|
|
14
|
+
* engine and the data textures at creation, so treat the SplatData as
|
|
15
|
+
* consumed and swap clouds by remounting (a keyed <Show>), not by a new
|
|
16
|
+
* `data` value. */
|
|
17
|
+
data: SplatData
|
|
18
|
+
/** How many splats draw; default all. Records are importance-sorted at
|
|
19
|
+
* bake, so the first n are the scene at n - the LOD/bench dial (rounded
|
|
20
|
+
* up to whole groups of SPLAT_GROUP). */
|
|
21
|
+
count?: number
|
|
22
|
+
/** A custom splat material (see SplatMeshOptions); fixed at creation. */
|
|
23
|
+
material?: SplatMeshOptions["material"]
|
|
24
|
+
ref?: (mesh: SplatMeshNode) => void
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A baked splat cloud in the scene (createSplatMesh as a component): an
|
|
28
|
+
* ordinary record mesh in the indexed form, drawn back to front by the
|
|
29
|
+
* core-side order with the scene feeding the view direction - no
|
|
30
|
+
* per-frame JS anywhere. The id buffer, the engine's record copy and the
|
|
31
|
+
* data textures are component-owned and freed on unmount. */
|
|
32
|
+
export let SplatMesh: VoidComponent<SplatMeshProps> = props => {
|
|
33
|
+
let ctx = useContext(SceneContext)
|
|
34
|
+
let mesh = untrack(() => createSplatMesh(props.data, { count: props.count, material: props.material }))
|
|
35
|
+
add(ctx.parent, mesh)
|
|
36
|
+
createEffect(
|
|
37
|
+
() => props.count,
|
|
38
|
+
c => {
|
|
39
|
+
if (c !== undefined) setRecordCount(mesh, c)
|
|
40
|
+
},
|
|
41
|
+
{ defer: true },
|
|
42
|
+
)
|
|
43
|
+
syncMesh(mesh, props)
|
|
44
|
+
untrack(() => props.ref)?.(mesh)
|
|
45
|
+
onCleanup(() => {
|
|
46
|
+
destroy(mesh)
|
|
47
|
+
disposeInstances(mesh)
|
|
48
|
+
})
|
|
49
|
+
return null
|
|
50
|
+
}
|
|
@@ -14,6 +14,9 @@ export type SpotLightProps = TransformProps & {
|
|
|
14
14
|
direction?: Vec3
|
|
15
15
|
color?: Vec3
|
|
16
16
|
intensity?: number
|
|
17
|
+
/** Layer filter bitmask, default all bits set: the light lights and
|
|
18
|
+
* shadows only the meshes whose `layers` intersect it. */
|
|
19
|
+
layers?: number
|
|
17
20
|
/** Falloff cutoff in world units (0 = no cutoff). */
|
|
18
21
|
distance?: number
|
|
19
22
|
/** Cone half-angle in DEGREES, (0, 90]; default 60 (degrees like
|
|
@@ -43,6 +46,7 @@ export let SpotLight: VoidComponent<SpotLightProps> = props => {
|
|
|
43
46
|
direction: props.direction,
|
|
44
47
|
color: props.color,
|
|
45
48
|
intensity: props.intensity,
|
|
49
|
+
layers: props.layers,
|
|
46
50
|
distance: props.distance,
|
|
47
51
|
angle: props.angle,
|
|
48
52
|
penumbra: props.penumbra,
|
|
@@ -55,9 +59,9 @@ export let SpotLight: VoidComponent<SpotLightProps> = props => {
|
|
|
55
59
|
syncNode(light, props)
|
|
56
60
|
createEffect(
|
|
57
61
|
() =>
|
|
58
|
-
[props.direction, props.color, props.intensity, props.distance, props.angle, props.penumbra, props.decay, props.castShadow, props.shadow] as const,
|
|
59
|
-
([direction, color, intensity, distance, angle, penumbra, decay, castShadow, shadow]) =>
|
|
60
|
-
setLight(light, { direction, color, intensity, distance, angle, penumbra, decay, castShadow, shadow }),
|
|
62
|
+
[props.direction, props.color, props.intensity, props.layers, props.distance, props.angle, props.penumbra, props.decay, props.castShadow, props.shadow] as const,
|
|
63
|
+
([direction, color, intensity, layers, distance, angle, penumbra, decay, castShadow, shadow]) =>
|
|
64
|
+
setLight(light, { direction, color, intensity, layers, distance, angle, penumbra, decay, castShadow, shadow }),
|
|
61
65
|
)
|
|
62
66
|
untrack(() => props.ref)?.(light)
|
|
63
67
|
onCleanup(() => destroy(light))
|
package/src/environment.ts
CHANGED
|
@@ -324,3 +324,58 @@ export function createEnvironmentPlaceholder(label: string): TextureId {
|
|
|
324
324
|
let black = new Uint8Array([0, 0, 0, 255])
|
|
325
325
|
return createCubeTexture([black, black, black, black, black, black], 1, { autoFree: false, label })
|
|
326
326
|
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* An environment: a cube map every image-lit and reflective term samples
|
|
330
|
+
* (scene.setEnvironment for the scene's, `Mesh.environment` for one
|
|
331
|
+
* mesh's own). The same three fields as a skybox, and typically the same
|
|
332
|
+
* cube - Three's `scene.environment` with `environmentIntensity` and
|
|
333
|
+
* `environmentRotation`; Unity's environment reflections source, Godot's
|
|
334
|
+
* reflected light from the sky.
|
|
335
|
+
*/
|
|
336
|
+
export type EnvironmentOptions = {
|
|
337
|
+
/** A cube map (loadEnvironment's baked chain; equirectToCube for an LDR
|
|
338
|
+
* panorama; createCubeTexture with `mipmap: true` for a hand-baked
|
|
339
|
+
* sky), faces in +X, -X, +Y, -Y, +Z, -Z order, looked up like the
|
|
340
|
+
* skybox (no flip). A 2D texture id throws. */
|
|
341
|
+
cube: TextureId
|
|
342
|
+
/** Multiplier on the reflected color, >= 0; default 1. */
|
|
343
|
+
intensity?: number
|
|
344
|
+
/** Turn about world y in RADIANS (default 0), the skybox's convention:
|
|
345
|
+
* the environment turns as a node with rotation [0, r, 0] would. */
|
|
346
|
+
rotation?: number
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// The uniform turn a rotated cube map is looked up through: the INVERSE
|
|
350
|
+
// of the sky's turn, because a lookup along view direction v must find
|
|
351
|
+
// the texel that sat at R(-r) v before the sky turned by +r.
|
|
352
|
+
export function cubeTurn(rotation: number): number[] {
|
|
353
|
+
let c = Math.cos(rotation)
|
|
354
|
+
let n = Math.sin(rotation)
|
|
355
|
+
// prettier-ignore
|
|
356
|
+
return [
|
|
357
|
+
c, 0, n, 0,
|
|
358
|
+
0, 1, 0, 0,
|
|
359
|
+
-n, 0, c, 0,
|
|
360
|
+
0, 0, 0, 1,
|
|
361
|
+
]
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export function checkCubeKnobs(o: { intensity?: number; rotation?: number }, site: string): { intensity: number; rotation: number } {
|
|
365
|
+
let intensity = o.intensity ?? 1
|
|
366
|
+
let rotation = o.rotation ?? 0
|
|
367
|
+
if (!Number.isFinite(intensity) || intensity < 0) throw new Error(site + ": intensity must be a finite number >= 0, got " + intensity)
|
|
368
|
+
if (!Number.isFinite(rotation)) throw new Error(site + ": rotation must be a finite angle in radians, got " + rotation)
|
|
369
|
+
return { intensity, rotation }
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// The params an environment compiles to (null = off: uEnvOn 0 makes the
|
|
373
|
+
// phong weight vanish and uEnvIntensity 0 zeroes the radiance and
|
|
374
|
+
// irradiance; the set ENVIRONMENT in `@solidrt/3d/glsl` declares them).
|
|
375
|
+
// Target params for the scene's environment, entry params for a mesh's
|
|
376
|
+
// own - the same names, and the entry's beat the target's.
|
|
377
|
+
export function environmentParams(env: EnvironmentOptions | null, site: string): Record<string, number | number[]> {
|
|
378
|
+
if (env === null) return { uEnvIntensity: 0, uEnvRotation: cubeTurn(0), uEnvOn: 0 }
|
|
379
|
+
let k = checkCubeKnobs(env, site)
|
|
380
|
+
return { uEnvIntensity: k.intensity, uEnvRotation: cubeTurn(k.rotation), uEnvOn: 1 }
|
|
381
|
+
}
|
package/src/geometry.ts
CHANGED
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
// format's codec.
|
|
38
38
|
//
|
|
39
39
|
// Pure module by design - geometry is data, and every function here is
|
|
40
|
-
// array math (
|
|
40
|
+
// array math (tests/geometry.test.ts runs it headless on
|
|
41
41
|
// flux). The GPU buffer step lives in geometry-gpu.ts.
|
|
42
42
|
|
|
43
43
|
import type { Topology, VertexAttribute, VertexFormat } from "@solidrt/core/gpu"
|