@solidrt/3d 0.0.58 → 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 +734 -264
- package/README.md +4 -2
- package/examples/README.md +50 -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 +24 -2
- 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 +172 -39
- package/src/geometry.ts +1573 -239
- package/src/glsl.ts +398 -113
- package/src/gltf.ts +363 -141
- package/src/index.ts +31 -21
- package/src/light.ts +7 -8
- package/src/lod.ts +148 -0
- package/src/material.ts +316 -202
- package/src/mesh.ts +490 -256
- package/src/mixer.ts +27 -14
- package/src/model-file.ts +54 -15
- package/src/model.ts +86 -21
- package/src/motion.ts +17 -0
- package/src/node.ts +421 -22
- 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 +605 -185
- package/src/sweep.ts +27 -8
- package/tools/model.ts +3 -1
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
|
}
|
|
@@ -15,8 +15,9 @@ export type OrbitCameraProps = OrbitCameraOptions & {
|
|
|
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 OrbitAxes, string | null>>
|
|
18
|
-
/** The control's handle (pose()/set()/
|
|
19
|
-
* verbs, `axes` - also the debug command
|
|
18
|
+
/** The control's handle (pose()/set()/glideTo()/fit()/eye()/
|
|
19
|
+
* orbiting()/active(), the verbs, `axes` - also the debug command
|
|
20
|
+
* shape). */
|
|
20
21
|
ref?: (orbit: OrbitCameraHandle) => void
|
|
21
22
|
}
|
|
22
23
|
|
|
@@ -29,9 +30,10 @@ export type OrbitCameraProps = OrbitCameraOptions & {
|
|
|
29
30
|
* (target, azimuth, elevation, distance) are initial values - change the
|
|
30
31
|
* pose at runtime through `ref`'s set() or the verbs; every other prop is
|
|
31
32
|
* live, forwarded to the control as a getter and read where it applies,
|
|
32
|
-
* so a clamp, a rate or an anchor callback follows its prop,
|
|
33
|
-
* change re-clamps the pose at once.
|
|
34
|
-
* `active()` (auto-orbit on,
|
|
33
|
+
* so a clamp, a rate, `damping` or an anchor callback follows its prop,
|
|
34
|
+
* and a clamp change (`clampPose` included) re-clamps the pose at once.
|
|
35
|
+
* The frame loop runs only while `active()` (auto-orbit on, a glide or
|
|
36
|
+
* damped wheel notch in flight, or an axis rate driving); a camera moved
|
|
35
37
|
* by drags alone leaves the app demand-driven idle.
|
|
36
38
|
*/
|
|
37
39
|
export let OrbitCamera: VoidComponent<OrbitCameraProps> = props => {
|
|
@@ -59,14 +61,15 @@ export let OrbitCamera: VoidComponent<OrbitCameraProps> = props => {
|
|
|
59
61
|
},
|
|
60
62
|
)
|
|
61
63
|
// A clamp prop change re-clamps the pose at once (set({}) applies the
|
|
62
|
-
// clamps and pushes
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
64
|
+
// clamps and pushes, and writes no pose field, so a glide in flight
|
|
65
|
+
// keeps running) - the update() call a Three OrbitControls app makes
|
|
66
|
+
// after setting minDistance, done by the prop. Deferred: the creation
|
|
67
|
+
// already clamped and pushed the initial pose. Untracked: the control
|
|
68
|
+
// reads the clamps through the getters while it applies them, and the
|
|
69
|
+
// apply wants the values of that moment (the compute above is what
|
|
70
|
+
// tracks them).
|
|
68
71
|
createEffect(
|
|
69
|
-
() => [options.minDistance, options.maxDistance, options.minElevation, options.maxElevation],
|
|
72
|
+
() => [options.minDistance, options.maxDistance, options.minElevation, options.maxElevation, options.clampPose],
|
|
70
73
|
() => untrack(() => orbit.set({})),
|
|
71
74
|
{ defer: true },
|
|
72
75
|
)
|
|
@@ -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 { createPointLight, setLight } from "../light.ts"
|
|
8
8
|
import type { PointLight as PointLightNode, SpotShadowOptions } from "../light.ts"
|
|
9
9
|
import type { Vec3 } from "../math.ts"
|
|
@@ -47,6 +47,6 @@ export let PointLight: VoidComponent<PointLightProps> = props => {
|
|
|
47
47
|
([color, intensity, distance, decay, castShadow, shadow]) => setLight(light, { color, intensity, distance, decay, castShadow, shadow }),
|
|
48
48
|
)
|
|
49
49
|
untrack(() => props.ref)?.(light)
|
|
50
|
-
onCleanup(() =>
|
|
50
|
+
onCleanup(() => destroy(light))
|
|
51
51
|
return null
|
|
52
52
|
}
|
|
@@ -3,7 +3,7 @@ import type { VoidComponent } from "@solidrt/core"
|
|
|
3
3
|
import { SceneContext } from "./context.tsx"
|
|
4
4
|
import { syncMesh } from "./mesh.tsx"
|
|
5
5
|
import type { PopulatedMeshProps } from "./mesh.tsx"
|
|
6
|
-
import { add } from "../node.ts"
|
|
6
|
+
import { add, destroy } from "../node.ts"
|
|
7
7
|
import { createRecordMesh, disposeInstances, setCastShadow, setGeometry, setRecordCount, setRecords } from "../mesh.ts"
|
|
8
8
|
import type { RecordMesh as RecordMeshNode } from "../mesh.ts"
|
|
9
9
|
import type { Geometry } from "../geometry.ts"
|
|
@@ -11,12 +11,14 @@ import type { Material } from "../material.ts"
|
|
|
11
11
|
|
|
12
12
|
export type RecordMeshProps = PopulatedMeshProps & {
|
|
13
13
|
geometry: Geometry
|
|
14
|
-
/** Must declare
|
|
14
|
+
/** Must declare instanceBuffers (shaderMaterialClass); the first is
|
|
15
|
+
* the records' layout. */
|
|
15
16
|
material: Material
|
|
16
|
-
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
|
|
17
|
+
/** The records, laid out in the material's first instance layout (a
|
|
18
|
+
* Float32Array over an all-float layout, bytes otherwise; see
|
|
19
|
+
* setRecords). Reactive; a later value larger than the buffer grows it
|
|
20
|
+
* (capacity doubles into a replacement buffer). */
|
|
21
|
+
records: ArrayBufferView
|
|
20
22
|
/** How many records draw; default all of the latest `records`. */
|
|
21
23
|
count?: number
|
|
22
24
|
/** LOCAL bounds covering every instance ([minX..maxZ]), fixed at
|
|
@@ -63,6 +65,9 @@ export let RecordMesh: VoidComponent<RecordMeshProps> = props => {
|
|
|
63
65
|
)
|
|
64
66
|
syncMesh(mesh, props)
|
|
65
67
|
untrack(() => props.ref)?.(mesh)
|
|
66
|
-
onCleanup(() =>
|
|
68
|
+
onCleanup(() => {
|
|
69
|
+
destroy(mesh)
|
|
70
|
+
disposeInstances(mesh)
|
|
71
|
+
})
|
|
67
72
|
return null
|
|
68
73
|
}
|