@solidrt/3d 0.0.55 → 0.0.57
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 +1206 -224
- package/README.md +16 -7
- package/examples/README.md +88 -9
- package/examples/aim.tsx +2 -3
- package/examples/cascades.tsx +1 -4
- package/examples/collision.tsx +263 -0
- package/examples/first-person.tsx +129 -0
- package/examples/fleet.tsx +200 -0
- package/examples/fog.tsx +2 -3
- package/examples/instanced.tsx +72 -32
- package/examples/lamps.tsx +92 -0
- package/examples/lit.tsx +3 -5
- package/examples/materials.tsx +150 -0
- package/examples/model-load.tsx +95 -0
- package/examples/model.glb +0 -0
- package/examples/model.tsx +35 -9
- package/examples/pick.tsx +145 -37
- package/examples/probe.tsx +107 -0
- package/examples/scene-background.tsx +2 -4
- package/examples/scene-basic.tsx +2 -4
- package/examples/scene-views.tsx +68 -48
- package/examples/shadows.tsx +2 -4
- package/examples/sky-lit.tsx +92 -0
- package/examples/skybox.tsx +134 -0
- package/examples/sprites.tsx +4 -6
- package/examples/standard.tsx +138 -0
- package/examples/sweep-paths.tsx +2 -4
- package/package.json +3 -3
- package/src/camera.ts +148 -0
- package/src/clip.ts +96 -0
- package/src/collision.ts +20 -0
- package/src/color.ts +32 -0
- package/src/components/context.tsx +35 -0
- package/src/components/directional-light.tsx +48 -0
- package/src/components/first-person-camera.tsx +72 -0
- package/src/components/group.tsx +18 -0
- package/src/components/hemisphere-light.tsx +24 -0
- package/src/components/index.ts +46 -0
- package/src/components/instanced-mesh.tsx +105 -0
- package/src/components/mesh.tsx +92 -0
- package/src/components/node-props.ts +90 -0
- package/src/components/orbit-camera.tsx +75 -0
- package/src/components/perspective-camera.tsx +32 -0
- package/src/components/point-light.tsx +52 -0
- package/src/components/record-mesh.tsx +68 -0
- package/src/components/scene.tsx +281 -0
- package/src/components/spot-light.tsx +65 -0
- package/src/components/sprite.tsx +38 -0
- package/src/components/view3d.tsx +152 -0
- package/src/environment-bake.ts +492 -0
- package/src/environment.ts +325 -0
- package/src/first-person.ts +258 -0
- package/src/geometry.ts +6 -2
- package/src/glsl.ts +1343 -82
- package/src/gltf.ts +432 -112
- package/src/index.ts +61 -9
- package/src/input.ts +70 -0
- package/src/light.ts +383 -0
- package/src/material.ts +667 -232
- package/src/math.ts +110 -0
- package/src/mesh.ts +728 -0
- package/src/mixer.ts +476 -0
- package/src/model-file.ts +80 -15
- package/src/model.ts +204 -35
- package/src/node.ts +579 -0
- package/src/orbit.ts +292 -212
- package/src/scene-pointer.ts +294 -0
- package/src/scene-shadows.ts +471 -0
- package/src/scene.ts +1435 -1576
- package/tools/environment.ts +69 -0
- package/tools/model.ts +4 -3
- package/src/components.tsx +0 -420
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
// srt tool 3d/environment: bake a Radiance .hdr panorama into a .srte
|
|
4
|
+
// environment file - the six cube faces and their GGX-prefiltered mip
|
|
5
|
+
// chain in linear float, the exact chain loadEnvironment uploads as an
|
|
6
|
+
// explicit rgba16f cube (no runtime conversion, no generated mipmaps, no
|
|
7
|
+
// half-float render support needed). Unity convolves its reflection
|
|
8
|
+
// probes at import the same way; Three (PMREMGenerator) and Godot do it
|
|
9
|
+
// at runtime every time the sky changes. Put the output under assets/ so
|
|
10
|
+
// it ships with the app.
|
|
11
|
+
//
|
|
12
|
+
// srt tool 3d/environment <in.hdr> [-o <out.srte>] [--size <edge>]
|
|
13
|
+
//
|
|
14
|
+
// --size is the cube's face edge: 128 (default) is Unity's probe default
|
|
15
|
+
// and enough for every surface short of a mirror; 256 (Godot's radiance
|
|
16
|
+
// default, Three's PMREM cap) for a mirror-finish showpiece, at four
|
|
17
|
+
// times the bytes (a float32 file is 21 KiB per 1k texels: 2 MiB at 128).
|
|
18
|
+
|
|
19
|
+
import { readFileSync, writeFileSync } from "node:fs"
|
|
20
|
+
import { basename, extname } from "node:path"
|
|
21
|
+
import { decodeHdr, encodeEnvironment, panoramaToCube, prefilterCube } from "../src/environment-bake.ts"
|
|
22
|
+
|
|
23
|
+
// The face edge default and its allowed range (powers of two).
|
|
24
|
+
const DEFAULT_SIZE = 128
|
|
25
|
+
const MIN_SIZE = 16
|
|
26
|
+
const MAX_SIZE = 1024
|
|
27
|
+
|
|
28
|
+
function usage(error?: string): never {
|
|
29
|
+
if (error) console.error(error)
|
|
30
|
+
console.log("Usage: srt tool 3d/environment <in.hdr> [-o <out.srte>] [--size <edge>]")
|
|
31
|
+
process.exit(error ? 1 : 0)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let input: string | undefined
|
|
35
|
+
let output: string | undefined
|
|
36
|
+
let size = DEFAULT_SIZE
|
|
37
|
+
let args = process.argv.slice(2)
|
|
38
|
+
for (let i = 0; i < args.length; i++) {
|
|
39
|
+
let arg = args[i]!
|
|
40
|
+
if (arg === "--help" || arg === "-h") usage()
|
|
41
|
+
else if (arg === "-o" || arg === "--output") {
|
|
42
|
+
output = args[++i]
|
|
43
|
+
if (output === undefined) usage("Missing value for " + arg)
|
|
44
|
+
} else if (arg === "--size") {
|
|
45
|
+
let value = args[++i]
|
|
46
|
+
if (value === undefined) usage("Missing value for --size")
|
|
47
|
+
size = Number(value)
|
|
48
|
+
if (!Number.isInteger(size) || size < MIN_SIZE || size > MAX_SIZE || (size & (size - 1)) !== 0) {
|
|
49
|
+
usage("--size must be a power of two from " + MIN_SIZE + " to " + MAX_SIZE + ", got " + value)
|
|
50
|
+
}
|
|
51
|
+
} else if (arg.startsWith("-")) usage("Unknown option " + arg)
|
|
52
|
+
else if (input === undefined) input = arg
|
|
53
|
+
else usage("Unexpected argument " + arg)
|
|
54
|
+
}
|
|
55
|
+
if (input === undefined) usage("Missing input file")
|
|
56
|
+
if (output === undefined) output = basename(input, extname(input)) + ".srte"
|
|
57
|
+
|
|
58
|
+
let started = performance.now()
|
|
59
|
+
let panorama = decodeHdr(new Uint8Array(readFileSync(input)))
|
|
60
|
+
let decoded = performance.now()
|
|
61
|
+
let levels = prefilterCube(panoramaToCube(panorama, size), size)
|
|
62
|
+
let baked = performance.now()
|
|
63
|
+
let encoded = encodeEnvironment(levels, size)
|
|
64
|
+
writeFileSync(output, encoded)
|
|
65
|
+
|
|
66
|
+
console.log(
|
|
67
|
+
`${output}: ${size} cube, ${levels.length} levels, ${(encoded.byteLength / 1024).toFixed(0)} KiB ` +
|
|
68
|
+
`(${panorama.width}x${panorama.height} panorama decoded in ${(decoded - started).toFixed(0)} ms, baked in ${(baked - decoded).toFixed(0)} ms)`,
|
|
69
|
+
)
|
package/tools/model.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { readFileSync, writeFileSync } from "node:fs"
|
|
|
12
12
|
import { basename, dirname, extname, join } from "node:path"
|
|
13
13
|
import { parseGltf } from "../src/gltf.ts"
|
|
14
14
|
import { encodeModel } from "../src/model-file.ts"
|
|
15
|
+
import { layoutStride } from "../src/geometry.ts"
|
|
15
16
|
|
|
16
17
|
function usage(error?: string): never {
|
|
17
18
|
if (error) console.error(error)
|
|
@@ -44,9 +45,9 @@ let encoded = encodeModel(data)
|
|
|
44
45
|
writeFileSync(output, encoded)
|
|
45
46
|
|
|
46
47
|
let triangles = data.parts.reduce((n, p) => n + p.geometry.indices.length / 3, 0)
|
|
47
|
-
let vertices = data.parts.reduce((n, p) => n + p.geometry.vertices.length /
|
|
48
|
+
let vertices = data.parts.reduce((n, p) => n + p.geometry.vertices.length / layoutStride(p.geometry.layout), 0)
|
|
48
49
|
console.log(
|
|
49
|
-
`${output}: ${data.parts.length} parts, ${vertices} vertices, ${triangles} triangles, ` +
|
|
50
|
-
`${data.materials.length} materials, ${data.images.length} images, ${(encoded.byteLength / 1024).toFixed(0)} KiB ` +
|
|
50
|
+
`${output}: ${data.nodes.length} nodes, ${data.parts.length} parts, ${vertices} vertices, ${triangles} triangles, ` +
|
|
51
|
+
`${data.clips.length} clips, ${data.materials.length} materials, ${data.images.length} images, ${(encoded.byteLength / 1024).toFixed(0)} KiB ` +
|
|
51
52
|
`(parsed in ${parsed.toFixed(0)} ms)`,
|
|
52
53
|
)
|
package/src/components.tsx
DELETED
|
@@ -1,420 +0,0 @@
|
|
|
1
|
-
// The Solid face: PascalCase components over context, syncing the retained
|
|
2
|
-
// scene (scene.ts) - no new intrinsic elements, no renderer changes. Props
|
|
3
|
-
// follow the Solid 2.0 model (reactive values, no destructuring); effects
|
|
4
|
-
// write into the retained nodes and the runtime's dirty flush renders.
|
|
5
|
-
// Anything moving at frame rate can bypass the declarative layer: grab the
|
|
6
|
-
// node with `ref` and call setTransform from onFrame - signals carry
|
|
7
|
-
// structure and slow state, per-frame motion goes straight to the scene.
|
|
8
|
-
|
|
9
|
-
import { createContext, createEffect, onCleanup, untrack, useContext } from "@solidrt/core"
|
|
10
|
-
import type { Element, ParentComponent, TextureId, VoidComponent } from "@solidrt/core"
|
|
11
|
-
import {
|
|
12
|
-
add,
|
|
13
|
-
createDirectionalLight,
|
|
14
|
-
createGroup,
|
|
15
|
-
createHemisphereLight,
|
|
16
|
-
createInstancedMesh,
|
|
17
|
-
createMesh,
|
|
18
|
-
createScene,
|
|
19
|
-
createSprite,
|
|
20
|
-
disposeInstances,
|
|
21
|
-
remove,
|
|
22
|
-
setGeometry,
|
|
23
|
-
setInstanceCount,
|
|
24
|
-
setInstances,
|
|
25
|
-
setLight,
|
|
26
|
-
setCastShadow,
|
|
27
|
-
setMaterial,
|
|
28
|
-
setMeshParams,
|
|
29
|
-
setRenderOrder,
|
|
30
|
-
setTransform,
|
|
31
|
-
setVisible,
|
|
32
|
-
} from "./scene.ts"
|
|
33
|
-
import type { ShaderParams } from "@solidrt/core/gpu"
|
|
34
|
-
import type { DirectionalLight as DirectionalLightNode, FogOptions, HemisphereLight as HemisphereLightNode, InstancedMesh as InstancedMeshNode, Mesh as MeshNode, Scene as SceneHandle, SceneNode, ScenePointerEvent, ShadowOptions } from "./scene.ts"
|
|
35
|
-
import type { Geometry } from "./geometry.ts"
|
|
36
|
-
import type { Material } from "./material.ts"
|
|
37
|
-
import type { Quat, Vec3 } from "./math.ts"
|
|
38
|
-
|
|
39
|
-
type SceneCtx = { scene: SceneHandle; parent: SceneNode }
|
|
40
|
-
let SceneContext = createContext<SceneCtx>()
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* The enclosing scene and parent node - the imperative escape hatch inside
|
|
44
|
-
* a component subtree (throws outside a `<Scene>`).
|
|
45
|
-
*/
|
|
46
|
-
export function useScene(): SceneCtx {
|
|
47
|
-
return useContext(SceneContext)
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export type TransformProps = {
|
|
51
|
-
position?: Vec3
|
|
52
|
-
/** Euler radians in XYZ order (x first), Three's `Euler` default. */
|
|
53
|
-
rotation?: Vec3
|
|
54
|
-
/** The rotation as a quaternion - what the node stores. Pass this or
|
|
55
|
-
* `rotation`, not both. */
|
|
56
|
-
quaternion?: Quat
|
|
57
|
-
scale?: Vec3 | number
|
|
58
|
-
visible?: boolean
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Mesh pointer events, the element vocabulary one tree deeper: the nearest
|
|
63
|
-
* hit mesh receives the event, down/move/up bubble to ancestor Groups
|
|
64
|
-
* (stopPropagation stops the walk), enter/leave pair on the mesh alone.
|
|
65
|
-
* Events flow while the element showing the scene carries scene.handlers -
|
|
66
|
-
* the built-in <Scene> leaf does (opt out with events={false}); an `output`
|
|
67
|
-
* leaf spreads them itself.
|
|
68
|
-
*/
|
|
69
|
-
export type PointerEventProps = {
|
|
70
|
-
onPointerDown?: (event: ScenePointerEvent) => void
|
|
71
|
-
onPointerMove?: (event: ScenePointerEvent) => void
|
|
72
|
-
onPointerUp?: (event: ScenePointerEvent) => void
|
|
73
|
-
/** Meshes only: a Group never receives enter/leave. */
|
|
74
|
-
onPointerEnter?: (event: ScenePointerEvent) => void
|
|
75
|
-
onPointerLeave?: (event: ScenePointerEvent) => void
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function syncNode(node: SceneNode, props: TransformProps & PointerEventProps): void {
|
|
79
|
-
createEffect(
|
|
80
|
-
() => [props.position, props.rotation, props.quaternion, props.scale, props.visible] as const,
|
|
81
|
-
([position, rotation, quaternion, scale, visible]) => {
|
|
82
|
-
setTransform(node, { position, rotation, quaternion, scale })
|
|
83
|
-
setVisible(node, visible !== false)
|
|
84
|
-
},
|
|
85
|
-
)
|
|
86
|
-
createEffect(
|
|
87
|
-
() => [props.onPointerDown, props.onPointerMove, props.onPointerUp, props.onPointerEnter, props.onPointerLeave] as const,
|
|
88
|
-
([down, move, up, enter, leave]) => {
|
|
89
|
-
node.onPointerDown = down
|
|
90
|
-
node.onPointerMove = move
|
|
91
|
-
node.onPointerUp = up
|
|
92
|
-
node.onPointerEnter = enter
|
|
93
|
-
node.onPointerLeave = leave
|
|
94
|
-
},
|
|
95
|
-
)
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export type SceneProps = {
|
|
99
|
-
/** Target pixels. With `output`, the leaf's own width/height are layout,
|
|
100
|
-
* so render size and display size separate (supersampling). */
|
|
101
|
-
width: number
|
|
102
|
-
height: number
|
|
103
|
-
clearColor?: [number, number, number, number]
|
|
104
|
-
/** Fragment GLSL drawn behind the meshes, inside the scene's own pass
|
|
105
|
-
* (scene.setBackground): vUV/iResolution/fragColor contract, so a
|
|
106
|
-
* createShaderTexture backdrop ports verbatim. Reactive - swapping the
|
|
107
|
-
* source replaces the background; undefined removes it. Three's
|
|
108
|
-
* `scene.background = color` is `clearColor` here. */
|
|
109
|
-
background?: string
|
|
110
|
-
/** Scene-wide fog (scene.setFog): linear `{ color, near, far }` or exp2
|
|
111
|
-
* `{ color, density }`, optionally thinning above `height` by
|
|
112
|
-
* `heightFalloff`; every standard material fades toward `color` by
|
|
113
|
-
* distance from the camera. Reactive; undefined removes it. Match
|
|
114
|
-
* `color` to clearColor or the background, which is not fogged. */
|
|
115
|
-
fog?: FogOptions
|
|
116
|
-
label?: string
|
|
117
|
-
/** Multisample count (1, 2, 4 or 8; default 1): anti-aliased mesh edges.
|
|
118
|
-
* Fixed at creation. */
|
|
119
|
-
samples?: 1 | 2 | 4 | 8
|
|
120
|
-
ref?: (scene: SceneHandle) => void
|
|
121
|
-
/**
|
|
122
|
-
* Compose the output yourself: called once (untracked) with the scene's
|
|
123
|
-
* texture id, and its return renders in place of the built-in `<texture>`
|
|
124
|
-
* leaf - a `<d-texture>`, a leaf carrying paint/pointer/layout props, or
|
|
125
|
-
* a post-effect chain (a shader target sampling the id; created in the
|
|
126
|
-
* callback it disposes with the Scene). Return null to render no leaf.
|
|
127
|
-
* Mesh pointer events then need the scene's handlers on your leaf:
|
|
128
|
-
* `<texture src={texture} {...useScene().scene.handlers} />`.
|
|
129
|
-
*/
|
|
130
|
-
output?: (texture: TextureId) => Element
|
|
131
|
-
/**
|
|
132
|
-
* Mesh pointer events (default on): the built-in leaf carries
|
|
133
|
-
* scene.handlers, so Mesh/Group onPointer* props receive events. `false`
|
|
134
|
-
* detaches them - the leaf then costs no pointer routing at all.
|
|
135
|
-
*/
|
|
136
|
-
events?: boolean
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Owns a draw target and composites it as an ordinary `<texture>` leaf, so
|
|
141
|
-
* the output takes layout, transforms, blendMode, and pointer events like
|
|
142
|
-
* any element - or hand `output` the texture id and compose it yourself.
|
|
143
|
-
* Children (Mesh/Group/PerspectiveCamera) render nothing themselves - they
|
|
144
|
-
* populate the retained scene through context.
|
|
145
|
-
*/
|
|
146
|
-
export let Scene: ParentComponent<SceneProps> = props => {
|
|
147
|
-
let scene = untrack(() =>
|
|
148
|
-
createScene(props.width, props.height, { clearColor: props.clearColor, label: props.label, samples: props.samples }),
|
|
149
|
-
)
|
|
150
|
-
createEffect(
|
|
151
|
-
() => [props.width, props.height] as const,
|
|
152
|
-
([w, h]) => scene.setSize(w, h),
|
|
153
|
-
)
|
|
154
|
-
createEffect(
|
|
155
|
-
() => props.background,
|
|
156
|
-
b => scene.setBackground(b ?? null),
|
|
157
|
-
)
|
|
158
|
-
createEffect(
|
|
159
|
-
() => props.fog,
|
|
160
|
-
f => scene.setFog(f ?? null),
|
|
161
|
-
)
|
|
162
|
-
untrack(() => props.ref)?.(scene)
|
|
163
|
-
let output = untrack(() => props.output)
|
|
164
|
-
let events = untrack(() => props.events) !== false
|
|
165
|
-
return (
|
|
166
|
-
<SceneContext value={{ scene, parent: scene.root }}>
|
|
167
|
-
{output ? (
|
|
168
|
-
untrack(() => output(scene.texture))
|
|
169
|
-
) : (
|
|
170
|
-
<texture
|
|
171
|
-
src={scene.texture}
|
|
172
|
-
width={props.width}
|
|
173
|
-
height={props.height}
|
|
174
|
-
onPointerDown={events ? scene.handlers.onPointerDown : undefined}
|
|
175
|
-
onPointerMove={events ? scene.handlers.onPointerMove : undefined}
|
|
176
|
-
onPointerUp={events ? scene.handlers.onPointerUp : undefined}
|
|
177
|
-
onPointerLeave={events ? scene.handlers.onPointerLeave : undefined}
|
|
178
|
-
/>
|
|
179
|
-
)}
|
|
180
|
-
{props.children}
|
|
181
|
-
</SceneContext>
|
|
182
|
-
)
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/** A transform node: children inherit its position/rotation/scale. */
|
|
186
|
-
export let Group: ParentComponent<TransformProps & PointerEventProps & { ref?: (node: SceneNode) => void }> = props => {
|
|
187
|
-
let ctx = useContext(SceneContext)
|
|
188
|
-
let node = createGroup()
|
|
189
|
-
add(ctx.parent, node)
|
|
190
|
-
syncNode(node, props)
|
|
191
|
-
untrack(() => props.ref)?.(node)
|
|
192
|
-
onCleanup(() => remove(node))
|
|
193
|
-
return <SceneContext value={{ scene: ctx.scene, parent: node }}>{props.children}</SceneContext>
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
export type MeshProps = TransformProps & PointerEventProps & {
|
|
197
|
-
geometry: Geometry
|
|
198
|
-
material: Material
|
|
199
|
-
/** Per-mesh uniforms for a custom material (setMeshParams as a prop).
|
|
200
|
-
* Keys merge - a key that disappears keeps its old value; there is no
|
|
201
|
-
* unset. Names must be declared by the material's shaders. For values
|
|
202
|
-
* changing every frame prefer `ref` + setMeshParams from onFrame, the
|
|
203
|
-
* same split as setTransform. */
|
|
204
|
-
params?: ShaderParams
|
|
205
|
-
/** Explicit draw-order key (setRenderOrder as a prop); default 0. */
|
|
206
|
-
renderOrder?: number
|
|
207
|
-
/** Draw into the scene's shadow map (setCastShadow as a prop); default
|
|
208
|
-
* false. Needs a `castShadow` DirectionalLight to show. */
|
|
209
|
-
castShadow?: boolean
|
|
210
|
-
ref?: (mesh: MeshNode) => void
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// The mesh-side props Mesh and Sprite share (Sprite has no geometry).
|
|
214
|
-
function syncMesh(mesh: MeshNode, props: SpriteProps): void {
|
|
215
|
-
createEffect(
|
|
216
|
-
() => props.material,
|
|
217
|
-
m => setMaterial(mesh, m),
|
|
218
|
-
{ defer: true },
|
|
219
|
-
)
|
|
220
|
-
createEffect(
|
|
221
|
-
() => props.params,
|
|
222
|
-
p => {
|
|
223
|
-
if (p !== undefined) setMeshParams(mesh, p)
|
|
224
|
-
},
|
|
225
|
-
)
|
|
226
|
-
createEffect(
|
|
227
|
-
() => props.renderOrder,
|
|
228
|
-
o => setRenderOrder(mesh, o ?? 0),
|
|
229
|
-
)
|
|
230
|
-
syncNode(mesh, props)
|
|
231
|
-
untrack(() => props.ref)?.(mesh)
|
|
232
|
-
onCleanup(() => remove(mesh))
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/** One draw entry: geometry drawn with a material at a transform. */
|
|
236
|
-
export let Mesh: VoidComponent<MeshProps> = props => {
|
|
237
|
-
let ctx = useContext(SceneContext)
|
|
238
|
-
let mesh = untrack(() => createMesh(props.geometry, props.material))
|
|
239
|
-
add(ctx.parent, mesh)
|
|
240
|
-
createEffect(
|
|
241
|
-
() => props.geometry,
|
|
242
|
-
g => setGeometry(mesh, g),
|
|
243
|
-
{ defer: true },
|
|
244
|
-
)
|
|
245
|
-
createEffect(
|
|
246
|
-
() => props.castShadow,
|
|
247
|
-
c => setCastShadow(mesh, c === true),
|
|
248
|
-
)
|
|
249
|
-
syncMesh(mesh, props)
|
|
250
|
-
return null
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
export type SpriteProps = TransformProps & PointerEventProps & {
|
|
254
|
-
/** A `sprite()` material (any material draws, only a sprite one turns). */
|
|
255
|
-
material: Material
|
|
256
|
-
/** Per-mesh uniforms, merge semantics - as on Mesh. */
|
|
257
|
-
params?: ShaderParams
|
|
258
|
-
/** Explicit draw-order key (setRenderOrder as a prop); default 0. */
|
|
259
|
-
renderOrder?: number
|
|
260
|
-
ref?: (mesh: MeshNode) => void
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/** A camera-facing unit quad (createSprite as a component): no geometry
|
|
264
|
-
* prop, `scale` is its world size, rotation is ignored. */
|
|
265
|
-
export let Sprite: VoidComponent<SpriteProps> = props => {
|
|
266
|
-
let ctx = useContext(SceneContext)
|
|
267
|
-
let mesh = untrack(() => createSprite(props.material))
|
|
268
|
-
add(ctx.parent, mesh)
|
|
269
|
-
syncMesh(mesh, props)
|
|
270
|
-
return null
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
export type InstancedMeshProps = TransformProps & PointerEventProps & {
|
|
274
|
-
geometry: Geometry
|
|
275
|
-
/** Must declare instanceAttributes (shaderMaterialClass). */
|
|
276
|
-
material: Material
|
|
277
|
-
/** Interleaved per-instance records (stride = the material's instance
|
|
278
|
-
* attributes summed). Reactive; a later array larger than the buffer
|
|
279
|
-
* grows it (capacity doubles into a replacement buffer). */
|
|
280
|
-
records: Float32Array
|
|
281
|
-
/** How many records draw; default all of the latest `records`. */
|
|
282
|
-
count?: number
|
|
283
|
-
/** LOCAL bounds covering every instance ([minX..maxZ]), fixed at
|
|
284
|
-
* creation. Without them the mesh has no picking leaf, so pointer events
|
|
285
|
-
* never target it. */
|
|
286
|
-
bounds?: ArrayLike<number>
|
|
287
|
-
/** Per-mesh uniforms, merge semantics - as on Mesh. */
|
|
288
|
-
params?: ShaderParams
|
|
289
|
-
/** Explicit draw-order key (setRenderOrder as a prop); default 0. */
|
|
290
|
-
renderOrder?: number
|
|
291
|
-
ref?: (mesh: InstancedMeshNode) => void
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/** One draw entry covering N instances: geometry repeated per record of
|
|
295
|
-
* `records` (createInstancedMesh as a component). The record buffer is
|
|
296
|
-
* component-owned and freed on unmount. */
|
|
297
|
-
export let InstancedMesh: VoidComponent<InstancedMeshProps> = props => {
|
|
298
|
-
let ctx = useContext(SceneContext)
|
|
299
|
-
let mesh = untrack(() =>
|
|
300
|
-
createInstancedMesh(props.geometry, props.material, props.records, props.count, { bounds: props.bounds }),
|
|
301
|
-
)
|
|
302
|
-
add(ctx.parent, mesh)
|
|
303
|
-
createEffect(
|
|
304
|
-
() => props.records,
|
|
305
|
-
r => setInstances(mesh, r, untrack(() => props.count)),
|
|
306
|
-
{ defer: true },
|
|
307
|
-
)
|
|
308
|
-
createEffect(
|
|
309
|
-
() => props.count,
|
|
310
|
-
c => {
|
|
311
|
-
if (c !== undefined) setInstanceCount(mesh, c)
|
|
312
|
-
},
|
|
313
|
-
{ defer: true },
|
|
314
|
-
)
|
|
315
|
-
createEffect(
|
|
316
|
-
() => props.geometry,
|
|
317
|
-
g => setGeometry(mesh, g),
|
|
318
|
-
{ defer: true },
|
|
319
|
-
)
|
|
320
|
-
createEffect(
|
|
321
|
-
() => props.material,
|
|
322
|
-
m => setMaterial(mesh, m),
|
|
323
|
-
{ defer: true },
|
|
324
|
-
)
|
|
325
|
-
createEffect(
|
|
326
|
-
() => props.params,
|
|
327
|
-
p => {
|
|
328
|
-
if (p !== undefined) setMeshParams(mesh, p)
|
|
329
|
-
},
|
|
330
|
-
)
|
|
331
|
-
createEffect(
|
|
332
|
-
() => props.renderOrder,
|
|
333
|
-
o => setRenderOrder(mesh, o ?? 0),
|
|
334
|
-
)
|
|
335
|
-
syncNode(mesh, props)
|
|
336
|
-
untrack(() => props.ref)?.(mesh)
|
|
337
|
-
onCleanup(() => disposeInstances(mesh))
|
|
338
|
-
return null
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
export type PerspectiveCameraProps = {
|
|
342
|
-
/** Vertical field of view in DEGREES (default 60). */
|
|
343
|
-
fov?: number
|
|
344
|
-
near?: number
|
|
345
|
-
far?: number
|
|
346
|
-
position?: Vec3
|
|
347
|
-
lookAt?: Vec3
|
|
348
|
-
up?: Vec3
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
/**
|
|
352
|
-
* Drives the scene's camera from props (the scene has a default camera, so
|
|
353
|
-
* this component is optional). The camera is scene state, not a tree node:
|
|
354
|
-
* to orbit it, update `position`/`lookAt`.
|
|
355
|
-
*/
|
|
356
|
-
export let PerspectiveCamera: VoidComponent<PerspectiveCameraProps> = props => {
|
|
357
|
-
let ctx = useContext(SceneContext)
|
|
358
|
-
createEffect(
|
|
359
|
-
() => [props.fov, props.near, props.far, props.position, props.lookAt, props.up] as const,
|
|
360
|
-
([fov, near, far, position, lookAt, up]) =>
|
|
361
|
-
ctx.scene.setCamera({ fov, near, far, position, target: lookAt, up }),
|
|
362
|
-
)
|
|
363
|
-
return null
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
export type HemisphereLightProps = { sky?: Vec3; ground?: Vec3; intensity?: number; ref?: (light: HemisphereLightNode) => void }
|
|
367
|
-
|
|
368
|
-
/** The scene's ambient term as a node (createHemisphereLight); one per
|
|
369
|
-
* scene, the last mounted wins. */
|
|
370
|
-
export let HemisphereLight: VoidComponent<HemisphereLightProps> = props => {
|
|
371
|
-
let ctx = useContext(SceneContext)
|
|
372
|
-
let light = untrack(() => createHemisphereLight({ sky: props.sky, ground: props.ground, intensity: props.intensity }))
|
|
373
|
-
add(ctx.parent, light)
|
|
374
|
-
createEffect(
|
|
375
|
-
() => [props.sky, props.ground, props.intensity] as const,
|
|
376
|
-
([sky, ground, intensity]) => setLight(light, { sky, ground, intensity }),
|
|
377
|
-
)
|
|
378
|
-
untrack(() => props.ref)?.(light)
|
|
379
|
-
onCleanup(() => remove(light))
|
|
380
|
-
return null
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
export type DirectionalLightProps = TransformProps & {
|
|
384
|
-
/** Travel direction in the node's local space; default [0, -1, 0]. */
|
|
385
|
-
direction?: Vec3
|
|
386
|
-
color?: Vec3
|
|
387
|
-
intensity?: number
|
|
388
|
-
/** Render a shadow map from this light (any directional light may;
|
|
389
|
-
* each is a pass). Its shadow camera sits at the light's WORLD
|
|
390
|
-
* position, so give a casting light a `position` above the scene. */
|
|
391
|
-
castShadow?: boolean
|
|
392
|
-
/** Shadow-map options (mapSize, bias, normalBias, camera frustum,
|
|
393
|
-
* cascades, distance), merged key by key. */
|
|
394
|
-
shadow?: ShadowOptions
|
|
395
|
-
ref?: (light: DirectionalLightNode) => void
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
/** A directional light node (createDirectionalLight): a parent Group's
|
|
399
|
-
* rotation turns it; up to MAX_LIGHTS per scene, in mount order. */
|
|
400
|
-
export let DirectionalLight: VoidComponent<DirectionalLightProps> = props => {
|
|
401
|
-
let ctx = useContext(SceneContext)
|
|
402
|
-
let light = untrack(() =>
|
|
403
|
-
createDirectionalLight({
|
|
404
|
-
direction: props.direction,
|
|
405
|
-
color: props.color,
|
|
406
|
-
intensity: props.intensity,
|
|
407
|
-
castShadow: props.castShadow,
|
|
408
|
-
shadow: props.shadow,
|
|
409
|
-
}),
|
|
410
|
-
)
|
|
411
|
-
add(ctx.parent, light)
|
|
412
|
-
syncNode(light, props)
|
|
413
|
-
createEffect(
|
|
414
|
-
() => [props.direction, props.color, props.intensity, props.castShadow, props.shadow] as const,
|
|
415
|
-
([direction, color, intensity, castShadow, shadow]) => setLight(light, { direction, color, intensity, castShadow, shadow }),
|
|
416
|
-
)
|
|
417
|
-
untrack(() => props.ref)?.(light)
|
|
418
|
-
onCleanup(() => remove(light))
|
|
419
|
-
return null
|
|
420
|
-
}
|