@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.
Files changed (72) hide show
  1. package/AGENTS.md +1206 -224
  2. package/README.md +16 -7
  3. package/examples/README.md +88 -9
  4. package/examples/aim.tsx +2 -3
  5. package/examples/cascades.tsx +1 -4
  6. package/examples/collision.tsx +263 -0
  7. package/examples/first-person.tsx +129 -0
  8. package/examples/fleet.tsx +200 -0
  9. package/examples/fog.tsx +2 -3
  10. package/examples/instanced.tsx +72 -32
  11. package/examples/lamps.tsx +92 -0
  12. package/examples/lit.tsx +3 -5
  13. package/examples/materials.tsx +150 -0
  14. package/examples/model-load.tsx +95 -0
  15. package/examples/model.glb +0 -0
  16. package/examples/model.tsx +35 -9
  17. package/examples/pick.tsx +145 -37
  18. package/examples/probe.tsx +107 -0
  19. package/examples/scene-background.tsx +2 -4
  20. package/examples/scene-basic.tsx +2 -4
  21. package/examples/scene-views.tsx +68 -48
  22. package/examples/shadows.tsx +2 -4
  23. package/examples/sky-lit.tsx +92 -0
  24. package/examples/skybox.tsx +134 -0
  25. package/examples/sprites.tsx +4 -6
  26. package/examples/standard.tsx +138 -0
  27. package/examples/sweep-paths.tsx +2 -4
  28. package/package.json +3 -3
  29. package/src/camera.ts +148 -0
  30. package/src/clip.ts +96 -0
  31. package/src/collision.ts +20 -0
  32. package/src/color.ts +32 -0
  33. package/src/components/context.tsx +35 -0
  34. package/src/components/directional-light.tsx +48 -0
  35. package/src/components/first-person-camera.tsx +72 -0
  36. package/src/components/group.tsx +18 -0
  37. package/src/components/hemisphere-light.tsx +24 -0
  38. package/src/components/index.ts +46 -0
  39. package/src/components/instanced-mesh.tsx +105 -0
  40. package/src/components/mesh.tsx +92 -0
  41. package/src/components/node-props.ts +90 -0
  42. package/src/components/orbit-camera.tsx +75 -0
  43. package/src/components/perspective-camera.tsx +32 -0
  44. package/src/components/point-light.tsx +52 -0
  45. package/src/components/record-mesh.tsx +68 -0
  46. package/src/components/scene.tsx +281 -0
  47. package/src/components/spot-light.tsx +65 -0
  48. package/src/components/sprite.tsx +38 -0
  49. package/src/components/view3d.tsx +152 -0
  50. package/src/environment-bake.ts +492 -0
  51. package/src/environment.ts +325 -0
  52. package/src/first-person.ts +258 -0
  53. package/src/geometry.ts +6 -2
  54. package/src/glsl.ts +1343 -82
  55. package/src/gltf.ts +432 -112
  56. package/src/index.ts +61 -9
  57. package/src/input.ts +70 -0
  58. package/src/light.ts +383 -0
  59. package/src/material.ts +667 -232
  60. package/src/math.ts +110 -0
  61. package/src/mesh.ts +728 -0
  62. package/src/mixer.ts +476 -0
  63. package/src/model-file.ts +80 -15
  64. package/src/model.ts +204 -35
  65. package/src/node.ts +579 -0
  66. package/src/orbit.ts +292 -212
  67. package/src/scene-pointer.ts +294 -0
  68. package/src/scene-shadows.ts +471 -0
  69. package/src/scene.ts +1435 -1576
  70. package/tools/environment.ts +69 -0
  71. package/tools/model.ts +4 -3
  72. 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 / 8, 0)
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
  )
@@ -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
- }