@solidrt/3d 0.0.51 → 0.0.52

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.
@@ -3,13 +3,12 @@
3
3
  // over the crate's edges like real webbing - while smooth-tagged points
4
4
  // share averaged normals, so the helix sweeps into ONE continuous coil
5
5
  // (per-segment boxes with unmitred gaps are exactly what this replaces).
6
- // tube() is the round-profile shorthand. Lit materials on purpose:
6
+ // tube() is the round-profile shorthand. lit() materials on purpose:
7
7
  // creased vs smooth joints differ only in normals, which unlit color
8
8
  // would hide.
9
9
  import { createSignal, onFrame, pct, render } from "@solidrt/core"
10
- import { box, Group, Mesh, PerspectiveCamera, plane, roundRect, Scene, shaderMaterial, sweep, tube, unlit } from "@solidrt/3d"
10
+ import { box, DirectionalLight, Group, HemisphereLight, lit, Mesh, PerspectiveCamera, plane, roundRect, Scene, sweep, tube, unlit } from "@solidrt/3d"
11
11
  import type { SweepPath } from "@solidrt/3d"
12
- import { HEMISPHERE, LAMBERT, LIT_VERTEX } from "@solidrt/3d/glsl"
13
12
 
14
13
  const SIZE = 720
15
14
 
@@ -17,23 +16,6 @@ function App() {
17
16
  let [spin, setSpin] = createSignal(0)
18
17
  onFrame(tick => setSpin(tick / 4000))
19
18
 
20
- // One hemisphere + lambert look per color (a material instance is the
21
- // pipeline handle, so each look is created once and shared).
22
- let lit = (r: number, g: number, b: number) =>
23
- shaderMaterial({
24
- vertex: LIT_VERTEX,
25
- fragment: `
26
- in vec3 vNormal;
27
- ${HEMISPHERE}
28
- ${LAMBERT}
29
- void main() {
30
- vec3 n = normalize(vNormal);
31
- vec3 l = normalize(vec3(0.5, 0.8, 0.4));
32
- vec3 base = vec3(${r}, ${g}, ${b});
33
- fragColor = vec4(base * (hemisphere(n, vec3(0.45), vec3(0.22)) + 0.8 * lambert(n, l)), 1.0);
34
- }`,
35
- })
36
-
37
19
  // The strap hugs the crate at half its thickness; every bend is a bare
38
20
  // (sharp) point, so each fold creases exactly on a crate edge.
39
21
  let o = 0.013
@@ -45,7 +27,7 @@ function App() {
45
27
  [-0.3 + o, o, 0],
46
28
  [0.15, o, 0],
47
29
  ]
48
- let strap = sweep(roundRect(0.3, 0.026, 0.008), strapPath, "strap")
30
+ let strap = sweep(roundRect(0.3, 0.026, 0.008), strapPath, { label: "strap" })
49
31
 
50
32
  // A smooth-tagged helix: one continuous tube, not a stack of segments.
51
33
  let coilPath: SweepPath = []
@@ -53,22 +35,24 @@ function App() {
53
35
  let a = (i / 60) * Math.PI * 5
54
36
  coilPath.push({ p: [0.85 + Math.cos(a) * 0.35, 0.055 + i * 0.0095, Math.sin(a) * 0.35], smooth: true })
55
37
  }
56
- let coil = tube(coilPath, 0.05, 12, "coil")
38
+ let coil = tube(coilPath, { radius: 0.05, radialSegments: 12, label: "coil" })
57
39
 
58
40
  return (
59
41
  <window>
60
- <view width={pct(100)} height={pct(100)} viewBox={[SIZE, SIZE]}>
42
+ <view width={pct(100)} height={pct(100)} designSize={[SIZE, SIZE]}>
61
43
  <Scene width={SIZE} height={SIZE} clearColor={[0.07, 0.07, 0.1, 1]} label="sweep-paths">
62
44
  <PerspectiveCamera fov={55} position={[0, 1.9, 3.9]} lookAt={[0, 0.35, 0]} />
45
+ <HemisphereLight sky={[0.45, 0.45, 0.45]} ground={[0.22, 0.22, 0.22]} />
46
+ <DirectionalLight direction={[-0.5, -0.8, -0.4]} intensity={0.8} />
63
47
  <Mesh
64
- geometry={plane(6, 6, "floor")}
48
+ geometry={plane({ width: 6, height: 6, label: "floor" })}
65
49
  material={unlit({ color: [0.16, 0.17, 0.22] })}
66
50
  rotation={[-Math.PI / 2, 0, 0]}
67
51
  />
68
52
  <Group rotation={[0, spin(), 0]}>
69
- <Mesh geometry={box(1, 0.6, 0.8)} material={lit(0.55, 0.42, 0.28)} position={[-0.8, 0.3, 0]} />
70
- <Mesh geometry={strap} material={lit(0.9, 0.55, 0.2)} />
71
- <Mesh geometry={coil} material={lit(0.45, 0.6, 0.8)} />
53
+ <Mesh geometry={box({ width: 1, height: 0.6, depth: 0.8 })} material={lit({ color: [0.55, 0.42, 0.28] })} position={[-0.8, 0.3, 0]} />
54
+ <Mesh geometry={strap} material={lit({ color: [0.9, 0.55, 0.2] })} />
55
+ <Mesh geometry={coil} material={lit({ color: [0.45, 0.6, 0.8] })} />
72
56
  </Group>
73
57
  </Scene>
74
58
  </view>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidrt/3d",
3
- "version": "0.0.51",
3
+ "version": "0.0.52",
4
4
  "license": "MIT",
5
5
  "funding": "https://github.com/sponsors/wellawaretech",
6
6
  "author": "Antoine van Wel",
@@ -14,10 +14,11 @@
14
14
  "files": [
15
15
  "src/",
16
16
  "examples/",
17
+ "demos/",
17
18
  "AGENTS.md"
18
19
  ],
19
20
  "peerDependencies": {
20
- "@solidjs/signals": "2.0.0-rc.0",
21
- "@solidrt/core": "0.0.51"
21
+ "@solidjs/signals": "2.0.0-rc.1",
22
+ "@solidrt/core": "0.0.52"
22
23
  }
23
24
  }
@@ -10,15 +10,20 @@ import { createContext, createEffect, onCleanup, untrack, useContext } from "@so
10
10
  import type { Element, ParentComponent, TextureId, VoidComponent } from "@solidrt/core"
11
11
  import {
12
12
  add,
13
+ createDirectionalLight,
13
14
  createGroup,
15
+ createHemisphereLight,
14
16
  createInstancedMesh,
15
17
  createMesh,
16
18
  createScene,
19
+ createSprite,
17
20
  disposeInstances,
18
21
  remove,
19
22
  setGeometry,
20
23
  setInstanceCount,
21
24
  setInstances,
25
+ setLight,
26
+ setCastShadow,
22
27
  setMaterial,
23
28
  setMeshParams,
24
29
  setRenderOrder,
@@ -26,7 +31,7 @@ import {
26
31
  setVisible,
27
32
  } from "./scene.ts"
28
33
  import type { ShaderParams } from "@solidrt/core/gpu"
29
- import type { InstancedMesh as InstancedMeshNode, Mesh as MeshNode, Scene as SceneHandle, SceneNode, ScenePointerEvent } from "./scene.ts"
34
+ import type { DirectionalLight as DirectionalLightNode, HemisphereLight as HemisphereLightNode, InstancedMesh as InstancedMeshNode, Mesh as MeshNode, Scene as SceneHandle, SceneNode, ScenePointerEvent, ShadowOptions } from "./scene.ts"
30
35
  import type { Geometry } from "./geometry.ts"
31
36
  import type { Material } from "./material.ts"
32
37
  import type { Quat, Vec3 } from "./math.ts"
@@ -103,6 +108,9 @@ export type SceneProps = {
103
108
  * `scene.background = color` is `clearColor` here. */
104
109
  background?: string
105
110
  label?: string
111
+ /** Multisample count (1, 2, 4 or 8; default 1): anti-aliased mesh edges.
112
+ * Fixed at creation. */
113
+ samples?: 1 | 2 | 4 | 8
106
114
  ref?: (scene: SceneHandle) => void
107
115
  /**
108
116
  * Compose the output yourself: called once (untracked) with the scene's
@@ -131,7 +139,7 @@ export type SceneProps = {
131
139
  */
132
140
  export let Scene: ParentComponent<SceneProps> = props => {
133
141
  let scene = untrack(() =>
134
- createScene(props.width, props.height, { clearColor: props.clearColor, label: props.label }),
142
+ createScene(props.width, props.height, { clearColor: props.clearColor, label: props.label, samples: props.samples }),
135
143
  )
136
144
  createEffect(
137
145
  () => [props.width, props.height] as const,
@@ -186,19 +194,14 @@ export type MeshProps = TransformProps & PointerEventProps & {
186
194
  params?: ShaderParams
187
195
  /** Explicit draw-order key (setRenderOrder as a prop); default 0. */
188
196
  renderOrder?: number
197
+ /** Draw into the scene's shadow map (setCastShadow as a prop); default
198
+ * false. Needs a `castShadow` DirectionalLight to show. */
199
+ castShadow?: boolean
189
200
  ref?: (mesh: MeshNode) => void
190
201
  }
191
202
 
192
- /** One draw entry: geometry drawn with a material at a transform. */
193
- export let Mesh: VoidComponent<MeshProps> = props => {
194
- let ctx = useContext(SceneContext)
195
- let mesh = untrack(() => createMesh(props.geometry, props.material))
196
- add(ctx.parent, mesh)
197
- createEffect(
198
- () => props.geometry,
199
- g => setGeometry(mesh, g),
200
- { defer: true },
201
- )
203
+ // The mesh-side props Mesh and Sprite share (Sprite has no geometry).
204
+ function syncMesh(mesh: MeshNode, props: SpriteProps): void {
202
205
  createEffect(
203
206
  () => props.material,
204
207
  m => setMaterial(mesh, m),
@@ -217,6 +220,43 @@ export let Mesh: VoidComponent<MeshProps> = props => {
217
220
  syncNode(mesh, props)
218
221
  untrack(() => props.ref)?.(mesh)
219
222
  onCleanup(() => remove(mesh))
223
+ }
224
+
225
+ /** One draw entry: geometry drawn with a material at a transform. */
226
+ export let Mesh: VoidComponent<MeshProps> = props => {
227
+ let ctx = useContext(SceneContext)
228
+ let mesh = untrack(() => createMesh(props.geometry, props.material))
229
+ add(ctx.parent, mesh)
230
+ createEffect(
231
+ () => props.geometry,
232
+ g => setGeometry(mesh, g),
233
+ { defer: true },
234
+ )
235
+ createEffect(
236
+ () => props.castShadow,
237
+ c => setCastShadow(mesh, c === true),
238
+ )
239
+ syncMesh(mesh, props)
240
+ return null
241
+ }
242
+
243
+ export type SpriteProps = TransformProps & PointerEventProps & {
244
+ /** A `sprite()` material (any material draws, only a sprite one turns). */
245
+ material: Material
246
+ /** Per-mesh uniforms, merge semantics - as on Mesh. */
247
+ params?: ShaderParams
248
+ /** Explicit draw-order key (setRenderOrder as a prop); default 0. */
249
+ renderOrder?: number
250
+ ref?: (mesh: MeshNode) => void
251
+ }
252
+
253
+ /** A camera-facing unit quad (createSprite as a component): no geometry
254
+ * prop, `scale` is its world size, rotation is ignored. */
255
+ export let Sprite: VoidComponent<SpriteProps> = props => {
256
+ let ctx = useContext(SceneContext)
257
+ let mesh = untrack(() => createSprite(props.material))
258
+ add(ctx.parent, mesh)
259
+ syncMesh(mesh, props)
220
260
  return null
221
261
  }
222
262
 
@@ -225,8 +265,8 @@ export type InstancedMeshProps = TransformProps & PointerEventProps & {
225
265
  /** Must declare instanceAttributes (shaderMaterialClass). */
226
266
  material: Material
227
267
  /** Interleaved per-instance records (stride = the material's instance
228
- * attributes summed). Reactive, but the buffer's CAPACITY is fixed by the
229
- * first value - a later array may hold at most that many records. */
268
+ * attributes summed). Reactive; a later array larger than the buffer
269
+ * grows it (capacity doubles into a replacement buffer). */
230
270
  records: Float32Array
231
271
  /** How many records draw; default all of the latest `records`. */
232
272
  count?: number
@@ -312,3 +352,59 @@ export let PerspectiveCamera: VoidComponent<PerspectiveCameraProps> = props => {
312
352
  )
313
353
  return null
314
354
  }
355
+
356
+ export type HemisphereLightProps = { sky?: Vec3; ground?: Vec3; intensity?: number; ref?: (light: HemisphereLightNode) => void }
357
+
358
+ /** The scene's ambient term as a node (createHemisphereLight); one per
359
+ * scene, the last mounted wins. */
360
+ export let HemisphereLight: VoidComponent<HemisphereLightProps> = props => {
361
+ let ctx = useContext(SceneContext)
362
+ let light = untrack(() => createHemisphereLight({ sky: props.sky, ground: props.ground, intensity: props.intensity }))
363
+ add(ctx.parent, light)
364
+ createEffect(
365
+ () => [props.sky, props.ground, props.intensity] as const,
366
+ ([sky, ground, intensity]) => setLight(light, { sky, ground, intensity }),
367
+ )
368
+ untrack(() => props.ref)?.(light)
369
+ onCleanup(() => remove(light))
370
+ return null
371
+ }
372
+
373
+ export type DirectionalLightProps = TransformProps & {
374
+ /** Travel direction in the node's local space; default [0, -1, 0]. */
375
+ direction?: Vec3
376
+ color?: Vec3
377
+ intensity?: number
378
+ /** Render a shadow map from this light (any directional light may;
379
+ * each is a pass). Its shadow camera sits at the light's WORLD
380
+ * position, so give a casting light a `position` above the scene. */
381
+ castShadow?: boolean
382
+ /** Shadow-map options (mapSize, bias, normalBias, camera frustum),
383
+ * merged key by key. */
384
+ shadow?: ShadowOptions
385
+ ref?: (light: DirectionalLightNode) => void
386
+ }
387
+
388
+ /** A directional light node (createDirectionalLight): a parent Group's
389
+ * rotation turns it; up to MAX_LIGHTS per scene, in mount order. */
390
+ export let DirectionalLight: VoidComponent<DirectionalLightProps> = props => {
391
+ let ctx = useContext(SceneContext)
392
+ let light = untrack(() =>
393
+ createDirectionalLight({
394
+ direction: props.direction,
395
+ color: props.color,
396
+ intensity: props.intensity,
397
+ castShadow: props.castShadow,
398
+ shadow: props.shadow,
399
+ }),
400
+ )
401
+ add(ctx.parent, light)
402
+ syncNode(light, props)
403
+ createEffect(
404
+ () => [props.direction, props.color, props.intensity, props.castShadow, props.shadow] as const,
405
+ ([direction, color, intensity, castShadow, shadow]) => setLight(light, { direction, color, intensity, castShadow, shadow }),
406
+ )
407
+ untrack(() => props.ref)?.(light)
408
+ onCleanup(() => remove(light))
409
+ return null
410
+ }
@@ -1,6 +1,9 @@
1
1
  // Geometry on the GPU: the lazy buffer step for geometry.ts's data. Buffers
2
- // are created on first acquire and shared by every mesh and scene drawing
3
- // the geometry; each draw entry holds one reference, and the buffers are
2
+ // (and the picking shape - the spatial core's own copy of positions, UVs
3
+ // and indices for the triangle narrowphase, one per geometry however many
4
+ // meshes share it) are created on first acquire and shared by every mesh
5
+ // and scene drawing the geometry; each draw entry holds one reference, and
6
+ // the buffers are
4
7
  // freed when the last reference is released - deferred to a microtask, so
5
8
  // a same-tick entry rebuild (a material swap, a geometry that comes right
6
9
  // back) keeps its upload. The handles and the reference count live in a
@@ -10,6 +13,9 @@
10
13
 
11
14
  import { createBuffer, destroyBuffer } from "@solidrt/core/gpu"
12
15
  import type { BufferId, IndexFormat } from "@solidrt/core/gpu"
16
+ import { createShape, destroyShape } from "flux:spatial"
17
+ import type { ShapeId } from "flux:spatial"
18
+ import { layoutStride } from "./geometry.ts"
13
19
  import type { Geometry } from "./geometry.ts"
14
20
 
15
21
  /** An acquired reference to a geometry's GPU buffers: what a draw entry
@@ -20,6 +26,8 @@ export type GeometryBuffers = {
20
26
  buffer: BufferId
21
27
  index: BufferId
22
28
  indexFormat: IndexFormat
29
+ /** The picking shape (positions at 0, uv at 6 of every layout). */
30
+ shape: ShapeId
23
31
  }
24
32
 
25
33
  type GpuEntry = GeometryBuffers & { geometry: Geometry; refs: number }
@@ -44,6 +52,7 @@ export function acquireGeometryBuffers(geometry: Geometry): GeometryBuffers {
44
52
  label: geometry.label ? geometry.label + "-indices" : undefined,
45
53
  }),
46
54
  indexFormat: geometry.indices instanceof Uint32Array ? "uint32" : "uint16",
55
+ shape: createShape(geometry.vertices, layoutStride(geometry.layout), 0, 6, geometry.indices),
47
56
  refs: 0,
48
57
  }
49
58
  entries.set(geometry, entry)
@@ -67,6 +76,7 @@ export function releaseGeometryBuffers(acquired: GeometryBuffers): void {
67
76
  entries.delete(entry.geometry)
68
77
  destroyBuffer(entry.buffer)
69
78
  destroyBuffer(entry.index)
79
+ destroyShape(entry.shape)
70
80
  })
71
81
  }
72
82
 
@@ -83,4 +93,5 @@ export function disposeGeometry(geometry: Geometry): void {
83
93
  entries.delete(geometry)
84
94
  destroyBuffer(entry.buffer)
85
95
  destroyBuffer(entry.index)
96
+ destroyShape(entry.shape)
86
97
  }