@solidrt/3d 0.0.46 → 0.0.47
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 +145 -35
- package/README.md +22 -5
- package/examples/scene-post-effect.tsx +82 -0
- package/package.json +4 -3
- package/src/components.tsx +20 -4
- package/src/geometry.ts +292 -37
- package/src/glsl.ts +112 -0
- package/src/index.ts +6 -4
- package/src/material.ts +43 -6
- package/src/math.ts +40 -0
- package/src/orbit.ts +187 -27
- package/src/profile.ts +520 -0
- package/src/scene.ts +91 -37
package/AGENTS.md
CHANGED
|
@@ -12,9 +12,9 @@ blendMode and pointer events like any element. Design rationale:
|
|
|
12
12
|
- Two layers. The imperative core is Solid-free: `createScene`,
|
|
13
13
|
`createMesh(geometry, material)`, `add`/`remove`, `setTransform`,
|
|
14
14
|
`setVisible` - plain objects with dirty flags, batched to a microtask,
|
|
15
|
-
one `setDrawParams` (
|
|
16
|
-
`setTargetParams` (the shared uViewProj
|
|
17
|
-
meshes. The component
|
|
15
|
+
one `setDrawParams` (uModel, plus uNormal for materials declaring it)
|
|
16
|
+
per changed mesh and ONE `setTargetParams` (the shared uViewProj +
|
|
17
|
+
uCamPos) per camera change, however many meshes. The component
|
|
18
18
|
face (`Scene`/`Group`/`Mesh`/`PerspectiveCamera`) syncs props into that
|
|
19
19
|
core over context and renders nothing itself.
|
|
20
20
|
- Rendering is the runtime's. The target is `render: "auto"`: it
|
|
@@ -23,9 +23,23 @@ blendMode and pointer events like any element. Design rationale:
|
|
|
23
23
|
own `onFrame` writing a signal (declarative) or `setTransform` on a
|
|
24
24
|
`ref`-grabbed node (the frame-rate escape hatch - signals carry
|
|
25
25
|
structure, per-frame motion goes straight to the scene).
|
|
26
|
-
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
- Two named vertex layouts (`Geometry.layout`, absent = "standard"):
|
|
27
|
+
"standard" is `aPos` vec3 + `aNormal` vec3 + `aUV` vec2 - what every
|
|
28
|
+
generator emits - and "colored" appends `aColor` vec4, the per-vertex
|
|
29
|
+
data channel (a tint, baked AO, any four scalars; standard name, your
|
|
30
|
+
contents). Derive colored geometry with `withColors(geometry, fill)` -
|
|
31
|
+
fill is a flat 4-per-vertex array or a per-vertex callback receiving
|
|
32
|
+
`(index, pos, normal, uv)`. Geometry and material layouts must match
|
|
33
|
+
(layout is stride); a mismatched pair throws at add(). The whole layout
|
|
34
|
+
ships whether a material reads every attribute or not (inactive
|
|
35
|
+
attributes only keep the stride), so colored vertices cost 12 floats
|
|
36
|
+
regardless - keep data-light passes (a wireframe reading only aPos) on
|
|
37
|
+
standard geometry.
|
|
38
|
+
Indices are uint16 or uint32 - the `Geometry.indices` array type picks
|
|
39
|
+
the draw's index format, so hand-built geometry past 64k vertices just
|
|
40
|
+
uses a Uint32Array (generators emit uint16). Geometry GPU buffers are
|
|
41
|
+
lazy, shared, and app-lifetime (owner-scoped free would break sharing);
|
|
42
|
+
`disposeGeometry` frees them.
|
|
29
43
|
- Materials dedupe hard: one program + one pipeline per material CLASS
|
|
30
44
|
(unlit color, unlit map), `depth: true` + `cull: "back"`; an instance is
|
|
31
45
|
just per-entry uniforms (`uColor`) and bindings (`uMap`).
|
|
@@ -34,42 +48,131 @@ blendMode and pointer events like any element. Design rationale:
|
|
|
34
48
|
|
|
35
49
|
| Component | Props |
|
|
36
50
|
| --- | --- |
|
|
37
|
-
| `Scene` | `width`, `height` (target pixels), `clearColor?`, `label?`, `ref?(scene)` |
|
|
51
|
+
| `Scene` | `width`, `height` (target pixels), `clearColor?`, `label?`, `ref?(scene)`, `output?(texture)` |
|
|
38
52
|
| `Group` | `position?`, `rotation?` (Euler radians, x-y-z order), `scale?` (number = uniform), `visible?`, `ref?(node)` |
|
|
39
53
|
| `Mesh` | `geometry`, `material`, transforms as Group, `ref?(mesh)` |
|
|
40
54
|
| `PerspectiveCamera` | `fov?` (vertical DEGREES, default 60), `near?`, `far?`, `position?`, `lookAt?`, `up?` |
|
|
41
55
|
|
|
56
|
+
Output composition: without `output`, `Scene` emits a minimal
|
|
57
|
+
`<texture width height>` leaf and nothing else is forwarded - anything
|
|
58
|
+
more goes through `output(texture)`, which renders in place of that leaf:
|
|
59
|
+
a `<d-texture>`, a leaf with blendMode/fit/pointer/layout props, or a
|
|
60
|
+
post-effect chain (`createShaderTarget` sampling the id with a
|
|
61
|
+
covering-triangle pass; created in the callback it disposes with the
|
|
62
|
+
Scene). Return null for no leaf at all and compose `scene.texture`
|
|
63
|
+
elsewhere. Called once, untracked, inside the scene context. Scene
|
|
64
|
+
`width`/`height` are target pixels and the leaf's own width/height are
|
|
65
|
+
layout, so render and display size separate - render at 2x and display
|
|
66
|
+
smaller for supersampling.
|
|
67
|
+
|
|
42
68
|
Camera control: `createOrbitCamera(scene, { target?, azimuth?, elevation?,
|
|
43
69
|
distance?, min/maxDistance?, min/maxElevation?, orbitSpeed?, rotateSpeed?,
|
|
44
|
-
zoomSpeed
|
|
70
|
+
zoomSpeed?, zoomAnchor?, rotateAnchor?, panSpeed?, viewport?, clampTarget? })`
|
|
71
|
+
- drag-to-rotate, pinch- and wheel-to-zoom, two-finger pan, optional
|
|
72
|
+
auto-orbit. Input runs on core's `createTransform` recognizer, so drag and
|
|
73
|
+
pinch arbitrate in the app-wide gesture arena (a viewport inside a scroller
|
|
74
|
+
does not double-handle) and rotation starts after the recognizer's slop;
|
|
75
|
+
`zoomSpeed` weights both wheel and pinch. Two-finger translation pans (the
|
|
76
|
+
scene tracks the fingers 1:1 at target depth, weighted by `panSpeed`) when
|
|
77
|
+
`viewport()` supplies `{ height, fov }` for the pixel-to-world mapping -
|
|
78
|
+
without it, it rotates like one finger; `clampTarget(target)` bounds where
|
|
79
|
+
a pan may put the pivot. Zoom aims
|
|
80
|
+
at the target unless `zoomAnchor(x, y, {eye, target})` maps the pinch focal
|
|
81
|
+
/ wheel cursor to a world point (ground hit, target-depth plane, ...) - then
|
|
82
|
+
that point stays pinned under the pointer and the target slides toward it;
|
|
83
|
+
only the app can build that mapping, since fov, aspect and element placement
|
|
84
|
+
are app state. Pair it with `rotateAnchor({eye, target})`: called at gesture
|
|
85
|
+
start, its point is projected onto the view axis and re-seats the pivot
|
|
86
|
+
without moving the picture, so a drag after an anchored zoom orbits what the
|
|
87
|
+
camera looks at, not wherever the zoom left the target. Spread
|
|
45
88
|
`orbit.handlers` onto the input-owning element, call `orbit.update(dt)`
|
|
46
89
|
from your onFrame (no frame loop of its own), and use its return - true
|
|
47
|
-
when the pose changed - to gate per-frame dependents like
|
|
48
|
-
|
|
90
|
+
when the pose changed - to gate per-frame dependents like reprojecting
|
|
91
|
+
HUD overlays. `orbiting()` is reactive (HUD-safe); the pose is plain state via
|
|
49
92
|
`pose()`/`set()` (also the debug-command shape). It drives position and
|
|
50
93
|
target only; fov/near/far stay on scene.setCamera. In a component tree,
|
|
51
94
|
reach the scene via `<Scene ref>` or useScene().
|
|
52
95
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
96
|
+
Overlay projection: `scene.project(point)` maps a world point to scene
|
|
97
|
+
pixels (top-left origin, y down - the output texture's own space; `w` is
|
|
98
|
+
clip-space w, the camera-forward distance) and returns null for a point
|
|
99
|
+
at or behind the camera plane. It reflects a pending `setCamera`
|
|
100
|
+
immediately, so set-then-project in one tick is exact. `scene.viewProj(out?)`
|
|
101
|
+
copies the view-projection matrix for batch work. Never rebuild the
|
|
102
|
+
camera matrices by hand for a HUD.
|
|
103
|
+
|
|
104
|
+
Geometry: `box(w?, h?, d?)`; `plane(w?, h?)`, `circle(radius?, seg?)` and
|
|
105
|
+
`ring(inner?, outer?, seg?)` (XY, facing +z - rotate `[-Math.PI/2, 0, 0]`
|
|
106
|
+
for a floor); `sphere(radius?, wSeg?, hSeg?)`;
|
|
107
|
+
`cylinder(rTop?, rBottom?, height?, radialSeg?)` (y axis, capped; unequal
|
|
108
|
+
radii taper it) and `cone(radius?, height?, radialSeg?)`;
|
|
109
|
+
`torus(radius?, tube?, radialSeg?, tubularSeg?)` (lying flat, hole on the
|
|
110
|
+
y axis) and `torusKnot(radius?, tube?, tubularSeg?, radialSeg?, p?, q?)`
|
|
111
|
+
(standing y-up) - both oriented for the y-up world, unlike Three's z-up.
|
|
112
|
+
`withColors(geometry, fill, label?)` derives a "colored"-layout copy of
|
|
113
|
+
any standard-layout geometry (generator or hand-built), adding the
|
|
114
|
+
`aColor` vec4 channel; the source is untouched.
|
|
115
|
+
`fillColors(vertices, fill, first?, count?)` is the in-place primitive
|
|
116
|
+
under it: writes the aColor slots of a colored-layout interleave you
|
|
117
|
+
already own (a merging builder's packed buffer), reading pos/normal/uv
|
|
118
|
+
from the buffer itself - so a packer that bakes transforms while writing
|
|
119
|
+
hands the baker world-space vertices. `fill` indexes relative to
|
|
120
|
+
`first`. It trusts the buffer's layout (no tag to check); withColors is
|
|
121
|
+
the checked path.
|
|
122
|
+
|
|
123
|
+
Profile kit (2D outlines to solids, real texture UVs): a `Profile` is a
|
|
124
|
+
closed XY polygon, bare `[x, y]` points crease, `{ p, smooth }` points
|
|
125
|
+
share an averaged normal - `fillet(points, radius, segs?)` and
|
|
126
|
+
`roundRect(w?, h?, radius?, segs?)` emit those (arc corners smooth).
|
|
127
|
+
Winding is normalized, so either authoring direction works.
|
|
128
|
+
`extrude(profile, depth?, bevel?, bevelSegs?)` sweeps along z, centered,
|
|
129
|
+
with a quarter-round bevel at both rims; `lathe(profile, segs?, angle?,
|
|
130
|
+
start?)` revolves a CLOSED (x = radius, y = height) profile about the y
|
|
131
|
+
axis - watertight by construction, flat caps on partial sweeps;
|
|
132
|
+
`shape(profile)` fills one flat (facing +z, like circle);
|
|
133
|
+
`triangulate(points)` is the ear-clipping core (fan fallback, never drops
|
|
134
|
+
a cap), exported for custom flat work. These pick uint16/uint32 indices
|
|
135
|
+
by vertex count automatically.
|
|
136
|
+
|
|
57
137
|
Materials:
|
|
58
138
|
|
|
59
139
|
- `unlit({ color?, map? })` - straight `[r, g, b, a?]` 0..1, premultiplied
|
|
60
140
|
internally.
|
|
61
141
|
- `shaderMaterial({ vertex, fragment, params?, textures?, depth?,
|
|
62
142
|
depthWrite?, blend?, cull?, topology?, label? })` - your own GLSL, the
|
|
63
|
-
custom-look escape hatch. The
|
|
64
|
-
`uniform mat4 uModel` (the mesh's world matrix,
|
|
65
|
-
`uniform mat4 uViewProj` (the camera, shared
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
143
|
+
custom-look escape hatch. The STANDARD UNIFORM SET: the vertex stage
|
|
144
|
+
MUST declare and use `uniform mat4 uModel` (the mesh's world matrix,
|
|
145
|
+
per entry) and `uniform mat4 uViewProj` (the camera, shared
|
|
146
|
+
target-level params) - transform with
|
|
147
|
+
`uViewProj * uModel * vec4(aPos, 1.0)`; a source missing either throws
|
|
148
|
+
at shaderMaterial() creation. The rest is opt-in by declare-and-use:
|
|
149
|
+
`uniform vec3 uCamPos` (the camera's world position, shared and written
|
|
150
|
+
with uViewProj - the specular/fresnel view vector is
|
|
151
|
+
`normalize(uCamPos - worldPos)`) and `uniform mat4 uNormal` (the world
|
|
152
|
+
inverse-transpose, written beside uModel for this material's meshes;
|
|
153
|
+
take `mat3(uNormal)` - correct under non-uniform scale, where
|
|
154
|
+
mat3(uModel) bends normals off the surface). Attributes come from the
|
|
155
|
+
geometry's layout by name; a vertex stage reading `in vec4 aColor` opts
|
|
156
|
+
the material into the "colored" layout, and its meshes then need
|
|
157
|
+
`withColors()` geometry. Sources without `#version` get the standard
|
|
158
|
+
pipeline preamble. App-driven uniforms beyond the standard set: seed
|
|
159
|
+
via `params`, then write per mesh with
|
|
70
160
|
`setMeshParams(mesh, { name: value })` (validated names; values persist
|
|
71
161
|
across entry rebuilds; frame-rate-safe like setTransform).
|
|
72
162
|
|
|
163
|
+
Lighting GLSL (`@solidrt/3d/glsl`): exported string constants composed
|
|
164
|
+
into shaderMaterial sources with plain template literals - `LIT_VERTEX`
|
|
165
|
+
(the standard vertex stage: clip position plus vWorldPos/vNormal/vUv
|
|
166
|
+
varyings, normals via mat3(uNormal)), `LIT_VERTEX_COLORED` (the same
|
|
167
|
+
plus the colored layout's aColor forwarded raw as vColor - using it opts
|
|
168
|
+
the material into that layout) and the pure functions `HEMISPHERE`
|
|
169
|
+
(`hemisphere(n, sky, ground)`), `LAMBERT` (`lambert(n, l)`),
|
|
170
|
+
`BLINN_SPECULAR` (`blinnSpecular(n, v, l, shininess)`), `FRESNEL`
|
|
171
|
+
(`fresnel(n, v, power)`). Lights, colors and exponents are arguments, so
|
|
172
|
+
nothing is pinned but the function names; future lit material classes
|
|
173
|
+
compose from these same constants - customizing never means leaving the
|
|
174
|
+
system.
|
|
175
|
+
|
|
73
176
|
## Traps
|
|
74
177
|
|
|
75
178
|
- The y-down clip flip is baked into `perspective()`; scene code and
|
|
@@ -87,13 +190,18 @@ Materials:
|
|
|
87
190
|
v1.
|
|
88
191
|
- Transforms have ONE write path: `setTransform`/`setVisible` (or the
|
|
89
192
|
props that call them). Mutating `node.position` directly does not sync.
|
|
90
|
-
- A camera change is ONE `setTargetParams` write (uViewProj
|
|
91
|
-
state), independent of mesh count - never reintroduce per-mesh
|
|
92
|
-
writes
|
|
193
|
+
- A camera change is ONE `setTargetParams` write (uViewProj + uCamPos are
|
|
194
|
+
target state), independent of mesh count - never reintroduce per-mesh
|
|
195
|
+
camera writes (uEye-style per-mesh params are exactly the O(scene) cost
|
|
196
|
+
the shared channel removed). Scene scale honestly: hundreds to a
|
|
93
197
|
few thousand objects, bounded by the interpreter, not the GPU.
|
|
94
198
|
- Entry rebuild order: `setGeometry`/`setMaterial` re-add the entry at the
|
|
95
199
|
list END. Irrelevant while everything is opaque + depth-tested; revisit
|
|
96
200
|
when transparency lands.
|
|
201
|
+
- `lathe` takes a CLOSED profile (a cross-section with thickness, or run
|
|
202
|
+
to the axis at x = 0) - it is a solid of revolution, NOT Three's open
|
|
203
|
+
polyline shell. An "open" outline must be closed by the author;
|
|
204
|
+
otherwise the shape is simply wrong, there is no open-profile mode.
|
|
97
205
|
- `useScene()`/`Group`/`Mesh` throw outside `<Scene>` (default-less
|
|
98
206
|
context).
|
|
99
207
|
- A `shaderMaterial` INSTANCE is the pipeline handle: identical sources
|
|
@@ -101,14 +209,16 @@ Materials:
|
|
|
101
209
|
content-keyed caches are the anti-pattern the GPU layer avoids). Create
|
|
102
210
|
one per look at app scope, share across meshes, `dispose()` when done
|
|
103
211
|
for good.
|
|
104
|
-
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
212
|
+
- The standard-set contract is checked TEXTUALLY at shaderMaterial()
|
|
213
|
+
creation (uModel and uViewProj must appear in the vertex source) and
|
|
214
|
+
strictly at add() for the per-entry names: a uModel or uNormal that is
|
|
215
|
+
declared but never USED compiles out, and the scene's entry seed then
|
|
216
|
+
throws at attach (the engine rejects unknown entry uniform names). The
|
|
217
|
+
shared names have no such backstop - a declared-but-unused uViewProj or
|
|
218
|
+
uCamPos is skipped silently (shared params tolerate zero coverage), so
|
|
219
|
+
the symptom is an untransformed or unlit render, not an error. Use what
|
|
220
|
+
you declare.
|
|
221
|
+
- The layout scan is textual the same way: any `aColor` token in the
|
|
222
|
+
vertex source - a comment counts - selects the "colored" layout, and
|
|
223
|
+
the material then rejects standard geometry at add(). Do not mention
|
|
224
|
+
aColor you do not read.
|
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@ A retained 3D scene graph for SolidRT: meshes, materials and a camera,
|
|
|
4
4
|
declared as Solid components, rendered by the runtime into an ordinary
|
|
5
5
|
texture in your UI tree.
|
|
6
6
|
|
|
7
|
+
_@solidrt/3d is experimental: expect more API churn here than in the rest of SolidRT._
|
|
8
|
+
|
|
7
9
|
```tsx
|
|
8
10
|
import { createSignal, onFrame, render } from "@solidrt/core"
|
|
9
11
|
import { box, Mesh, PerspectiveCamera, Scene, unlit } from "@solidrt/3d"
|
|
@@ -27,16 +29,31 @@ The scene compiles to one depth-buffered GPU draw target: one draw entry
|
|
|
27
29
|
per mesh, one shared pipeline per material class, cross-mesh occlusion
|
|
28
30
|
from the shared depth buffer. A static scene costs zero GPU passes - the
|
|
29
31
|
runtime re-renders the target only when something changes - and a moved
|
|
30
|
-
mesh costs one uniform write.
|
|
32
|
+
mesh costs one uniform write. By default `<Scene>` composites the target
|
|
33
|
+
as a plain `<texture>` leaf; the `output` prop receives the texture id
|
|
34
|
+
and replaces that leaf - place a `<d-texture>`, add paint or pointer
|
|
35
|
+
props, chain a post-effect shader target, or return null and composite
|
|
36
|
+
`scene.texture` yourself.
|
|
31
37
|
|
|
32
38
|
There is also an imperative layer underneath (`createScene`, `createMesh`,
|
|
33
39
|
`setTransform`, ...) usable without components, plus a small math module
|
|
34
|
-
(`@solidrt/3d/math`: column-major mat4, perspective, lookAt).
|
|
40
|
+
(`@solidrt/3d/math`: column-major mat4, perspective, lookAt). For HUD
|
|
41
|
+
overlays, `scene.project(point)` maps a world point to scene pixels.
|
|
42
|
+
Custom materials get a standard uniform set - per-mesh `uModel`/`uNormal`,
|
|
43
|
+
shared `uViewProj`/`uCamPos`, each written once per change - and
|
|
44
|
+
`@solidrt/3d/glsl` exports the lighting pieces (hemisphere, lambert,
|
|
45
|
+
blinn, fresnel, a standard vertex stage) to compose your own lit looks
|
|
46
|
+
from plain template literals.
|
|
35
47
|
|
|
36
48
|
v1 scope: unlit color/textured materials plus `shaderMaterial` (your own
|
|
37
|
-
GLSL as a first-class material), box
|
|
38
|
-
|
|
39
|
-
|
|
49
|
+
GLSL as a first-class material), geometry generators (box, plane, circle,
|
|
50
|
+
ring, sphere, cylinder, cone, torus, torus knot), a profile kit for custom
|
|
51
|
+
solids (`extrude` with bevels, `lathe`, flat `shape`, with `fillet`/
|
|
52
|
+
`roundRect`/`triangulate` helpers), a per-vertex data channel
|
|
53
|
+
(`withColors` adds an `aColor` vec4 - tint, baked AO, any four scalars -
|
|
54
|
+
to any geometry, for materials that read it), one perspective camera
|
|
55
|
+
with an orbit control (`createOrbitCamera`: drag, pinch/wheel zoom, auto-orbit).
|
|
56
|
+
Lights, transparency, model loading and picking are
|
|
40
57
|
staged next - see `okf/research/scene-graph-3d.md` for the roadmap. Full
|
|
41
58
|
usage notes and traps: [AGENTS.md](AGENTS.md); runnable examples:
|
|
42
59
|
[examples/](examples/).
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// The `output` prop: Scene hands you its texture id and you compose the
|
|
2
|
+
// leaf yourself, in place of the built-in `<texture>`. Here a fragment
|
|
3
|
+
// pass samples the scene and adds chromatic aberration plus a vignette,
|
|
4
|
+
// and the scene renders at 2x display size - the post pass doubles as the
|
|
5
|
+
// downsample (the default linear sampler box-averages the 2x2 quad), so
|
|
6
|
+
// the same chain is also free supersampling. Sampled textures are live
|
|
7
|
+
// dependencies: the post pass re-renders exactly when the scene target
|
|
8
|
+
// does, and a static scene still costs zero passes.
|
|
9
|
+
//
|
|
10
|
+
// The same slot takes a `<d-texture>`, a leaf with blendMode/fit/pointer
|
|
11
|
+
// props, or `() => null` (headless - composite scene.texture elsewhere).
|
|
12
|
+
import { createSignal, onFrame, pct, render } from "@solidrt/core"
|
|
13
|
+
import { createShaderTexture } from "@solidrt/core/gpu"
|
|
14
|
+
import { box, Group, Mesh, PerspectiveCamera, plane, Scene, sphere, unlit } from "@solidrt/3d"
|
|
15
|
+
|
|
16
|
+
const SIZE = 720 // display pixels (the leaf)
|
|
17
|
+
const RENDER = SIZE * 2 // target pixels (the scene)
|
|
18
|
+
|
|
19
|
+
// vUV, iResolution, and fragColor come from the standard shader-texture
|
|
20
|
+
// preamble; uSource is bound via the `textures` option.
|
|
21
|
+
const POST = `
|
|
22
|
+
uniform sampler2D uSource;
|
|
23
|
+
void main() {
|
|
24
|
+
vec2 c = vUV - 0.5;
|
|
25
|
+
float r2 = dot(c, c);
|
|
26
|
+
// Chromatic aberration: red and blue sample at slightly shifted radii.
|
|
27
|
+
vec2 shift = c * r2 * 0.04;
|
|
28
|
+
vec3 col = vec3(
|
|
29
|
+
texture(uSource, vUV + shift).r,
|
|
30
|
+
texture(uSource, vUV).g,
|
|
31
|
+
texture(uSource, vUV - shift).b);
|
|
32
|
+
// Vignette.
|
|
33
|
+
col *= 1.0 - 1.1 * r2;
|
|
34
|
+
fragColor = vec4(col, 1.0);
|
|
35
|
+
}`
|
|
36
|
+
|
|
37
|
+
function App() {
|
|
38
|
+
let [spin, setSpin] = createSignal(0)
|
|
39
|
+
onFrame(tick => setSpin(tick / 2000))
|
|
40
|
+
|
|
41
|
+
let cube = box()
|
|
42
|
+
let floor = plane(6, 6, "floor")
|
|
43
|
+
let ball = sphere(0.35)
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<window>
|
|
47
|
+
<view width={pct(100)} height={pct(100)} viewBox={[SIZE, SIZE]}>
|
|
48
|
+
<Scene
|
|
49
|
+
width={RENDER}
|
|
50
|
+
height={RENDER}
|
|
51
|
+
clearColor={[0.07, 0.07, 0.1, 1]}
|
|
52
|
+
label="scene-post"
|
|
53
|
+
output={tex => {
|
|
54
|
+
// Created inside the callback, the post target disposes with
|
|
55
|
+
// the Scene.
|
|
56
|
+
let post = createShaderTexture(POST, SIZE, SIZE, null, { textures: { uSource: tex } })
|
|
57
|
+
return <texture src={post} width={SIZE} height={SIZE} />
|
|
58
|
+
}}
|
|
59
|
+
>
|
|
60
|
+
<PerspectiveCamera fov={55} position={[0, 1.6, 3.6]} lookAt={[0, 0.3, 0]} />
|
|
61
|
+
<Mesh
|
|
62
|
+
geometry={floor}
|
|
63
|
+
material={unlit({ color: [0.16, 0.17, 0.22] })}
|
|
64
|
+
rotation={[-Math.PI / 2, 0, 0]}
|
|
65
|
+
/>
|
|
66
|
+
<Group rotation={[0, spin(), 0]}>
|
|
67
|
+
<Mesh geometry={cube} material={unlit({ color: [0.85, 0.3, 0.3] })} position={[0, 0.5, 0]} />
|
|
68
|
+
<Mesh
|
|
69
|
+
geometry={cube}
|
|
70
|
+
material={unlit({ color: [0.9, 0.8, 0.35] })}
|
|
71
|
+
position={[-1.1, 0.7, 0]}
|
|
72
|
+
scale={[0.5, 1.4, 0.5]}
|
|
73
|
+
/>
|
|
74
|
+
<Mesh geometry={ball} material={unlit({ color: [0.35, 0.65, 0.9] })} position={[1.1, 0.35, 0]} />
|
|
75
|
+
</Group>
|
|
76
|
+
</Scene>
|
|
77
|
+
</view>
|
|
78
|
+
</window>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
render(() => <App />)
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidrt/3d",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.47",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Antoine van Wel",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "src/index.ts",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": "./src/index.ts",
|
|
10
|
-
"./math": "./src/math.ts"
|
|
10
|
+
"./math": "./src/math.ts",
|
|
11
|
+
"./glsl": "./src/glsl.ts"
|
|
11
12
|
},
|
|
12
13
|
"files": [
|
|
13
14
|
"src/",
|
|
@@ -16,6 +17,6 @@
|
|
|
16
17
|
],
|
|
17
18
|
"peerDependencies": {
|
|
18
19
|
"@solidjs/signals": "2.0.0-beta.31",
|
|
19
|
-
"@solidrt/core": "0.0.
|
|
20
|
+
"@solidrt/core": "0.0.47"
|
|
20
21
|
}
|
|
21
22
|
}
|
package/src/components.tsx
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// structure and slow state, per-frame motion goes straight to the scene.
|
|
8
8
|
|
|
9
9
|
import { createContext, createEffect, onCleanup, untrack, useContext } from "@solidrt/core"
|
|
10
|
-
import type { ParentComponent, VoidComponent } from "@solidrt/core"
|
|
10
|
+
import type { Element, ParentComponent, TextureId, VoidComponent } from "@solidrt/core"
|
|
11
11
|
import {
|
|
12
12
|
add,
|
|
13
13
|
createGroup,
|
|
@@ -54,18 +54,29 @@ function syncNode(node: SceneNode, props: TransformProps): void {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
export type SceneProps = {
|
|
57
|
+
/** Target pixels. With `output`, the leaf's own width/height are layout,
|
|
58
|
+
* so render size and display size separate (supersampling). */
|
|
57
59
|
width: number
|
|
58
60
|
height: number
|
|
59
61
|
clearColor?: [number, number, number, number]
|
|
60
62
|
label?: string
|
|
61
63
|
ref?: (scene: SceneHandle) => void
|
|
64
|
+
/**
|
|
65
|
+
* Compose the output yourself: called once (untracked) with the scene's
|
|
66
|
+
* texture id, and its return renders in place of the built-in `<texture>`
|
|
67
|
+
* leaf - a `<d-texture>`, a leaf carrying paint/pointer/layout props, or
|
|
68
|
+
* a post-effect chain (a shader target sampling the id; created in the
|
|
69
|
+
* callback it disposes with the Scene). Return null to render no leaf.
|
|
70
|
+
*/
|
|
71
|
+
output?: (texture: TextureId) => Element
|
|
62
72
|
}
|
|
63
73
|
|
|
64
74
|
/**
|
|
65
75
|
* Owns a draw target and composites it as an ordinary `<texture>` leaf, so
|
|
66
76
|
* the output takes layout, transforms, blendMode, and pointer events like
|
|
67
|
-
* any element
|
|
68
|
-
*
|
|
77
|
+
* any element - or hand `output` the texture id and compose it yourself.
|
|
78
|
+
* Children (Mesh/Group/PerspectiveCamera) render nothing themselves - they
|
|
79
|
+
* populate the retained scene through context.
|
|
69
80
|
*/
|
|
70
81
|
export let Scene: ParentComponent<SceneProps> = props => {
|
|
71
82
|
let scene = untrack(() =>
|
|
@@ -76,9 +87,14 @@ export let Scene: ParentComponent<SceneProps> = props => {
|
|
|
76
87
|
([w, h]) => scene.setSize(w, h),
|
|
77
88
|
)
|
|
78
89
|
untrack(() => props.ref)?.(scene)
|
|
90
|
+
let output = untrack(() => props.output)
|
|
79
91
|
return (
|
|
80
92
|
<SceneContext value={{ scene, parent: scene.root }}>
|
|
81
|
-
|
|
93
|
+
{output ? (
|
|
94
|
+
untrack(() => output(scene.texture))
|
|
95
|
+
) : (
|
|
96
|
+
<texture src={scene.texture} width={props.width} height={props.height} />
|
|
97
|
+
)}
|
|
82
98
|
{props.children}
|
|
83
99
|
</SceneContext>
|
|
84
100
|
)
|