@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.
Files changed (72) hide show
  1. package/AGENTS.md +1479 -678
  2. package/README.md +4 -2
  3. package/examples/README.md +56 -4
  4. package/examples/bloom.tsx +113 -0
  5. package/examples/cascades.tsx +4 -4
  6. package/examples/collision.tsx +6 -6
  7. package/examples/exits.tsx +96 -0
  8. package/examples/first-person.tsx +5 -5
  9. package/examples/fleet.tsx +4 -4
  10. package/examples/fog.tsx +5 -5
  11. package/examples/instanced.tsx +70 -50
  12. package/examples/lamps.tsx +4 -4
  13. package/examples/lod.tsx +125 -0
  14. package/examples/materials.tsx +9 -9
  15. package/examples/morph.tsx +174 -0
  16. package/examples/normals.tsx +118 -0
  17. package/examples/{lit.tsx → phong.tsx} +9 -9
  18. package/examples/polyhedra.tsx +61 -0
  19. package/examples/scene-atlas.tsx +70 -0
  20. package/examples/scene-views.tsx +5 -5
  21. package/examples/shadows.tsx +9 -9
  22. package/examples/sky-lit.tsx +3 -3
  23. package/examples/skybox.tsx +5 -5
  24. package/examples/standard.tsx +2 -2
  25. package/examples/streams.tsx +122 -0
  26. package/examples/sweep-paths.tsx +5 -5
  27. package/examples/wireframe.tsx +129 -0
  28. package/package.json +3 -3
  29. package/src/clip.ts +18 -5
  30. package/src/color.ts +11 -2
  31. package/src/components/context.tsx +11 -2
  32. package/src/components/directional-light.tsx +2 -2
  33. package/src/components/first-person-camera.tsx +4 -4
  34. package/src/components/group.tsx +9 -5
  35. package/src/components/hemisphere-light.tsx +2 -2
  36. package/src/components/index.ts +4 -1
  37. package/src/components/instanced-mesh.tsx +28 -10
  38. package/src/components/lod.tsx +102 -0
  39. package/src/components/mesh.tsx +24 -6
  40. package/src/components/node-props.ts +35 -5
  41. package/src/components/orbit-camera.tsx +15 -12
  42. package/src/components/point-light.tsx +2 -2
  43. package/src/components/record-mesh.tsx +12 -7
  44. package/src/components/scene.tsx +55 -2
  45. package/src/components/spot-light.tsx +2 -2
  46. package/src/components/sprite.tsx +2 -2
  47. package/src/components/view3d.tsx +18 -1
  48. package/src/environment.ts +9 -8
  49. package/src/first-person.ts +125 -38
  50. package/src/geometry-gpu.ts +187 -22
  51. package/src/geometry.ts +1718 -203
  52. package/src/glsl.ts +398 -113
  53. package/src/gltf.ts +457 -154
  54. package/src/index.ts +32 -21
  55. package/src/input.ts +11 -2
  56. package/src/light.ts +7 -8
  57. package/src/lod.ts +148 -0
  58. package/src/material.ts +322 -190
  59. package/src/mesh.ts +492 -259
  60. package/src/mixer.ts +27 -14
  61. package/src/model-file.ts +54 -15
  62. package/src/model.ts +151 -54
  63. package/src/motion.ts +17 -0
  64. package/src/node.ts +423 -23
  65. package/src/orbit.ts +378 -116
  66. package/src/profile.ts +4 -25
  67. package/src/resolve.ts +181 -0
  68. package/src/scene-shadows.ts +94 -93
  69. package/src/scene.ts +608 -185
  70. package/src/skeleton.ts +211 -0
  71. package/src/sweep.ts +27 -8
  72. 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.57",
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.6",
22
- "@solidrt/core": "0.0.57"
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. Pure array math:
12
- * step holds the earlier key, linear lerps (rotation slerps the shortest
13
- * path), cubic evaluates the glTF CUBICSPLINE Hermite (rotation
14
- * renormalized, per spec).
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.path === "rotation" ? 4 : 3
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 and the fragment's output stage
8
- // (OUTPUT in ./glsl) encodes the result back. Values written straight to
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 camera()). */
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, remove } from "../node.ts"
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(() => remove(light))
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()/active(), the
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 - so
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)
@@ -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, PointerEventProps } from "./node-props.ts"
6
- import { add, createGroup, remove } from "../node.ts"
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
- /** A transform node: children inherit its position/rotation/scale. */
10
- export let Group: ParentComponent<TransformProps & PointerEventProps & { ref?: (node: SceneNode) => void }> = props => {
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(() => remove(node))
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, remove } from "../node.ts"
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(() => remove(light))
22
+ onCleanup(() => destroy(light))
23
23
  return null
24
24
  }
@@ -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 { addInstance, createInstancedMesh, disposeInstances, removeInstance, setCastShadow, setGeometry, setInstanceStyle } from "../mesh.ts"
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 in slot 0 and any style layout in slot 1. */
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
- let InstancedMeshContext = createContext<InstancedMeshNode | null>(null)
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(() => disposeInstances(mesh))
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): the
76
- * floats of the material's slot-1 instance attributes - `[r, g, b, a]`
77
- * under a stock material's `instanceColors`. Reactive; absent, the
78
- * instance keeps the material's instanceStyle (white for a tint). */
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(() => removeInstance(instance))
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
+ }
@@ -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, remove } from "../node.ts"
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 => setMaterial(mesh, 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(() => remove(mesh))
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 ref.
92
- export type PopulatedMeshProps = Omit<MeshProps, "geometry" | "material" | "ref">
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
  }