@solidrt/3d 0.0.58 → 0.0.59
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 +734 -264
- package/README.md +4 -2
- package/examples/README.md +50 -4
- package/examples/bloom.tsx +113 -0
- package/examples/cascades.tsx +4 -4
- package/examples/collision.tsx +6 -6
- package/examples/exits.tsx +96 -0
- package/examples/first-person.tsx +5 -5
- package/examples/fleet.tsx +4 -4
- package/examples/fog.tsx +5 -5
- package/examples/instanced.tsx +70 -50
- package/examples/lamps.tsx +4 -4
- package/examples/lod.tsx +125 -0
- package/examples/materials.tsx +9 -9
- package/examples/morph.tsx +174 -0
- package/examples/normals.tsx +118 -0
- package/examples/{lit.tsx → phong.tsx} +9 -9
- package/examples/polyhedra.tsx +61 -0
- package/examples/scene-atlas.tsx +70 -0
- package/examples/scene-views.tsx +5 -5
- package/examples/shadows.tsx +9 -9
- package/examples/sky-lit.tsx +3 -3
- package/examples/skybox.tsx +5 -5
- package/examples/standard.tsx +2 -2
- package/examples/streams.tsx +122 -0
- package/examples/sweep-paths.tsx +5 -5
- package/examples/wireframe.tsx +24 -2
- package/package.json +3 -3
- package/src/clip.ts +18 -5
- package/src/color.ts +11 -2
- package/src/components/context.tsx +11 -2
- package/src/components/directional-light.tsx +2 -2
- package/src/components/first-person-camera.tsx +4 -4
- package/src/components/group.tsx +9 -5
- package/src/components/hemisphere-light.tsx +2 -2
- package/src/components/index.ts +4 -1
- package/src/components/instanced-mesh.tsx +28 -10
- package/src/components/lod.tsx +102 -0
- package/src/components/mesh.tsx +24 -6
- package/src/components/node-props.ts +35 -5
- package/src/components/orbit-camera.tsx +15 -12
- package/src/components/point-light.tsx +2 -2
- package/src/components/record-mesh.tsx +12 -7
- package/src/components/scene.tsx +55 -2
- package/src/components/spot-light.tsx +2 -2
- package/src/components/sprite.tsx +2 -2
- package/src/components/view3d.tsx +18 -1
- package/src/environment.ts +9 -8
- package/src/first-person.ts +125 -38
- package/src/geometry-gpu.ts +172 -39
- package/src/geometry.ts +1573 -239
- package/src/glsl.ts +398 -113
- package/src/gltf.ts +363 -141
- package/src/index.ts +31 -21
- package/src/light.ts +7 -8
- package/src/lod.ts +148 -0
- package/src/material.ts +316 -202
- package/src/mesh.ts +490 -256
- package/src/mixer.ts +27 -14
- package/src/model-file.ts +54 -15
- package/src/model.ts +86 -21
- package/src/motion.ts +17 -0
- package/src/node.ts +421 -22
- package/src/orbit.ts +378 -116
- package/src/profile.ts +4 -25
- package/src/resolve.ts +181 -0
- package/src/scene-shadows.ts +94 -93
- package/src/scene.ts +605 -185
- package/src/sweep.ts +27 -8
- package/tools/model.ts +3 -1
package/AGENTS.md
CHANGED
|
@@ -24,6 +24,7 @@ Contents:
|
|
|
24
24
|
- [Rendering is the runtime's](#rendering-is-the-runtimes)
|
|
25
25
|
- [Views and layers](#views-and-layers)
|
|
26
26
|
- [Culling](#culling)
|
|
27
|
+
- [Level of detail](#level-of-detail)
|
|
27
28
|
- [Shadows](#shadows)
|
|
28
29
|
- [Retargeted motion](#retargeted-motion)
|
|
29
30
|
- [Geometry layout, indices and topology](#geometry-layout-indices-and-topology)
|
|
@@ -56,7 +57,7 @@ Contents:
|
|
|
56
57
|
- [Lighting GLSL](#lighting-glsl)
|
|
57
58
|
- [Custom looks: three tiers](#custom-looks-three-tiers)
|
|
58
59
|
- [Lights](#lights)
|
|
59
|
-
- [
|
|
60
|
+
- [phong](#phong)
|
|
60
61
|
- [standard](#standard)
|
|
61
62
|
- [Models](#models)
|
|
62
63
|
- [Three layers](#three-layers)
|
|
@@ -103,16 +104,24 @@ nothing itself.
|
|
|
103
104
|
|
|
104
105
|
### Node lifecycle
|
|
105
106
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
107
|
+
Two removal verbs, one meaning each, the same words with the same
|
|
108
|
+
meanings in @solidrt/2d. `remove(child)` DETACHES an intact subtree -
|
|
109
|
+
Three's `parent.remove`, Godot's remove_child: children stay under the
|
|
110
|
+
removed node, core nodes free on leave and recreate on re-enter, so a
|
|
111
|
+
removed subtree re-adds cleanly (`add(parent, child)` re-parents through
|
|
112
|
+
it); it SNAPS, since a detached node is coming back. `destroy(node)` is
|
|
113
|
+
Unity's Destroy, Godot's queue_free: the subtree is gone for good, every
|
|
114
|
+
handle in it inert (writes are no-ops, add throws, remove skips), and it
|
|
115
|
+
is the removal an `exit` rides on (see Retargeted motion). The components
|
|
116
|
+
destroy on unmount. Nothing else is destroyed: GPU resources stay on
|
|
117
|
+
their own disposers (`disposeGeometry`, `disposeInstances`,
|
|
118
|
+
`model.dispose`, the scene's `dispose`), each of which frees a node still
|
|
119
|
+
animating out on the spot. An instance node (addInstance on an instanced
|
|
120
|
+
mesh) is slot-bound to its mesh: `destroy` frees it (the slot recycles at
|
|
121
|
+
the free) and the generic add/remove throw on it. (@solidrt/2d has no
|
|
122
|
+
detach - a sprite cannot exist outside its layer - so `destroySprite` /
|
|
123
|
+
`destroyGroup` are its only removal, and a sprite that should come back
|
|
124
|
+
is hidden.)
|
|
116
125
|
|
|
117
126
|
### Rendering is the runtime's
|
|
118
127
|
|
|
@@ -138,7 +147,14 @@ sets itself (or its `fog` option, below) become VIEW-OWNED: the
|
|
|
138
147
|
scene's setParams/setFog fan-out skips them from then on, so a view
|
|
139
148
|
override survives scene-wide writes. A view's backdrop is its
|
|
140
149
|
clearColor (the scene background draws on PROBES, not views); a view
|
|
141
|
-
has no picking.
|
|
150
|
+
has no picking. A view is a buffer plus a resolve like the scene
|
|
151
|
+
(`view.hdrTexture`, `view.texture`, the `resolve` option and
|
|
152
|
+
`view.setResolve` - see Color); a tiled view (`into`) renders LINEAR
|
|
153
|
+
light into the atlas and the app resolves the atlas once: a
|
|
154
|
+
`createShaderTexture` over `resolveFragment()` from `/glsl` (the stock
|
|
155
|
+
source with `uScene` and the RESOLVE set declared) with the atlas bound
|
|
156
|
+
as `uScene` and `resolveParams({ toneMapping, exposure })` as its params
|
|
157
|
+
- `examples/scene-atlas.tsx`. LAYERS select what a target
|
|
142
158
|
draws, Three's model exactly: `layers` on a mesh is its membership
|
|
143
159
|
bitmask (default 1, `setLayers`/the `layers` prop, NOT inherited from
|
|
144
160
|
Groups), and each target carries a mask (default 1) - `layers` on
|
|
@@ -157,7 +173,7 @@ fogged scene); absent follows the scene. `overrideMaterial` (Three's
|
|
|
157
173
|
`scene.overrideMaterial`, scoped to the view) draws every mesh with one
|
|
158
174
|
material - a depth pass, a normal/id visualizer - skips instanced
|
|
159
175
|
meshes (unless the override itself declares their exact
|
|
160
|
-
`
|
|
176
|
+
`instanceBuffers` record layouts) and draws in add order. `depth: "texture"` exposes `view.depthTexture`, the shadow-map
|
|
161
177
|
input; the same option on createScene exposes `scene.depthTexture`,
|
|
162
178
|
the input for a depth-reading post effect in `output` (not combinable
|
|
163
179
|
with `samples` - no multisampled sampleable depth). `ortho: { left, right, top, bottom }` on any camera swaps
|
|
@@ -195,6 +211,62 @@ knob is needed. Probe faces set no frustum (six cameras, one target).
|
|
|
195
211
|
Shadow tiles cull casters against their light frustum, which is what
|
|
196
212
|
makes `shadow.distance` and cascades cheaper.
|
|
197
213
|
|
|
214
|
+
### Level of detail
|
|
215
|
+
|
|
216
|
+
LOD is the core's too, beside culling: a group's level is picked per
|
|
217
|
+
target after each flush by PROJECTED SIZE - the sphere around the
|
|
218
|
+
group's boxes (radius = half the box diagonal, conservative like the
|
|
219
|
+
frustum box), its diameter as a fraction of the viewport height,
|
|
220
|
+
`radius / (distance * tan(fov / 2))` under a perspective camera and
|
|
221
|
+
`radius * 2 / (top - bottom)` under an orthographic one (Unity's
|
|
222
|
+
screen-relative model; Three's distance is wrong for an ortho minimap,
|
|
223
|
+
a zoomed camera or a scaled-up tree). `createLod([{ node, size }, ...],
|
|
224
|
+
{ fade? })` / `<Lod fade?>` with `lodSize` on its direct children: the
|
|
225
|
+
levels nearest first, `size` the projected size BELOW which a level
|
|
226
|
+
hands over to the next, the last one's the cull threshold (0 = never
|
|
227
|
+
culled); a level is a direct child (a parentless node is adopted by
|
|
228
|
+
`setLod`, one parented elsewhere is refused, never moved). Every camera write also sets its target's LOD view (eye,
|
|
229
|
+
focal, ortho flag, `lodBias`): the scene and each view pick by their
|
|
230
|
+
own camera (a minimap sees the far level of the same tree), shadow
|
|
231
|
+
tiles by the SCENE camera (a caster draws the level the camera sees, so
|
|
232
|
+
its shadow matches - Unity's and Godot's rule), probe faces pick
|
|
233
|
+
nothing. The gate composes with the frustum and `visible` into the same
|
|
234
|
+
instance-count switch, so a still camera re-tests nothing and a thousand
|
|
235
|
+
groups cost no JS per frame; a child of the group that carries no
|
|
236
|
+
`lodSize` is drawn always. Without a `fade` the switch is hard with a
|
|
237
|
+
one-sided hysteresis band (a level is left at its threshold and
|
|
238
|
+
re-entered only once the size climbs a tenth above it), so a boundary
|
|
239
|
+
never flickers; with one, each threshold `s` widens into `[s, s * (1 +
|
|
240
|
+
fade))` where both levels draw with COMPLEMENTARY screen-hash dithers
|
|
241
|
+
(the near one keeps the pixels below the band position, the far one
|
|
242
|
+
the rest - a partition, never a double draw) through the `uLodFade`
|
|
243
|
+
vec2 every stock fragment composes, its band position in 64 steps so an
|
|
244
|
+
entry is rewritten when its step changes, not every frame, and written
|
|
245
|
+
solid again when the entry switches off; a custom class opts in by composing
|
|
246
|
+
`LOD_FADE` from `@solidrt/3d/glsl` and calling `lodFade()` first in
|
|
247
|
+
main. Shadow tiles never fade: their depth pass switches hard at the
|
|
248
|
+
band's midpoint, invisible for two variants of one shape.
|
|
249
|
+
`createInstancedLod([{ geometry, material, size }, ...], opts)` /
|
|
250
|
+
`<InstancedLod levels capacity?>` is the population form: one instance
|
|
251
|
+
set, one matrix buffer PER LEVEL, and the core stages each instance's
|
|
252
|
+
record into the level ITS OWN size picks - a spread forest as three
|
|
253
|
+
entries, near trees full and far ones cards. Returned as the first
|
|
254
|
+
level's mesh (the other levels are its children at identity, sharing
|
|
255
|
+
its instance slots; `setLayers`/`setCastShadow` on it reach them,
|
|
256
|
+
`setInstanceStyle` writes every level's style stream; a pick or pointer
|
|
257
|
+
event on an instance names that first mesh whatever level the instance
|
|
258
|
+
is drawn at). Records are
|
|
259
|
+
target-agnostic, so instances pick by the SCENE camera and every view
|
|
260
|
+
draws that choice; instanced levels switch hard (`fade` is refused).
|
|
261
|
+
`scene.setLodBias` (Unity's lodBias) multiplies every measured size:
|
|
262
|
+
below 1 hands over sooner, the one-line quality knob for a low-end
|
|
263
|
+
device. Picking and collision see the level the queried target draws
|
|
264
|
+
(`scene.pick`/overlap/sweep/moveAndSlide the scene's, `view.pick` the
|
|
265
|
+
view's); a collider that must not follow the camera is its own undrawn
|
|
266
|
+
mesh on a collision layer. Nested groups chain. Mesh simplification is
|
|
267
|
+
a bake job, not a runtime one. `examples/lod.tsx` is the shape;
|
|
268
|
+
`probes/3d-lod-bench.tsx` the cost.
|
|
269
|
+
|
|
198
270
|
### Shadows
|
|
199
271
|
|
|
200
272
|
SHADOWS are a view: `<DirectionalLight castShadow shadow={{ mapSize?,
|
|
@@ -205,9 +277,20 @@ the `castShadow` meshes (`<Mesh castShadow>`, `setCastShadow`) from an
|
|
|
205
277
|
orthographic camera at the light's WORLD position along its world
|
|
206
278
|
direction, `shadow.camera` (+-5, 0.5..500 by default) as the frustum.
|
|
207
279
|
Any light may cast, bounded by the shadow-slot budget
|
|
208
|
-
(MAX_SHADOW_MAPS = 8, its own constant
|
|
209
|
-
`
|
|
210
|
-
|
|
280
|
+
(`MAX_SHADOW_MAPS` = 8, its own constant, exported from the root and
|
|
281
|
+
`/glsl` with `MAX_LIGHTS` and `MAX_CASCADES`: a directional light claims
|
|
282
|
+
`shadow.cascades` consecutive slots, a point light six, a spot one).
|
|
283
|
+
The budget is checked when the set settles (the scene's sync), never
|
|
284
|
+
at attach, so a `<Show>`/`<Switch>` swap whose branches each fit is
|
|
285
|
+
fine even though both are attached for an instant. A set over budget
|
|
286
|
+
once settled is reported once per change of the set: `<Scene>`
|
|
287
|
+
rethrows it inside the tree, so the app's error boundary (or an
|
|
288
|
+
`<Errored>` closer in) shows it, and Reset returns to the scene, which
|
|
289
|
+
kept rendering - the casters that fit in attach order keep their maps,
|
|
290
|
+
the rest light the scene unshadowed. A bare `createScene` throws it
|
|
291
|
+
from its sync unless given `onError`. A deliberate overlap must fit
|
|
292
|
+
alongside its successor: a casting light with its own exit transition
|
|
293
|
+
keeps casting until it frees. `<SpotLight castShadow
|
|
211
294
|
shadow={{ mapSize?, bias?, normalBias?, near? }}>` is the same
|
|
212
295
|
machinery with a PERSPECTIVE camera: at the light's world position
|
|
213
296
|
along its world direction, fov = its cone (2 * angle), near from
|
|
@@ -255,8 +338,8 @@ its view's own view-projection (the whole array is one write per
|
|
|
255
338
|
shadow-camera move) - and per light i `uShadowFirst[i]`/`uShadowCount[i]`
|
|
256
339
|
name its slots (count 0 = it does not cast) with
|
|
257
340
|
`uShadowBias[i]`/`uShadowNormalBias[i]` its knobs; `SHADOW_SLOTS` in
|
|
258
|
-
glsl declares the set. Every `
|
|
259
|
-
(Godot's and Three's default); `
|
|
341
|
+
glsl declares the set. Every `phong` material RECEIVES by default
|
|
342
|
+
(Godot's and Three's default); `phong({ receiveShadow: false })` opts a
|
|
260
343
|
material out and drops the map from its program - a material option,
|
|
261
344
|
as with vertexColors/triplanar, because the material picks the program
|
|
262
345
|
(Godot's `disable_receive_shadows`). The factor is `SHADOW`'s one
|
|
@@ -273,28 +356,88 @@ the quaternion geodesic - a spring keeps its velocity through
|
|
|
273
356
|
retargets), so a mesh gliding to a slot or a camera rig easing costs
|
|
274
357
|
one JS write per target change, zero per frame. The declaration lives
|
|
275
358
|
on the SceneNode and re-applies on every scene enter; the pose a node
|
|
276
|
-
enters with
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
359
|
+
enters with snaps, unless a component's `from` (`position: { duration,
|
|
360
|
+
from: [x, y, z] }`, a quaternion for `rotation`) animates it in from
|
|
361
|
+
there - at every scene enter, since each enter creates the core node
|
|
362
|
+
anew. A component's `exit` is where it animates to when `destroy` lets
|
|
363
|
+
go of the node: it stays drawn while it leaves and is a GHOST meanwhile -
|
|
364
|
+
no pick, raycast, overlap, sweep or pointer event sees it - then frees
|
|
365
|
+
when the last exit settles; children go first and the parent stays their
|
|
366
|
+
frame until the last of them has settled, so a dying character's parts
|
|
367
|
+
leave in its frame (a mixer driving its joints keeps driving them). Either
|
|
368
|
+
endpoint takes `{ value, duration?, curve?, bounce?, delay? }` to own its
|
|
369
|
+
direction's motion (an ease-out enter, an ease-in exit), and `delay` on
|
|
370
|
+
an entry holds its writes on the animation clock (a late frame catches
|
|
371
|
+
up). `stagger` (ms) goes on an ANCESTOR's declaration (a `<Group
|
|
372
|
+
transition={{ stagger: 40 }}>`), never on the animating nodes: it spaces
|
|
373
|
+
the enters and exits of its descendants that begin in one frame by
|
|
374
|
+
`index * stagger` (add order in, tree order out whatever order the
|
|
375
|
+
unmount lets them go in; nearest declaring ancestor wins) and cascades
|
|
376
|
+
nothing unless they declare `from`/`exit` -
|
|
377
|
+
a squad of instances spawning in is one `stagger` on their group, and
|
|
378
|
+
for the nodes straight under the scene root it is `<Scene stagger>` /
|
|
379
|
+
`createScene({ stagger })` (a declaration on `scene.root`). A populated
|
|
380
|
+
mesh unmounts as `destroy(mesh)` then `disposeInstances(mesh)`, and the
|
|
381
|
+
dispose waits for a mesh still animating out, so an `<Instance>`'s exit
|
|
382
|
+
plays through its `<InstancedMesh>`'s unmount (`model.dispose` waits the
|
|
383
|
+
same way); the scene's and a layer's `dispose` cut a corpse short - the
|
|
384
|
+
whole target is going. `examples/exits.tsx` is the live example: crates
|
|
385
|
+
that pop in and shrink away on tap, the shelf cascading out and back
|
|
386
|
+
through `<Show>`. Each natural settle calls the node's `onTransitionEnd`
|
|
387
|
+
(plain field like the pointer handlers) with `{ component }` - never on
|
|
388
|
+
a cancel, snap, leave or exit; the raw "spatialTransitionEnd" engine
|
|
389
|
+
event (srt:events, carrying the CORE node id `_node`) stays for
|
|
390
|
+
flux:spatial consumers.
|
|
281
391
|
|
|
282
392
|
### Geometry layout, indices and topology
|
|
283
393
|
|
|
284
394
|
One interleaved vertex buffer per geometry, described by an open layout
|
|
285
|
-
(`Geometry.layout`, absent = "
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
`
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
395
|
+
(`Geometry.layout`, absent = "base"): an ordered attribute list that
|
|
396
|
+
starts with `aPos` float32x3 and may carry any named channels after it,
|
|
397
|
+
each in a vertex format. Formats are WebGPU's spelling of (component
|
|
398
|
+
type, count, normalized): the float32 family (`float32` .. `float32x4`),
|
|
399
|
+
`float16x2/x4`, the normalized integers `unorm8x4`, `snorm8x4`,
|
|
400
|
+
`unorm16x2/x4`, `snorm16x2/x4`, the unsigned integers `uint8x4`,
|
|
401
|
+
`uint16x2/x4`, `uint32` .. `uint32x4` and the signed integers `sint8x4`,
|
|
402
|
+
`sint16x2/x4`, `sint32` .. `sint32x4`; `VERTEX_FORMATS` is the table.
|
|
403
|
+
Every format is a multiple of 4 bytes, so offsets and strides are
|
|
404
|
+
4-aligned by construction. A format feeds the shader `in` of its KIND
|
|
405
|
+
and component count, WebGPU's rule: float and normalized formats feed
|
|
406
|
+
`in vec4` (the fetch converts: `in vec4 aColor` reads a `unorm8x4` as
|
|
407
|
+
0..1), `uint*` formats feed `in uvec4` and `sint*` formats `in ivec4`,
|
|
408
|
+
exact at every width - so a per-vertex id or an index into a wide
|
|
409
|
+
table rides as `uint32` into `in uint aId`, and the "skinned" layout's
|
|
410
|
+
`aJoints` is `uint8x4` into `in uvec4 aJoints`. A layout that crosses
|
|
411
|
+
kinds (`uint8x4` into `in vec4`) is the missing-attribute error at
|
|
412
|
+
add() (`formatFeeds` is the rule). The base prefix `aPos` float32x3 + `aNormal`
|
|
413
|
+
float32x3 + `aUV` float32x2 is what every generator emits and what the
|
|
414
|
+
stock materials read, not a rule: a hand-built geometry declares what
|
|
415
|
+
it has, so a point cloud may carry `[aPos float32x3, aData float32]` at
|
|
416
|
+
16 bytes a point instead of 36, or a color as `unorm8x4` at 4 bytes
|
|
417
|
+
instead of 16. Pair a prefixless layout with a material that reads only
|
|
418
|
+
what it carries: `unlit` (aPos only) or a shaderMaterial; a stock lit
|
|
419
|
+
material over it throws the ordinary missing-attribute error at add().
|
|
420
|
+
A layout handed to a generator must start with the prefix and stay
|
|
421
|
+
float32-family (the generator writes floats); packing is a pass over
|
|
422
|
+
the result. `withAttribute(geometry, { name, format }, fill)` appends
|
|
423
|
+
one channel in any format (Three's `setAttribute` for an interleave;
|
|
424
|
+
the fill is given in float and encoded on write); "colored" names the
|
|
425
|
+
common case, the prefix plus `aColor` float32x4 - the per-vertex data
|
|
426
|
+
channel (a tint, baked AO, any four scalars; standard name, your
|
|
427
|
+
contents) - and `withColors(geometry, fill)` is its spelling. Fill is a
|
|
428
|
+
flat size-per-vertex array or a per-vertex callback receiving `(index,
|
|
429
|
+
pos, normal, uv)` (zeros for a channel the layout lacks). The fill is
|
|
430
|
+
raw floats: a tint the stock materials read under `vertexColors` is
|
|
431
|
+
premultiplied linear like every shader color, so encode an sRGB pick
|
|
432
|
+
with `premultipliedColor(color)`. `Geometry.vertices` is an
|
|
433
|
+
`ArrayBufferView` (a Float32Array for an all-float layout, a Uint8Array
|
|
434
|
+
when a channel is packed; never the contract): read and write a channel
|
|
435
|
+
through `geometryAttribute(geometry, name)`, an accessor whose
|
|
436
|
+
`get(i, k)`/`set(i, k, v)` speak floats whatever the format, and count
|
|
437
|
+
vertices with `vertexCount`. Materials read attributes BY NAME: a
|
|
438
|
+
material's vertex stage may declare any subset of its geometry's
|
|
439
|
+
channels, and a channel the program reads that the geometry lacks (by
|
|
440
|
+
name, or with a different component count) throws at add(). What a program reads is the ENGINE's word (`material.attributes()`
|
|
298
441
|
= `programAttributes` reflection of the linked program, instance
|
|
299
442
|
attributes excluded), not a parse of the GLSL: an `in` the compiler
|
|
300
443
|
dropped does not count, and the engine also rejects a pipeline whose
|
|
@@ -302,15 +445,39 @@ attribute lists leave a read attribute uncovered. The material
|
|
|
302
445
|
keeps one program and builds one pipeline per layout its meshes bring,
|
|
303
446
|
so a geometry may carry more than a material reads. The whole layout
|
|
304
447
|
ships whether a material reads every attribute or not (inactive
|
|
305
|
-
attributes only keep the stride), so extra channels cost their
|
|
448
|
+
attributes only keep the stride), so extra channels cost their bytes on
|
|
306
449
|
every draw of that geometry - keep data-light passes (a wireframe
|
|
307
|
-
reading only aPos) on
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
`
|
|
313
|
-
|
|
450
|
+
reading only aPos) on base-layout geometry. `layoutStride` (bytes)/
|
|
451
|
+
`layoutSlot` (byte offset, format, components)/`layoutKey`/
|
|
452
|
+
`layoutAttributes` are the layout arithmetic; two layouts with equal
|
|
453
|
+
keys interleave identically (merge requires that).
|
|
454
|
+
STREAMS: a geometry may carry more than one vertex buffer.
|
|
455
|
+
`Geometry.vertices` under `layout` is stream 0 (where aPos lives) and
|
|
456
|
+
`Geometry.streams` holds extra `{ layout, vertices }` buffers, each with
|
|
457
|
+
its own attribute list and the same vertex count (Unity's vertex
|
|
458
|
+
streams; Three's one buffer per channel when a stream carries one
|
|
459
|
+
attribute). `withAttribute(geometry, attr, fill, { stream })` appends a
|
|
460
|
+
channel to stream 1..n or opens stream n + 1; `geometryAttribute`,
|
|
461
|
+
`geometrySlot`, `geometryKey`, `geometryLayouts`, `geometryStreams`
|
|
462
|
+
and `geometryVertexCount` read across streams, and a material's
|
|
463
|
+
pipeline is one per `geometryKey` (every stream's layout, `|` between
|
|
464
|
+
them). Streams exist for data on its own schedule: `updateVertices(
|
|
465
|
+
geometry, { stream?, first?, count? })` re-uploads that vertex range of
|
|
466
|
+
that stream from the geometry's own array in place (write the array
|
|
467
|
+
through the accessor first; Three's `needsUpdate` with an update
|
|
468
|
+
range), reaching every mesh, view and wireframe over the geometry
|
|
469
|
+
while the other streams never move; a stream-0 update drops the cached
|
|
470
|
+
bounds and rewrites the same range of the core's copy of the positions,
|
|
471
|
+
so the mesh picks and culls where it is now drawn (the box follows at
|
|
472
|
+
the next flush, the triangle index is rebuilt by the next query - a
|
|
473
|
+
deforming mesh needs no re-attach). transformGeometry and
|
|
474
|
+
the edge builders share extra streams with their source; merge
|
|
475
|
+
concatenates every stream; the `.srtm` container refuses streams (run
|
|
476
|
+
time data). `setDrawRange(mesh, first, count?)` draws indices `[first,
|
|
477
|
+
first + count)` of the mesh's geometry (Three's `setDrawRange`, on the
|
|
478
|
+
mesh because the scene owns the entries), applied to every entry of the
|
|
479
|
+
mesh and reset by setGeometry. The worked example is
|
|
480
|
+
`examples/streams.tsx`.
|
|
314
481
|
Indices are uint16 or uint32 - the `Geometry.indices` array type picks
|
|
315
482
|
the draw's index format, so hand-built geometry past 64k vertices just
|
|
316
483
|
uses a Uint32Array (generators emit uint16). What the indices LIST is
|
|
@@ -321,8 +488,9 @@ SetIndices topology), a material builds one pipeline per (layout,
|
|
|
321
488
|
topology) pair its meshes bring, and validateGeometry enforces the
|
|
322
489
|
count rule at add() (a whole number of triangles or lines, one
|
|
323
490
|
primitive's worth for a strip). Materials have no topology of their
|
|
324
|
-
own. Only a triangle list gets a
|
|
325
|
-
and collide by their bounds box
|
|
491
|
+
own. Only a triangle list gets a triangle narrowphase; lines and points
|
|
492
|
+
pick and collide by their bounds box (which follows `updateVertices` all
|
|
493
|
+
the same), and cast no shadow. `geometryTopology`
|
|
326
494
|
reads the field with the default applied. POINTS: a point cloud is
|
|
327
495
|
ordinary indexed geometry - one vertex per point, the index buffer
|
|
328
496
|
listing them, `topology: "points"`, no constraint on the index count.
|
|
@@ -336,7 +504,7 @@ cost model). Geometry GPU buffers are lazy, shared, and
|
|
|
336
504
|
reference-counted by draw entries: removing the last
|
|
337
505
|
entry frees them at the end of the microtask (a same-tick rebuild keeps
|
|
338
506
|
the upload), so swapping `<Mesh geometry>` reactively never accumulates
|
|
339
|
-
old generations; the vertex upload is keyed on the
|
|
507
|
+
old generations; the vertex upload is keyed on the vertices view
|
|
340
508
|
itself, so geometries sharing a vertex array (a wireframe over its
|
|
341
509
|
source) share one buffer. `disposeGeometry` is the immediate explicit
|
|
342
510
|
free.
|
|
@@ -345,9 +513,9 @@ free.
|
|
|
345
513
|
|
|
346
514
|
Materials dedupe hard: one program + one pipeline per material CLASS
|
|
347
515
|
(a `shaderMaterialClass` per option combination for unlit, lit and
|
|
348
|
-
sprite alike: map x transparent x cull x alphaTest,
|
|
516
|
+
sprite alike: map x transparent x cull x alphaTest, phong's extras on
|
|
349
517
|
top), `depth: true` + `cull: "back"` unless the material says otherwise
|
|
350
|
-
(`cull: "none"` for double-sided geometry;
|
|
518
|
+
(`cull: "none"` for double-sided geometry; phong flips the normal on back
|
|
351
519
|
faces); an instance is just per-entry uniforms (`uColor`) and bindings
|
|
352
520
|
(`uMap`).
|
|
353
521
|
|
|
@@ -379,14 +547,16 @@ collision claims - two copies of this contract have drifted before.
|
|
|
379
547
|
|
|
380
548
|
| Component | Props |
|
|
381
549
|
| --- | --- |
|
|
382
|
-
| `Scene` | `width?`, `height?` (target pixels - both, or neither = FILL, below), `clearColor?`, `camera?` (partial CameraUpdate, `ortho` included - the declarative scene.setCamera; same state as `PerspectiveCamera`, use one form; the camera CONTROLS are not a third form - `OrbitCamera`/`FirstPersonCamera` drive position and target only, so `fov`/`near`/`far` come from here even while a control moves the camera, and the default `far` of 100 is what clips a scene in metres), `background?` (fragment GLSL, or a skybox `{ cube, intensity?, rotation? }`), `environment?` (`{ cube, intensity?, rotation? }`, the cube reflective materials mirror), `fog?` (`{ color, near, far }`, linear by camera distance), `toneMapping?` (`"none"` default or `"
|
|
383
|
-
| `View3d` | a Scene child rendering the scene again from a camera of its own (scene.createView as a component): `width`, `height` (target pixels, live; fixed-size only for now), `x?`, `y?` (the tile's top-left in `into`, live), `into?` (tile an app-owned draw target - one pass for every view into it; fixed at creation), `camera?` (partial CameraUpdate on the view's camera, live; same state as a `PerspectiveCamera` child), `layers?` (the view's mask, live), `clearColor?`, `label?`, `overrideMaterial?`, `fog?` (FogOptions, or null for none), `depth?`, `samples?`, `filter?`, `wrap?` (createView's, fixed), `ref?(view)`, `output?(texture)` (else a built-in `<texture>` leaf at the target size, a tile shown through srcX/srcY), `events?`, `pointer?` (the view leaf's feed, fed from the view's root), `onPointerDown/Move/Up?`, `onWheel?`, `onTap?` (the view's own handlers); camera-control children drive the VIEW (inside, `useScene()` reports the view as `viewport` and the view's feed as `pointer`); node children mount to the scene as outside, and under the view's leaf get their ordinary pointer handlers, picked through the view's camera (`view.pick`), the view as the root of that walk |
|
|
384
|
-
| `Group` | `position?`, `rotation?` (Euler radians, XYZ order), `quaternion?` (either, not both), `scale?` (number = uniform), `visible?`, pointer events (below), `ref?(node)` |
|
|
550
|
+
| `Scene` | `width?`, `height?` (target pixels - both, or neither = FILL, below), `clearColor?`, `camera?` (partial CameraUpdate, `ortho` included - the declarative scene.setCamera; same state as `PerspectiveCamera`, use one form; the camera CONTROLS are not a third form - `OrbitCamera`/`FirstPersonCamera` drive position and target only, so `fov`/`near`/`far` come from here even while a control moves the camera, and the default `far` of 100 is what clips a scene in metres), `background?` (fragment GLSL, or a skybox `{ cube, intensity?, rotation? }`), `environment?` (`{ cube, intensity?, rotation? }`, the cube reflective materials mirror), `fog?` (`{ color, near, far }`, linear by camera distance), `toneMapping?` (`"none"` default, `"aces"`, `"agx"` or `"neutral"`), `exposure?` (default 1), `bloom?` (`{ threshold?, intensity?, radius? }`, the stock bloom on the resolve, reactive), `layers?` (target mask, default 1), `depth?` (`"texture"` exposes scene.depthTexture; not with samples), `samples?` (1/2/4/8 MSAA), `label?`, `ref?(scene)`, `output?(texture)`, `resolve?` (the source, `{ source, textures }`, or a function of the buffer id returning either; fixed at creation - see Color), `events?` (pointer events, default on), `pointer?` (the leaf's pointer feed, fed from the scene's root), `onPointerDown/Move/Up?`, `onWheel?`, `onTap?` (the scene's own handlers, the last stop of the walk - `event.mesh` null over empty space) |
|
|
551
|
+
| `View3d` | a Scene child rendering the scene again from a camera of its own (scene.createView as a component): `width`, `height` (target pixels, live; fixed-size only for now), `x?`, `y?` (the tile's top-left in `into`, live), `into?` (tile an app-owned draw target - one pass for every view into it; fixed at creation), `camera?` (partial CameraUpdate on the view's camera, live; same state as a `PerspectiveCamera` child), `layers?` (the view's mask, live), `clearColor?`, `label?`, `overrideMaterial?`, `fog?` (FogOptions, or null for none), `depth?`, `samples?`, `filter?`, `wrap?` (createView's, fixed), `ref?(view)`, `output?(texture)` (else a built-in `<texture>` leaf at the target size, a tile shown through srcX/srcY), `resolve?` (as Scene's; not with `into`), `bloom?` (BloomOptions overrides the scene's, null turns it off in this view, absent follows the scene; not with `into`), `events?`, `pointer?` (the view leaf's feed, fed from the view's root), `onPointerDown/Move/Up?`, `onWheel?`, `onTap?` (the view's own handlers); camera-control children drive the VIEW (inside, `useScene()` reports the view as `viewport` and the view's feed as `pointer`); node children mount to the scene as outside, and under the view's leaf get their ordinary pointer handlers, picked through the view's camera (`view.pick`), the view as the root of that walk |
|
|
552
|
+
| `Group` | `position?`, `rotation?` (Euler radians, XYZ order), `quaternion?` (either, not both), `scale?` (number = uniform), `visible?`, the bubbling pointer events (below: down/move/up/wheel/tap from a hit descendant; a group is never the struck node, so it takes no `onPointerEnter`/`onPointerLeave` - `Lod` likewise), `ref?(node)` |
|
|
553
|
+
| `Lod` | a Group whose direct children carrying `lodSize` (a prop every node component takes: the projected size below which that child hands over, see Level of detail) are its levels, sorted by size, never by JSX position; plus `fade?` (the cross-fade band fraction, default 0); a child without `lodSize` is drawn always |
|
|
554
|
+
| `InstancedLod` | as InstancedMesh minus `material`, plus `levels` (`[{ geometry, material, size }]` nearest first, fixed at creation; instanced materials as InstancedMesh's), `castShadow?` (every level); `<Instance>` children populate it as under `InstancedMesh`, each drawing the level its own projected size picks |
|
|
385
555
|
| `Mesh` | `geometry`, `material`, transforms as Group, `params?` (per-mesh uniforms, merge semantics - no unset), `renderOrder?`, `castShadow?`, `layers?` (membership bitmask, default 1), pointer events (below), `ref?(mesh)` |
|
|
386
556
|
| `Sprite` | as Mesh minus `geometry`: a camera-facing unit quad, `scale` is its world size, rotation is ignored; pair with a `sprite()` material |
|
|
387
557
|
| `InstancedMesh` | as Mesh (an instanced `material`: a stock one with `instanced`/`instanceColors`, or a class declaring INSTANCE_MATRIX_ATTRIBUTES), plus `capacity?` (instance slots, default 64; the buffers double past it), `bounds?` (optional: instances pick by themselves and the mesh culls by their union), `label?`; a PARENT: `<Instance>` children populate it, `<Group>` children are squads; the record buffers are component-owned and freed on unmount |
|
|
388
|
-
| `Instance` | one instance of the enclosing `InstancedMesh`: transforms, `transition`, pointer events as Group, plus `style?` (the material's
|
|
389
|
-
| `RecordMesh` | as Mesh, plus `records` (
|
|
558
|
+
| `Instance` | one instance of the enclosing `InstancedMesh`: transforms, `transition`, pointer events as Group, plus `style?` (the material's style record, one value per component - `[r, g, b, a]` under `instanceColors`), `ref?(instance)`; a parent too (a `<Mesh>` under an instance rides with it) |
|
|
559
|
+
| `RecordMesh` | as Mesh, plus `records` (the per-instance records in the material's first instance layout, an ArrayBufferView; buffer capacity starts at the first value and grows on larger rewrites), `count?` (records drawn, default all), `bounds?` (local [minX..maxZ] over the population - without it the mesh never picks); the record buffers are component-owned and freed on unmount |
|
|
390
560
|
| `PerspectiveCamera` | `fov?` (vertical DEGREES, default 60), `near?`, `far?`, `position?`, `lookAt?`, `up?` - or the Scene `camera` prop, the same state (last write wins) |
|
|
391
561
|
| `SpotLight` | transforms as Group, `direction?` (local aim, default [0, -1, 0]), `color?`, `intensity?`, `distance?` (falloff cutoff, 0 = none), `angle?` (cone half-angle DEGREES, default 60), `penumbra?` (0..1 rim fade, default 0), `decay?` (falloff exponent, default 2), `castShadow?`, `shadow?` (mapSize, bias, normalBias, near), `ref?(light)` |
|
|
392
562
|
| `PointLight` | transforms as Group (position is what matters), `color?`, `intensity?`, `distance?`, `decay?`, `castShadow?` (six face maps, six shadow slots), `shadow?` (mapSize, bias, normalBias, near), `ref?(light)` |
|
|
@@ -400,7 +570,9 @@ a `<d-texture>`, a leaf with blendMode/fit/pointer/layout props, or a
|
|
|
400
570
|
post-effect chain (`createShaderTarget` sampling the id with a
|
|
401
571
|
covering-triangle pass; created in the callback it disposes with the
|
|
402
572
|
Scene). Return null for no leaf at all and compose `scene.texture`
|
|
403
|
-
elsewhere. Called once, untracked, inside the scene context.
|
|
573
|
+
elsewhere. Called once, untracked, inside the scene context. `output`
|
|
574
|
+
composes the DISPLAYED texture, the resolve's output: an LDR effect
|
|
575
|
+
lives here; one that needs the scene's radiance is a `resolve` (Color). Scene
|
|
404
576
|
`width`/`height` are target pixels and the leaf's own width/height are
|
|
405
577
|
layout, so render and display size separate - render at 2x and display
|
|
406
578
|
smaller for supersampling.
|
|
@@ -475,21 +647,42 @@ a drag and a stick both turn the eye.
|
|
|
475
647
|
|
|
476
648
|
Camera control: `createOrbitCamera(scene, { target?, azimuth?, elevation?,
|
|
477
649
|
distance?, min/maxDistance?, min/maxElevation?, orbitSpeed?, rotateSpeed?,
|
|
478
|
-
zoomSpeed?, panSpeed?,
|
|
479
|
-
azimuth/elevation/distance around a target with optional auto-orbit. The
|
|
480
|
-
first argument is anything with the scene's `setCamera
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
650
|
+
zoomSpeed?, panSpeed?, damping?, clampPose?, zoomAnchor?, rotateAnchor? })`
|
|
651
|
+
- azimuth/elevation/distance around a target with optional auto-orbit. The
|
|
652
|
+
first argument is anything with the scene's `setCamera`, `camera()` (the
|
|
653
|
+
fov maps pan travel to world) and `size()` (the aspect `fit` frames
|
|
654
|
+
against): a Scene, or a View to drive one view's camera independently.
|
|
655
|
+
Its `axes` are `rotate` (a delta of one element height sweeps one full
|
|
656
|
+
turn, Three's OrbitControls convention, so a drag feels the same on a
|
|
657
|
+
phone and a 4k window; a rate turns at 0.5 turn/s at full deflection),
|
|
658
|
+
`zoom` (octaves: a delta of 1 halves the distance, a rate of 1 halves it
|
|
659
|
+
per second) and `pan` (the target slides so the scene tracks the fingers
|
|
660
|
+
1:1 at the target's depth, three.js DOLLY_PAN, weighted by `panSpeed`). A
|
|
661
|
+
delta bracketed by a gesture (a drag, a pinch, two fingers) applies at
|
|
662
|
+
once, the content staying under the fingers; an unbracketed one (a wheel
|
|
663
|
+
notch, a key step) is an impulse and glides in - `damping` scales that
|
|
664
|
+
settle time, 0 applies it at once - notches compounding on the pending
|
|
665
|
+
value so a fast scroll is one push, the 2d camera's rule and the same
|
|
666
|
+
knob there. The verbs: `rotateBy(azimuth, elevation)` radians,
|
|
667
|
+
`zoomBy(factor, anchor?)` (factor > 1 in, as the 2d camera's zoomAt),
|
|
668
|
+
`panBy(right, up)` world units, `setPivot(point)`, `set(pose)`; every one
|
|
669
|
+
pushes the pose at once. Two commanded moves ease instead, inside
|
|
670
|
+
update(dt): `glideTo({ azimuth?, elevation?, distance?, target? })`, and
|
|
671
|
+
`fit(bounds, { glide? })`, which frames a `[minX, minY, minZ, maxX, maxY,
|
|
672
|
+
maxZ]` box (geometryBounds, a model's `bounds`) - target to its centre,
|
|
673
|
+
distance to where the bounding sphere fills the tighter of the vertical
|
|
674
|
+
and horizontal fov at the target's size, azimuth and elevation kept,
|
|
675
|
+
clamps applied; a snap unless `glide`, so a park-then-snapshot repeats
|
|
676
|
+
(under ortho only the target moves). Any input drops a glide (a finger
|
|
677
|
+
landing holds the view), and a `set()` that writes a pose field snaps
|
|
678
|
+
and drops any motion; `set({})` and `set({ orbiting })` leave it running.
|
|
679
|
+
Every write - input, verb, glide frame, set() - goes through the range
|
|
680
|
+
clamps and then `clampPose(pose)`, which sees the whole pose and returns
|
|
681
|
+
the fields to change: bound where a pan may put the target, or hold the
|
|
682
|
+
eye above a floor (an elevation floor that depends on the distance, which
|
|
683
|
+
a fixed `minElevation` cannot say); a glide's goal is clamped when set
|
|
684
|
+
and its frames as they land, so it never shows an illegal pose.
|
|
685
|
+
Zoom aims at the target unless `zoomAnchor(focal, {eye, target})`
|
|
493
686
|
maps the gesture's focal point - a FRACTION of the element, [0..1, 0..1] -
|
|
494
687
|
to a world point (ground hit, target-depth plane, ...): then that point
|
|
495
688
|
stays pinned under the pointer and the target slides toward it; only the
|
|
@@ -502,13 +695,14 @@ called when a rotate gesture begins, its point is projected onto the
|
|
|
502
695
|
view axis and re-seats the pivot without moving the picture, so a drag
|
|
503
696
|
after an anchored zoom orbits what the camera looks at, not wherever the
|
|
504
697
|
zoom left the target. Call `orbit.update(dt)` from your onFrame to
|
|
505
|
-
integrate the rates
|
|
506
|
-
use its return - true when the pose changed since the previous
|
|
507
|
-
nudges included - to gate per-frame dependents like reprojecting
|
|
508
|
-
overlays. `orbiting()` (the auto-orbit switch) and `active()` (the
|
|
509
|
-
frame-loop gate: orbiting with a non-zero rate,
|
|
510
|
-
|
|
511
|
-
|
|
698
|
+
integrate the rates, the auto-orbit and any glide (no frame loop of its
|
|
699
|
+
own), and use its return - true when the pose changed since the previous
|
|
700
|
+
update, nudges included - to gate per-frame dependents like reprojecting
|
|
701
|
+
HUD overlays. `orbiting()` (the auto-orbit switch) and `active()` (the
|
|
702
|
+
frame-loop gate: orbiting with a non-zero rate, a glide or damped notch
|
|
703
|
+
in flight, or any rate driving - the predicate every camera control
|
|
704
|
+
shares) are reactive (HUD-safe); the pose is plain state via
|
|
705
|
+
`pose()`/`set()` (also the debug-command shape). It
|
|
512
706
|
drives position and target only; fov/near/far stay on scene.setCamera
|
|
513
707
|
(or the Scene `camera` prop). The auto-orbit pauses while a gesture is
|
|
514
708
|
open.
|
|
@@ -522,10 +716,13 @@ reconnects) and nothing else, and runs a frame loop only while
|
|
|
522
716
|
`active()`, so a camera moved by drags alone keeps the app demand-driven
|
|
523
717
|
idle. The pose props are initial values: runtime pose changes (and the
|
|
524
718
|
debug-command hookup) go through `ref`'s handle, whose set() and verbs
|
|
525
|
-
push the pose. Every other prop is live -
|
|
526
|
-
getter and read where it applies, never
|
|
527
|
-
|
|
528
|
-
|
|
719
|
+
push the pose and whose glideTo/fit ease it. Every other prop is live -
|
|
720
|
+
forwarded to the control as a getter and read where it applies, never
|
|
721
|
+
snapshotted - so clamps, rates, `damping` and anchors follow their props
|
|
722
|
+
without a remount, and a clamp change (`clampPose` included) re-clamps
|
|
723
|
+
the pose at once. A `fit` from `ref` at mount reads the scene's size of
|
|
724
|
+
that moment: a fill-mode `<Scene>` has its creation size until its first
|
|
725
|
+
layout, so fit after the first frame (or hand a fixed-size scene).
|
|
529
726
|
|
|
530
727
|
### First-person camera
|
|
531
728
|
|
|
@@ -544,7 +741,12 @@ length so they walk no faster) and `rise` (world up at `moveSpeed`, fly
|
|
|
544
741
|
mode only). Walking (the default) flattens the heading onto the ground
|
|
545
742
|
plane at fixed height; `fly` moves along the view. The verbs:
|
|
546
743
|
`lookBy(yaw, pitch)` radians, `moveBy(right, forward, up?)` in the
|
|
547
|
-
walker's frame, `set(pose)
|
|
744
|
+
walker's frame, `set(pose)`, and `glideTo({ position?, yaw?, pitch? })`,
|
|
745
|
+
which eases there inside update(dt) (yaw to the number given, not the
|
|
746
|
+
shortest turn), dropped by any input and by a set() of a pose field -
|
|
747
|
+
the orbit camera's glide, without its damping: this control's unbracketed
|
|
748
|
+
delta is mouse motion under pointer lock, and that is never eased. Every
|
|
749
|
+
option but the initial pose is read
|
|
548
750
|
where it applies (`fly` per step, `clampPosition` per move, the rates and
|
|
549
751
|
pitch clamps per input), so a field changed on the options object takes
|
|
550
752
|
effect on the next move: walk and fly are one control. The control NEVER
|
|
@@ -554,8 +756,10 @@ collision of its own: `clampPosition(next, current)` is the whole hook -
|
|
|
554
756
|
bounds, a floor height, or `moveAndSlide` over `next - current` against
|
|
555
757
|
the collision layer (see `examples/collision.tsx`, whose `jump` is one
|
|
556
758
|
more action on the same map: Space or the pad's south button, read by
|
|
557
|
-
name)
|
|
558
|
-
|
|
759
|
+
name); a glide consults it every frame as a walk does every step. Call
|
|
760
|
+
`update(dt)` from onFrame; `active()` is reactive - a rate driving (a
|
|
761
|
+
held key, a deflected stick), or a glide in flight - and gates the loop.
|
|
762
|
+
The map
|
|
559
763
|
tracks held keys from the down/up pair through `input.handlers`; spread
|
|
560
764
|
its `onBlur` too, since the up never arrives once focus has left.
|
|
561
765
|
|
|
@@ -609,7 +813,9 @@ The index and the narrowphase live in the spatial core: every attached
|
|
|
609
813
|
mesh's local box is a leaf in a dynamic AABB tree the flush refits from
|
|
610
814
|
the fresh world matrices (O(moved) per frame, a query O(log meshes)), and
|
|
611
815
|
an ordinary mesh is then tested per triangle against its geometry's
|
|
612
|
-
shape (one CPU copy per distinct geometry, created with
|
|
816
|
+
shape (one CPU copy of the positions per distinct geometry, created with
|
|
817
|
+
its GPU buffers and rewritten by `updateVertices`; the mesh's box comes
|
|
818
|
+
from the same copy, so a deforming mesh is found where it is drawn),
|
|
613
819
|
so hits carry `face`, `uv` and a world-space `normal` facing the ray, and
|
|
614
820
|
a ray through a knot's hole misses. A large geometry's triangles are
|
|
615
821
|
BVH-indexed too - built by the first ray that reaches the shape, log-cost
|
|
@@ -723,7 +929,9 @@ pointerType, button, modifiers) plus `x`/`y` in scene pixels
|
|
|
723
929
|
(screenRay's input - a drag plane is one intersection away). Root
|
|
724
930
|
listeners all run, in registration order (the root is the last stop,
|
|
725
931
|
nothing is left to claim). Wiring: the built-in `<Scene>` leaf carries
|
|
726
|
-
`scene.handlers` automatically (opt out: `events={false}
|
|
932
|
+
`scene.handlers` automatically (opt out: `events={false}`, which throws
|
|
933
|
+
at mount together with a `pointer` feed - the feed would listen at a root
|
|
934
|
+
no event reaches; the same on `<View3d>`); an `output`
|
|
727
935
|
leaf or imperative composition spreads `{...scene.handlers}` onto the
|
|
728
936
|
element showing the texture. `scene.handlers` assumes that leaf is LAID
|
|
729
937
|
OUT at the target size - true for the built-in leaf and a d-texture at
|
|
@@ -742,15 +950,36 @@ a default, named as Three names them: `box({ width, height, depth })`
|
|
|
742
950
|
`ring({ innerRadius, outerRadius, segments })` (XY, facing +z - rotate
|
|
743
951
|
`[-Math.PI/2, 0, 0]` for a floor); `sphere({ radius, widthSegments,
|
|
744
952
|
heightSegments })`; `cylinder({ radiusTop, radiusBottom, height,
|
|
745
|
-
radialSegments })` (y axis, capped; unequal radii taper
|
|
746
|
-
`cone({ radius, height, radialSegments })`;
|
|
953
|
+
radialSegments, heightSegments })` (y axis, capped; unequal radii taper
|
|
954
|
+
it) and `cone({ radius, height, radialSegments, heightSegments })`;
|
|
955
|
+
`capsule({ radius, height, capSegments, radialSegments, heightSegments
|
|
956
|
+
})` (y axis, `height` the TOTAL extent like
|
|
957
|
+
cylinder's and Godot's/Unity's where Three's is the middle section
|
|
958
|
+
only, so `height: 2 * radius` is a sphere and less throws; its collision
|
|
959
|
+
volume is `{ a: [0, -(height / 2 - radius), 0], b: [0, height / 2 -
|
|
960
|
+
radius, 0], radius }`); `torus({ radius, tube,
|
|
747
961
|
radialSegments, tubularSegments })` (lying flat, hole on the y axis) and
|
|
748
962
|
`torusKnot({ radius, tube, tubularSegments, radialSegments, p, q })`
|
|
749
963
|
(standing y-up) - both oriented for the y-up world, unlike Three's z-up.
|
|
750
964
|
No positional form: `box()` is the default cube, `box({ label: "rock" })`
|
|
751
|
-
names it.
|
|
965
|
+
names it. The polyhedra: `tetrahedron`, `octahedron`, `icosahedron` and
|
|
966
|
+
`dodecahedron` (`{ radius, detail }`, radius 0.5 like sphere where
|
|
967
|
+
Three's is 1) and the generic `polyhedron(vertices, indices, { radius,
|
|
968
|
+
detail })` over Three's data form (flat xyz list, CCW triangle list, the
|
|
969
|
+
data first like `box3Helper`), each solid projected onto its
|
|
970
|
+
circumsphere. `detail` splits every edge `detail + 1` ways first, so
|
|
971
|
+
`detail: 0` is the flat-shaded solid (face normals) and anything above a
|
|
972
|
+
sphere of uniform triangles with radial normals: `icosahedron({ detail:
|
|
973
|
+
3 })` is the icosphere, no pole pinch. Non-indexed like Three, every
|
|
974
|
+
triangle owning its three vertices; `edgesGeometry` welds by position,
|
|
975
|
+
so `edgesGeometry(dodecahedron())` is the twelve pentagons (the fan
|
|
976
|
+
diagonals are coplanar and drop out) where `wireframeGeometry` shows the
|
|
977
|
+
triangulation. UVs are the spherical map with Three's seam patch,
|
|
978
|
+
stretched toward the poles: the textured sphere stays `sphere()`, the
|
|
979
|
+
polyhedra are for flat-shaded, low-poly and procedurally shaded looks
|
|
980
|
+
(examples/polyhedra.tsx). Every options object (the profile kit's `extrude`/`lathe`/
|
|
752
981
|
`sweep`/`tube` too) also takes `label` and `layout` - `layout` makes the
|
|
753
|
-
generator emit that layout in one pass (
|
|
982
|
+
generator emit that layout in one pass (base channels written, the
|
|
754
983
|
extra slots zero), so `box({ layout: "colored" })` then
|
|
755
984
|
`fillColors(g, fill)` builds colored geometry without the
|
|
756
985
|
generate-then-repack copy; the result is byte-identical to
|
|
@@ -759,7 +988,8 @@ the tail every generator ends in, for your own generators.
|
|
|
759
988
|
`withAttribute(geometry, attr, fill, label?)` derives a copy of any
|
|
760
989
|
geometry (generator or hand-built) with one more channel after its
|
|
761
990
|
current layout; the source is untouched. `withColors(geometry, fill,
|
|
762
|
-
label?)` is the aColor
|
|
991
|
+
label?)` is the aColor float32x4 case, keeping the "colored" preset
|
|
992
|
+
name (`withAttribute` with `unorm8x4` is the 4-byte color).
|
|
763
993
|
`fillAttribute(geometry, name, fill, first?, count?)` is the in-place
|
|
764
994
|
primitive under both: overwrites one channel the geometry's layout
|
|
765
995
|
already carries (withAttribute ADDS one), reading pos/normal/uv from the
|
|
@@ -796,13 +1026,86 @@ until the threshold passes 360 / radialSegments. The wireframe of a
|
|
|
796
1026
|
"skinned" part draws in its pose under `unlit({ skinned: true })` with
|
|
797
1027
|
the mesh's palette, and the swap onto a live mesh is
|
|
798
1028
|
`setGeometry` + `setMaterial` (`examples/wireframe.tsx`). Lines are one
|
|
799
|
-
pixel wide on GL ES; thick lines are quad geometry.
|
|
1029
|
+
pixel wide on GL ES; thick lines are quad geometry. The debug helpers are
|
|
1030
|
+
`"lines"` builders too, Three's helper classes camelCased:
|
|
1031
|
+
`gridHelper({ size?, divisions?, color?, centerColor? })` is the XZ grid
|
|
1032
|
+
at y 0 (defaults 10 by 10, Three's grays; the two lines through the
|
|
1033
|
+
origin take `centerColor` and exist only for an even `divisions`),
|
|
1034
|
+
`axesHelper({ size? })` the X red, Y green, Z blue triad from the origin,
|
|
1035
|
+
`box3Helper(bounds, options?)` the twelve edges of a
|
|
1036
|
+
`[minX, minY, minZ, maxX, maxY, maxZ]` box in local space, and
|
|
1037
|
+
`planeHelper({ size? })` a square outline with its diagonals and a unit
|
|
1038
|
+
normal tick in the XY plane facing +z, placed like `plane()`, and
|
|
1039
|
+
`arrowHelper({ length?, headLength?, headWidth? })` an arrow along +y
|
|
1040
|
+
with a pyramid-outline head, aimed by the node's rotation
|
|
1041
|
+
(`quatFromTo(out, [0, 1, 0], direction)`), and `capsuleHelper(volume,
|
|
1042
|
+
{ segments? })` the outline of a `{ a, b, radius }` collision capsule
|
|
1043
|
+
(rings at both ends, four lines, two half circles per cap; `a == b`
|
|
1044
|
+
draws a sphere's three great circles) in the volume's own space, the
|
|
1045
|
+
gizmo Unity and Godot draw for a capsule collider. Godot and
|
|
1046
|
+
Unity keep these in the editor and Three makes them scene objects; here
|
|
1047
|
+
they are plain geometry a node places and a material draws. The grid and
|
|
1048
|
+
the triad are "colored" layout (sRGB colors in, premultiplied linear
|
|
1049
|
+
aColor out, the material contract) drawn by `unlit({ vertexColors:
|
|
1050
|
+
true })`; a material that reads no aColor draws them in its own color.
|
|
1051
|
+
The box, the plane, the arrow and the capsule are base layout. For a bounds box that moves
|
|
1052
|
+
every frame, draw `edgesGeometry(box())` on a node whose position is the
|
|
1053
|
+
box center and whose scale is its size and update the transform (Three's
|
|
1054
|
+
Box3Helper does exactly that) instead of rebuilding.
|
|
1055
|
+
`examples/wireframe.tsx` draws a grid, the triad and the rover's bounds
|
|
1056
|
+
box. `geometryBounds(geometry)`
|
|
800
1057
|
returns the cached local AABB `[minX, minY, minZ, maxX, maxY, maxZ]`, and
|
|
801
1058
|
`rayBoxDistance(ox, oy, oz, dx, dy, dz, minX, .., maxZ)` is the picking
|
|
802
1059
|
slab test (entry t >= 0 in units of the direction's length, 0 from
|
|
803
1060
|
inside, -1 for a miss) - for ray-testing boxes you keep yourself
|
|
804
1061
|
(triggers, collision volumes) without meshes you do not want to draw.
|
|
805
1062
|
|
|
1063
|
+
Normals as data: two ops, split by whether the vertex count survives.
|
|
1064
|
+
`computeVertexNormals(geometry)` recomputes a triangle geometry's
|
|
1065
|
+
normals IN PLACE from the faces that name each vertex by index (Three's
|
|
1066
|
+
`computeVertexNormals`, Unity's `RecalculateNormals`, Godot's
|
|
1067
|
+
`generate_normals`, all in place), each face weighted by its corner
|
|
1068
|
+
angle so the result does not depend on how a surface was triangulated.
|
|
1069
|
+
The math runs in the core in one call over the vertex floats (so aNormal
|
|
1070
|
+
must be float32x3; a packed normal is an authoring format, pack after
|
|
1071
|
+
withNormals), and it returns the stream carrying aNormal, which
|
|
1072
|
+
`updateVertices` re-uploads: the CPU deformation loop is
|
|
1073
|
+
`fillAttribute(g, "aPos", ...)`, `computeVertexNormals(g)`,
|
|
1074
|
+
`updateVertices(g)`. That loop is for data that genuinely changes on the
|
|
1075
|
+
CPU - a streamed cloud, a cloth solved in JS, an editor edit. A
|
|
1076
|
+
per-frame ripple, sway or wave belongs in a vertex shader: displace and
|
|
1077
|
+
derive the normal in the vertex stage (`shaderMaterial`), and no
|
|
1078
|
+
per-vertex work touches the CPU at all; the fillAttribute callback alone
|
|
1079
|
+
is an interpreter call per vertex per frame. A vertex shared across faces
|
|
1080
|
+
shades smooth and a split one per face, so a merged or deformed
|
|
1081
|
+
generator geometry comes out right; a uv seam's copies each see one side
|
|
1082
|
+
and the seam shows (Three's artifact too). `withNormals(
|
|
1083
|
+
geometry, creaseAngle = 60, label?)` is the authoring path and a COPY:
|
|
1084
|
+
faces are matched by position, so a triangle soup, a hand-written face
|
|
1085
|
+
list, a merged result or a geometry with no aNormal channel at all (one
|
|
1086
|
+
is added, float32x3) all work and a seam shades smooth across it; 0 is
|
|
1087
|
+
flat shading, 180 smooths everything, 60 (Three's `toCreasedNormals`
|
|
1088
|
+
and Unity's import smoothing angle) keeps hard edges hard. It is
|
|
1089
|
+
`toNonIndexed` + per-corner normals + `mergeVertices`, so the result is
|
|
1090
|
+
indexed and split only where a crease or a seam needs it (a box stays
|
|
1091
|
+
24 vertices under 90 degrees). Both pieces are exported:
|
|
1092
|
+
`toNonIndexed(geometry, label?)` gives every index its own vertex
|
|
1093
|
+
(identity indices; per-face colors via fillAttribute, flat shading via
|
|
1094
|
+
computeVertexNormals), `mergeVertices(geometry, tolerance = 1e-4,
|
|
1095
|
+
label?)` welds vertices equal in every channel back together, the exact
|
|
1096
|
+
inverse (a normal or uv that differs keeps them apart). Neither takes a
|
|
1097
|
+
morphed geometry: split or weld before `withMorphTargets`.
|
|
1098
|
+
`normalsHelper(geometry, { size?, color? })` draws one line per vertex
|
|
1099
|
+
along its normal (Three's VertexNormalsHelper as a static builder,
|
|
1100
|
+
"colored" layout, red by default, drawn by `unlit({ vertexColors: true
|
|
1101
|
+
})` under the mesh's node), the way to look at any of this;
|
|
1102
|
+
`examples/normals.tsx` shows the three crease angles on one soup, a
|
|
1103
|
+
rippling sphere on the per-frame loop, and the helper refreshed in place
|
|
1104
|
+
with the same loop. Three's `center()` is `transformGeometry(g, {
|
|
1105
|
+
position: [-(b[0] + b[3]) / 2, -(b[1] + b[4]) / 2, -(b[2] + b[5]) / 2]
|
|
1106
|
+
})` over `geometryBounds`; there is no bounding sphere op, the box is
|
|
1107
|
+
what every query and the LOD read.
|
|
1108
|
+
|
|
806
1109
|
### Profile kit
|
|
807
1110
|
|
|
808
1111
|
Profile kit (2D outlines to solids, real texture UVs): a `Profile` is a
|
|
@@ -825,8 +1128,8 @@ Closed loops are NOT supported yet - overlap the ends by a segment to
|
|
|
825
1128
|
fake one. `tube(path, { radius, radialSegments })` is the round-profile
|
|
826
1129
|
shorthand (wire, rope, pipe), and `pathFrames(path)` exports the
|
|
827
1130
|
per-segment frames (tangents, cross-section axes, arc lengths) for
|
|
828
|
-
custom work along a path. `
|
|
829
|
-
like circle); `triangulate(points)` is the ear-clipping core (fan
|
|
1131
|
+
custom work along a path. `polygon(profile, options?)` fills one flat (facing
|
|
1132
|
+
+z, like circle); `triangulate(points)` is the ear-clipping core (fan
|
|
830
1133
|
fallback, never drops a cap), exported for custom flat work. These pick
|
|
831
1134
|
uint16/uint32 indices by vertex count automatically.
|
|
832
1135
|
|
|
@@ -834,15 +1137,20 @@ uint16/uint32 indices by vertex count automatically.
|
|
|
834
1137
|
|
|
835
1138
|
#### unlit
|
|
836
1139
|
|
|
837
|
-
`unlit({ color?, map?, transparent?, cull?, alphaTest?, fog? })` -
|
|
1140
|
+
`unlit({ color?, map?, vertexColors?, transparent?, blend?, cull?, alphaTest?, fog? })` -
|
|
838
1141
|
straight `[r, g, b, a?]` 0..1 sRGB (decoded to linear light, see Color
|
|
839
|
-
below), premultiplied internally; `
|
|
840
|
-
|
|
1142
|
+
below), premultiplied internally; `vertexColors: true` multiplies by the
|
|
1143
|
+
"colored" layout's aColor (the geometry must carry it: withColors, a
|
|
1144
|
+
gridHelper or axesHelper); `blend` the factors of a transparent draw,
|
|
1145
|
+
"alpha" when absent, `"add"` for a glow (any mode but "none" implies
|
|
1146
|
+
`transparent` unless told `transparent: false`, the shaderMaterial
|
|
1147
|
+
rule); `cull` and
|
|
1148
|
+
`alphaTest` as on phong (a mapped cutout casts its cutout); `fog: false`
|
|
841
1149
|
opts out of the scene's fog (all four library materials take it).
|
|
842
1150
|
|
|
843
1151
|
#### sprite
|
|
844
1152
|
|
|
845
|
-
`sprite({ color?, map?, transparent?, billboard? })` - unlit on a quad
|
|
1153
|
+
`sprite({ color?, map?, transparent?, blend?, billboard? })` - unlit on a quad
|
|
846
1154
|
that turns to face the camera IN THE VERTEX STAGE (off the shared
|
|
847
1155
|
uCamRight/uCamUp, or uCamPos for `billboard: "fixed-y"`, which yaws
|
|
848
1156
|
only and stays upright on world y - Godot's BILLBOARD_FIXED_Y, the
|
|
@@ -872,12 +1180,7 @@ with uViewProj - the specular/fresnel view vector is
|
|
|
872
1180
|
uViewProj rows, that carries the clip flip), `uniform mat4
|
|
873
1181
|
uInvViewProj` (the camera's inverse view-projection, shared likewise -
|
|
874
1182
|
a clip position back to world, the world-space ray through a pixel
|
|
875
|
-
without knowing the projection)
|
|
876
|
-
uExposure` / `uToneMapping` (compose `OUTPUT` from `/glsl` and end with
|
|
877
|
-
`fragColor = outputColor(rgb, alpha)` to take the scene's exposure and
|
|
878
|
-
tone mapping and encode like the library materials do; a fragment
|
|
879
|
-
writing fragColor directly writes final encoded pixels) and `uniform
|
|
880
|
-
mat4 uNormal` (the world
|
|
1183
|
+
without knowing the projection) and `uniform mat4 uNormal` (the world
|
|
881
1184
|
inverse-transpose, written beside uModel for this material's meshes;
|
|
882
1185
|
take `mat3(uNormal)` - correct under non-uniform scale, where
|
|
883
1186
|
mat3(uModel) bends normals off the surface). Attributes come from the
|
|
@@ -907,21 +1210,33 @@ pipeline with its own values. `dispose()` lives on the class alone.
|
|
|
907
1210
|
|
|
908
1211
|
#### Instanced materials
|
|
909
1212
|
|
|
910
|
-
`
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
the
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
1213
|
+
`instanceBuffers: [{ attributes: [{ name, format }] }]` (one layout per
|
|
1214
|
+
per-instance buffer, instance-step by definition and tightly packed in
|
|
1215
|
+
list order like a geometry layout; `format` is any of the vertex
|
|
1216
|
+
vocabulary, so a per-instance tint is a `unorm8x4` at four bytes and a
|
|
1217
|
+
frame index a `uint16x2`; explicit strides and offsets stay the
|
|
1218
|
+
engine's, through core's pipeline API) on either shader-material form
|
|
1219
|
+
makes an INSTANCED material: the vertex stage reads the attributes as
|
|
1220
|
+
`in` variables beside the layout's own, and each drawn instance gets one
|
|
1221
|
+
record from each of the mesh's instance buffers. An instance buffer IS a
|
|
1222
|
+
vertex stream stepped per instance: on the mesh it is an `InstanceStream`
|
|
1223
|
+
(the layout, a byte mirror, a mesh-owned GPU buffer), written through
|
|
1224
|
+
`instanceAttribute(mesh, name)` - the accessor `geometryAttribute`
|
|
1225
|
+
returns, record index in, values as the shader sees them, the codec
|
|
1226
|
+
packing the bytes - and published with `updateRecords(mesh, { stream?,
|
|
1227
|
+
first?, count? })`, which is `updateVertices` for records: one coalesced
|
|
1228
|
+
buffer write per dirty stream at the scene's sync, so ten moved records
|
|
1229
|
+
of ten thousand cost ten. Its meshes come from `createInstancedMesh`
|
|
1230
|
+
(the first buffer is the core-written matrix, the rest are streams; the
|
|
1231
|
+
second is the STYLE record, below) or `createRecordMesh` (every buffer a
|
|
1232
|
+
stream); a `createMesh` mesh is rejected at add(). `instanceStyle: [..]`
|
|
1233
|
+
is the style record a fresh instance starts with, one value per
|
|
1234
|
+
component. The stock materials do the same with one flag:
|
|
1235
|
+
`lit/standard/unlit({ instanced: true })` places by the instance matrix
|
|
1236
|
+
(the shadow pass too, so the fleet casts), and `{ instanceColors: true
|
|
1237
|
+
}` adds a per-instance premultiplied `[r, g, b, a]` style record
|
|
1238
|
+
starting white, stored as `float16x4` (eight bytes, no banding in the
|
|
1239
|
+
darks) - Unity's per-material instancing switch with Three's setColorAt.
|
|
925
1240
|
|
|
926
1241
|
### Instancing
|
|
927
1242
|
|
|
@@ -953,32 +1268,36 @@ matrix RELATIVE to the mesh the spatial core writes into the record
|
|
|
953
1268
|
buffer (`bindMatrixRecord` anchored on the mesh node: one coalesced
|
|
954
1269
|
buffer write per flush however many instances moved, so native
|
|
955
1270
|
transitions and clip players move instances with zero per-frame JS).
|
|
956
|
-
The material declares `INSTANCE_MATRIX_ATTRIBUTES`
|
|
957
|
-
columns, 16 floats; anything else throws at creation)
|
|
1271
|
+
The material declares `INSTANCE_MATRIX_ATTRIBUTES` as its first instance
|
|
1272
|
+
buffer (four vec4 columns, 16 floats; anything else throws at creation)
|
|
1273
|
+
and its vertex
|
|
958
1274
|
stage splices `INSTANCE_MATRIX` from `@solidrt/3d/glsl`: `uModel *
|
|
959
1275
|
instanceMatrix() * vec4(aPos, 1.0)`, normals through
|
|
960
1276
|
`instanceNormalMatrix()` (Three's derivation, exact for any rotation and
|
|
961
1277
|
scale at no per-instance data; a sheared hierarchy - a non-uniformly
|
|
962
1278
|
scaled group above a rotated instance - takes
|
|
963
1279
|
`transpose(inverse(mat3(instanceMatrix())))` instead) - or is a stock
|
|
964
|
-
material with `instanced`.
|
|
965
|
-
|
|
966
|
-
(`MeshInstances.
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1280
|
+
material with `instanced`. A second instance buffer, when the material
|
|
1281
|
+
declares one, gives every instance a STYLE record beside its matrix
|
|
1282
|
+
(`MeshInstances.streams[0]`): an app-owned stream in the material's
|
|
1283
|
+
layout, written per instance with `setInstanceStyle(instance, values)`
|
|
1284
|
+
(one value per attribute component, encoded by the codecs) or by name
|
|
1285
|
+
through `instanceAttribute` + `updateRecords`, into a JS mirror and
|
|
1286
|
+
published as ONE coalesced buffer write per stream at the scene's sync
|
|
1287
|
+
(a frame-rate path, like setTransform); a fresh or recycled slot starts
|
|
1288
|
+
from the material's `instanceStyle` (white under `instanceColors`, zeros
|
|
1289
|
+
otherwise); a third buffer is a further stream, reached by name only;
|
|
1290
|
+
under `alphaTest` the cutout shadow
|
|
972
1291
|
multiplies by the same color, so an instance faded below the cutoff
|
|
973
1292
|
casts nothing, like its pixels (Godot's alpha scissor holds in its
|
|
974
|
-
shadow pass too). The core's pose
|
|
1293
|
+
shadow pass too). The core's pose buffer and the app's style buffer are
|
|
975
1294
|
@solidrt/2d's split one dimension up. Slots are fixed
|
|
976
|
-
for an instance's life and recycle on `
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
and past `capacity` (default 64)
|
|
980
|
-
with the live records retargeted in one core call
|
|
981
|
-
republished. `parent` may be a group inside the mesh's subtree (a squad
|
|
1295
|
+
for an instance's life and recycle on `destroy` (at the free: an
|
|
1296
|
+
instance cannot exist outside its mesh, so the generic add/remove throw
|
|
1297
|
+
on one), the drawn count is the slot high-water mark,
|
|
1298
|
+
and past `capacity` (default 64) every buffer doubles into a
|
|
1299
|
+
replacement, with the live matrix records retargeted in one core call
|
|
1300
|
+
and each stream's mirror republished. `parent` may be a group inside the mesh's subtree (a squad
|
|
982
1301
|
in a fleet): the record stays mesh-relative through it. Picking is per
|
|
983
1302
|
instance: every instance node carries the geometry's bounds and
|
|
984
1303
|
triangle shape, so `Hit`, `Overlap` and `Impact` name the `instance` and
|
|
@@ -1004,17 +1323,23 @@ or in five figures, belong to the function face.
|
|
|
1004
1323
|
#### createRecordMesh
|
|
1005
1324
|
|
|
1006
1325
|
`createRecordMesh(geometry, material, records, count?, { bounds?,
|
|
1007
|
-
label? })` is the raw form: `records` is the
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
records
|
|
1326
|
+
label? })` is the raw form: `records` is the per-instance data laid out
|
|
1327
|
+
in the material's first instance layout - a Float32Array over an
|
|
1328
|
+
all-float layout, otherwise bytes built with `vertexView(layout, buffer)`
|
|
1329
|
+
and `attributeAccess` like a geometry stream (a length that is not whole
|
|
1330
|
+
records throws) - copied into the first stream's mirror, whose capacity
|
|
1331
|
+
starts at the records given; every further instance buffer the material
|
|
1332
|
+
declares is a zeroed stream of the same capacity, reached by name
|
|
1333
|
+
through `instanceAttribute`. `count` picks how many draw (default all).
|
|
1012
1334
|
`setRecords(mesh, records, count?)` rewrites from the start (count
|
|
1013
1335
|
defaults to the records written; more than capacity GROWS: capacity
|
|
1014
|
-
doubles into
|
|
1015
|
-
`setDrawBuffers`, the old
|
|
1016
|
-
|
|
1017
|
-
|
|
1336
|
+
doubles into replacement buffers, the entry is re-pointed via
|
|
1337
|
+
`setDrawBuffers`, the old ones are freed), `instanceAttribute` +
|
|
1338
|
+
`updateRecords(mesh, { first, count })` rewrites a few (the range is
|
|
1339
|
+
against capacity, so write ahead and dial after), and
|
|
1340
|
+
`setRecordCount(mesh, n)` is the population dial (clamped to capacity;
|
|
1341
|
+
frame-rate-safe). Records are opaque data (position/yaw/tint/whatever
|
|
1342
|
+
your shader reads), so the
|
|
1018
1343
|
library cannot know where they place instances: a record mesh has NO
|
|
1019
1344
|
picking leaf unless you pass `bounds` (local, covering the population) -
|
|
1020
1345
|
then it picks and transparent-sorts conservatively as one box. The
|
|
@@ -1050,11 +1375,10 @@ no separate resize plumbing. Two forms:
|
|
|
1050
1375
|
written through `scene.setParams` (an app clock for an animated sky).
|
|
1051
1376
|
Godot's sky shader and Unity's skybox material are the same idea; the
|
|
1052
1377
|
radiance bake for environment lighting will consume this same source
|
|
1053
|
-
later, so a procedural sky written here lights the scene then.
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
encoded pixels.
|
|
1378
|
+
later, so a procedural sky written here lights the scene then. A sky
|
|
1379
|
+
writes LINEAR light to fragColor (the skybox form does) and the
|
|
1380
|
+
scene's resolve exposes, tone maps and encodes it with everything
|
|
1381
|
+
else.
|
|
1058
1382
|
- A skybox `{ cube, intensity?, rotation? }` (SkyboxOptions): a cube
|
|
1059
1383
|
map from createCubeTexture sampled along the same ray - Three's
|
|
1060
1384
|
`scene.background = cubeTexture` with `backgroundIntensity` and
|
|
@@ -1091,7 +1415,7 @@ prop - the cube map every `standard` material is lit by (always: the
|
|
|
1091
1415
|
split sum `envRadiance` at its roughness times PBR's `envBrdf` for the
|
|
1092
1416
|
specular, `envIrradiance` - the fully rough sample along the normal, as
|
|
1093
1417
|
Three's getIBLIrradiance and Godot read it - added to the hemisphere
|
|
1094
|
-
for the diffuse) and every `
|
|
1418
|
+
for the diffuse) and every `phong({ reflectivity })` material mirrors,
|
|
1095
1419
|
typically the skybox's own cube turned with it. The cube to use is a
|
|
1096
1420
|
BAKED one: `bunx srt tool 3d/environment sky.hdr -o assets/sky.srte`
|
|
1097
1421
|
turns an equirectangular Radiance .hdr (Poly Haven's are CC0) into the
|
|
@@ -1127,7 +1451,7 @@ tints instead; not offered). The reflection blurs with `shininess`:
|
|
|
1127
1451
|
roughness `sqrt(2 / (shininess + 2))` picks a mip level of the cube
|
|
1128
1452
|
(`textureLod`), so the environment cube wants `mipmap: true`; a cube
|
|
1129
1453
|
without mipmaps stays sharp. `specularMap`'s red scales it like
|
|
1130
|
-
`specular`. For `
|
|
1454
|
+
`specular`. For `phong` it is not an ambient light source: the hemisphere
|
|
1131
1455
|
light stays its only ambient term (`standard` adds envIrradiance; SH9
|
|
1132
1456
|
is a later, additive mode). A declared `reflectivity` with no
|
|
1133
1457
|
environment set contributes nothing (uEnvOn 0), not a black reflection.
|
|
@@ -1141,7 +1465,11 @@ near?, far?, layers?, clearColor?, label? })` renders the scene into a
|
|
|
1141
1465
|
cube map from a point - Three's CubeCamera, Unity's and Godot's
|
|
1142
1466
|
realtime ReflectionProbe - and returns `{ cube, setPosition, update(),
|
|
1143
1467
|
dispose() }`; `cube` is what `environment={{ cube }}` (a chrome ball
|
|
1144
|
-
mirroring its surroundings) or `background` takes.
|
|
1468
|
+
mirroring its surroundings) or `background` takes. `dispose()` destroys
|
|
1469
|
+
the cube, and a scene whose environment or background names it drops
|
|
1470
|
+
that first (setEnvironment(null) / setBackground(null)), so a subtree
|
|
1471
|
+
that owned the environment leaves the scene unlit by it rather than
|
|
1472
|
+
sampling a destroyed texture; set another when it goes. A view under the
|
|
1145
1473
|
hood: one entry list mirrored from the scene, the light set and scene
|
|
1146
1474
|
params fanned out, its own layer mask (keep the mirroring object out of
|
|
1147
1475
|
its own probe with `layers`), the scene's background drawn first on
|
|
@@ -1168,13 +1496,12 @@ costs ~14 ms of GPU there against ~1 ms for the six face passes: fine at
|
|
|
1168
1496
|
probes are the expensive option in every engine (Unity time-slices
|
|
1169
1497
|
them): update a prefiltered probe when the surroundings changed, or
|
|
1170
1498
|
every few frames, and keep `prefilter: false` for a probe that must
|
|
1171
|
-
refresh every frame on a tight budget. The faces hold LINEAR light
|
|
1172
|
-
|
|
1173
|
-
names the scene's fan-out then skips), HALF FLOAT where the device
|
|
1499
|
+
refresh every frame on a tight budget. The faces hold LINEAR light like every buffer (a
|
|
1500
|
+
probe is a buffer without a resolve), HALF FLOAT where the device
|
|
1174
1501
|
renders it (`limits.halfFloatRenderable`, every GLES 3 device here: a
|
|
1175
1502
|
sun's or an emissive's range survives into the reflection, as in every
|
|
1176
1503
|
engine's HDR probe) and 8-bit clamped elsewhere - the renderer decides
|
|
1177
|
-
for
|
|
1504
|
+
for every target (Godot), not a per-probe knob (`bufferFormat()` in
|
|
1178
1505
|
environment.ts) - and the probe never samples its own cube while
|
|
1179
1506
|
rendering (a black environment stands in: one bounce). The face cameras are plain world-up cameras through an
|
|
1180
1507
|
x-mirrored projection (`Camera.mirror`), because a GL cube face is seen
|
|
@@ -1185,9 +1512,8 @@ passes so cull modes keep their meaning. `examples/probe.tsx`.
|
|
|
1185
1512
|
|
|
1186
1513
|
Baked sky: `scene.bakeBackground(size?)` is a reflection probe at the
|
|
1187
1514
|
origin that sees no mesh (layer mask 0): the scene's background - the
|
|
1188
|
-
GLSL sky or the skybox - alone on its six faces, LINEAR (a sky
|
|
1189
|
-
|
|
1190
|
-
bytes as light), prefilters it like a probe and returns the chain's
|
|
1515
|
+
GLSL sky or the skybox - alone on its six faces, LINEAR (a sky fragment writes light, so the bake stores exactly
|
|
1516
|
+
what the backdrop draws), prefilters it like a probe and returns the chain's
|
|
1191
1517
|
TextureId for `environment={{ cube }}`: Godot's sky-to-radiance bake, so
|
|
1192
1518
|
a procedural sky lights the scene with no light nodes, and the runtime
|
|
1193
1519
|
way to turn a hi-res LDR skybox into a properly convolved environment
|
|
@@ -1195,8 +1521,8 @@ way to turn a hi-res LDR skybox into a properly convolved environment
|
|
|
1195
1521
|
alternative). A snapshot at the probe format (half float where
|
|
1196
1522
|
renderable; default size 128): bake again when the sky changes, and
|
|
1197
1523
|
destroy the old cube - it is not auto-freed (an environment normally
|
|
1198
|
-
lives as long as the app).
|
|
1199
|
-
|
|
1524
|
+
lives as long as the app). A buffer is UNCLAMPED (the clamp sits in the
|
|
1525
|
+
resolve's encode), so a sky's sun disc of 40.0
|
|
1200
1526
|
bakes as 40.0 and shows as the broad bright highlight on rough metal
|
|
1201
1527
|
that HDR is for. A sky that reads
|
|
1202
1528
|
`uCamPos` bakes from the origin; scene params (an app clock) are seen as
|
|
@@ -1245,7 +1571,8 @@ material drops the code from the program (Three's `material.fog`) - a
|
|
|
1245
1571
|
sky sphere, a far backdrop. A shaderMaterial opts in by composing `FOG`
|
|
1246
1572
|
from `/glsl` (declares the set; `fog(rgb, alpha, worldPos, camPos)`, or
|
|
1247
1573
|
`fogAdditive(rgb, worldPos, camPos)` for a `blend: "add"` look, which
|
|
1248
|
-
fades toward black instead of the fog color
|
|
1574
|
+
fades toward black instead of the fog color; a stock material with
|
|
1575
|
+
`blend: "add"` composes that form by itself).
|
|
1249
1576
|
The BACKGROUND is not fogged: it is entry zero with no depth or
|
|
1250
1577
|
distance, so match the fog color to `clearColor` or the background's
|
|
1251
1578
|
horizon, and put `far` at or inside the camera's far plane to hide the
|
|
@@ -1258,26 +1585,65 @@ Color: the scene shades in LINEAR light and outputs sRGB, like Three
|
|
|
1258
1585
|
(ColorManagement), Godot and Unity's linear space - no gamma mode. Every
|
|
1259
1586
|
`[r, g, b]` color option is sRGB, what a color picker shows: material
|
|
1260
1587
|
`color` and `emissive`, light `color`, the hemisphere's `sky`/`ground`,
|
|
1261
|
-
fog `color
|
|
1262
|
-
|
|
1263
|
-
to a uniform yourself). Color
|
|
1264
|
-
base color, emissive or sky
|
|
1265
|
-
(createTexture, createCubeTexture;
|
|
1266
|
-
color and emissive images) - a plain
|
|
1267
|
-
|
|
1268
|
-
stay rgba8, and an HDR image
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1588
|
+
fog `color`, the scene's and a view's `clearColor`; the library decodes
|
|
1589
|
+
it when it writes the uniform or the clear (`srgbToLinear`/`linearColor`
|
|
1590
|
+
are exported for values you write straight to a uniform yourself). Color
|
|
1591
|
+
MAPS decode through their format: create a base color, emissive or sky
|
|
1592
|
+
image with `format: "rgba8-srgb"` (createTexture, createCubeTexture;
|
|
1593
|
+
createModel does it for glTF's base color and emissive images) - a plain
|
|
1594
|
+
rgba8 map reads as linear data and renders washed out; data maps
|
|
1595
|
+
(normal, specular, roughness, light maps) stay rgba8, and an HDR image
|
|
1596
|
+
is "rgba16f". Vertex colors are linear, as glTF stores them.
|
|
1597
|
+
|
|
1598
|
+
Every target is a BUFFER plus a RESOLVE, Godot's and Unity's pipeline:
|
|
1599
|
+
the meshes draw into a linear buffer (`scene.hdrTexture` /
|
|
1600
|
+
`view.hdrTexture`: premultiplied linear light, half float where the
|
|
1601
|
+
device renders it - `limits.halfFloatRenderable`, every GLES 3 device
|
|
1602
|
+
here - else rgba8 linear and clamped; the renderer decides, no knob),
|
|
1603
|
+
and one full-screen pass, the resolve, writes the displayed rgba8
|
|
1604
|
+
`scene.texture`: exposure (`scene.setExposure`, default 1), tone mapping
|
|
1605
|
+
(`scene.setToneMapping("none" | "aces" | "agx" | "neutral")` - ACES the
|
|
1606
|
+
filmic curve every engine ships, AgX Blender's (Three, Godot 4.3+),
|
|
1607
|
+
Neutral the Khronos PBR curve (Three, Unity) that keeps product colors
|
|
1608
|
+
where a filmic curve shifts them; the reactive `toneMapping`/`exposure`
|
|
1609
|
+
props), then the sRGB encode with an ordered dither against banding,
|
|
1610
|
+
premultiplied. So a fragment - stock or custom -
|
|
1611
|
+
writes LINEAR light and never encodes (`sceneOutput` is fog only; a
|
|
1612
|
+
material has no output stage), a `transparent` mesh blends in linear
|
|
1613
|
+
space, the clearColor is tone mapped like a background is, and the
|
|
1614
|
+
resolve is an auto target, so a static scene still costs zero passes.
|
|
1615
|
+
The resolve is the post-effect slot: `resolve` on createScene/createView
|
|
1616
|
+
and the `<Scene>`/`<View3d>` prop - a source, `{ source, textures }`,
|
|
1617
|
+
or a function of the buffer id returning either, so a chain over
|
|
1618
|
+
`hdrTexture` is built before the resolve compiles - and
|
|
1619
|
+
`scene.setResolve` live with the same input. The
|
|
1620
|
+
source gets the shader-target contract (vUV, iResolution, fragColor),
|
|
1621
|
+
`uniform sampler2D uScene` (the buffer) and the RESOLVE set from `/glsl`
|
|
1622
|
+
declared: end with `fragColor = resolveColor(rgb, alpha)`; any
|
|
1623
|
+
`scene.setParams` name is readable; `DEFAULT_RESOLVE` is the stock one
|
|
1624
|
+
sample. BLOOM is stock: `bloom: { threshold?, intensity?, radius? }` on
|
|
1625
|
+
createScene, `scene.setBloom(opts | null)`, the reactive `bloom` prop -
|
|
1626
|
+
radiance above `threshold` (default 1, what a fully lit white surface
|
|
1627
|
+
reaches) blurred over `radius` rounds (default 2) of a separable blur
|
|
1628
|
+
at a quarter of the target's size and added back at `intensity`
|
|
1629
|
+
(default 0.5): Godot's glow, Unity's Bloom, Three's UnrealBloomPass. A
|
|
1630
|
+
chain of small auto passes per resolving target, re-rendered when the
|
|
1631
|
+
buffer is; views follow the scene's bloom unless they carry a `bloom`
|
|
1632
|
+
of their own (null = off, a clean minimap), the fog model. The glow is
|
|
1633
|
+
light: over a transparent backdrop (a view with no clearColor over UI)
|
|
1634
|
+
it still shows, its alpha its own brightness, and an opaque pixel is the
|
|
1635
|
+
plain sum. The default resolve composes it; a custom resolve does by declaring `uniform
|
|
1636
|
+
sampler2D uBloom; uniform float uBloomIntensity;` and adding the term
|
|
1637
|
+
(start from `BLOOM_RESOLVE` in `/glsl`; `examples/bloom.tsx` adds a
|
|
1638
|
+
vignette that way). `output` composes the DISPLAYED texture after the
|
|
1639
|
+
resolve, the place for an LDR effect (`examples/scene-post-effect.tsx`). Cost: one full-screen pass and
|
|
1640
|
+
double the color bandwidth per target, about 0.2 ms per 1080p target on
|
|
1641
|
+
an Intel iGPU (21 full-HD targets at once still ran at 36 fps); a
|
|
1642
|
+
device that cannot afford it renders a smaller buffer and the leaf
|
|
1643
|
+
scales it up. What changed for a scene tuned before linear light:
|
|
1644
|
+
terminators soften, mid-tones brighten, highlights widen - drop ambient
|
|
1645
|
+
rather than lights. `emissiveIntensity` scales the emissive in linear
|
|
1646
|
+
light.
|
|
1281
1647
|
|
|
1282
1648
|
### Lighting GLSL
|
|
1283
1649
|
|
|
@@ -1310,22 +1676,22 @@ three) and `SHADOW_LOOKUP`
|
|
|
1310
1676
|
cast; it walks the light's slots and samples the first map that covers
|
|
1311
1677
|
the point, which is the cascade select, blended into the next map over
|
|
1312
1678
|
the outer `SHADOW_BLEND` of the map). A receiving fragment
|
|
1313
|
-
multiplies light i's term by `lightShadow(i, ...)`, exactly what `
|
|
1679
|
+
multiplies light i's term by `lightShadow(i, ...)`, exactly what `phong`
|
|
1314
1680
|
composes; a non-receiving one composes none of the three and declares no
|
|
1315
1681
|
samplers. Lights, colors and exponents are arguments, so
|
|
1316
|
-
nothing is pinned but the function names; `
|
|
1682
|
+
nothing is pinned but the function names; `phong` is composed from these
|
|
1317
1683
|
same constants - customizing never means leaving the system.
|
|
1318
1684
|
|
|
1319
1685
|
### Custom looks: three tiers
|
|
1320
1686
|
|
|
1321
|
-
A custom look is a citizen of the scene - lit by its lights, shadowed
|
|
1322
|
-
fogged, exposed and tone mapped
|
|
1323
|
-
three tiers, top first:
|
|
1687
|
+
A custom look is a citizen of the scene - lit by its lights, shadowed
|
|
1688
|
+
and fogged like the stock materials, exposed and tone mapped by the same
|
|
1689
|
+
resolve - at one of three tiers, top first:
|
|
1324
1690
|
|
|
1325
1691
|
1. STANDARD FRAGMENT, CUSTOM VERTEX. Any vertex stage that writes the lit
|
|
1326
1692
|
varyings (vWorldPos, vNormal, vUv, plus vColor with `vertexColors`,
|
|
1327
|
-
vUv2 with `lightMap`) pairs with `
|
|
1328
|
-
`standardFragment(options)` (`/glsl`): the exact fragment `
|
|
1693
|
+
vUv2 with `lightMap`) pairs with `phongFragment(options)` /
|
|
1694
|
+
`standardFragment(options)` (`/glsl`): the exact fragment `phong` /
|
|
1329
1695
|
`standard` compile, the same option names and defaults as their
|
|
1330
1696
|
options with the texture options boolean. An instanced or displaced
|
|
1331
1697
|
mesh keeps the stock shading whole (`examples/instanced.tsx`: the
|
|
@@ -1337,7 +1703,7 @@ three tiers, top first:
|
|
|
1337
1703
|
params: { uColor: [1, 1, 1, 1], uSpecular: 0.12, uShininess: 24 } })`
|
|
1338
1704
|
is the white starting point (the stock materials seed exactly this
|
|
1339
1705
|
from their `color` option).
|
|
1340
|
-
2. A SURFACE FUNCTION inside the stock fragment. `
|
|
1706
|
+
2. A SURFACE FUNCTION inside the stock fragment. `phongFragment({ surface,
|
|
1341
1707
|
prelude })`: `prelude` is file scope (uniforms and helpers; a uniform
|
|
1342
1708
|
it declares is an ordinary `instance()` param), `surface` declares
|
|
1343
1709
|
`void surface(inout Surface s)`, called once the program has filled
|
|
@@ -1354,25 +1720,26 @@ three tiers, top first:
|
|
|
1354
1720
|
surface: a point splat discarding outside `gl_PointCoord`'s inscribed
|
|
1355
1721
|
circle and flipping a fitted normal towards the viewer keeps the
|
|
1356
1722
|
scene's whole light model in about fifteen lines, where hand-rolling
|
|
1357
|
-
the light loop (tier 3) would have to match
|
|
1358
|
-
|
|
1723
|
+
the light loop (tier 3) would have to match every light's falloff,
|
|
1724
|
+
cone and shadow by hand.
|
|
1359
1725
|
3. A FRAGMENT OF YOUR OWN over the scene set. Compose `SCENE` (or
|
|
1360
1726
|
`sceneSource({ lights, receiveShadow, env, fog })`, each flag leaving a
|
|
1361
1727
|
declaration out): it declares uCamPos, uHemiSky/uHemiGround, the
|
|
1362
|
-
light list, the shadow set, the environment
|
|
1363
|
-
|
|
1728
|
+
light list, the shadow set, the environment and fog - declare none
|
|
1729
|
+
of them yourself. Build a `Surface` with `surfaceOf(base,
|
|
1364
1730
|
normal)`, set the fields you mean, call `shadeBlinn(s, position)` or
|
|
1365
1731
|
`shadePbr(s, position)` (premultiplied rgb back: hemisphere, every
|
|
1366
1732
|
light with its shadow, the environment term, the emissive), add your
|
|
1367
1733
|
own terms times the alpha, and end with `sceneOutput(rgb, alpha,
|
|
1368
|
-
position)` (fog
|
|
1734
|
+
position)` (fog; the pixel is linear light, the resolve exposes, tone
|
|
1735
|
+
maps and encodes it). A custom LIGHT MODEL
|
|
1369
1736
|
loops `sceneLight(i, position, normal)` to `uLightCount` instead of a
|
|
1370
1737
|
shade function: light i's direction and its color already attenuated,
|
|
1371
1738
|
cone-faded and shadowed (zero when it cannot reach). The stock
|
|
1372
1739
|
materials are built from this same set, so the tiers cannot drift.
|
|
1373
1740
|
The demo `the-third-dimension.tsx` has tier 2 (the ground) and tier 3
|
|
1374
1741
|
(the knot's rim term).
|
|
1375
|
-
`standardFragment(options)` is the same for `standard`:
|
|
1742
|
+
`standardFragment(options)` is the same for `standard`: phong's options
|
|
1376
1743
|
minus `specularMap`/`env` (the environment is always composed) plus
|
|
1377
1744
|
`metalnessMap`/`roughnessMap`, on the same `litVertex(options)`, with
|
|
1378
1745
|
`uMetalness`/`uRoughness` in place of `uSpecular`/`uShininess`.
|
|
@@ -1391,7 +1758,7 @@ instance or every caster move warns about the skipped write.
|
|
|
1391
1758
|
|
|
1392
1759
|
### Lights
|
|
1393
1760
|
|
|
1394
|
-
Lights and `
|
|
1761
|
+
Lights and `phong`: lights are graph NODES, like Three. `createDirectionalLight({
|
|
1395
1762
|
direction?, color?, intensity? })` / `<DirectionalLight>` is parallel light
|
|
1396
1763
|
travelling along `direction` in the node's LOCAL space (default `[0, -1,
|
|
1397
1764
|
0]`, a sun overhead; length ignored), so a parent Group's rotation turns it
|
|
@@ -1413,10 +1780,12 @@ only, same falloff, no cone. `createHemisphereLight({ sky?, ground?, intensity?
|
|
|
1413
1780
|
normal's tilt (fixed to world up, the node's transform is ignored); one per
|
|
1414
1781
|
scene, the last attached wins. Placement goes through setTransform, the
|
|
1415
1782
|
light's own fields through `setLight(light, { ... })` (frame-rate-safe,
|
|
1416
|
-
like setMeshParams). At most `MAX_LIGHTS` (8, exported from `/glsl`)
|
|
1783
|
+
like setMeshParams). At most `MAX_LIGHTS` (8, exported from the root and `/glsl`)
|
|
1417
1784
|
lights per scene, directional, spot and point together (the hemisphere
|
|
1418
|
-
is not in the list) -
|
|
1419
|
-
|
|
1785
|
+
is not in the list) - a ninth is reported at the sync that settles the
|
|
1786
|
+
set, not at add() (a declarative swap's transient overlap is fine),
|
|
1787
|
+
the way a shadow-budget overflow is (see there), and the lights past
|
|
1788
|
+
the cap are not lit; it is a shader-source constant, fixed per app. `uLightDir` and `uLightPos` are core-driven:
|
|
1420
1789
|
each light's slots are spatial-core shared-slot sinks following the
|
|
1421
1790
|
node's world rotation (direction, negated so the shader reads the
|
|
1422
1791
|
vector TOWARD the light) and world position, so a MOVING light costs no
|
|
@@ -1427,7 +1796,7 @@ LIGHT_POINT), `uLightDir[N]`/`uLightPos[N]`/`uLightColor[N]` (intensity
|
|
|
1427
1796
|
folded into the color) and `uLightParams[N]` (cosInner, cosOuter,
|
|
1428
1797
|
distance, decay) - so a custom fragment composing `LIGHT_SLOTS` +
|
|
1429
1798
|
`LIGHT_LOOKUP` from `/glsl` reads the same list through `lightVector(i,
|
|
1430
|
-
worldPos, out l)` (returns the attenuation, 0 = skip the light; `
|
|
1799
|
+
worldPos, out l)` (returns the attenuation, 0 = skip the light; `phong`
|
|
1431
1800
|
is the shape), and a light change costs one write however many meshes.
|
|
1432
1801
|
A custom fragment that declares only the old directional subset
|
|
1433
1802
|
(`uLightCount`/`uLightDir`/`uLightColor`) still works - it just shades
|
|
@@ -1436,15 +1805,14 @@ scenes. Everything starts black: a lit scene with no light shows
|
|
|
1436
1805
|
nothing, on purpose, like Three. `examples/lamps.tsx` is the spot/point
|
|
1437
1806
|
shape (soft vs hard cone, casting spots, an orbiting bulb).
|
|
1438
1807
|
|
|
1439
|
-
###
|
|
1808
|
+
### phong
|
|
1440
1809
|
|
|
1441
|
-
`
|
|
1442
|
-
the
|
|
1810
|
+
`phong(opts)` is the Blinn-Phong material (Three's MeshPhongMaterial, Unity
|
|
1811
|
+
URP's Simple Lit) beside `unlit`: hemisphere ambient plus the light list, Lambert diffuse, Blinn-Phong highlight when
|
|
1443
1812
|
`specular` (0..1 strength) is set with `shininess` (default 30), a
|
|
1444
1813
|
mirror of the scene's environment when `reflectivity` (0..1, the
|
|
1445
1814
|
face-on weight; see Environment above) is set, blurred by the same
|
|
1446
|
-
`shininess`, the same `color`/`map`/`transparent` as unlit,
|
|
1447
|
-
multiply by the colored layout's aColor (so the geometry must carry it),
|
|
1815
|
+
`shininess`, the same `color`/`map`/`transparent`/`blend`/`vertexColors` as unlit,
|
|
1448
1816
|
`triplanar: n` to sample `map` by world position at `n` repeats per
|
|
1449
1817
|
world unit, blended across the three axis planes by the normal, and
|
|
1450
1818
|
`alphaTest: t` for a cutout (a fragment whose final alpha is below `t`
|
|
@@ -1520,7 +1888,7 @@ split sum, `envRadiance` at the roughness over the cube's mip chain
|
|
|
1520
1888
|
times the analytic `envBrdf` for the specular, `envIrradiance` on the
|
|
1521
1889
|
diffuse beside the hemisphere - with no `reflectivity` switch: the
|
|
1522
1890
|
environment is intrinsic to the model, and a baked environment with no
|
|
1523
|
-
lights at all lights a scene (`examples/environment.tsx`). Light intensities read as
|
|
1891
|
+
lights at all lights a scene (`examples/environment.tsx`). Light intensities read as phong's
|
|
1524
1892
|
(1 lights a white matte surface to 1; Godot's and Unity's convention -
|
|
1525
1893
|
a Three scene's intensities divide by pi). Without an environment a
|
|
1526
1894
|
metal shows only its highlights: no diffuse, nothing to reflect (Three
|
|
@@ -1554,7 +1922,7 @@ pre-order - name, parent index, local TRS; matrix-form nodes are
|
|
|
1554
1922
|
TRS-decomposed, shear dropped; nodes that carry no part, joint or
|
|
1555
1923
|
animation target anywhere - cameras, lights, unused empties - are
|
|
1556
1924
|
pruned), `parts` (one per mesh primitive, its node's NAME kept, `node`
|
|
1557
|
-
index, vertices in the
|
|
1925
|
+
index, vertices in the base layout LOCAL to the node - except
|
|
1558
1926
|
skinned parts: "skinned" layout, model-space bind pose, `skin` index),
|
|
1559
1927
|
`skins` (joint node indices + inverse binds), `clips` (the animations
|
|
1560
1928
|
as baked channel buffers: node/path/interpolation, times, values),
|
|
@@ -1564,7 +1932,9 @@ index into `images`, `doubleSided`, `transparent` = alphaMode BLEND,
|
|
|
1564
1932
|
normal and emissive slots, `metalness`/`roughness` factors and the
|
|
1565
1933
|
packed `metalnessRoughnessMap` - standard's inputs), `images`
|
|
1566
1934
|
(the encoded PNG/JPEG bytes, undecoded) and `bounds` (world-space rest
|
|
1567
|
-
pose
|
|
1935
|
+
pose: a part's box through its node, a skinned part's per-joint boxes
|
|
1936
|
+
through the joints' rest transforms - where the skin places it, armature
|
|
1937
|
+
scale included; conservative under rotation). External
|
|
1568
1938
|
files come through `resolve(uri)` (uri as written, still
|
|
1569
1939
|
percent-encoded; `gltfExternalUris(bytes)` lists them so an async
|
|
1570
1940
|
caller can read them first) - for a .gltf AND for a .glb, which is
|
|
@@ -1581,13 +1951,13 @@ first error a real file hits.
|
|
|
1581
1951
|
|
|
1582
1952
|
#### createModel
|
|
1583
1953
|
|
|
1584
|
-
`createModel(data, { material?, label? })` - uploads the images (repeat
|
|
1954
|
+
`createModel(data, { material?, label?, autoFree? })` - uploads the images (repeat
|
|
1585
1955
|
wrap, mipmapped, 4x anisotropic), makes one material per glTF material (default `standard`
|
|
1586
1956
|
with the file's color, maps, normal scale, metalness/roughness and
|
|
1587
1957
|
packed map, emissive and transparency - the glTF material model, so a
|
|
1588
1958
|
scene showing a model wants an `environment` (a glTF metal in a scene
|
|
1589
1959
|
with none renders near black); `material: (m, maps, skinned) =>
|
|
1590
|
-
|
|
1960
|
+
phong({ color: m.color, map: maps.map ?? undefined, skinned })` for the
|
|
1591
1961
|
Blinn-Phong look, or any other material; it is called once per material and shared), the node table as nested
|
|
1592
1962
|
Groups with the file's local TRS, and one mesh per part under its node,
|
|
1593
1963
|
all inside the returned `Model` (a Group): `add(scene.root, model)`,
|
|
@@ -1604,7 +1974,13 @@ them, nothing else frees them.
|
|
|
1604
1974
|
#### loadGltf and loadModel
|
|
1605
1975
|
|
|
1606
1976
|
`loadGltf(path)` / `loadModel(path)` - read from `assets/` with flux:fs
|
|
1607
|
-
and build.
|
|
1977
|
+
and build. Like every resource creator, a model frees with the owning
|
|
1978
|
+
reactive scope (`dispose()` at its cleanup; `autoFree: false` opts out):
|
|
1979
|
+
for the loaders that scope is the caller's at the call, captured before
|
|
1980
|
+
the await, so a model loaded inside a memo goes with the memo's rerun
|
|
1981
|
+
and one loaded in a component with its unmount. A load that settles
|
|
1982
|
+
after its scope is gone (unmounted mid-load) is freed on arrival and
|
|
1983
|
+
resolves already disposed - nothing owns it any more. `loadModel` reads the baked `.srtm` written by `srt tool
|
|
1608
1984
|
3d/model <in.gltf|glb> -o assets/<name>.srtm`: the same parse run once
|
|
1609
1985
|
under bun, stored in the GPU layout, so loading is views onto the file's
|
|
1610
1986
|
bytes plus the image decodes. Numbers from a 32k-vertex, 6-texture model
|
|
@@ -1688,14 +2064,23 @@ Applied: `doubleSided` (the default material draws it with `cull:
|
|
|
1688
2064
|
factor (a zero factor skips the map too - glTF's product rule, emission
|
|
1689
2065
|
off), `pbrMetallicRoughness` factors as `m.metalness`/`m.roughness` and
|
|
1690
2066
|
its packed texture as BOTH `maps.metalnessMap` and `maps.roughnessMap`
|
|
1691
|
-
(standard's channel-select options; the default `
|
|
1692
|
-
The `material(m, maps)` callback receives every
|
|
1693
|
-
by
|
|
1694
|
-
`maps.
|
|
1695
|
-
`data.materials` is in file order, so the calls
|
|
2067
|
+
(standard's channel-select options; the default `phong` ignores them).
|
|
2068
|
+
The `material(m, maps, skinned, vertexColors)` callback receives every
|
|
2069
|
+
uploaded texture by phong()/standard() option name (`maps.map`/
|
|
2070
|
+
`maps.normalMap`/`maps.emissiveMap`/`maps.metalnessMap`/
|
|
2071
|
+
`maps.roughnessMap`); `data.materials` is in file order, so the calls
|
|
2072
|
+
arrive in file order. A primitive's `COLOR_0` lands in its geometry's
|
|
2073
|
+
aColor (the "colored" layout, or the skinned list plus aColor for a
|
|
2074
|
+
rigged one; linear and premultiplied as glTF stores it) and the default
|
|
2075
|
+
material takes `vertexColors: true` for such parts - the callback's
|
|
2076
|
+
fourth argument says so, and a material shared by painted and unpainted
|
|
2077
|
+
parts is made once per variant, like the skinned split.
|
|
1696
2078
|
A `.srtm` baked before the material records carried the PBR fields
|
|
1697
|
-
(file version 3)
|
|
1698
|
-
|
|
2079
|
+
(file version 3), or before the vertex formats took the WebGPU spelling
|
|
2080
|
+
and byte counts (file version 5), is rejected by loadModel - re-bake
|
|
2081
|
+
with `srt tool 3d/model`. A baked part keeps a quantized export's
|
|
2082
|
+
bytes: u8 colors, u8/u16 joints and normalized weights, u16 uvs land in
|
|
2083
|
+
their own formats, positions and normals as floats.
|
|
1699
2084
|
|
|
1700
2085
|
### Animation
|
|
1701
2086
|
|
|
@@ -1779,14 +2164,66 @@ computed-once texture. `sampleChannel` (pure, from the root) stays the
|
|
|
1779
2164
|
JS sampling core for checks and custom drivers;
|
|
1780
2165
|
okf/done/animation-core.md records the design.
|
|
1781
2166
|
|
|
2167
|
+
### Morph targets
|
|
2168
|
+
|
|
2169
|
+
Morph targets (blend shapes): a geometry carries named per-vertex
|
|
2170
|
+
deltas - `withMorphTargets(geometry, [{ name, position, normal? }])`
|
|
2171
|
+
for a hand-built shape, the loader's primitive targets for a glTF (its
|
|
2172
|
+
`extras.targetNames`, else `target<i>`; sparse accessors read) - packed
|
|
2173
|
+
SPARSE BY VERTEX on `geometry.morphs` (a header texel per vertex, then
|
|
2174
|
+
only the entries of the targets that move it; memory follows the
|
|
2175
|
+
authored deltas, never vertices x targets) and uploaded once per
|
|
2176
|
+
geometry as one rgba32f texture. A `morph: true` material (`phong`,
|
|
2177
|
+
`unlit`, `standard`; a custom class splices MORPH_DECLS and MORPH_APPLY
|
|
2178
|
+
from `@solidrt/3d/glsl` before its skin math) walks each vertex's
|
|
2179
|
+
entries in the vertex stage, before any skinning; a mesh under it
|
|
2180
|
+
whose geometry carries no targets is rejected at add(), and a plain
|
|
2181
|
+
material over morphed geometry draws the base shape. WEIGHTS belong to
|
|
2182
|
+
a NODE, glTF's `node.weights` model: a standalone mesh owns its own; a
|
|
2183
|
+
model's parts follow their glTF node (`model.nodes`), so a face split
|
|
2184
|
+
into skin, eyes and teeth parts takes one write. `setMorphWeights(node,
|
|
2185
|
+
{ smile: 0.7 })` writes by name (the others untouched), an array writes
|
|
2186
|
+
every target in order, the `<Mesh morphWeights>` prop is the
|
|
2187
|
+
declarative form; `getMorphWeights(node)` reads them back (from the
|
|
2188
|
+
core while in a scene), `getMorphNames(node)` lists the targets. The
|
|
2189
|
+
write lands in the spatial core's register (a row of a small weights
|
|
2190
|
+
texture, published at the flush like a skin palette - Three's
|
|
2191
|
+
morphTargetInfluences, Godot's set_blend_shape_value); a file's
|
|
2192
|
+
`mesh.weights` seed it. Weights ANIMATE two ways, both native: a glTF
|
|
2193
|
+
`weights` animation channel plays through the mixer like a TRS
|
|
2194
|
+
channel (into the owning node's register, crossfading across players
|
|
2195
|
+
by the same weighted average), and a write is a TARGET like a
|
|
2196
|
+
transform write - `setTransition(node, { weights: { duration: 300 } })`
|
|
2197
|
+
makes every setMorphWeights animate (springs by default; `from`/`exit`
|
|
2198
|
+
endpoints are one number per target, `delay` and `stagger` apply), and
|
|
2199
|
+
`setMorphWeights(node, { smile: 1 }, { duration: 300, bounce: 0.2 })`
|
|
2200
|
+
animates that one write on its own motion, declaration or not; a
|
|
2201
|
+
settle calls `onTransitionEnd` with component "weights". A playing
|
|
2202
|
+
weights track and a JS write are both producers of the register: the
|
|
2203
|
+
players advance before the frame's JS, so a write from onFrame wins
|
|
2204
|
+
that frame. `getMorphWeights` reads the core, so it shows a playing
|
|
2205
|
+
track and a mid-flight transition; the by-name partial write merges
|
|
2206
|
+
into the last JS-WRITTEN set, not the core's - write every target
|
|
2207
|
+
while a clip plays. An INSTANCED mesh morphs per instance: the
|
|
2208
|
+
population owns one weights texture with a row per record slot, each
|
|
2209
|
+
`<Instance>` (or addInstance node) owns a register published into its
|
|
2210
|
+
slot's row, and the shader reads the row by `gl_InstanceID` - so
|
|
2211
|
+
`setMorphWeights(instance, { smile: 1 })`, the `<Instance morphWeights>`
|
|
2212
|
+
prop, a `transition.weights` spring and a clip's weights track all work
|
|
2213
|
+
per copy, the shadow variants included, with no per-record data
|
|
2214
|
+
(Three's InstancedMesh morphs). Skinned plus morphed plus instanced
|
|
2215
|
+
composes. A custom class splicing MORPH_DECLS reads the row as
|
|
2216
|
+
`uMorphRow + gl_InstanceID`, which the stock decls already do. A record
|
|
2217
|
+
mesh (JS-written records, no nodes) cannot morph: rejected at add().
|
|
2218
|
+
|
|
1782
2219
|
### Not in the subset
|
|
1783
2220
|
|
|
1784
|
-
Not in the subset, dropped:
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
2221
|
+
Not in the subset, dropped: tangents and further UV sets; samplers are
|
|
2222
|
+
ignored (every texture repeats); additive blending draws as base color.
|
|
2223
|
+
The follow-ups are filed in okf/backlog/3d-model-loader.md. The `.srtm`
|
|
2224
|
+
container is VERSION 8 (node table in the header, node-local vertices,
|
|
2225
|
+
skins, clips, packed morph targets); older bakes are rejected - re-bake
|
|
2226
|
+
with `srt tool 3d/model`.
|
|
1790
2227
|
|
|
1791
2228
|
## Traps
|
|
1792
2229
|
|
|
@@ -1809,6 +2246,16 @@ older bakes are rejected - re-bake with `srt tool 3d/model`.
|
|
|
1809
2246
|
MODEL moves both. Shadows are the exception: the shadow variants
|
|
1810
2247
|
(depth and cutout) skin by the same uBones palette, so a caster casts
|
|
1811
2248
|
its pose.
|
|
2249
|
+
- Morphing is the same kind of effect: picking and the transparent sort
|
|
2250
|
+
see the base shape, the shadow variants morph. The one thing the
|
|
2251
|
+
retained side knows is the box: `geometryBounds` (and a model's
|
|
2252
|
+
bounds) grow by the targets' extent, so a morphed shape is never
|
|
2253
|
+
culled while its weights stay in 0..1 - a weight past 1 can leave it.
|
|
2254
|
+
Normal deltas blend linearly like positions (Three's, Godot's and
|
|
2255
|
+
glTF's rule): exact at a target, an approximation between two, so a
|
|
2256
|
+
half-blend of EXTREME targets can show a ragged self-shadow
|
|
2257
|
+
terminator where the lit and shadowed halves disagree; a modeller's
|
|
2258
|
+
blend shapes are far too small for it to show.
|
|
1812
2259
|
|
|
1813
2260
|
### Materials and color
|
|
1814
2261
|
|
|
@@ -1818,8 +2265,8 @@ older bakes are rejected - re-bake with `srt tool 3d/model`.
|
|
|
1818
2265
|
ambient probe, this does not). glTF's default metallic factor is 1,
|
|
1819
2266
|
so an untextured asset is all metal, and createModel's default is
|
|
1820
2267
|
`standard`: give a model scene an `environment` (loadEnvironment's
|
|
1821
|
-
baked .srte, or the skybox's cube), or pass `
|
|
1822
|
-
- Light intensities are the same numbers for `
|
|
2268
|
+
baked .srte, or the skybox's cube), or pass `phong` as the material.
|
|
2269
|
+
- Light intensities are the same numbers for `phong` and `standard`: 1
|
|
1823
2270
|
lights a white matte surface to 1 face-on. A Three scene's intensities
|
|
1824
2271
|
are a factor pi larger for the same look; divide when porting.
|
|
1825
2272
|
- Transparency is an EXPLICIT material flag, Three's rule: `unlit({ color:
|
|
@@ -1833,8 +2280,8 @@ older bakes are rejected - re-bake with `srt tool 3d/model`.
|
|
|
1833
2280
|
(or `shaderMaterial({ transparent: true })`) builds the pipeline with
|
|
1834
2281
|
`blend: "alpha"` and `depthWrite: false` (depth test stays on, so it hides
|
|
1835
2282
|
behind opaques without occluding other translucents). The one inference:
|
|
1836
|
-
a `shaderMaterial` with any `blend` but "none" is transparent
|
|
1837
|
-
`transparent: false` - every blended draw belongs after the opaques, and
|
|
2283
|
+
a stock material or `shaderMaterial` with any `blend` but "none" is transparent
|
|
2284
|
+
unless told `transparent: false` - every blended draw belongs after the opaques, and
|
|
1838
2285
|
back-to-front is harmless for add/multiply. The scene owns the
|
|
1839
2286
|
order: background, opaque meshes by `renderOrder` then add order,
|
|
1840
2287
|
transparent meshes by `renderOrder` then back-to-front by the CENTER of
|
|
@@ -1857,9 +2304,11 @@ older bakes are rejected - re-bake with `srt tool 3d/model`.
|
|
|
1857
2304
|
custom fragment (no material option yet); a draw target you create
|
|
1858
2305
|
yourself can be `format: "rgba8-srgb"` and then decodes on sample (it
|
|
1859
2306
|
is sampler-only: no display, readback or copy).
|
|
1860
|
-
- Reading
|
|
1861
|
-
|
|
1862
|
-
|
|
2307
|
+
- Reading `scene.texture` back (readTexture, a snapshot) gives ENCODED
|
|
2308
|
+
pixels: an expected linear value v shows as `linearToSrgb(v) * 255` -
|
|
2309
|
+
intensity 0.5 reads 188, not 128. The buffer, `scene.hdrTexture`, is
|
|
2310
|
+
sampler-only (no readback, copy or display): read radiance through a
|
|
2311
|
+
`createShaderTexture` pass over it, scaled into 0..1.
|
|
1863
2312
|
|
|
1864
2313
|
### Environment and background
|
|
1865
2314
|
|
|
@@ -1886,7 +2335,7 @@ older bakes are rejected - re-bake with `srt tool 3d/model`.
|
|
|
1886
2335
|
- The environment binds through the light rewrite's map set (uEnv
|
|
1887
2336
|
beside uShadowAtlas on every receiving target, new views included) and
|
|
1888
2337
|
directly on setEnvironment; the placeholder cube is app-lifetime like
|
|
1889
|
-
the shadow placeholder. A `
|
|
2338
|
+
the shadow placeholder. A `phong` without `reflectivity` declares no
|
|
1890
2339
|
environment sampler - the flag is part of the class key.
|
|
1891
2340
|
|
|
1892
2341
|
### Coordinates and rotation
|
|
@@ -1989,26 +2438,42 @@ older bakes are rejected - re-bake with `srt tool 3d/model`.
|
|
|
1989
2438
|
flashes at the world origin for a frame.
|
|
1990
2439
|
- Instancing pairs strictly at add(), like layout: an instanced material
|
|
1991
2440
|
needs a createInstancedMesh or createRecordMesh mesh and vice versa,
|
|
1992
|
-
and the
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2441
|
+
and every instance buffer the material declares must be a layout the
|
|
2442
|
+
mesh carries - the matrix (INSTANCE_MATRIX_ATTRIBUTES, the first
|
|
2443
|
+
buffer of an instanced mesh) or a stream with the same attribute names
|
|
2444
|
+
AND formats (a byte-equal layout in other formats decodes differently)
|
|
2445
|
+
- each mismatch throws there, at creation for the mesh's own material
|
|
2446
|
+
and at add() for a swapped one. The instance buffers are MESH-owned
|
|
2447
|
+
(unlike shared geometry buffers): `disposeInstances` is their one
|
|
2448
|
+
free, and the mesh cannot be re-added afterwards. Capacity grows by
|
|
2449
|
+
REPLACEMENT, never resize: an addInstance or setRecords past capacity
|
|
2450
|
+
doubles into new buffers and swaps them in (an instanced mesh's live
|
|
2451
|
+
matrix records move in one core `retargetRecords`, every stream's
|
|
2452
|
+
mirror republishes) - amortized like a dynamic array, same policy as
|
|
2453
|
+
@solidrt/2d; size `capacity` or the initial records to skip the
|
|
2454
|
+
copies. A mesh's stream layouts come from its material AT CREATION: a
|
|
2455
|
+
later setMaterial to a class with another layout throws at the
|
|
2006
2456
|
rebuild.
|
|
2007
|
-
-
|
|
2008
|
-
`
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2457
|
+
- Record writes are mirrored, not immediate: `setInstanceStyle`,
|
|
2458
|
+
`setRecords` and an `instanceAttribute` write land in the stream's
|
|
2459
|
+
`data` and the scene's sync publishes the `dirty` range (`updateRecords`
|
|
2460
|
+
marks it for accessor writes) - so a write on a mesh outside any scene
|
|
2461
|
+
shows once it is added (the dirty range waits), and reading the GPU
|
|
2462
|
+
buffer back mid-frame can lag the mirror by one sync. Growth replaces
|
|
2463
|
+
`data`: hold the accessor, not the view.
|
|
2464
|
+
- The sugar is for FEW records, the mirror is for MANY. `setInstanceStyle`
|
|
2465
|
+
over an all-float layout is indexed stores; over a packed layout (the
|
|
2466
|
+
stock `float16x4` color included) it is a codec call per component,
|
|
2467
|
+
and so is every accessor write. Measured at ten thousand instances
|
|
2468
|
+
restyled every frame on a laptop under QuickJS: about 11 ms of JS per
|
|
2469
|
+
frame through setInstanceStyle on a float layout, 19 ms on the half
|
|
2470
|
+
color, 9 ms through the accessor, 5 ms through the mirror - and that
|
|
2471
|
+
5 ms is the app's own per-instance tint math, the write itself is
|
|
2472
|
+
free. A population restyled per frame writes the mirror in bulk:
|
|
2473
|
+
a typed view over `stream.data` (a Float32Array as handed out, a
|
|
2474
|
+
`Float16Array` over the stock color, indexed by slot times components)
|
|
2475
|
+
filled in a plain loop, then ONE `updateRecords(mesh, { first, count
|
|
2476
|
+
})` - zero per-component calls, the same shape as `setRecords`.
|
|
2012
2477
|
- Instanced casters: `castShadow` on a populated mesh needs a depth pass
|
|
2013
2478
|
with the instance placement in it - the stock materials' `instanced`
|
|
2014
2479
|
carries one, a custom class needs `shadowVertex` (see shadows below);
|
|
@@ -2023,8 +2488,8 @@ older bakes are rejected - re-bake with `srt tool 3d/model`.
|
|
|
2023
2488
|
- An instance node's own fields hold its LOCAL pose (relative to its
|
|
2024
2489
|
parent, as for any node); the record the core writes is its pose
|
|
2025
2490
|
relative to the MESH, composed through any group between them. A
|
|
2026
|
-
|
|
2027
|
-
|
|
2491
|
+
destroyed instance takes its children with it, like any destroyed
|
|
2492
|
+
node. `disposeInstances` on an instanced mesh flushes the
|
|
2028
2493
|
core before freeing the buffer, so the destroyed instances' hiding
|
|
2029
2494
|
writes land in a live buffer; keep that order if you ever free a
|
|
2030
2495
|
record buffer by hand.
|
|
@@ -2076,7 +2541,10 @@ older bakes are rejected - re-bake with `srt tool 3d/model`.
|
|
|
2076
2541
|
`overrideMaterial` is validated against every mesh's layout (at
|
|
2077
2542
|
createView for the meshes present, at add() for later ones) exactly like
|
|
2078
2543
|
a mesh's own material, so an override reading `aColor` throws for a
|
|
2079
|
-
|
|
2544
|
+
base-layout mesh. An instanced mesh the override cannot place (it
|
|
2545
|
+
declares no `instanceBuffers`) is skipped, which the view's output
|
|
2546
|
+
cannot show, so the scene warns once per view naming it and the count
|
|
2547
|
+
(over the settled set, at the flush after the attach). Views are disposed by the scene; `view.dispose()`
|
|
2080
2548
|
only for dropping one early.
|
|
2081
2549
|
- SCENE-WIDE uniforms go through that same shared channel via
|
|
2082
2550
|
`scene.setParams({ uTime })`, and this is the single highest-leverage
|
|
@@ -2088,9 +2556,9 @@ older bakes are rejected - re-bake with `srt tool 3d/model`.
|
|
|
2088
2556
|
however many meshes read it, with the motion itself in vertex shaders
|
|
2089
2557
|
off that one clock. `params`/`setMeshParams` is the PER-MESH answer and
|
|
2090
2558
|
is O(meshes) per frame; reach for it only when the value genuinely
|
|
2091
|
-
differs per mesh. (`scene.texture`
|
|
2092
|
-
`setTargetParams(scene.texture, ...)`
|
|
2093
|
-
the
|
|
2559
|
+
differs per mesh. (`scene.texture` is the RESOLVE's id, not the buffer the meshes
|
|
2560
|
+
draw into, so `setTargetParams(scene.texture, ...)` reaches the
|
|
2561
|
+
resolve alone - setParams is the only spelling.)
|
|
2094
2562
|
|
|
2095
2563
|
### Shader materials
|
|
2096
2564
|
|
|
@@ -2121,7 +2589,8 @@ older bakes are rejected - re-bake with `srt tool 3d/model`.
|
|
|
2121
2589
|
the material then rejects standard geometry at add(). Do not mention
|
|
2122
2590
|
aColor you do not read.
|
|
2123
2591
|
- A custom fragment that ends in `fragColor = vec4(...)` bypasses the
|
|
2124
|
-
scene: no fog
|
|
2592
|
+
scene: no fog (exposure, tone mapping and the encode still happen, in
|
|
2593
|
+
the resolve, so it must write LINEAR premultiplied light) - and a
|
|
2125
2594
|
hand-rolled loop over `uLightDir` renders a spot light as a
|
|
2126
2595
|
directional one (a lit rectangle on the floor, no cone). What you
|
|
2127
2596
|
declare is what runs, and the engine injects nothing, so pick a tier
|
|
@@ -2183,7 +2652,8 @@ older bakes are rejected - re-bake with `srt tool 3d/model`.
|
|
|
2183
2652
|
otherwise the shape is simply wrong, there is no open-profile mode.
|
|
2184
2653
|
- `useScene()`/`Group`/`Mesh` throw outside `<Scene>` (default-less
|
|
2185
2654
|
context).
|
|
2186
|
-
- Geometry local bounds cache on the Geometry (like its GPU buffers
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2655
|
+
- Geometry local bounds cache on the Geometry (like its GPU buffers and
|
|
2656
|
+
the core's picking copy): mutating `vertices` after a mesh used them
|
|
2657
|
+
leaves stale bounds, a stale GPU buffer AND a stale picking shape
|
|
2658
|
+
until `updateVertices` publishes the range - write through the
|
|
2659
|
+
accessor and call it, or make a new Geometry.
|