@solidrt/3d 0.0.57 → 0.0.59
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 +1479 -678
- package/README.md +4 -2
- package/examples/README.md +56 -4
- package/examples/bloom.tsx +113 -0
- package/examples/cascades.tsx +4 -4
- package/examples/collision.tsx +6 -6
- package/examples/exits.tsx +96 -0
- package/examples/first-person.tsx +5 -5
- package/examples/fleet.tsx +4 -4
- package/examples/fog.tsx +5 -5
- package/examples/instanced.tsx +70 -50
- package/examples/lamps.tsx +4 -4
- package/examples/lod.tsx +125 -0
- package/examples/materials.tsx +9 -9
- package/examples/morph.tsx +174 -0
- package/examples/normals.tsx +118 -0
- package/examples/{lit.tsx → phong.tsx} +9 -9
- package/examples/polyhedra.tsx +61 -0
- package/examples/scene-atlas.tsx +70 -0
- package/examples/scene-views.tsx +5 -5
- package/examples/shadows.tsx +9 -9
- package/examples/sky-lit.tsx +3 -3
- package/examples/skybox.tsx +5 -5
- package/examples/standard.tsx +2 -2
- package/examples/streams.tsx +122 -0
- package/examples/sweep-paths.tsx +5 -5
- package/examples/wireframe.tsx +129 -0
- package/package.json +3 -3
- package/src/clip.ts +18 -5
- package/src/color.ts +11 -2
- package/src/components/context.tsx +11 -2
- package/src/components/directional-light.tsx +2 -2
- package/src/components/first-person-camera.tsx +4 -4
- package/src/components/group.tsx +9 -5
- package/src/components/hemisphere-light.tsx +2 -2
- package/src/components/index.ts +4 -1
- package/src/components/instanced-mesh.tsx +28 -10
- package/src/components/lod.tsx +102 -0
- package/src/components/mesh.tsx +24 -6
- package/src/components/node-props.ts +35 -5
- package/src/components/orbit-camera.tsx +15 -12
- package/src/components/point-light.tsx +2 -2
- package/src/components/record-mesh.tsx +12 -7
- package/src/components/scene.tsx +55 -2
- package/src/components/spot-light.tsx +2 -2
- package/src/components/sprite.tsx +2 -2
- package/src/components/view3d.tsx +18 -1
- package/src/environment.ts +9 -8
- package/src/first-person.ts +125 -38
- package/src/geometry-gpu.ts +187 -22
- package/src/geometry.ts +1718 -203
- package/src/glsl.ts +398 -113
- package/src/gltf.ts +457 -154
- package/src/index.ts +32 -21
- package/src/input.ts +11 -2
- package/src/light.ts +7 -8
- package/src/lod.ts +148 -0
- package/src/material.ts +322 -190
- package/src/mesh.ts +492 -259
- package/src/mixer.ts +27 -14
- package/src/model-file.ts +54 -15
- package/src/model.ts +151 -54
- package/src/motion.ts +17 -0
- package/src/node.ts +423 -23
- package/src/orbit.ts +378 -116
- package/src/profile.ts +4 -25
- package/src/resolve.ts +181 -0
- package/src/scene-shadows.ts +94 -93
- package/src/scene.ts +608 -185
- package/src/skeleton.ts +211 -0
- package/src/sweep.ts +27 -8
- package/tools/model.ts +3 -1
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// The rover drawn solid, as a wireframe or as its feature edges. Both line
|
|
2
|
+
// forms are GEOMETRY: wireframeGeometry lists every edge of a triangle
|
|
3
|
+
// geometry once, edgesGeometry only the edges where two faces meet at the
|
|
4
|
+
// threshold angle or more (plus the open borders), each as a "lines"
|
|
5
|
+
// geometry over the source's OWN vertex array. The topology rides on the
|
|
6
|
+
// geometry, so the plain unlit material draws it as lines, and the vertex
|
|
7
|
+
// upload is shared, so a mode costs an index buffer, not a second model.
|
|
8
|
+
// The swap is setGeometry + setMaterial on the live mesh: the node, its
|
|
9
|
+
// transform and its pointer handlers are untouched. Space, a tap on the
|
|
10
|
+
// rover or the `wire` debug command ({ mode }) cycles the modes.
|
|
11
|
+
//
|
|
12
|
+
// Under the rover, the debug helpers: gridHelper on its floor and
|
|
13
|
+
// axesHelper at the origin (world space, so they stand still while the
|
|
14
|
+
// rover turns) and box3Helper around its bounds, turning with it. Lines
|
|
15
|
+
// geometry too; the two colored ones draw through one
|
|
16
|
+
// unlit({ vertexColors: true }) material.
|
|
17
|
+
|
|
18
|
+
import { createSignal, onFrame, pct, render } from "@solidrt/core"
|
|
19
|
+
import { createTexture } from "@solidrt/core/gpu"
|
|
20
|
+
import type { TextureId } from "@solidrt/core/gpu"
|
|
21
|
+
import { registerDebug } from "srt:dev"
|
|
22
|
+
import { add, axesHelper, box3Helper, createModel, DirectionalLight, edgesGeometry, equirectToCube, gridHelper, Group, Mesh, parseGltf, PerspectiveCamera, Scene, setGeometry, setMaterial, unlit, wireframeGeometry } from "@solidrt/3d"
|
|
23
|
+
import type { SceneNode } from "@solidrt/3d"
|
|
24
|
+
import modelBytes from "./model.glb" with { type: "binary" }
|
|
25
|
+
|
|
26
|
+
const MODES = ["solid", "wireframe", "edges"] as const
|
|
27
|
+
type Mode = (typeof MODES)[number]
|
|
28
|
+
// Feature-edge threshold in degrees: above the facet angle of the rover's
|
|
29
|
+
// rounded parts, so their seams stay hidden and only rims and creases draw.
|
|
30
|
+
const EDGE_ANGLE = 20
|
|
31
|
+
const LINE_COLOR: [number, number, number] = [0.85, 0.95, 1]
|
|
32
|
+
// The grid's subdivision lines and its two center lines, and the bounds
|
|
33
|
+
// box (Three's Box3Helper yellow), against the dark clear color.
|
|
34
|
+
const GRID_COLOR: [number, number, number] = [0.3, 0.33, 0.4]
|
|
35
|
+
const GRID_CENTER_COLOR: [number, number, number] = [0.6, 0.66, 0.8]
|
|
36
|
+
const BOUNDS_COLOR: [number, number, number] = [1, 1, 0]
|
|
37
|
+
// Grid cells across the rover's floor.
|
|
38
|
+
const GRID_DIVISIONS = 20
|
|
39
|
+
// Face edge of the gradient environment cube: a gradient needs no detail.
|
|
40
|
+
const ENV_FACE = 16
|
|
41
|
+
// The gradient, zenith to nadir, sRGB bytes: sky, horizon, ground, below.
|
|
42
|
+
const ENV_ROWS: [number, number, number][] = [
|
|
43
|
+
[96, 122, 168],
|
|
44
|
+
[176, 190, 210],
|
|
45
|
+
[120, 105, 90],
|
|
46
|
+
[60, 52, 45],
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
// A tiny equirectangular panorama turned into the cube the solid rover's
|
|
50
|
+
// standard materials light by (see model.tsx).
|
|
51
|
+
function gradientEnvironment(): TextureId {
|
|
52
|
+
let px = new Uint8Array(2 * ENV_ROWS.length * 4)
|
|
53
|
+
ENV_ROWS.forEach((c, y) => {
|
|
54
|
+
for (let x = 0; x < 2; x++) px.set([c[0], c[1], c[2], 255], (y * 2 + x) * 4)
|
|
55
|
+
})
|
|
56
|
+
let panorama = createTexture(px, 2, ENV_ROWS.length, { format: "rgba8-srgb", filter: "linear" })
|
|
57
|
+
return equirectToCube(panorama, ENV_FACE, { format: "rgba8-srgb", mipmap: true, label: "gradient-env" })
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function App() {
|
|
61
|
+
let cube = gradientEnvironment()
|
|
62
|
+
let [t, setT] = createSignal(0)
|
|
63
|
+
onFrame(tick => setT(tick / 1000))
|
|
64
|
+
|
|
65
|
+
let model = createModel(parseGltf(modelBytes), { label: "rover" })
|
|
66
|
+
let b = model.bounds
|
|
67
|
+
let center: [number, number, number] = [(b[0]! + b[3]!) / 2, (b[1]! + b[4]!) / 2, (b[2]! + b[5]!) / 2]
|
|
68
|
+
let radius = Math.hypot(b[3]! - b[0]!, b[4]! - b[1]!, b[5]! - b[2]!) / 2
|
|
69
|
+
|
|
70
|
+
let helperLines = unlit({ vertexColors: true })
|
|
71
|
+
let grid = gridHelper({ size: radius * 4, divisions: GRID_DIVISIONS, color: GRID_COLOR, centerColor: GRID_CENTER_COLOR })
|
|
72
|
+
let axes = axesHelper({ size: radius })
|
|
73
|
+
let bounds = box3Helper(b)
|
|
74
|
+
|
|
75
|
+
// One line material for every part and both line modes. Per part, what
|
|
76
|
+
// each mode swaps in: the line geometries share the solid's vertices,
|
|
77
|
+
// so building both up front costs their index buffers only.
|
|
78
|
+
let lines = unlit({ color: LINE_COLOR })
|
|
79
|
+
let looks = model.parts.map(part => ({
|
|
80
|
+
mesh: part.mesh,
|
|
81
|
+
solid: { geometry: part.mesh.geometry, material: part.mesh.material },
|
|
82
|
+
wireframe: wireframeGeometry(part.mesh.geometry),
|
|
83
|
+
edges: edgesGeometry(part.mesh.geometry, EDGE_ANGLE),
|
|
84
|
+
}))
|
|
85
|
+
let mode: Mode = "solid"
|
|
86
|
+
let apply = (next: Mode): void => {
|
|
87
|
+
mode = next
|
|
88
|
+
for (let look of looks) {
|
|
89
|
+
// Each call rebuilds the draw entry, so the intermediate state is
|
|
90
|
+
// always the line material over the solid's triangles - a pipeline
|
|
91
|
+
// unlit has anyway - never the solid material over lines.
|
|
92
|
+
if (next === "solid") {
|
|
93
|
+
setGeometry(look.mesh, look.solid.geometry)
|
|
94
|
+
setMaterial(look.mesh, look.solid.material)
|
|
95
|
+
} else {
|
|
96
|
+
setMaterial(look.mesh, lines)
|
|
97
|
+
setGeometry(look.mesh, look[next])
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
let cycle = (): void => apply(MODES[(MODES.indexOf(mode) + 1) % MODES.length]!)
|
|
102
|
+
for (let part of model.parts) part.mesh.onTap = cycle
|
|
103
|
+
registerDebug("wire", (args?: Record<string, unknown>) => {
|
|
104
|
+
if (typeof args?.mode === "string" && (MODES as readonly string[]).includes(args.mode)) apply(args.mode as Mode)
|
|
105
|
+
else cycle()
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<window
|
|
110
|
+
onKeyDown={e => {
|
|
111
|
+
if (e.key === " ") cycle()
|
|
112
|
+
}}
|
|
113
|
+
>
|
|
114
|
+
<view width={pct(100)} height={pct(100)}>
|
|
115
|
+
<Scene clearColor={[0.1, 0.11, 0.14, 1]} environment={{ cube }} samples={4} label="wireframe">
|
|
116
|
+
<PerspectiveCamera fov={40} position={[center[0] + radius * 1.4, center[1] + radius * 1.1, center[2] + radius * 2.2]} lookAt={center} />
|
|
117
|
+
<DirectionalLight direction={[0.5, -0.8, 0.3]} color={[1, 0.95, 0.85]} intensity={0.9} />
|
|
118
|
+
<Mesh geometry={grid} material={helperLines} position={[0, b[1]!, 0]} />
|
|
119
|
+
<Mesh geometry={axes} material={helperLines} />
|
|
120
|
+
<Group rotation={[0, t() / 3, 0]} ref={(g: SceneNode) => add(g, model)}>
|
|
121
|
+
<Mesh geometry={bounds} material={unlit({ color: BOUNDS_COLOR })} />
|
|
122
|
+
</Group>
|
|
123
|
+
</Scene>
|
|
124
|
+
</view>
|
|
125
|
+
</window>
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
render(() => <App />)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidrt/3d",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.59",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"funding": "https://github.com/sponsors/wellawaretech",
|
|
6
6
|
"author": "Antoine van Wel",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"AGENTS.md"
|
|
19
19
|
],
|
|
20
20
|
"peerDependencies": {
|
|
21
|
-
"@solidjs/signals": "2.0.0-rc.
|
|
22
|
-
"@solidrt/core": "0.0.
|
|
21
|
+
"@solidjs/signals": "2.0.0-rc.8",
|
|
22
|
+
"@solidrt/core": "0.0.59"
|
|
23
23
|
}
|
|
24
24
|
}
|
package/src/clip.ts
CHANGED
|
@@ -6,18 +6,31 @@ import { quatNormalize, quatSlerp } from "./math.ts"
|
|
|
6
6
|
import type { Quat } from "./math.ts"
|
|
7
7
|
import type { ModelChannel } from "./gltf.ts"
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Floats per key value of a channel: 3 for position and scale, 4 for a
|
|
11
|
+
* rotation, and for a weights channel one per morph target - read off
|
|
12
|
+
* the data, since the key count and the cubic triple fix it.
|
|
13
|
+
*/
|
|
14
|
+
export function channelElements(channel: ModelChannel): number {
|
|
15
|
+
if (channel.path === "rotation") return 4
|
|
16
|
+
if (channel.path !== "weights") return 3
|
|
17
|
+
let perKey = channel.interpolation === "cubic" ? 3 : 1
|
|
18
|
+
return channel.times.length === 0 ? 0 : channel.values.length / (channel.times.length * perKey)
|
|
19
|
+
}
|
|
20
|
+
|
|
9
21
|
/**
|
|
10
22
|
* Sample one channel at `time` (seconds, clamped to the key range) into
|
|
11
|
-
* `out` - 3 floats for position/scale, 4 for rotation
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* renormalized, per
|
|
23
|
+
* `out` - 3 floats for position/scale, 4 for rotation, one per target
|
|
24
|
+
* for weights (`channelElements`). Pure array math: step holds the
|
|
25
|
+
* earlier key, linear lerps (rotation slerps the shortest path), cubic
|
|
26
|
+
* evaluates the glTF CUBICSPLINE Hermite (rotation renormalized, per
|
|
27
|
+
* spec).
|
|
15
28
|
*/
|
|
16
29
|
export function sampleChannel(channel: ModelChannel, time: number, out: number[]): void {
|
|
17
30
|
let times = channel.times
|
|
18
31
|
let values = channel.values
|
|
19
32
|
let keys = times.length
|
|
20
|
-
let elements = channel
|
|
33
|
+
let elements = channelElements(channel)
|
|
21
34
|
let stride = channel.interpolation === "cubic" ? elements * 3 : elements
|
|
22
35
|
// The value element offset within a key: cubic keys are [in, value, out].
|
|
23
36
|
let mid = channel.interpolation === "cubic" ? elements : 0
|
package/src/color.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
// is sRGB-encoded, what a color picker shows, and the library decodes it to
|
|
5
5
|
// linear light when it writes the uniform (Three's ColorManagement,
|
|
6
6
|
// Godot's inspector colors, Unity's linear color space all do the same).
|
|
7
|
-
// Shading then runs in linear light
|
|
8
|
-
// (
|
|
7
|
+
// Shading then runs in linear light into the scene's buffer, and the
|
|
8
|
+
// target's resolve pass (RESOLVE in ./glsl) encodes the result back. Values written straight to
|
|
9
9
|
// a uniform (scene.setParams, setMeshParams) and vertex colors are linear,
|
|
10
10
|
// as in every engine; encode or decode them here when they come from a
|
|
11
11
|
// picker.
|
|
@@ -30,3 +30,12 @@ export function linearColor(color: readonly number[]): number[] {
|
|
|
30
30
|
for (let i = 0; i < 3 && i < out.length; i++) out[i] = srgbToLinear(out[i]!)
|
|
31
31
|
return out
|
|
32
32
|
}
|
|
33
|
+
|
|
34
|
+
/** An [r, g, b] or [r, g, b, a] sRGB color as the premultiplied linear
|
|
35
|
+
* vec4 the shaders take (uColor, aColor, iColor): rgb decoded to linear
|
|
36
|
+
* light and multiplied by the alpha, which is 1 when absent. */
|
|
37
|
+
export function premultipliedColor(color: readonly number[]): number[] {
|
|
38
|
+
let a = color.length === 4 ? color[3]! : 1
|
|
39
|
+
let c = linearColor(color)
|
|
40
|
+
return [c[0]! * a, c[1]! * a, c[2]! * a, a]
|
|
41
|
+
}
|
|
@@ -5,8 +5,8 @@ import type { Scene as SceneHandle } from "../scene.ts"
|
|
|
5
5
|
import type { CameraState, CameraUpdate } from "../camera.ts"
|
|
6
6
|
|
|
7
7
|
/** The camera state a camera control drives: the scene, or a view (both
|
|
8
|
-
* expose setCamera and
|
|
9
|
-
export type CameraTarget = { setCamera(update: CameraUpdate): void; camera(): CameraState }
|
|
8
|
+
* expose setCamera, camera() and size()). */
|
|
9
|
+
export type CameraTarget = { setCamera(update: CameraUpdate): void; camera(): CameraState; size(): { width: number; height: number } }
|
|
10
10
|
|
|
11
11
|
export type SceneCtx = { scene: SceneHandle; parent: SceneNode; viewport: CameraTarget; pointer: PointerFeed | null }
|
|
12
12
|
export let SceneContext = createContext<SceneCtx>()
|
|
@@ -33,3 +33,12 @@ export function provide(ctx: SceneCtx, parent: SceneNode, props: { children?: El
|
|
|
33
33
|
if (!("children" in props)) return undefined
|
|
34
34
|
return <SceneContext value={{ scene: ctx.scene, parent, viewport: ctx.viewport, pointer: ctx.pointer }}>{props.children}</SceneContext>
|
|
35
35
|
}
|
|
36
|
+
|
|
37
|
+
/** What a `<Lod>` hands its children: a direct child with `lodSize`
|
|
38
|
+
* registers itself as a level. */
|
|
39
|
+
export type LodRegistry = {
|
|
40
|
+
group: SceneNode
|
|
41
|
+
register(node: SceneNode, size: number): void
|
|
42
|
+
unregister(node: SceneNode): void
|
|
43
|
+
}
|
|
44
|
+
export let LodContext = createContext<LodRegistry | undefined>(undefined)
|
|
@@ -3,7 +3,7 @@ import type { VoidComponent } from "@solidrt/core"
|
|
|
3
3
|
import { SceneContext } from "./context.tsx"
|
|
4
4
|
import { syncNode } from "./node-props.ts"
|
|
5
5
|
import type { TransformProps } from "./node-props.ts"
|
|
6
|
-
import { add,
|
|
6
|
+
import { add, destroy } from "../node.ts"
|
|
7
7
|
import { createDirectionalLight, setLight } from "../light.ts"
|
|
8
8
|
import type { DirectionalLight as DirectionalLightNode, ShadowOptions } from "../light.ts"
|
|
9
9
|
import type { Vec3 } from "../math.ts"
|
|
@@ -43,6 +43,6 @@ export let DirectionalLight: VoidComponent<DirectionalLightProps> = props => {
|
|
|
43
43
|
([direction, color, intensity, castShadow, shadow]) => setLight(light, { direction, color, intensity, castShadow, shadow }),
|
|
44
44
|
)
|
|
45
45
|
untrack(() => props.ref)?.(light)
|
|
46
|
-
onCleanup(() =>
|
|
46
|
+
onCleanup(() => destroy(light))
|
|
47
47
|
return null
|
|
48
48
|
}
|
|
@@ -15,8 +15,8 @@ export type FirstPersonCameraProps = FirstPersonCameraOptions & {
|
|
|
15
15
|
input?: InputMap<any>
|
|
16
16
|
/** Action names per axis when the map's differ (null skips an axis). */
|
|
17
17
|
actions?: Partial<Record<keyof FirstPersonAxes, string | null>>
|
|
18
|
-
/** The control's handle (pose()/set()/eye()/forward()/
|
|
19
|
-
* verbs, `axes` - also the debug command shape). */
|
|
18
|
+
/** The control's handle (pose()/set()/glideTo()/eye()/forward()/
|
|
19
|
+
* active(), the verbs, `axes` - also the debug command shape). */
|
|
20
20
|
ref?: (camera: FirstPersonCameraHandle) => void
|
|
21
21
|
}
|
|
22
22
|
|
|
@@ -35,8 +35,8 @@ export type FirstPersonCameraProps = FirstPersonCameraOptions & {
|
|
|
35
35
|
* `fly={flying()}` swaps walk and fly on the running control (pose
|
|
36
36
|
* carries over, no remount), `moveSpeed`, `lookSpeed` and
|
|
37
37
|
* `clampPosition` follow their props, and a pitch clamp change re-clamps
|
|
38
|
-
* at once. A frame loop runs only while `active()` - a rate driving
|
|
39
|
-
* a still scene stays demand-driven idle.
|
|
38
|
+
* at once. A frame loop runs only while `active()` - a rate driving, or
|
|
39
|
+
* a glide in flight - so a still scene stays demand-driven idle.
|
|
40
40
|
*/
|
|
41
41
|
export let FirstPersonCamera: VoidComponent<FirstPersonCameraProps> = props => {
|
|
42
42
|
let ctx = useContext(SceneContext)
|
package/src/components/group.tsx
CHANGED
|
@@ -2,17 +2,21 @@ import { onCleanup, untrack, useContext } from "@solidrt/core"
|
|
|
2
2
|
import type { ParentComponent } from "@solidrt/core"
|
|
3
3
|
import { SceneContext, provide } from "./context.tsx"
|
|
4
4
|
import { syncNode } from "./node-props.ts"
|
|
5
|
-
import type { TransformProps,
|
|
6
|
-
import { add, createGroup,
|
|
5
|
+
import type { TransformProps, BubblingPointerEventProps } from "./node-props.ts"
|
|
6
|
+
import { add, createGroup, destroy } from "../node.ts"
|
|
7
7
|
import type { SceneNode } from "../node.ts"
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
export type GroupProps = TransformProps & BubblingPointerEventProps & { ref?: (node: SceneNode) => void }
|
|
10
|
+
|
|
11
|
+
/** A transform node: children inherit its position/rotation/scale. Its
|
|
12
|
+
* pointer events are the ones bubbling up from a hit descendant; a group
|
|
13
|
+
* is never the struck node, so it takes no enter/leave. */
|
|
14
|
+
export let Group: ParentComponent<GroupProps> = props => {
|
|
11
15
|
let ctx = useContext(SceneContext)
|
|
12
16
|
let node = createGroup()
|
|
13
17
|
add(ctx.parent, node)
|
|
14
18
|
syncNode(node, props)
|
|
15
19
|
untrack(() => props.ref)?.(node)
|
|
16
|
-
onCleanup(() =>
|
|
20
|
+
onCleanup(() => destroy(node))
|
|
17
21
|
return provide(ctx, node, props)
|
|
18
22
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createEffect, onCleanup, untrack, useContext } from "@solidrt/core"
|
|
2
2
|
import type { VoidComponent } from "@solidrt/core"
|
|
3
3
|
import { SceneContext } from "./context.tsx"
|
|
4
|
-
import { add,
|
|
4
|
+
import { add, destroy } from "../node.ts"
|
|
5
5
|
import { createHemisphereLight, setLight } from "../light.ts"
|
|
6
6
|
import type { HemisphereLight as HemisphereLightNode } from "../light.ts"
|
|
7
7
|
import type { Vec3 } from "../math.ts"
|
|
@@ -19,6 +19,6 @@ export let HemisphereLight: VoidComponent<HemisphereLightProps> = props => {
|
|
|
19
19
|
([sky, ground, intensity]) => setLight(light, { sky, ground, intensity }),
|
|
20
20
|
)
|
|
21
21
|
untrack(() => props.ref)?.(light)
|
|
22
|
-
onCleanup(() =>
|
|
22
|
+
onCleanup(() => destroy(light))
|
|
23
23
|
return null
|
|
24
24
|
}
|
package/src/components/index.ts
CHANGED
|
@@ -16,18 +16,21 @@
|
|
|
16
16
|
|
|
17
17
|
export { useScene } from "./context.tsx"
|
|
18
18
|
export type { CameraTarget } from "./context.tsx"
|
|
19
|
-
export type { PointerEventProps, TransformProps } from "./node-props.ts"
|
|
19
|
+
export type { BubblingPointerEventProps, PointerEventProps, TransformProps } from "./node-props.ts"
|
|
20
20
|
export { Scene } from "./scene.tsx"
|
|
21
21
|
export type { ScenePointerProps, SceneProps } from "./scene.tsx"
|
|
22
22
|
export { View3d } from "./view3d.tsx"
|
|
23
23
|
export type { View3dProps } from "./view3d.tsx"
|
|
24
24
|
export { Group } from "./group.tsx"
|
|
25
|
+
export type { GroupProps } from "./group.tsx"
|
|
25
26
|
export { Mesh } from "./mesh.tsx"
|
|
26
27
|
export type { MeshProps } from "./mesh.tsx"
|
|
27
28
|
export { Sprite } from "./sprite.tsx"
|
|
28
29
|
export type { SpriteProps } from "./sprite.tsx"
|
|
29
30
|
export { Instance, InstancedMesh } from "./instanced-mesh.tsx"
|
|
30
31
|
export type { InstancedMeshProps, InstanceProps } from "./instanced-mesh.tsx"
|
|
32
|
+
export { InstancedLod, Lod } from "./lod.tsx"
|
|
33
|
+
export type { InstancedLodProps, LodProps } from "./lod.tsx"
|
|
31
34
|
export { RecordMesh } from "./record-mesh.tsx"
|
|
32
35
|
export type { RecordMeshProps } from "./record-mesh.tsx"
|
|
33
36
|
export { PerspectiveCamera } from "./perspective-camera.tsx"
|
|
@@ -5,8 +5,9 @@ import { syncNode } from "./node-props.ts"
|
|
|
5
5
|
import type { TransformProps, PointerEventProps } from "./node-props.ts"
|
|
6
6
|
import { syncMesh } from "./mesh.tsx"
|
|
7
7
|
import type { PopulatedMeshProps } from "./mesh.tsx"
|
|
8
|
-
import { add } from "../node.ts"
|
|
9
|
-
import {
|
|
8
|
+
import { add, destroy, setMorphWeights } from "../node.ts"
|
|
9
|
+
import type { MorphWeights } from "../node.ts"
|
|
10
|
+
import { addInstance, createInstancedMesh, disposeInstances, setCastShadow, setGeometry, setInstanceStyle } from "../mesh.ts"
|
|
10
11
|
import type { InstancedMesh as InstancedMeshNode, InstanceNode } from "../mesh.ts"
|
|
11
12
|
import type { Geometry } from "../geometry.ts"
|
|
12
13
|
import type { Material } from "../material.ts"
|
|
@@ -15,7 +16,7 @@ export type InstancedMeshProps = PopulatedMeshProps & {
|
|
|
15
16
|
geometry: Geometry
|
|
16
17
|
/** An instanced material: a stock one with `instanced` (or
|
|
17
18
|
* `instanceColors`, for a per-instance tint), or a class declaring
|
|
18
|
-
* INSTANCE_MATRIX_ATTRIBUTES
|
|
19
|
+
* INSTANCE_MATRIX_ATTRIBUTES first and any style layout second. */
|
|
19
20
|
material: Material
|
|
20
21
|
/** Instance slots reserved up front (default 64; fixed at creation).
|
|
21
22
|
* Past it the buffers double into replacements - amortized, but size it
|
|
@@ -35,7 +36,9 @@ export type InstancedMeshProps = PopulatedMeshProps & {
|
|
|
35
36
|
ref?: (mesh: InstancedMeshNode) => void
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
|
|
39
|
+
/** The enclosing population, what `<Instance>` adds itself to: an
|
|
40
|
+
* `<InstancedMesh>`, or an `<InstancedLod>`'s first level. */
|
|
41
|
+
export let InstancedMeshContext = createContext<InstancedMeshNode | null>(null)
|
|
39
42
|
|
|
40
43
|
/**
|
|
41
44
|
* One draw entry covering N instance NODES (createInstancedMesh as a
|
|
@@ -63,7 +66,10 @@ export let InstancedMesh: ParentComponent<InstancedMeshProps> = props => {
|
|
|
63
66
|
)
|
|
64
67
|
syncMesh(mesh, props)
|
|
65
68
|
untrack(() => props.ref)?.(mesh)
|
|
66
|
-
onCleanup(() =>
|
|
69
|
+
onCleanup(() => {
|
|
70
|
+
destroy(mesh)
|
|
71
|
+
disposeInstances(mesh)
|
|
72
|
+
})
|
|
67
73
|
return (
|
|
68
74
|
<SceneContext value={{ scene: ctx.scene, parent: mesh, viewport: ctx.viewport, pointer: ctx.pointer }}>
|
|
69
75
|
<InstancedMeshContext value={mesh}>{props.children}</InstancedMeshContext>
|
|
@@ -72,11 +78,17 @@ export let InstancedMesh: ParentComponent<InstancedMeshProps> = props => {
|
|
|
72
78
|
}
|
|
73
79
|
|
|
74
80
|
export type InstanceProps = TransformProps & PointerEventProps & {
|
|
75
|
-
/** The instance's style record (setInstanceStyle as a prop):
|
|
76
|
-
*
|
|
77
|
-
* under a stock material's `instanceColors`. Reactive;
|
|
78
|
-
* instance keeps the material's instanceStyle (white for
|
|
81
|
+
/** The instance's style record (setInstanceStyle as a prop): one value
|
|
82
|
+
* per component of the material's second instance buffer, in order -
|
|
83
|
+
* `[r, g, b, a]` under a stock material's `instanceColors`. Reactive;
|
|
84
|
+
* absent, the instance keeps the material's instanceStyle (white for
|
|
85
|
+
* a tint). */
|
|
79
86
|
style?: ArrayLike<number>
|
|
87
|
+
/** The instance's morph target weights (setMorphWeights as a prop),
|
|
88
|
+
* for a population over geometry with `morphs` under a `morph: true`
|
|
89
|
+
* material: by name (keys merge) or every weight in target order; a
|
|
90
|
+
* `weights` entry in `transition` animates each change. */
|
|
91
|
+
morphWeights?: MorphWeights
|
|
80
92
|
ref?: (instance: InstanceNode) => void
|
|
81
93
|
}
|
|
82
94
|
|
|
@@ -99,7 +111,13 @@ export let Instance: ParentComponent<InstanceProps> = props => {
|
|
|
99
111
|
if (s !== undefined) setInstanceStyle(instance, s)
|
|
100
112
|
},
|
|
101
113
|
)
|
|
114
|
+
createEffect(
|
|
115
|
+
() => props.morphWeights,
|
|
116
|
+
w => {
|
|
117
|
+
if (w !== undefined) setMorphWeights(instance, w)
|
|
118
|
+
},
|
|
119
|
+
)
|
|
102
120
|
untrack(() => props.ref)?.(instance)
|
|
103
|
-
onCleanup(() =>
|
|
121
|
+
onCleanup(() => destroy(instance))
|
|
104
122
|
return provide(ctx, instance, props)
|
|
105
123
|
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { createEffect, onCleanup, untrack, useContext } from "@solidrt/core"
|
|
2
|
+
import type { ParentComponent } from "@solidrt/core"
|
|
3
|
+
import { LodContext, SceneContext, provide } from "./context.tsx"
|
|
4
|
+
import type { LodRegistry } from "./context.tsx"
|
|
5
|
+
import { syncNode } from "./node-props.ts"
|
|
6
|
+
import type { TransformProps, BubblingPointerEventProps } from "./node-props.ts"
|
|
7
|
+
import { syncMesh } from "./mesh.tsx"
|
|
8
|
+
import type { PopulatedMeshProps } from "./mesh.tsx"
|
|
9
|
+
import { InstancedMeshContext } from "./instanced-mesh.tsx"
|
|
10
|
+
import { add, createGroup, destroy } from "../node.ts"
|
|
11
|
+
import type { SceneNode } from "../node.ts"
|
|
12
|
+
import { createInstancedLod, setLod } from "../lod.ts"
|
|
13
|
+
import type { InstancedLodLevel } from "../lod.ts"
|
|
14
|
+
import { disposeInstances, setCastShadow } from "../mesh.ts"
|
|
15
|
+
import type { InstancedMesh as InstancedMeshNode } from "../mesh.ts"
|
|
16
|
+
|
|
17
|
+
export type LodProps = TransformProps &
|
|
18
|
+
BubblingPointerEventProps & {
|
|
19
|
+
/** The cross-fade band fraction (createLod's `fade`); default 0. */
|
|
20
|
+
fade?: number
|
|
21
|
+
ref?: (node: SceneNode) => void
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A LOD group (createLod as a component): its levels are the direct
|
|
26
|
+
* children carrying `lodSize` - the projected size below which each hands
|
|
27
|
+
* over - in any JSX order (levels sort by size, never by position, since
|
|
28
|
+
* attach order is not JSX order); a child without one is drawn always.
|
|
29
|
+
* Levels may come and go (`<Show>`): the group re-declares itself.
|
|
30
|
+
*/
|
|
31
|
+
export let Lod: ParentComponent<LodProps> = props => {
|
|
32
|
+
let ctx = useContext(SceneContext)
|
|
33
|
+
let node = createGroup()
|
|
34
|
+
add(ctx.parent, node)
|
|
35
|
+
syncNode(node, props)
|
|
36
|
+
let sizes = new Map<SceneNode, number>()
|
|
37
|
+
let declare = () => {
|
|
38
|
+
let levels = [...sizes.entries()].sort((a, b) => b[1] - a[1]).map(([n, size]) => ({ node: n, size }))
|
|
39
|
+
setLod(node, levels, { fade: untrack(() => props.fade) })
|
|
40
|
+
}
|
|
41
|
+
let registry: LodRegistry = {
|
|
42
|
+
group: node,
|
|
43
|
+
register(child, size) {
|
|
44
|
+
if (child.parent !== node) throw new Error("lodSize goes on a direct child of <Lod>")
|
|
45
|
+
sizes.set(child, size)
|
|
46
|
+
declare()
|
|
47
|
+
},
|
|
48
|
+
unregister(child) {
|
|
49
|
+
if (sizes.delete(child)) declare()
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
createEffect(
|
|
53
|
+
() => props.fade,
|
|
54
|
+
() => declare(),
|
|
55
|
+
{ defer: true },
|
|
56
|
+
)
|
|
57
|
+
untrack(() => props.ref)?.(node)
|
|
58
|
+
onCleanup(() => destroy(node))
|
|
59
|
+
return <LodContext value={registry}>{provide(ctx, node, props)}</LodContext>
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type InstancedLodProps = PopulatedMeshProps & {
|
|
63
|
+
/** The levels nearest first, each what the population draws at it and
|
|
64
|
+
* the per-instance projected size below which it hands over (see
|
|
65
|
+
* createInstancedLod); fixed at creation. */
|
|
66
|
+
levels: InstancedLodLevel[]
|
|
67
|
+
/** Instance slots reserved up front (default 64; fixed at creation). */
|
|
68
|
+
capacity?: number
|
|
69
|
+
/** LOCAL bounds covering the population, fixed at creation (see
|
|
70
|
+
* InstancedMesh). */
|
|
71
|
+
bounds?: ArrayLike<number>
|
|
72
|
+
label?: string
|
|
73
|
+
/** Every level draws into the scene's shadow map; default false. */
|
|
74
|
+
castShadow?: boolean
|
|
75
|
+
ref?: (mesh: InstancedMeshNode) => void
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* An instanced LOD (createInstancedLod as a component): `<Instance>`
|
|
80
|
+
* children populate it exactly as under `<InstancedMesh>`, and each
|
|
81
|
+
* instance draws the level its own projected size picks.
|
|
82
|
+
*/
|
|
83
|
+
export let InstancedLod: ParentComponent<InstancedLodProps> = props => {
|
|
84
|
+
let ctx = useContext(SceneContext)
|
|
85
|
+
let mesh = untrack(() => createInstancedLod(props.levels, { capacity: props.capacity, bounds: props.bounds, label: props.label }))
|
|
86
|
+
add(ctx.parent, mesh)
|
|
87
|
+
createEffect(
|
|
88
|
+
() => props.castShadow,
|
|
89
|
+
c => setCastShadow(mesh, c === true),
|
|
90
|
+
)
|
|
91
|
+
syncMesh(mesh, props)
|
|
92
|
+
untrack(() => props.ref)?.(mesh)
|
|
93
|
+
onCleanup(() => {
|
|
94
|
+
destroy(mesh)
|
|
95
|
+
disposeInstances(mesh)
|
|
96
|
+
})
|
|
97
|
+
return (
|
|
98
|
+
<SceneContext value={{ scene: ctx.scene, parent: mesh, viewport: ctx.viewport, pointer: ctx.pointer }}>
|
|
99
|
+
<InstancedMeshContext value={mesh}>{props.children}</InstancedMeshContext>
|
|
100
|
+
</SceneContext>
|
|
101
|
+
)
|
|
102
|
+
}
|
package/src/components/mesh.tsx
CHANGED
|
@@ -3,7 +3,8 @@ import type { VoidComponent } from "@solidrt/core"
|
|
|
3
3
|
import { SceneContext } from "./context.tsx"
|
|
4
4
|
import { syncNode } from "./node-props.ts"
|
|
5
5
|
import type { TransformProps, PointerEventProps } from "./node-props.ts"
|
|
6
|
-
import { add,
|
|
6
|
+
import { add, destroy, setMorphWeights } from "../node.ts"
|
|
7
|
+
import type { MorphWeights } from "../node.ts"
|
|
7
8
|
import { createMesh, setCastShadow, setCulling, setGeometry, setLayers, setMaterial, setMeshParams, setRenderOrder } from "../mesh.ts"
|
|
8
9
|
import type { Mesh as MeshNode } from "../mesh.ts"
|
|
9
10
|
import type { ShaderParams } from "@solidrt/core/gpu"
|
|
@@ -32,6 +33,13 @@ export type MeshProps = TransformProps & PointerEventProps & {
|
|
|
32
33
|
frustumCulled?: boolean
|
|
33
34
|
/** World units the culled box grows by (setCulling as a prop; default 0). */
|
|
34
35
|
cullMargin?: number
|
|
36
|
+
/** Morph target weights (setMorphWeights as a prop) for geometry that
|
|
37
|
+
* carries `morphs` under a `morph: true` material: by name (keys merge,
|
|
38
|
+
* a key that disappears keeps its weight) or every weight in target
|
|
39
|
+
* order. A `weights` entry in `transition` animates each change. For
|
|
40
|
+
* weights changing every frame prefer `ref` + setMorphWeights from
|
|
41
|
+
* onFrame, the same split as setTransform. */
|
|
42
|
+
morphWeights?: MorphWeights
|
|
35
43
|
ref?: (mesh: MeshNode) => void
|
|
36
44
|
}
|
|
37
45
|
|
|
@@ -40,10 +48,13 @@ export type MeshProps = TransformProps & PointerEventProps & {
|
|
|
40
48
|
// take them); the
|
|
41
49
|
// ref and the cleanup are each component's own (a populated mesh frees
|
|
42
50
|
// its buffers, a plain one is removed).
|
|
43
|
-
export function syncMesh(mesh: MeshNode, props: Omit<MeshProps, "geometry" | "castShadow" | "ref">): void {
|
|
51
|
+
export function syncMesh(mesh: MeshNode, props: Omit<MeshProps, "geometry" | "material" | "castShadow" | "ref"> & { material?: Material }): void {
|
|
52
|
+
// Absent on an <InstancedLod>, whose levels each carry their own.
|
|
44
53
|
createEffect(
|
|
45
54
|
() => props.material,
|
|
46
|
-
m =>
|
|
55
|
+
m => {
|
|
56
|
+
if (m !== undefined) setMaterial(mesh, m)
|
|
57
|
+
},
|
|
47
58
|
{ defer: true },
|
|
48
59
|
)
|
|
49
60
|
createEffect(
|
|
@@ -81,12 +92,19 @@ export let Mesh: VoidComponent<MeshProps> = props => {
|
|
|
81
92
|
() => props.castShadow,
|
|
82
93
|
c => setCastShadow(mesh, c === true),
|
|
83
94
|
)
|
|
95
|
+
createEffect(
|
|
96
|
+
() => props.morphWeights,
|
|
97
|
+
w => {
|
|
98
|
+
if (w !== undefined) setMorphWeights(mesh, w)
|
|
99
|
+
},
|
|
100
|
+
)
|
|
84
101
|
syncMesh(mesh, props)
|
|
85
102
|
untrack(() => props.ref)?.(mesh)
|
|
86
|
-
onCleanup(() =>
|
|
103
|
+
onCleanup(() => destroy(mesh))
|
|
87
104
|
return null
|
|
88
105
|
}
|
|
89
106
|
|
|
90
107
|
// The props both populated meshes share with Mesh: everything but the
|
|
91
|
-
// geometry/material pair (documented per component) and the
|
|
92
|
-
|
|
108
|
+
// geometry/material pair (documented per component), the ref and the
|
|
109
|
+
// morph weights (a populated mesh does not morph yet).
|
|
110
|
+
export type PopulatedMeshProps = Omit<MeshProps, "geometry" | "material" | "ref" | "morphWeights">
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { createEffect } from "@solidrt/core"
|
|
1
|
+
import { createEffect, onCleanup, useContext } from "@solidrt/core"
|
|
2
|
+
import { LodContext } from "./context.tsx"
|
|
2
3
|
import { setTransform, setTransition, setVisible } from "../node.ts"
|
|
3
4
|
import type { NodePointerEvent, NodeTapEvent, NodeWheelEvent, SceneNode, TransitionEndEvent } from "../node.ts"
|
|
4
5
|
import type { NodeTransition } from "flux:spatial"
|
|
@@ -18,6 +19,10 @@ export type TransformProps = {
|
|
|
18
19
|
transition?: NodeTransition | string | null
|
|
19
20
|
/** A declared transition settled on one component. */
|
|
20
21
|
onTransitionEnd?: (event: TransitionEndEvent) => void
|
|
22
|
+
/** Under a `<Lod>`: this node is a level of it, drawn while the group's
|
|
23
|
+
* projected size is at or above this and below the next larger level's
|
|
24
|
+
* (see createLod). Only on a direct child of `<Lod>`; throws elsewhere. */
|
|
25
|
+
lodSize?: number
|
|
21
26
|
}
|
|
22
27
|
|
|
23
28
|
/**
|
|
@@ -31,13 +36,20 @@ export type TransformProps = {
|
|
|
31
36
|
* the built-in <Scene> leaf does (opt out with events={false}); an `output`
|
|
32
37
|
* leaf spreads them itself.
|
|
33
38
|
*/
|
|
34
|
-
export type PointerEventProps = {
|
|
39
|
+
export type PointerEventProps = BubblingPointerEventProps & {
|
|
40
|
+
/** Hover: enter/leave pair on the struck node alone, so only a hit
|
|
41
|
+
* target - a mesh or an instance - takes them; `<Group>` and `<Lod>`
|
|
42
|
+
* bubble and omit the pair. */
|
|
43
|
+
onPointerEnter?: (event: NodePointerEvent) => void
|
|
44
|
+
onPointerLeave?: (event: NodePointerEvent) => void
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The pointer props every node takes: the events that bubble from the
|
|
48
|
+
* hit up through the Groups above it, without the hover pair. */
|
|
49
|
+
export type BubblingPointerEventProps = {
|
|
35
50
|
onPointerDown?: (event: NodePointerEvent) => void
|
|
36
51
|
onPointerMove?: (event: NodePointerEvent) => void
|
|
37
52
|
onPointerUp?: (event: NodePointerEvent) => void
|
|
38
|
-
/** Meshes and instances only: a Group never receives enter/leave. */
|
|
39
|
-
onPointerEnter?: (event: NodePointerEvent) => void
|
|
40
|
-
onPointerLeave?: (event: NodePointerEvent) => void
|
|
41
53
|
/** The wheel over the node: `deltaX`/`deltaY`. */
|
|
42
54
|
onWheel?: (event: NodeWheelEvent) => void
|
|
43
55
|
/** A press released on the node within the slop, alone for its whole
|
|
@@ -87,4 +99,22 @@ export function syncNode(node: SceneNode, props: TransformProps & PointerEventPr
|
|
|
87
99
|
() => props.transition,
|
|
88
100
|
transition => setTransition(node, transition ?? null),
|
|
89
101
|
)
|
|
102
|
+
// A level of the enclosing <Lod>, keyed by size; built only when the
|
|
103
|
+
// prop is present (the compiler emits the getter only then), so a plain
|
|
104
|
+
// node pays nothing.
|
|
105
|
+
if ("lodSize" in props) {
|
|
106
|
+
let lod = useContext(LodContext)
|
|
107
|
+
createEffect(
|
|
108
|
+
() => props.lodSize,
|
|
109
|
+
size => {
|
|
110
|
+
if (size === undefined) {
|
|
111
|
+
lod?.unregister(node)
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
if (lod === undefined) throw new Error("lodSize needs a <Lod> parent")
|
|
115
|
+
lod.register(node, size)
|
|
116
|
+
},
|
|
117
|
+
)
|
|
118
|
+
onCleanup(() => lod?.unregister(node))
|
|
119
|
+
}
|
|
90
120
|
}
|