@solidrt/3d 0.0.48 → 0.0.50

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 CHANGED
@@ -11,7 +11,7 @@ blendMode and pointer events like any element. Design rationale:
11
11
 
12
12
  - Two layers. The imperative core is Solid-free: `createScene`,
13
13
  `createMesh(geometry, material)`, `add`/`remove`, `setTransform`,
14
- `lookAt`, `getRotation`, `setVisible` - plain objects with dirty flags, batched to a
14
+ `lookAt`, `getRotation`, `setVisible`, `setRenderOrder` - plain objects with dirty flags, batched to a
15
15
  microtask,
16
16
  one `setDrawParams` (uModel, plus uNormal for materials declaring it)
17
17
  per changed mesh and ONE `setTargetParams` (the shared uViewProj +
@@ -42,16 +42,22 @@ blendMode and pointer events like any element. Design rationale:
42
42
  lazy, shared, and app-lifetime (owner-scoped free would break sharing);
43
43
  `disposeGeometry` frees them.
44
44
  - Materials dedupe hard: one program + one pipeline per material CLASS
45
- (unlit color, unlit map), `depth: true` + `cull: "back"`; an instance is
45
+ (unlit color, unlit map, each opaque or transparent), `depth: true` +
46
+ `cull: "back"`; an instance is
46
47
  just per-entry uniforms (`uColor`) and bindings (`uMap`).
48
+ - The pure pieces (`math.ts`, `bvh.ts`, `order.ts`) have check rigs in
49
+ `checks/`, run headless on flux from the repo root:
50
+ `bunx srt bundle -f --stdout packages/3d/checks/<name>-check.ts | target/release/flux - [seed]`.
51
+ They print PASS or FAIL lines and throw on failure (the flux binary exits
52
+ 0 either way - read the output). Extend the rig when you change the module.
47
53
 
48
54
  ## Components
49
55
 
50
56
  | Component | Props |
51
57
  | --- | --- |
52
- | `Scene` | `width`, `height` (target pixels), `clearColor?`, `label?`, `ref?(scene)`, `output?(texture)` |
53
- | `Group` | `position?`, `rotation?` (Euler radians, XYZ order), `quaternion?` (either, not both), `scale?` (number = uniform), `visible?`, `ref?(node)` |
54
- | `Mesh` | `geometry`, `material`, transforms as Group, `params?` (per-mesh uniforms, merge semantics - no unset), `ref?(mesh)` |
58
+ | `Scene` | `width`, `height` (target pixels), `clearColor?`, `background?` (fragment GLSL), `label?`, `ref?(scene)`, `output?(texture)`, `events?` (mesh pointer events, default on) |
59
+ | `Group` | `position?`, `rotation?` (Euler radians, XYZ order), `quaternion?` (either, not both), `scale?` (number = uniform), `visible?`, pointer events (below), `ref?(node)` |
60
+ | `Mesh` | `geometry`, `material`, transforms as Group, `params?` (per-mesh uniforms, merge semantics - no unset), pointer events (below), `ref?(mesh)` |
55
61
  | `PerspectiveCamera` | `fov?` (vertical DEGREES, default 60), `near?`, `far?`, `position?`, `lookAt?`, `up?` |
56
62
 
57
63
  Output composition: without `output`, `Scene` emits a minimal
@@ -102,6 +108,40 @@ immediately, so set-then-project in one tick is exact. `scene.viewProj(out?)`
102
108
  copies the view-projection matrix for batch work. Never rebuild the
103
109
  camera matrices by hand for a HUD.
104
110
 
111
+ Picking: `scene.pick(x, y)` is project()'s inverse - the camera ray
112
+ through a scene pixel, returning `Hit[]` (`{ mesh, distance, point }`,
113
+ world units, nearest first; every hit along the ray, not just the front
114
+ one). `scene.raycast(origin, direction)` is the world-space primitive
115
+ under it. The volume tier: hits test each mesh's local bounding box,
116
+ transformed exactly under any node transform (non-uniform scale
117
+ included), so results are conservative - a ray through a knot's hole
118
+ still hits (no `face`/`uv` fields until a triangle tier exists).
119
+ Broadphase is a dynamic AABB tree (BVH) the sync walk keeps current from
120
+ its own dirty set - maintenance is O(changed), a query O(log meshes) -
121
+ so per-pointer-move picking puts no ceiling on scene size. Both methods
122
+ flush pending writes first (the lookAt/project immediacy contract), and
123
+ both skip invisible meshes.
124
+
125
+ Mesh pointer events - the element vocabulary one tree deeper:
126
+ `onPointerDown/Move/Up/Enter/Leave` as plain fields on any node (and as
127
+ Mesh/Group props). The nearest hit mesh is the target; down/move/up
128
+ bubble mesh -> ancestor groups (`stopPropagation()` stops the walk);
129
+ enter/leave fire on the mesh alone, pairing on hover changes. A
130
+ pointer-down CAPTURES its mesh until the up: moves and the up keep
131
+ dispatching to it off-mesh (the platform's captured-drag rule), with
132
+ `point`/`distance` null while the ray misses it. The event carries the
133
+ element fields (pointerId, pointerType, button, modifiers) plus `mesh`,
134
+ `currentTarget`, `point`, `distance`, and `x`/`y` in scene pixels.
135
+ Wiring: the built-in `<Scene>` leaf carries `scene.handlers`
136
+ automatically (opt out: `events={false}`); an `output` leaf or
137
+ imperative composition spreads `{...scene.handlers}` onto the element
138
+ showing the texture. `scene.handlers` assumes that leaf is LAID OUT at
139
+ the target size - true for the built-in leaf and a d-texture at natural
140
+ size, under any ancestor transforms or viewBox fits (the hit test
141
+ undoes them; localX/localY arrive in the leaf's layout frame). A leaf
142
+ laid out at a different size (the supersampling pattern) uses
143
+ `scene.handlersFor(() => ({ width, height }))` with its layout size.
144
+
105
145
  Geometry: `box(w?, h?, d?)`; `plane(w?, h?)`, `circle(radius?, seg?)` and
106
146
  `ring(inner?, outer?, seg?)` (XY, facing +z - rotate `[-Math.PI/2, 0, 0]`
107
147
  for a floor); `sphere(radius?, wSeg?, hSeg?)`;
@@ -160,7 +200,10 @@ Materials:
160
200
  at shaderMaterial() creation. The rest is opt-in by declare-and-use:
161
201
  `uniform vec3 uCamPos` (the camera's world position, shared and written
162
202
  with uViewProj - the specular/fresnel view vector is
163
- `normalize(uCamPos - worldPos)`) and `uniform mat4 uNormal` (the world
203
+ `normalize(uCamPos - worldPos)`), `uniform vec3 uCamRight` / `uCamUp`
204
+ (the camera's world-space view axes, shared likewise - a billboard is
205
+ `center + uCamRight * x + uCamUp * y`; do NOT rebuild them from
206
+ uViewProj rows, that carries the clip flip) and `uniform mat4 uNormal` (the world
164
207
  inverse-transpose, written beside uModel for this material's meshes;
165
208
  take `mat3(uNormal)` - correct under non-uniform scale, where
166
209
  mat3(uModel) bends normals off the surface). Attributes come from the
@@ -174,6 +217,29 @@ Materials:
174
217
  with the `Mesh` `params` prop (same merge semantics - a key that
175
218
  disappears from the object keeps its old value; for per-frame values
176
219
  prefer `ref` + setMeshParams from onFrame, the setTransform split).
220
+ Scene-wide values (a clock, a sun direction, fog) go through
221
+ `scene.setParams({ uTime })` instead - one write for every mesh.
222
+ - `shaderMaterialClass({ vertex, fragment, ...pipeline state })` - the
223
+ class/instance split for your own GLSL: compiles once, and
224
+ `cls.instance({ params?, textures? })` returns a Material sharing that
225
+ pipeline with its own values. `dispose()` lives on the class alone.
226
+ `shaderMaterial(opts)` is exactly a class with one instance (its
227
+ `dispose` forwards to the class).
228
+
229
+ Background: `scene.setBackground(source | null)`, the `background` option
230
+ on createScene, and the reactive `Scene` prop. Fragment GLSL drawn as the
231
+ FIRST entry of the scene's own pass (attributeless fullscreen triangle,
232
+ depth off) - one target instead of a backdrop texture stacked under the
233
+ scene, with no separate resize plumbing. The source gets the
234
+ shader-target fragment contract exactly (vUV 0..1 top-left origin,
235
+ iResolution, fragColor; no `#version` line = the standard preamble), so
236
+ a `createShaderTexture` backdrop ports verbatim. Three's
237
+ `scene.background = color` is `clearColor` here; a texture-id form can
238
+ widen the signature later (a branded TextureId is a number, so
239
+ `string | TextureId` disambiguates at runtime). No app-driven uniforms:
240
+ a background is static art - anything animated is a mesh's own
241
+ shaderMaterial (or, until blend factors land, a separate shader texture
242
+ underneath, which translucent grounds also still need).
177
243
 
178
244
  Lighting GLSL (`@solidrt/3d/glsl`): exported string constants composed
179
245
  into shaderMaterial sources with plain template literals - `LIT_VERTEX`
@@ -197,10 +263,30 @@ system.
197
263
  and show mesh interiors.
198
264
  - `visible: false` keeps the entry, drawn with `instanceCount: 0` (a
199
265
  cheap off switch). Hidden meshes skip uModel writes; the fresh matrix is
200
- written on unhide.
201
- - Alpha does not blend in v1: pipelines are opaque (`blend: "none"`), a
202
- translucent color overwrites. Transparency waits on blend factors +
203
- sorting (research note, staging step 4).
266
+ written on unhide. A freshly attached entry starts off the same way and
267
+ sync() turns it on when it writes uModel - never add one live: it has no
268
+ world matrix yet, and drawn before the sync microtask it flashes at the
269
+ world origin for a frame.
270
+ - Transparency is an EXPLICIT material flag, Three's rule: `unlit({ color:
271
+ [r, g, b, 0.5] })` still draws opaque; `unlit({ ..., transparent: true })`
272
+ (or `shaderMaterial({ transparent: true })`) builds the pipeline with
273
+ `blend: "alpha"` and `depthWrite: false` (depth test stays on, so it hides
274
+ behind opaques without occluding other translucents). The one inference:
275
+ a `shaderMaterial` with any `blend` but "none" is transparent unless told
276
+ `transparent: false` - every blended draw belongs after the opaques, and
277
+ back-to-front is harmless for add/multiply. The scene owns the
278
+ order: background, opaque meshes by `renderOrder` then add order,
279
+ transparent meshes by `renderOrder` then back-to-front by the CENTER of
280
+ the mesh's world bounds in view space (not the origin: off-origin geometry
281
+ sorts by where it is; not the nearest bounds point: a big translucent
282
+ ground plane would cover the small translucents on it) - one `setDrawOrder` from sync() whenever the list changed, a
283
+ renderOrder changed, or (with two or more transparent meshes) the camera
284
+ or a transparent mesh moved, and skipped when the resort lands on the
285
+ permutation already issued. Per-mesh sort only: one non-convex translucent
286
+ mesh still overlaps itself in vertex order, and two large interpenetrating
287
+ translucents can sort wrong (center distance, not per-pixel) - that is the
288
+ engine contract, no OIT. A `shaderMaterial({ transparent: true })`
289
+ fragment must write PREMULTIPLIED output (`vec4(rgb * a, a)`).
204
290
  - Rotation is stored as a QUATERNION (`node.quaternion`, `[x, y, z, w]`,
205
291
  always unit). There is exactly one rotation field: no `node.rotation`
206
292
  shadowing it, because a second field is a second thing to go stale (an
@@ -277,9 +363,36 @@ system.
277
363
  camera writes (uEye-style per-mesh params are exactly the O(scene) cost
278
364
  the shared channel removed). Scene scale honestly: hundreds to a
279
365
  few thousand objects, bounded by the interpreter, not the GPU.
366
+ - SCENE-WIDE uniforms go through that same shared channel via
367
+ `scene.setParams({ uTime })`, and this is the single highest-leverage
368
+ pattern in the library. It merges an app-owned name in beside
369
+ uViewProj/uCamPos/uCamRight/uCamUp - names merge, a target tolerates
370
+ zero coverage, neither side clobbers the other. One write per frame
371
+ however many meshes read it, with the motion itself in vertex shaders
372
+ off that one clock. `params`/`setMeshParams` is the PER-MESH answer and
373
+ is O(meshes) per frame; reach for it only when the value genuinely
374
+ differs per mesh. (`scene.texture` IS the draw target id, so
375
+ `setTargetParams(scene.texture, ...)` is the same write - setParams is
376
+ the sanctioned spelling.)
377
+ - Vec3/Quat arguments are COPIED IN everywhere (`setTransform`, `lookAt`,
378
+ `setCamera`, params), so ONE scratch array reused every frame is safe -
379
+ allocating three arrays per node per frame is pure waste. The node's own
380
+ `position`/`quaternion`/`scale` are the live arrays: read them, do not
381
+ hand them out and do not mutate them (that write does not sync).
382
+ - `setTransform` early-outs on an unchanged value (rotation compared AFTER
383
+ euler conversion), so driving every node unconditionally from `onFrame`
384
+ costs only the compare for nodes that did not move. Compares are exact,
385
+ like `setVisible`.
386
+ - Per-generator conventions - orientation, UV mapping, which axis a solid
387
+ stands on, what a cap looks like - live on each generator's doc comment,
388
+ not here. They are consistent (`plane`/`circle`/`ring` face +z, `torus`
389
+ lies flat with the hole on y, discs and cylinder caps get a PLANAR disc
390
+ map inscribed in the unit square) but the doc comment is the source.
280
391
  - Entry rebuild order: `setGeometry`/`setMaterial` re-add the entry at the
281
- list END. Irrelevant while everything is opaque + depth-tested; revisit
282
- when transparency lands.
392
+ list END and dirty the order, so the next sync() re-sorts and the mesh
393
+ keeps its place. `_transparent` on the mesh is the flag AS ATTACHED
394
+ (setMaterial swaps `mesh.material` before the rebuild, so _detach must
395
+ not read the new material's flag).
283
396
  - `lathe` takes a CLOSED profile (a cross-section with thickness, or run
284
397
  to the axis at x = 0) - it is a solid of revolution, NOT Three's open
285
398
  polyline shell. An "open" outline must be closed by the author;
@@ -290,7 +403,14 @@ system.
290
403
  compile twice - no dedupe by source value (deliberate; hidden
291
404
  content-keyed caches are the anti-pattern the GPU layer avoids). Create
292
405
  one per look at app scope, share across meshes, `dispose()` when done
293
- for good.
406
+ for good. Looks that differ only in params/textures are ONE
407
+ `shaderMaterialClass` and many `instance()`s - the app-owned split, not
408
+ a cache. A class instance has no `dispose` of its own; disposing the
409
+ class invalidates every instance.
410
+ - A parameterised class whose variants (mapped/unmapped, ...) are SEPARATE
411
+ classes must have every variant reference every shared uniform it is
412
+ seeded with: a declared-but-unused per-entry name compiles out and
413
+ throws at add(). Open item: `okf/backlog/gpu-inactive-uniform-two-tier.md`.
294
414
  - The standard-set contract is checked TEXTUALLY at shaderMaterial()
295
415
  creation (uModel and uViewProj must appear in the vertex source) and
296
416
  strictly at add() for the per-entry names: a uModel or uNormal that is
@@ -304,3 +424,32 @@ system.
304
424
  vertex source - a comment counts - selects the "colored" layout, and
305
425
  the material then rejects standard geometry at add(). Do not mention
306
426
  aColor you do not read.
427
+ - Picking is the VOLUME tier: a hit means the ray crossed the mesh's
428
+ transformed bounding box, not its surface. Never present `point` as a
429
+ surface point (it is the box-entry point), and never add a
430
+ triangle-accurate path in JS - per-triangle rays at mesh scale are
431
+ interpreter-hostile; that tier is core work (BVH descent per the
432
+ differentiators ladder).
433
+ - `scene.handlers` vs `handlersFor`: localX/localY arrive in the leaf's
434
+ LAYOUT frame (every ancestor transform and viewBox fit is already
435
+ undone by the element hit test). `handlers` therefore assumes leaf
436
+ layout == target pixels; scaling by `getBoundingBox` would be WRONG -
437
+ the box composes transforms, and it would double-correct the built-in
438
+ leaf under a viewBox. Only a leaf whose layout size deliberately
439
+ differs from the target (supersampling) needs `handlersFor`, fed the
440
+ layout size the app itself set.
441
+ - Hover (enter/leave) reacts to pointer MOTION only: a mesh animating
442
+ under a still pointer fires nothing until the next move - the same
443
+ limit the element hit test has (hit-test-per-frame is an open platform
444
+ item). Do not poll pick() per frame to fake it.
445
+ - Geometry local bounds cache on the Geometry (like its GPU buffers):
446
+ geometry is immutable after creation. Mutating `vertices` after a mesh
447
+ used them leaves stale bounds AND a stale GPU buffer - make a new
448
+ Geometry instead.
449
+ - The background covers the whole target with depth off, drawn first: it
450
+ REPLACES the clearColor visually (the clear still runs; you just never
451
+ see it), and a `transparent: true` mesh blends over it in-pass since the
452
+ background is always entry zero.
453
+ - The background pipeline/program are SCENE-OWNED (unlike shared
454
+ material pipelines): setBackground(null), replacement, and dispose()
455
+ destroy them. Do not hand the background's pipeline to anything else.
package/README.md CHANGED
@@ -51,10 +51,23 @@ Three's `Euler` default, and `getRotation(node)` reads one back. The
51
51
  verbs: `quatFromAxisAngle`, `quatMultiply`, and `quatSlerp` (smooth
52
52
  tracking, damped follows) round out `quatFromTo`; `examples/aim.tsx`
53
53
  shows each aiming style live.
54
+
55
+ Meshes take pointer events like elements do: `onPointerDown/Move/Up/
56
+ Enter/Leave` props on `<Mesh>` and `<Group>`, with bubbling, capture on
57
+ drag, and hover enter/leave pairs - hit testing runs over a BVH the
58
+ scene maintains incrementally, so events put no ceiling on scene size.
59
+ Underneath sit `scene.pick(x, y)` (the camera ray through a pixel,
60
+ `project()`'s inverse) and `scene.raycast(origin, direction)`; hits are
61
+ bounding-box accurate in v1. A scene also takes a `background` - fragment
62
+ GLSL drawn inside its own pass behind the meshes, replacing the stacked
63
+ backdrop-texture pattern.
54
64
  Custom materials get a standard uniform set - per-mesh `uModel`/`uNormal`,
55
- shared `uViewProj`/`uCamPos`, each written once per change - plus your own
56
- uniforms per mesh, declaratively via the `params` prop on `<Mesh>` or
57
- imperatively via `setMeshParams` - and
65
+ shared `uViewProj`/`uCamPos`/`uCamRight`/`uCamUp`, each written once per
66
+ change - plus your own uniforms: scene-wide via `scene.setParams` (one write
67
+ however many meshes read it), or per mesh, declaratively via the `params`
68
+ prop on `<Mesh>` or imperatively via `setMeshParams`. `shaderMaterialClass`
69
+ compiles one program and hands out `instance()` materials that differ only
70
+ in params/textures. And
58
71
  `@solidrt/3d/glsl` exports the lighting pieces (hemisphere, lambert,
59
72
  blinn, fresnel, a standard vertex stage) to compose your own lit looks
60
73
  from plain template literals.
@@ -67,8 +80,9 @@ mitred joints, flat `shape`, with `fillet`/`roundRect`/`triangulate`
67
80
  helpers), a per-vertex data channel
68
81
  (`withColors` adds an `aColor` vec4 - tint, baked AO, any four scalars -
69
82
  to any geometry, for materials that read it), one perspective camera
70
- with an orbit control (`createOrbitCamera`: drag, pinch/wheel zoom, auto-orbit).
71
- Lights, transparency, model loading and picking are
83
+ with an orbit control (`createOrbitCamera`: drag, pinch/wheel zoom, auto-orbit),
84
+ mesh picking with pointer events, and scene backgrounds.
85
+ Lights, transparency and model loading are
72
86
  staged next - see `okf/research/scene-graph-3d.md` for the roadmap. Full
73
87
  usage notes and traps: [AGENTS.md](AGENTS.md); runnable examples:
74
88
  [examples/](examples/).
@@ -15,3 +15,11 @@ depends on `@solidrt/3d` (or in-repo from the package directory).
15
15
  target: `lookAt` for a +z solid, `quatFromTo` for aiming a y-axis cone,
16
16
  and a `quatSlerp` damped follow that visibly lags; all driven from
17
17
  onFrame through refs, no per-frame signals.
18
+ - `pick.tsx` - mesh pointer events: hover tints (enter/leave), click
19
+ pops (down), a Group hearing its children's clicks through bubbling
20
+ and one mesh stopping the walk; a STATIC scene rendered only when an
21
+ event changes something, with hit testing over the scene's BVH.
22
+ - `scene-background.tsx` - a fragment-GLSL background drawn inside the
23
+ scene's own pass (`<Scene background>`): one target, no stacked
24
+ backdrop texture, no resize plumbing; the source is shader-target
25
+ compatible verbatim.
@@ -0,0 +1,98 @@
1
+ // Mesh pointer events: hover to tint (enter/leave), click to pop (down),
2
+ // with a Group seeing its children's clicks through bubbling and one mesh
3
+ // stopping the walk. The scene is STATIC - no onFrame - so it renders only
4
+ // when an event changes something: picking is demand-driven like
5
+ // everything else. Events flow because the built-in <Scene> leaf carries
6
+ // scene.handlers (default on); hit testing runs over the scene's BVH, so a
7
+ // pointer move costs O(log meshes).
8
+ //
9
+ // The volume tier is honest about its shape: hits test each mesh's
10
+ // bounding box, so hovering just outside the ball's silhouette (inside its
11
+ // box corners) still counts. Triangle-accurate hits are a later tier.
12
+
13
+ import { createSignal, pct, render } from "@solidrt/core"
14
+ import { box, cone, Group, Mesh, PerspectiveCamera, plane, Scene, setMeshParams, setTransform, sphere, torus, unlit } from "@solidrt/3d"
15
+ import type { Geometry, ScenePointerEvent, Vec3 } from "@solidrt/3d"
16
+
17
+ const SIZE = 720
18
+
19
+ type Color = [number, number, number]
20
+
21
+ let [hovered, setHovered] = createSignal("nothing")
22
+ let popped = new Set<object>()
23
+
24
+ /** A pickable mesh: unlit color that brightens on hover, scale-pop on
25
+ * click. setMeshParams writes raw uniform values, so the tint premultiplies
26
+ * here (alpha 1: the rgb passes through). */
27
+ function Pickable(p: { name: string; color: Color; geometry: Geometry; position: Vec3; scale?: number; stop?: boolean }) {
28
+ let lift = (up: number): [number, number, number, number] => [
29
+ Math.min(p.color[0] + up, 1),
30
+ Math.min(p.color[1] + up, 1),
31
+ Math.min(p.color[2] + up, 1),
32
+ 1,
33
+ ]
34
+ return (
35
+ <Mesh
36
+ geometry={p.geometry}
37
+ material={unlit({ color: p.color })}
38
+ position={p.position}
39
+ scale={p.scale}
40
+ onPointerEnter={(e: ScenePointerEvent) => {
41
+ setHovered(p.name)
42
+ setMeshParams(e.mesh, { uColor: lift(0.25) })
43
+ console.log(`enter ${p.name}`)
44
+ }}
45
+ onPointerLeave={(e: ScenePointerEvent) => {
46
+ setHovered("nothing")
47
+ setMeshParams(e.mesh, { uColor: lift(0) })
48
+ console.log(`leave ${p.name}`)
49
+ }}
50
+ onPointerDown={(e: ScenePointerEvent) => {
51
+ let pop = !popped.has(e.mesh)
52
+ if (pop) popped.add(e.mesh)
53
+ else popped.delete(e.mesh)
54
+ setTransform(e.mesh, { scale: pop ? (p.scale ?? 1) * 1.25 : (p.scale ?? 1) })
55
+ let pt = e.point!
56
+ console.log(`down ${p.name} at ${pt[0].toFixed(2)},${pt[1].toFixed(2)},${pt[2].toFixed(2)} d=${e.distance!.toFixed(2)}`)
57
+ // The cone demonstrates stopping the bubble: its group never hears it.
58
+ if (p.stop) e.stopPropagation()
59
+ }}
60
+ />
61
+ )
62
+ }
63
+
64
+ function App() {
65
+ let floor = plane(8, 8, "floor")
66
+ let crate = box()
67
+ let ball = sphere(0.45)
68
+ let spike = cone(0.45, 1)
69
+ let ring = torus(0.4, 0.14)
70
+
71
+ return (
72
+ <window>
73
+ <view width={pct(100)} height={pct(100)} viewBox={[SIZE, SIZE]}>
74
+ <Scene width={SIZE} height={SIZE} clearColor={[0.07, 0.07, 0.1, 1]} label="pick">
75
+ <PerspectiveCamera fov={55} position={[0, 2.6, 5]} lookAt={[0, 0.4, 0]} />
76
+ <Mesh geometry={floor} material={unlit({ color: [0.15, 0.16, 0.2] })} rotation={[-Math.PI / 2, 0, 0]} />
77
+ {/* The group hears every child click that is not stopped. */}
78
+ <Group onPointerDown={() => console.log("group saw down")}>
79
+ <Pickable name="crate" color={[0.85, 0.3, 0.3]} geometry={crate} position={[-1.6, 0.4, 0]} scale={0.8} />
80
+ <Pickable name="cone" color={[0.35, 0.65, 0.9]} geometry={spike} position={[1.6, 0.5, 0]} stop />
81
+ </Group>
82
+ <Pickable name="ball" color={[0.9, 0.8, 0.35]} geometry={ball} position={[0, 0.45, 0]} />
83
+ <Pickable name="ring" color={[0.5, 0.85, 0.5]} geometry={ring} position={[0.9, 0.35, -1.6]} />
84
+ </Scene>
85
+ <view position="absolute" x={0} y={0} padding={16} gap={4}>
86
+ <text color="#eef4ff" fontSize={22} fontWeight={700}>
87
+ {`hover: ${hovered()}`}
88
+ </text>
89
+ <text color="#8fa6c8" fontSize={13}>
90
+ hover tints, click pops - a static scene, rendered only on change
91
+ </text>
92
+ </view>
93
+ </view>
94
+ </window>
95
+ )
96
+ }
97
+
98
+ render(() => <App />)
@@ -0,0 +1,43 @@
1
+ // A scene background: fragment GLSL drawn as the first entry of the
2
+ // scene's own pass - no second target, no stacked texture layers, no
3
+ // resize plumbing. The source below uses only the shader-target contract
4
+ // (vUV, iResolution, fragColor), so it would work unchanged in
5
+ // createShaderTexture; here it costs nothing extra - the one scene pass
6
+ // paints backdrop and meshes together, and a static scene still renders
7
+ // zero passes when idle.
8
+
9
+ import { pct, render } from "@solidrt/core"
10
+ import { glsl } from "@solidrt/core/gpu"
11
+ import { box, Mesh, PerspectiveCamera, Scene, sphere, torusKnot, unlit } from "@solidrt/3d"
12
+
13
+ const SIZE = 720
14
+
15
+ // A radial night-sky gradient with hash grain so the ramp does not band.
16
+ let BACKDROP = glsl`
17
+ void main() {
18
+ float d = distance(vUV, vec2(0.5, 0.35));
19
+ vec3 near = vec3(0.10, 0.13, 0.22);
20
+ vec3 far = vec3(0.02, 0.03, 0.06);
21
+ vec3 col = mix(near, far, smoothstep(0.05, 0.9, d));
22
+ float n = fract(sin(dot(vUV * iResolution, vec2(12.9898, 78.233))) * 43758.5453);
23
+ col += (n - 0.5) * 0.012;
24
+ fragColor = vec4(col, 1.0);
25
+ }
26
+ `
27
+
28
+ function App() {
29
+ return (
30
+ <window>
31
+ <view width={pct(100)} height={pct(100)} viewBox={[SIZE, SIZE]}>
32
+ <Scene width={SIZE} height={SIZE} background={BACKDROP} label="backdrop-demo">
33
+ <PerspectiveCamera fov={55} position={[0, 1.8, 4.4]} lookAt={[0, 0.4, 0]} />
34
+ <Mesh geometry={torusKnot(0.7, 0.2, 128, 16)} material={unlit({ color: [0.85, 0.55, 0.25] })} position={[0, 0.9, 0]} />
35
+ <Mesh geometry={box()} material={unlit({ color: [0.3, 0.5, 0.8] })} position={[-1.5, 0.4, -0.5]} scale={0.8} />
36
+ <Mesh geometry={sphere(0.4)} material={unlit({ color: [0.4, 0.75, 0.45] })} position={[1.5, 0.4, -0.5]} />
37
+ </Scene>
38
+ </view>
39
+ </window>
40
+ )
41
+ }
42
+
43
+ render(() => <App />)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidrt/3d",
3
- "version": "0.0.48",
3
+ "version": "0.0.50",
4
4
  "license": "MIT",
5
5
  "author": "Antoine van Wel",
6
6
  "type": "module",
@@ -16,7 +16,7 @@
16
16
  "AGENTS.md"
17
17
  ],
18
18
  "peerDependencies": {
19
- "@solidjs/signals": "2.0.0-beta.31",
20
- "@solidrt/core": "0.0.48"
19
+ "@solidjs/signals": "2.0.0-rc.0",
20
+ "@solidrt/core": "0.0.50"
21
21
  }
22
22
  }