@solidrt/3d 0.0.60 → 0.0.61
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 +436 -139
- package/examples/cascades.tsx +10 -4
- package/examples/pick.tsx +6 -4
- package/examples/sprites.tsx +7 -1
- package/package.json +4 -3
- package/src/components/context.tsx +13 -2
- package/src/components/first-person-camera.tsx +1 -1
- package/src/components/index.ts +2 -0
- package/src/components/instanced-mesh.tsx +25 -6
- package/src/components/lod.tsx +3 -1
- package/src/components/orbit-camera.tsx +105 -20
- package/src/components/shots.tsx +93 -0
- package/src/first-person.ts +48 -16
- package/src/gltf.ts +316 -62
- package/src/index.ts +8 -3
- package/src/input.ts +61 -17
- package/src/light.ts +25 -2
- package/src/material.ts +101 -25
- package/src/math.ts +11 -0
- package/src/mesh.ts +68 -21
- package/src/model-data.ts +27 -0
- package/src/model-file.ts +112 -37
- package/src/model.ts +90 -29
- package/src/node.ts +1 -1
- package/src/orbit.ts +526 -108
- package/src/scene-shadows.ts +4 -3
- package/src/scene.ts +201 -189
- package/src/shots.ts +63 -0
- package/src/skeleton.ts +39 -7
- package/tools/environment.ts +1 -0
- package/tools/model.ts +4 -3
- package/tools/node-env.d.ts +25 -0
- package/src/motion.ts +0 -17
- package/src/order.ts +0 -58
package/AGENTS.md
CHANGED
|
@@ -167,6 +167,13 @@ pick()/raycast()/overlap()/sweep() skip scene-masked-out meshes like
|
|
|
167
167
|
invisible ones - unless the query passes its own `{ layers }`, which is
|
|
168
168
|
how a low-poly collision mesh lives undrawn in the scene yet answers
|
|
169
169
|
ground and collision queries (the physics-collider pattern).
|
|
170
|
+
SEALED INTERIORS are the other manual use: from outside, a closed shell
|
|
171
|
+
hides its whole interior, but frustum culling cannot know that and
|
|
172
|
+
there is no occlusion or portal culling. Put the interior, the shell
|
|
173
|
+
and an opening "plug" on their own layers and switch the scene mask by
|
|
174
|
+
the camera's zone (inside, outside, in the doorway) - the reactive
|
|
175
|
+
`<Scene layers>` makes it one signal, and a light's reach is masked
|
|
176
|
+
the same way once lights carry layers.
|
|
170
177
|
Per-view fog: `fog: FogOptions | null` on createView overrides the
|
|
171
178
|
scene's fog for that view (null = unfogged - the clear minimap over a
|
|
172
179
|
fogged scene); absent follows the scene. `overrideMaterial` (Three's
|
|
@@ -225,12 +232,13 @@ a zoomed camera or a scaled-up tree). `createLod([{ node, size }, ...],
|
|
|
225
232
|
levels nearest first, `size` the projected size BELOW which a level
|
|
226
233
|
hands over to the next, the last one's the cull threshold (0 = never
|
|
227
234
|
culled); a level is a direct child (a parentless node is adopted by
|
|
228
|
-
`setLod`, one parented elsewhere is refused, never moved). Every camera
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
235
|
+
`setLod`, one parented elsewhere is refused, never moved). Every camera
|
|
236
|
+
write is also its target's view for the core, which derives the eye,
|
|
237
|
+
focal and ortho flag it measures with (under `lodBias`): the scene and
|
|
238
|
+
each view pick by their own camera (a minimap sees the far level of the
|
|
239
|
+
same tree), shadow tiles by the SCENE camera (a caster draws the level
|
|
240
|
+
the camera sees, so its shadow matches - Unity's and Godot's rule),
|
|
241
|
+
probe faces by each face's camera. The gate composes with the frustum and `visible` into the same
|
|
234
242
|
instance-count switch, so a still camera re-tests nothing and a thousand
|
|
235
243
|
groups cost no JS per frame; a child of the group that carries no
|
|
236
244
|
`lodSize` is drawn always. Without a `fade` the switch is hard with a
|
|
@@ -264,8 +272,14 @@ device. Picking and collision see the level the queried target draws
|
|
|
264
272
|
(`scene.pick`/overlap/sweep/moveAndSlide the scene's, `view.pick` the
|
|
265
273
|
view's); a collider that must not follow the camera is its own undrawn
|
|
266
274
|
mesh on a collision layer. Nested groups chain. Mesh simplification is
|
|
267
|
-
a bake job, not a runtime one
|
|
268
|
-
|
|
275
|
+
a bake job, not a runtime one, and a far level for EMISSIVE DETAIL is a
|
|
276
|
+
design job: geometry simplifies, point lights do not. Fine lights
|
|
277
|
+
aggregated into bigger dots read as a uniform point cloud from afar;
|
|
278
|
+
what works is folding them into a dim emissive floor under the coarse
|
|
279
|
+
blocks, brightness proportional to the local light density squared,
|
|
280
|
+
visible only through the gaps - and tiny lights kept in the MIDDLE
|
|
281
|
+
level bring the speckle back one level closer. `examples/lod.tsx` is
|
|
282
|
+
the shape; `probes/3d-lod-bench.tsx` the cost.
|
|
269
283
|
|
|
270
284
|
### Shadows
|
|
271
285
|
|
|
@@ -317,7 +331,9 @@ PCF taps clamp at face-tile edges, so a face seam hardens slightly
|
|
|
317
331
|
instead of bleeding into the neighbour.
|
|
318
332
|
`shadow: { cascades: N }` (1..MAX_CASCADES = 4) replaces the box with
|
|
319
333
|
N maps fitted to slices of the SCENE camera's frustum (near ..
|
|
320
|
-
`shadow.distance`, default the camera far; the practical split
|
|
334
|
+
`shadow.distance`, default the camera far; the practical split, or
|
|
335
|
+
`shadow.splits` - `N - 1` ascending fractions of that range, Godot's
|
|
336
|
+
shadow_split_1..3 - to push resolution outward or pull it in; each
|
|
321
337
|
slice's bounding sphere as an ortho box along the light, its centre
|
|
322
338
|
snapped to the map's texel grid so edges do not swim; re-fitted
|
|
323
339
|
whenever the scene camera or the light moves) - a receiver samples the
|
|
@@ -521,13 +537,16 @@ faces); an instance is just per-entry uniforms (`uColor`) and bindings
|
|
|
521
537
|
|
|
522
538
|
### Pure pieces and checks
|
|
523
539
|
|
|
524
|
-
The pure pieces (`math.ts`, `
|
|
540
|
+
The pure pieces (`math.ts`, `color.ts`, `geometry.ts`,
|
|
525
541
|
`profile.ts`, `sweep.ts`, `gltf.ts`, `model-file.ts`) are Solid-free and
|
|
526
|
-
GPU-free BY DESIGN so they can be checked headless
|
|
527
|
-
|
|
542
|
+
GPU-free BY DESIGN so they can be checked headless and run under bun:
|
|
543
|
+
`src/model-data.ts` re-exports exactly this set as the published
|
|
544
|
+
`@solidrt/3d/model` entry (the bake tool and app bake scripts), and
|
|
545
|
+
`tests/model-data.test.ts` imports it under `bun test`, so a runtime
|
|
546
|
+
import creeping into any of them fails there; keep them that way.
|
|
528
547
|
The rigs under `checks/`
|
|
529
548
|
(`geometry-check`, `sweep-check`, `pick-check`, `dispatch-check`,
|
|
530
|
-
`
|
|
549
|
+
`gltf-check`) run on
|
|
531
550
|
flux from the repo root: `bunx srt bundle -f --stdout
|
|
532
551
|
packages/3d/checks/<name>.ts | target/release/flux -`. Run the ones
|
|
533
552
|
touching what you changed. `raycast-check.tsx` and
|
|
@@ -547,15 +566,15 @@ collision claims - two copies of this contract have drifted before.
|
|
|
547
566
|
|
|
548
567
|
| Component | Props |
|
|
549
568
|
| --- | --- |
|
|
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 }`,
|
|
569
|
+
| `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?` (FogOptions: linear `{ color, near, far }` or exp2 `{ color, density }`, either plus `height`/`heightFalloff` - see Fog), `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
570
|
| `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
571
|
| `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
572
|
| `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
573
|
| `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 |
|
|
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)` |
|
|
574
|
+
| `Mesh` | `geometry`, `material`, transforms as Group, `params?` (per-mesh uniforms, merge semantics - no unset), `renderOrder?`, `castShadow?`, `layers?` (membership bitmask, default 1), `frustumCulled?` (default true; false for geometry a vertex stage moves beyond the node's box - a billboard, a fullscreen quad; see Culling), `cullMargin?` (world units of slack around the box for bounded displacement), pointer events (below), `ref?(mesh)` |
|
|
556
575
|
| `Sprite` | as Mesh minus `geometry`: a camera-facing unit quad, `scale` is its world size, rotation is ignored; pair with a `sprite()` material |
|
|
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 |
|
|
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) |
|
|
576
|
+
| `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?` (an optional cull box, never a hit: instances pick by themselves, and without it 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, and `anchor?` (a SceneNode, fixed at creation: the ancestor the records are relative to, so `<Instance mesh>` children may sit anywhere under it - see createInstancedMesh) |
|
|
577
|
+
| `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), and `mesh?` (the population, fixed at creation, when the instance is placed outside its `<InstancedMesh>` - inside that mesh's `anchor` subtree) |
|
|
559
578
|
| `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 |
|
|
560
579
|
| `PerspectiveCamera` | `fov?` (vertical DEGREES, default 60), `near?`, `far?`, `position?`, `lookAt?`, `up?` - or the Scene `camera` prop, the same state (last write wins) |
|
|
561
580
|
| `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)` |
|
|
@@ -637,17 +656,27 @@ input.bind(orbitBindings({ pointer, gamepad: gamepad() }))
|
|
|
637
656
|
|
|
638
657
|
The vocabulary is shared with @solidrt/2d, one kind and unit per word:
|
|
639
658
|
`rotate` vec2 (orbit turns), `zoom` axis (octaves, positive in), `pan`
|
|
640
|
-
vec2 (element heights), `
|
|
641
|
-
|
|
659
|
+
vec2 (element heights), `focus` axis (a nudge with a focal: a double
|
|
660
|
+
tap's point), `look` vec2 (turns), `move` vec2 ([right, forward],
|
|
661
|
+
forward = -y in the screen convention), `rise` axis. Keys and
|
|
642
662
|
sticks move the CAMERA where a drag moves the content, so the presets
|
|
643
663
|
bind them to `rotate`/`pan` through `invert()`; `look` is the exception,
|
|
644
664
|
a drag and a stick both turn the eye.
|
|
645
665
|
|
|
646
666
|
### Orbit camera
|
|
647
667
|
|
|
668
|
+
The camera controls run one pipeline (okf/design/camera-controls.md):
|
|
669
|
+
a SOURCE (an input nudge, a verb, the follow) writes the pose, the
|
|
670
|
+
FRAMING eases a followed point into its zones, the LANES (a screen
|
|
671
|
+
offset, shakes) are summed on top of the pose, the CONSTRAINTS (the
|
|
672
|
+
clamps, `clampPose`, occlusion) apply, and the PUSH writes the final
|
|
673
|
+
camera. `pose()` is the pose alone; `camera()` the final camera as
|
|
674
|
+
pushed.
|
|
675
|
+
|
|
648
676
|
Camera control: `createOrbitCamera(scene, { target?, azimuth?, elevation?,
|
|
649
677
|
distance?, min/maxDistance?, min/maxElevation?, orbitSpeed?, rotateSpeed?,
|
|
650
|
-
zoomSpeed?, panSpeed?, damping?,
|
|
678
|
+
zoomSpeed?, panSpeed?, damping?, push?, panPlane?, follow?, offset?,
|
|
679
|
+
clampPose?, zoomAnchor?, rotateAnchor?, occluder? })`
|
|
651
680
|
- azimuth/elevation/distance around a target with optional auto-orbit. The
|
|
652
681
|
first argument is anything with the scene's `setCamera`, `camera()` (the
|
|
653
682
|
fov maps pan travel to world) and `size()` (the aspect `fit` frames
|
|
@@ -656,78 +685,186 @@ Its `axes` are `rotate` (a delta of one element height sweeps one full
|
|
|
656
685
|
turn, Three's OrbitControls convention, so a drag feels the same on a
|
|
657
686
|
phone and a 4k window; a rate turns at 0.5 turn/s at full deflection),
|
|
658
687
|
`zoom` (octaves: a delta of 1 halves the distance, a rate of 1 halves it
|
|
659
|
-
per second)
|
|
660
|
-
1:1 at the target's depth, three.js
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
688
|
+
per second - a DOLLY, bounded by the target), `pan` (the target slides so
|
|
689
|
+
the scene tracks the fingers 1:1 at the target's depth, three.js
|
|
690
|
+
DOLLY_PAN, weighted by `panSpeed`; `panPlane: "ground"` slides in the
|
|
691
|
+
plane orthogonal to world up instead, Three's screenSpacePanning off,
|
|
692
|
+
the map's choice) and `focus` (an axis: a nudge with a focal - a double
|
|
693
|
+
tap - glides the target to the point under it through `zoomAnchor`;
|
|
694
|
+
without a focal, to the point under the view centre). A delta bracketed
|
|
695
|
+
by a gesture (a drag, a pinch, two fingers) applies at once, the content
|
|
696
|
+
staying under the fingers; an unbracketed one (a wheel notch, a key
|
|
697
|
+
step) is an impulse and glides in - `damping` scales that settle time, 0
|
|
698
|
+
applies it at once - notches compounding on the pending value so a fast
|
|
699
|
+
scroll is one push, the 2d camera's rule and the same knob there. The
|
|
700
|
+
verbs: `rotateBy(azimuth, elevation)` radians, `zoomBy(factor, anchor?)`
|
|
701
|
+
(factor > 1 in, as the 2d camera's zoomAt), `panBy(right, up)` world
|
|
702
|
+
units, `setPivot(point)`, `setOrbitPoint(point)`, `set(pose)`,
|
|
703
|
+
`shake(strength, duration, { frequency?, direction? })`; every one
|
|
669
704
|
pushes the pose at once. Two commanded moves ease instead, inside
|
|
670
705
|
update(dt): `glideTo({ azimuth?, elevation?, distance?, target? })`, and
|
|
671
|
-
`fit(bounds, { glide? })`, which frames a `[minX, minY, minZ, maxX,
|
|
672
|
-
maxZ]` box (geometryBounds, a model's `bounds`) - target to its
|
|
673
|
-
distance to where the bounding sphere fills the tighter of the
|
|
674
|
-
and horizontal fov at the target's size, azimuth and elevation
|
|
675
|
-
clamps applied; a snap unless `glide`, so a park-then-snapshot
|
|
676
|
-
(under ortho only the target moves). Any input drops a glide (a
|
|
677
|
-
landing holds the view), and a `set()` that writes a pose field
|
|
678
|
-
and drops any motion; `set({})` and `set({ orbiting })` leave it
|
|
679
|
-
Every write - input, verb, glide frame, set() - goes through
|
|
680
|
-
clamps and then `clampPose(pose)`, which sees the whole pose
|
|
681
|
-
the fields to change: bound where a pan may put the target,
|
|
682
|
-
eye above a floor (an elevation floor that depends on the
|
|
683
|
-
a fixed `minElevation` cannot say); a glide's goal is
|
|
684
|
-
and its frames as they land, so it never shows an
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
706
|
+
`fit(bounds, { glide? })`, which frames a `[minX, minY, minZ, maxX,
|
|
707
|
+
maxY, maxZ]` box (geometryBounds, a model's `bounds`) - target to its
|
|
708
|
+
centre, distance to where the bounding sphere fills the tighter of the
|
|
709
|
+
vertical and horizontal fov at the target's size, azimuth and elevation
|
|
710
|
+
kept, clamps applied; a snap unless `glide`, so a park-then-snapshot
|
|
711
|
+
repeats (under ortho only the target moves). Any input drops a glide (a
|
|
712
|
+
finger landing holds the view), and a `set()` that writes a pose field
|
|
713
|
+
snaps and drops any motion; `set({})` and `set({ orbiting })` leave it
|
|
714
|
+
running. Every write - input, verb, glide frame, set() - goes through
|
|
715
|
+
the range clamps and then `clampPose(pose)`, which sees the whole pose
|
|
716
|
+
and returns the fields to change: bound where a pan may put the target,
|
|
717
|
+
or hold the eye above a floor (an elevation floor that depends on the
|
|
718
|
+
distance, which a fixed `minElevation` cannot say); a glide's goal is
|
|
719
|
+
clamped when set and its frames as they land, so it never shows an
|
|
720
|
+
illegal pose.
|
|
721
|
+
|
|
722
|
+
**Push.** `push: true` lets a zoom step past `minDistance` (or
|
|
723
|
+
`maxDistance`) move eye and target together by the overflow - along the
|
|
724
|
+
view axis, or the anchor's ray when the step is anchored, so the point
|
|
725
|
+
under the fingers keeps its pixel - instead of stopping at the floor
|
|
726
|
+
(camera-controls' infinityDolly): the eye's speed is continuous across
|
|
727
|
+
the floor, so a pinch moves THROUGH a model at a speed that scales with
|
|
728
|
+
the distance. Off by default: a dolly that ends at the subject is the
|
|
729
|
+
viewer's default.
|
|
730
|
+
|
|
731
|
+
**Anchors.** Zoom aims at the target unless `zoomAnchor(focal, {eye,
|
|
732
|
+
target})` maps the gesture's focal point - a FRACTION of the element,
|
|
733
|
+
[0..1, 0..1] - to a world point: then that point stays pinned under the
|
|
734
|
+
pointer and the target slides toward it. A pinch holds one anchor for
|
|
735
|
+
its whole gesture (the fingers' interleaved events make the span
|
|
736
|
+
oscillate, and re-anchoring per delta turns that into a crawl); the
|
|
737
|
+
wheel, arriving unbracketed, anchors per notch. `rotateAnchor({eye,
|
|
738
|
+
target})`, called when a rotate gesture begins, names the point its
|
|
739
|
+
drag should pivot about: projected onto the view axis, it re-seats the
|
|
740
|
+
pivot without moving the picture (`setPivot`), so a drag after an
|
|
741
|
+
anchored zoom orbits what the camera looks at, not wherever the zoom
|
|
742
|
+
left the target. `<OrbitCamera>` builds both from its scene (`anchor`
|
|
743
|
+
below); a function-face caller supplies them (`scene.pick` at the
|
|
744
|
+
focal, `scene.unproject` at the target's depth as the fallback).
|
|
745
|
+
|
|
746
|
+
**Orbit point and the lanes.** `setOrbitPoint(point)` re-seats the
|
|
747
|
+
pivot EXACTLY: the target becomes the point, the distance its depth, and
|
|
748
|
+
the offset lane takes up the point's sideways offset, so the camera's
|
|
749
|
+
position and orientation do not change and the next drag orbits that
|
|
750
|
+
point (camera-controls' setOrbitPoint with a focal offset) - a viewer
|
|
751
|
+
pushed through a model keeps turning about the model's centre. The
|
|
752
|
+
`offset` option is the lane's persistent half, in view HEIGHTS from the
|
|
753
|
+
centre (x right, y down: `[0.25, 0]` shows the target a quarter of the
|
|
754
|
+
height right of centre - Babylon's targetScreenOffset); `shake` adds
|
|
755
|
+
decaying oscillations to it. Lanes are added on top of the pose at push
|
|
756
|
+
and never enter `pose()`; a verb that writes the target (`set`,
|
|
757
|
+
`glideTo`, `fit`, `focus`) clears setOrbitPoint's share.
|
|
758
|
+
|
|
759
|
+
**Follow.** `follow(point)` makes the target chase a world point
|
|
760
|
+
through the framing in `follow: { damping?, deadZone?, hardLimits?,
|
|
761
|
+
lookahead?, heading? }` (core's camera-control, shared with the 2d
|
|
762
|
+
camera): damping per view axis (`{ x, y, z }`: right, up, forward - a
|
|
763
|
+
lazy vertical, a tight horizontal), the dead zone and hard limits as
|
|
764
|
+
fractions of the view centred on the target (inside the dead zone the
|
|
765
|
+
point roams free; in the band up to the hard limits it is eased back;
|
|
766
|
+
beyond them it is clamped inside at once, whatever the damping - a fast
|
|
767
|
+
point never leaves the view), lookahead by `time` seconds of the
|
|
768
|
+
point's velocity smoothed over `smoothing` seconds. Cinemachine's
|
|
769
|
+
Orbital Follow: the orbit's own input keeps working around the followed
|
|
770
|
+
point - a rotate orbits it, a zoom dollies toward it, a pan moves the
|
|
771
|
+
target and the follow eases it back. `follow(point, heading)` also
|
|
772
|
+
recentres the azimuth on `heading`, the followed thing's yaw in the
|
|
773
|
+
first-person convention (0 faces -z, positive turns left: a walker's
|
|
774
|
+
`pose().yaw` feeds a chase camera directly), by the shortest turn,
|
|
775
|
+
after `follow.heading.wait` seconds (default 1) without rotate input at
|
|
776
|
+
`follow.heading.damping` - Cinemachine's recentering: a drag looks
|
|
777
|
+
around, the camera settles back behind the walker. Call `follow` every
|
|
778
|
+
frame for a moving point; the control settles once the point rests
|
|
779
|
+
inside the zones and the heading is reached. `unfollow()`, or a verb
|
|
780
|
+
that writes the target, ends it.
|
|
781
|
+
|
|
782
|
+
**Occlusion.** `occluder(target, eye)` reports the free distance from
|
|
783
|
+
the target toward the eye when something stands between them (null
|
|
784
|
+
otherwise); a free distance under the pose's pulls the FINAL eye in to
|
|
785
|
+
it at once, and the return eases as the obstacle clears (Cinemachine's
|
|
786
|
+
deoccluder damping; Godot's SpringArm3D snaps both ways). The pose's
|
|
787
|
+
distance is untouched: a zoom out from behind a wall still goes where
|
|
788
|
+
the pose says once the wall is gone. `<OrbitCamera occlusion>` builds
|
|
789
|
+
the hook from a scene raycast.
|
|
790
|
+
|
|
791
|
+
Call `orbit.update(dt)` from your onFrame to integrate the rates, the
|
|
792
|
+
auto-orbit, any glide, the follow, the lanes and the occlusion return
|
|
793
|
+
(no frame loop of its own), and use its return - true when the camera
|
|
794
|
+
changed since the previous update, nudges included - to gate per-frame
|
|
795
|
+
dependents like reprojecting HUD overlays. `orbiting()` (the auto-orbit
|
|
796
|
+
switch) and `active()` (the frame-loop gate: orbiting with a non-zero
|
|
797
|
+
rate, a glide, follow, shake or occlusion return in flight, or any rate
|
|
798
|
+
driving - the predicate every camera control shares) are reactive
|
|
799
|
+
(HUD-safe); the pose is plain state via `pose()`/`set()` (also the
|
|
800
|
+
debug-command shape). It drives position and target only; fov/near/far
|
|
801
|
+
stay on scene.setCamera (or the Scene `camera` prop). The auto-orbit
|
|
802
|
+
pauses while a gesture is open. checks/orbit-check.ts pins every stage
|
|
803
|
+
headless.
|
|
709
804
|
|
|
710
805
|
In a component tree, skip the wiring: `<OrbitCamera input={input}
|
|
711
806
|
azimuth={1.2} distance={7} />` as a Scene child reaches the scene through
|
|
712
807
|
context (as a `<View3d>` child, that view: the context's `viewport` is
|
|
713
808
|
the nearest owner's), drives from the map in `input` (its `rotate`,
|
|
714
|
-
`zoom`, `pan` actions, or the names in `actions`; live - a new
|
|
715
|
-
reconnects) and nothing else, and runs a frame loop only while
|
|
809
|
+
`zoom`, `pan`, `focus` actions, or the names in `actions`; live - a new
|
|
810
|
+
map reconnects) and nothing else, and runs a frame loop only while
|
|
716
811
|
`active()`, so a camera moved by drags alone keeps the app demand-driven
|
|
717
|
-
idle. The
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
812
|
+
idle. The projection-dependent hooks are built here, since the
|
|
813
|
+
component sits where the projection is: `anchor` ("pick", the default:
|
|
814
|
+
the scene under the focal, else the plane at the target's depth;
|
|
815
|
+
"plane": that plane only; false: zoom toward the target, no pivot
|
|
816
|
+
re-seat) fills `zoomAnchor` and `rotateAnchor`, `occlusion={{ radius?,
|
|
817
|
+
layers?, meshes? }}` fills `occluder` from a raycast between target and
|
|
818
|
+
eye (mask the followed character out with `layers`, or name the level's
|
|
819
|
+
meshes); a hook prop overrides its half. The pose props are initial
|
|
820
|
+
values: runtime pose changes (and the debug-command hookup) go through
|
|
821
|
+
`ref`'s handle, whose set() and verbs push the pose and whose
|
|
822
|
+
glideTo/fit ease it. Every other prop is live - forwarded to the
|
|
823
|
+
control as a getter and read where it applies, never snapshotted - so
|
|
824
|
+
clamps, rates, `damping`, the follow's zones, `offset` and anchors
|
|
825
|
+
follow their props without a remount, and a clamp or offset change
|
|
826
|
+
re-clamps the pose at once. A `fit` from `ref` at mount reads the
|
|
827
|
+
scene's size of that moment: a fill-mode `<Scene>` has its creation
|
|
828
|
+
size until its first layout, so fit after the first frame (or hand a
|
|
829
|
+
fixed-size scene). `orbitBindings` is the viewer's wiring (a drag
|
|
830
|
+
rotates, Ctrl-drag, right-drag and two fingers pan, pinch and wheel
|
|
831
|
+
zoom, a double tap focuses); `mapBindings` the map's (a drag pans,
|
|
832
|
+
Ctrl-drag, right-drag and two fingers rotate), paired with `panPlane:
|
|
833
|
+
"ground"`.
|
|
834
|
+
|
|
835
|
+
### Shots and blends
|
|
836
|
+
|
|
837
|
+
`createShots(scene | view, { blend? })` (core's blender over the
|
|
838
|
+
scene's CameraState, Cinemachine's virtual cameras): `shot(name, {
|
|
839
|
+
priority? })` returns a target an orbit or first-person control drives
|
|
840
|
+
instead of the scene (its `camera()` is the shot's own, its `size()` the
|
|
841
|
+
owner's) - a chase camera and a cinematic one, each with its own
|
|
842
|
+
control - and `activate(name, { blend? })` makes the highest-priority
|
|
843
|
+
enabled shot live, blending the scene from the current output over
|
|
844
|
+
`blend` seconds (default 0.5; 0 cuts; position, target, up, fov, near
|
|
845
|
+
and far linear, two ortho extents linear, a perspective-to-ortho switch
|
|
846
|
+
cuts at the midpoint; `mixCamera`); `deactivate` falls back by
|
|
847
|
+
priority. A live shot's pushes reach the scene at once; a blend needs
|
|
848
|
+
`update(dt)` while `active()`, the controls' rule. `live()` and
|
|
849
|
+
`camera()` read the state. A switch mid-blend starts from the output of
|
|
850
|
+
that moment, so a quick back-and-forth never jumps. Every shot starts
|
|
851
|
+
from the owner's camera at creation. The component form: `<Shots
|
|
852
|
+
blend? ref?>` as a Scene (or View3d) child with `<Shot name priority?
|
|
853
|
+
active? blend?>` children, each providing a context whose `viewport` is
|
|
854
|
+
the shot's recording target, so an `<OrbitCamera>` or
|
|
855
|
+
`<FirstPersonCamera>` inside a `<Shot>` drives that shot; `active`
|
|
856
|
+
(default true, live) withdraws and restores a shot, an unmount removes
|
|
857
|
+
it, the container runs the blend on frames while one is in flight. A
|
|
858
|
+
shot's picks and raycasts (`anchor`, `occlusion`) go through the
|
|
859
|
+
owner's actual camera, which is the shot's own only while it is live
|
|
860
|
+
and at rest. Pinned in checks/orbit-check.ts; probes/shots-3d-probe.tsx
|
|
861
|
+
is the live guard.
|
|
726
862
|
|
|
727
863
|
### First-person camera
|
|
728
864
|
|
|
729
865
|
First-person control: `createFirstPersonCamera(scene, { position?, yaw?,
|
|
730
|
-
pitch?, min/maxPitch?, moveSpeed?, lookSpeed?, fly?,
|
|
866
|
+
pitch?, min/maxPitch?, moveSpeed?, boostSpeed?, lookSpeed?, fly?,
|
|
867
|
+
clampPosition? })` -
|
|
731
868
|
a position plus yaw/pitch (yaw 0 faces -z, positive turns left; pitch
|
|
732
869
|
positive looks up), Unity's FirstPersonController shape (look AND move in
|
|
733
870
|
one control) where Three splits PointerLockControls from a hand-written
|
|
@@ -737,15 +874,24 @@ pointer feed's `mouseDelta` delivers mouse motion under pointer lock in
|
|
|
737
874
|
the same unit; a rate turns at 0.4 turn/s), `move` ([right, forward],
|
|
738
875
|
forward = -y: a stick pushed up or W reads [0, -1]; a rate walks at
|
|
739
876
|
`moveSpeed`, a delta is a step in world units, diagonals clamped to unit
|
|
740
|
-
length so they walk no faster)
|
|
741
|
-
mode only)
|
|
877
|
+
length so they walk no faster), `rise` (world up at `moveSpeed`, fly
|
|
878
|
+
mode only) and `boost` (an axis, since the control has no button kind:
|
|
879
|
+
a bound button reads 1 while held, and any non-zero read multiplies the
|
|
880
|
+
move and rise RATES by `boostSpeed`, default 2 - the sprint; a delta
|
|
881
|
+
stays a one-unit step; `firstPersonBindings` puts it on Shift and the
|
|
882
|
+
left stick's press). Walking (the default) flattens the heading onto the ground
|
|
742
883
|
plane at fixed height; `fly` moves along the view. The verbs:
|
|
743
884
|
`lookBy(yaw, pitch)` radians, `moveBy(right, forward, up?)` in the
|
|
744
885
|
walker's frame, `set(pose)`, and `glideTo({ position?, yaw?, pitch? })`,
|
|
745
886
|
which eases there inside update(dt) (yaw to the number given, not the
|
|
746
887
|
shortest turn), dropped by any input and by a set() of a pose field -
|
|
747
888
|
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.
|
|
889
|
+
delta is mouse motion under pointer lock, and that is never eased.
|
|
890
|
+
`shake(strength, duration, { frequency?, direction? })` is the lane
|
|
891
|
+
every control has (okf/design/camera-controls.md), here a view KICK in
|
|
892
|
+
TURNS of yaw and pitch (the look axis's unit; 0.01 is 3.6 degrees), the
|
|
893
|
+
first-person convention: a hit, a recoil, a footstep; the pose and the
|
|
894
|
+
eye are untouched. Every
|
|
749
895
|
option but the initial pose is read
|
|
750
896
|
where it applies (`fly` per step, `clampPosition` per move, the rates and
|
|
751
897
|
pitch clamps per input), so a field changed on the options object takes
|
|
@@ -820,7 +966,9 @@ so hits carry `face`, `uv` and a world-space `normal` facing the ray, and
|
|
|
820
966
|
a ray through a knot's hole misses. A large geometry's triangles are
|
|
821
967
|
BVH-indexed too - built by the first ray that reaches the shape, log-cost
|
|
822
968
|
after - so raycasting a merged static scene stays cheap (see the batching
|
|
823
|
-
advice). An instanced mesh
|
|
969
|
+
advice). An instanced mesh picks per instance (each a leaf with the
|
|
970
|
+
geometry's shape; its own node is never a hit, its `bounds` cull
|
|
971
|
+
only). A record mesh is box-only (its
|
|
824
972
|
explicit population bounds; records are opaque), and so is a mesh whose
|
|
825
973
|
geometry is not a triangle list (lines, points, strips have no triangle
|
|
826
974
|
narrowphase): it is tested by the
|
|
@@ -1150,7 +1298,8 @@ opts out of the scene's fog (all four library materials take it).
|
|
|
1150
1298
|
|
|
1151
1299
|
#### sprite
|
|
1152
1300
|
|
|
1153
|
-
`sprite({ color?, map?, transparent?, blend?, billboard
|
|
1301
|
+
`sprite({ color?, map?, transparent?, blend?, billboard?, shape?,
|
|
1302
|
+
falloff? })` - unlit on a quad
|
|
1154
1303
|
that turns to face the camera IN THE VERTEX STAGE (off the shared
|
|
1155
1304
|
uCamRight/uCamUp, or uCamPos for `billboard: "fixed-y"`, which yaws
|
|
1156
1305
|
only and stays upright on world y - Godot's BILLBOARD_FIXED_Y, the
|
|
@@ -1160,7 +1309,11 @@ defaults to TRUE here (cutouts; Three's SpriteMaterial default), cull is
|
|
|
1160
1309
|
off. Draw with `createSprite(material)` / `<Sprite>`: a Mesh over a
|
|
1161
1310
|
shared unit plane, no geometry argument, `scale` = world size, rotation
|
|
1162
1311
|
ignored. Picks by a unit box around its center (its reach at any
|
|
1163
|
-
facing), so hits carry no normal/face/uv. `
|
|
1312
|
+
facing), so hits carry no normal/face/uv. `shape: "radial"` is a
|
|
1313
|
+
procedural falloff over the quad's inscribed disc, `(1 - 2|uv -
|
|
1314
|
+
0.5|)^falloff` (default 1; 2 a soft puff) multiplying color and alpha,
|
|
1315
|
+
so a glow, flare or puff needs no texture; it composes with a `map`
|
|
1316
|
+
and with `blend: "add"`. `examples/sprites.tsx`.
|
|
1164
1317
|
|
|
1165
1318
|
#### shaderMaterial
|
|
1166
1319
|
|
|
@@ -1205,6 +1358,12 @@ Scene-wide values (a clock, a sun direction, fog) go through
|
|
|
1205
1358
|
class/instance split for your own GLSL: compiles once, and
|
|
1206
1359
|
`cls.instance({ params?, textures? })` returns a Material sharing that
|
|
1207
1360
|
pipeline with its own values. `dispose()` lives on the class alone.
|
|
1361
|
+
Seed only the names the material owns: a per-entry param is applied
|
|
1362
|
+
AFTER the target's shared params, so a scene-wide name seeded in
|
|
1363
|
+
`instance({ params })` (uTime, a sun direction) pins that mesh to the
|
|
1364
|
+
seeded value and `scene.setParams` never reaches it again. Declare the
|
|
1365
|
+
uniform in the source, leave it out of the instance, and the scene's
|
|
1366
|
+
write lands.
|
|
1208
1367
|
`shaderMaterial(opts)` is exactly a class with one instance (its
|
|
1209
1368
|
`dispose` forwards to the class).
|
|
1210
1369
|
|
|
@@ -1259,7 +1418,7 @@ tree, a crowd member), not to submit a lot of vertices. The tell in
|
|
|
1259
1418
|
|
|
1260
1419
|
#### createInstancedMesh
|
|
1261
1420
|
|
|
1262
|
-
`createInstancedMesh(geometry, material, { capacity?, bounds?, label? })`
|
|
1421
|
+
`createInstancedMesh(geometry, material, { capacity?, bounds?, anchor?, label? })`
|
|
1263
1422
|
draws the geometry once per instance NODE: `addInstance(mesh,
|
|
1264
1423
|
transform?, parent?)` returns a scene node (kind "instance") placed
|
|
1265
1424
|
inside the mesh - setTransform/setTransition/setVisible, lookAt,
|
|
@@ -1268,6 +1427,15 @@ matrix RELATIVE to the mesh the spatial core writes into the record
|
|
|
1268
1427
|
buffer (`bindMatrixRecord` anchored on the mesh node: one coalesced
|
|
1269
1428
|
buffer write per flush however many instances moved, so native
|
|
1270
1429
|
transitions and clip players move instances with zero per-frame JS).
|
|
1430
|
+
`anchor` moves that frame to an ANCESTOR the mesh sits under at
|
|
1431
|
+
identity: the records are then relative to the anchor and instances may
|
|
1432
|
+
be placed under any node of the anchor's subtree, not only under the
|
|
1433
|
+
mesh - a population whose copies ride a hierarchy the mesh is not the
|
|
1434
|
+
root of (createModel's shared parts: the mesh at identity under the
|
|
1435
|
+
model root, an instance under every placement node). The anchor must be
|
|
1436
|
+
an ancestor when the mesh is added (it throws otherwise); keeping the
|
|
1437
|
+
chain between them identity is the caller's contract, the same one an
|
|
1438
|
+
instanced LOD's levels live under.
|
|
1271
1439
|
The material declares `INSTANCE_MATRIX_ATTRIBUTES` as its first instance
|
|
1272
1440
|
buffer (four vec4 columns, 16 floats; anything else throws at creation)
|
|
1273
1441
|
and its vertex
|
|
@@ -1378,7 +1546,12 @@ no separate resize plumbing. Two forms:
|
|
|
1378
1546
|
later, so a procedural sky written here lights the scene then. A sky
|
|
1379
1547
|
writes LINEAR light to fragColor (the skybox form does) and the
|
|
1380
1548
|
scene's resolve exposes, tone maps and encodes it with everything
|
|
1381
|
-
else.
|
|
1549
|
+
else. Derivatives in uniform control flow only: `fwidth()`/`dFdx()`
|
|
1550
|
+
after an early `return` or inside a data-dependent branch are
|
|
1551
|
+
undefined in the 2x2 quads that straddle it, and on Mesa Intel a
|
|
1552
|
+
hash-grid sky with an "empty cell" early out drew stray 1-px lines
|
|
1553
|
+
and L-shapes along every cell edge. Take the derivatives at the top
|
|
1554
|
+
of main, before any branch.
|
|
1382
1555
|
- A skybox `{ cube, intensity?, rotation? }` (SkyboxOptions): a cube
|
|
1383
1556
|
map from createCubeTexture sampled along the same ray - Three's
|
|
1384
1557
|
`scene.background = cubeTexture` with `backgroundIntensity` and
|
|
@@ -1703,25 +1876,32 @@ resolve - at one of three tiers, top first:
|
|
|
1703
1876
|
params: { uColor: [1, 1, 1, 1], uSpecular: 0.12, uShininess: 24 } })`
|
|
1704
1877
|
is the white starting point (the stock materials seed exactly this
|
|
1705
1878
|
from their `color` option).
|
|
1706
|
-
2. A SURFACE FUNCTION inside the stock
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
`
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1879
|
+
2. A SURFACE FUNCTION inside the stock material. `phong({ ..., prelude,
|
|
1880
|
+
surface })` / `standard({ ..., prelude, surface })`: every other
|
|
1881
|
+
option seeds as usual (`color`, the maps, metalness and roughness),
|
|
1882
|
+
`prelude` is file scope (uniforms and helpers; a uniform it declares
|
|
1883
|
+
is an ordinary per-entry param, `params` on the mesh or
|
|
1884
|
+
`setMeshParams`), `surface` declares `void surface(inout Surface s)`,
|
|
1885
|
+
called once the program has filled the Surface struct from the
|
|
1886
|
+
options (base from the color, the map and the vertex color; the
|
|
1887
|
+
normal, bent by the normal map; emissive, ambient, the light model's
|
|
1888
|
+
fields) and before it shades. Rewrite any field or `discard`; it
|
|
1889
|
+
reads the varyings, the declared uniforms and prelude's names, and
|
|
1890
|
+
runs in the shadow twin too (attached whenever a surface is given),
|
|
1891
|
+
so what it discards casts no shadow - with the twin seeing a prelude
|
|
1892
|
+
uniform at its zero value, since mesh params reach the main entry
|
|
1893
|
+
only. One program per distinct source, keyed like every option. The
|
|
1894
|
+
struct is the contract, no local of the generated program is; colors
|
|
1895
|
+
are linear light, premultiplied throughout, `Surface.base` included.
|
|
1896
|
+
The material describes the surface, the package shades it (Godot's
|
|
1897
|
+
fragment(), Filament's material()). This is the tier for anything
|
|
1898
|
+
whose geometry is not a surface: a point splat discarding outside
|
|
1899
|
+
`gl_PointCoord`'s inscribed circle and flipping a fitted normal
|
|
1900
|
+
towards the viewer keeps the scene's whole light model in about
|
|
1901
|
+
fifteen lines, where hand-rolling the light loop (tier 3) would have
|
|
1902
|
+
to match every light's falloff, cone and shadow by hand. The same
|
|
1903
|
+
two slots on `phongFragment` / `standardFragment` are the class form
|
|
1904
|
+
for a custom vertex stage (tier 1 and 2 together).
|
|
1725
1905
|
3. A FRAGMENT OF YOUR OWN over the scene set. Compose `SCENE` (or
|
|
1726
1906
|
`sceneSource({ lights, receiveShadow, env, fog })`, each flag leaving a
|
|
1727
1907
|
declaration out): it declares uCamPos, uHemiSky/uHemiGround, the
|
|
@@ -1820,7 +2000,9 @@ is discarded; Three's alphaTest, glTF MASK): opaque, depth-written, no
|
|
|
1820
2000
|
sorting, usually with `cull: "none"` for cards, and `instanced: true` /
|
|
1821
2001
|
`instanceColors: true` for an instanced mesh's population (see
|
|
1822
2002
|
Instancing: the placement, the depth pass and a per-instance tint with
|
|
1823
|
-
no GLSL; the material then draws populated meshes only)
|
|
2003
|
+
no GLSL; the material then draws populated meshes only), and `prelude`
|
|
2004
|
+
/ `surface` for one procedural term inside the stock shading (tier 2
|
|
2005
|
+
of Custom looks below; also on `standard`). Triplanar
|
|
1824
2006
|
is an OPTION, not the default: generators emit 0..1 UVs per face, so a
|
|
1825
2007
|
map on a plane is a decal (UV) while a map on generated scenery wants one
|
|
1826
2008
|
density across parts of any size (triplanar); the map must be created
|
|
@@ -1892,7 +2074,11 @@ lights at all lights a scene (`examples/environment.tsx`). Light intensities rea
|
|
|
1892
2074
|
(1 lights a white matte surface to 1; Godot's and Unity's convention -
|
|
1893
2075
|
a Three scene's intensities divide by pi). Without an environment a
|
|
1894
2076
|
metal shows only its highlights: no diffuse, nothing to reflect (Three
|
|
1895
|
-
and Godot do the same), so give the scene one.
|
|
2077
|
+
and Godot do the same), so give the scene one. The same inside a closed
|
|
2078
|
+
volume: a dark, mostly-metal surface there gets little diffuse light
|
|
2079
|
+
and has little to reflect, so a hemisphere fill barely registers on it
|
|
2080
|
+
(a 5x fill and an interior reflection probe both did nothing visible);
|
|
2081
|
+
keep directional key and fill lights on inside. `examples/standard.tsx`
|
|
1896
2082
|
is the sphere grid. Internally one
|
|
1897
2083
|
`shaderMaterialClass` per option combination (map x vertexColors x
|
|
1898
2084
|
triplanar x transparent x cull x alphaTest x fog x the surface maps),
|
|
@@ -1949,6 +2135,33 @@ naming it, and Draco or meshopt compression throws "re-export without
|
|
|
1949
2135
|
mesh compression" - Blender exports Draco by DEFAULT, so that is the
|
|
1950
2136
|
first error a real file hits.
|
|
1951
2137
|
|
|
2138
|
+
Reuse is kept, not unrolled. A mesh several nodes reference (Blender's
|
|
2139
|
+
linked duplicates export that way) becomes ONE part per primitive with
|
|
2140
|
+
`node` = the first node and `placements` = the further ones, and
|
|
2141
|
+
`EXT_mesh_gpu_instancing` lands the same way: its per-instance TRS
|
|
2142
|
+
become synthesized child nodes of the referencing node, named
|
|
2143
|
+
`<node>[i]`, so the hierarchy stays one table and an instance moves
|
|
2144
|
+
like any node (Three builds an InstancedMesh, Godot a MultiMesh;
|
|
2145
|
+
Unity's importer shares the mesh and lets its batcher instance).
|
|
2146
|
+
What cannot share one geometry stays a part per node: a skinned use
|
|
2147
|
+
(bind-pose model space, placed by its skin), a morphed one (its
|
|
2148
|
+
weights are the node's), and a placement whose rest-pose world
|
|
2149
|
+
MIRRORS the first's (opposite determinant sign - the winding flip is
|
|
2150
|
+
baked into the indices, so one index order cannot serve both). The
|
|
2151
|
+
file's `extras` ride through where glTF puts them, the way Three fills
|
|
2152
|
+
`userData` and Godot node metadata: `data.extras` (root), `node.extras`,
|
|
2153
|
+
`part.extras` (the glTF MESH's) and `material.extras`, each present
|
|
2154
|
+
only when non-empty - so a collider box authored as a Blender custom
|
|
2155
|
+
property arrives with the model. Parts come out in WALK order (each
|
|
2156
|
+
where its first placing node was visited, a node's primitives in
|
|
2157
|
+
order), and the parts a primitive cannot fold still share its built
|
|
2158
|
+
geometry: two skinned placements are two parts over ONE `Geometry`
|
|
2159
|
+
object, a mirrored copy shares the vertex buffer with its own index
|
|
2160
|
+
order - and the container writes each object and each buffer once
|
|
2161
|
+
(`decodeModel` restores the identities, so the runtime uploads them
|
|
2162
|
+
once too). A document with no `scene`/`scenes` draws everything from
|
|
2163
|
+
its true roots; a hierarchy reaching a node twice (a cycle) throws.
|
|
2164
|
+
|
|
1952
2165
|
#### createModel
|
|
1953
2166
|
|
|
1954
2167
|
`createModel(data, { material?, label?, autoFree? })` - uploads the images (repeat
|
|
@@ -1962,14 +2175,32 @@ Blinn-Phong look, or any other material; it is called once per material and shar
|
|
|
1962
2175
|
Groups with the file's local TRS, and one mesh per part under its node,
|
|
1963
2176
|
all inside the returned `Model` (a Group): `add(scene.root, model)`,
|
|
1964
2177
|
place it with `setTransform`, find parts by name in `model.parts`
|
|
1965
|
-
(`{ name, mesh }`), spin a wheel relative to its axle through
|
|
1966
|
-
`model.nodes` (`{ name, node }` in table order, parents first; names
|
|
2178
|
+
(`{ name, mesh, extras? }`), spin a wheel relative to its axle through
|
|
2179
|
+
`model.nodes` (`{ name, node, extras? }` in table order, parents first; names
|
|
1967
2180
|
repeat when the file's do - `.find()` yours), `model.bounds` for
|
|
1968
|
-
framing a camera.
|
|
2181
|
+
framing a camera, `model.extras` and `model.blobs` for the app data a
|
|
2182
|
+
file or a bake carried. Skinned parts get the `skinned: true` material
|
|
1969
2183
|
variant and hang off the model ROOT (the spec ignores their node's
|
|
1970
|
-
transform; the palette places them - see the mixer below).
|
|
1971
|
-
|
|
1972
|
-
|
|
2184
|
+
transform; the palette places them - see the mixer below). A part with
|
|
2185
|
+
`placements` is a POPULATION: one InstancedMesh at identity under the
|
|
2186
|
+
model root (its `anchor`, see Instancing), `instanced: true` material
|
|
2187
|
+
variant, with an instance node at identity under every placement node
|
|
2188
|
+
- `part.instances`, in placement order, the part's own node first - so
|
|
2189
|
+
the copies are one draw entry (`/gpu` shows one entry labelled with the
|
|
2190
|
+
part's name, `instanceCount` = placements + 1; every mesh's entry
|
|
2191
|
+
carries its geometry's label there) and still ride the hierarchy: move,
|
|
2192
|
+
hide or animate the placement node in `model.nodes` and its copy
|
|
2193
|
+
follows; a pick lands on the copy (`hit.mesh` the part's mesh,
|
|
2194
|
+
`hit.instance` the instance, whose `parent` is the placement node);
|
|
2195
|
+
culling is per copy through the mesh's instance union. The one thing a
|
|
2196
|
+
population cannot do per copy is a material swap - `setMaterial` on the
|
|
2197
|
+
part's mesh restyles every copy; per-copy tint is `setInstanceStyle`
|
|
2198
|
+
under a material with `instanceColors`. The `material` callback's
|
|
2199
|
+
last flag is `instanced` (after `skinned`, `vertexColors`, `morphed`):
|
|
2200
|
+
pass it through, or the instanced draw has no matrix to read.
|
|
2201
|
+
`dispose()` detaches it and frees the geometry buffers, a population's
|
|
2202
|
+
record buffers and the textures - the model owns them, nothing else
|
|
2203
|
+
frees them.
|
|
1973
2204
|
|
|
1974
2205
|
#### loadGltf and loadModel
|
|
1975
2206
|
|
|
@@ -1990,6 +2221,32 @@ models and a binary import (`import bytes from "./x.glb" with { type:
|
|
|
1990
2221
|
"binary" }` then `createModel(parseGltf(bytes))`, see
|
|
1991
2222
|
`examples/model.tsx`); bake anything big.
|
|
1992
2223
|
|
|
2224
|
+
#### Baking your own geometry
|
|
2225
|
+
|
|
2226
|
+
Geometry an app GENERATES (a terrain, a city block, a hundred thousand
|
|
2227
|
+
vertices of anything) pays the same interpreter cost every start; when
|
|
2228
|
+
it does not depend on runtime input, bake it once too. The whole data
|
|
2229
|
+
side of the package - the parser, the container and the pure geometry
|
|
2230
|
+
kit (generators, `withAttribute`, `mergeGeometries`, `transformGeometry`,
|
|
2231
|
+
the sweeps and profiles, the math) - is published without the runtime
|
|
2232
|
+
as `@solidrt/3d/model`, so a bake script runs under `bun` with no
|
|
2233
|
+
`flux:*` shim:
|
|
2234
|
+
|
|
2235
|
+
```ts
|
|
2236
|
+
import { box, encodeModel, transformGeometry } from "@solidrt/3d/model"
|
|
2237
|
+
import type { ModelData } from "@solidrt/3d/model"
|
|
2238
|
+
// nodes in pre-order, one part per drawable (a part several nodes place
|
|
2239
|
+
// takes `placements`), materials as glTF-shaped records, app data in
|
|
2240
|
+
// `extras` (JSON) and `blobs` (named bytes, each an aligned block).
|
|
2241
|
+
let data: ModelData = { nodes, parts, skins: [], clips: [], materials, images: [], bounds, extras: { spawn }, blobs: { grid } }
|
|
2242
|
+
await Bun.write("assets/level.srtm", encodeModel(data))
|
|
2243
|
+
```
|
|
2244
|
+
|
|
2245
|
+
`loadModel("assets/level.srtm")` then builds it like any authored model;
|
|
2246
|
+
`model.extras` and `model.blobs` hand the app data back (blobs are views
|
|
2247
|
+
onto the loaded bytes). `srt tool 3d/model` is the same entry applied
|
|
2248
|
+
to a glTF, so the two bakes share one code path.
|
|
2249
|
+
|
|
1993
2250
|
### Async loading
|
|
1994
2251
|
|
|
1995
2252
|
Loading is async everywhere but the binary import: loadGltf/loadModel
|
|
@@ -2036,7 +2293,10 @@ WITH a skin over the body's joints and WITHOUT clips, so beside an
|
|
|
2036
2293
|
animated body they hold their bind pose. `bindSkeleton(body, piece)`
|
|
2037
2294
|
drives them from the body's skeleton - Three's SkinnedMesh.bind, Unity's
|
|
2038
2295
|
`bones =`, Godot's shared Skeleton3D. The piece's palette rows re-bind
|
|
2039
|
-
onto the body's joint nodes (matched by name, case-insensitive
|
|
2296
|
+
onto the body's joint nodes (matched by name, case-insensitive; a
|
|
2297
|
+
pipeline whose names differ by a prefix or suffix - Mixamo's
|
|
2298
|
+
`mixamorig:`, a one-sided `_JNT` - passes `{ match: pieceName =>
|
|
2299
|
+
bodyName }`, Unity's leave-it-to-the-app in one option) with
|
|
2040
2300
|
the piece's own inverse binds, so the flush writes the body's pose into
|
|
2041
2301
|
the piece's skin and no per-frame code exists; joints with no body
|
|
2042
2302
|
counterpart (a hat's internal bones) and rigid parts hanging off a
|
|
@@ -2106,11 +2366,9 @@ write) - read poses with `getTransform(node)`, which reads the core;
|
|
|
2106
2366
|
writes to such a node always go through (setTransform skips its
|
|
2107
2367
|
usual equal-value short-circuit there, so a teleport back to the last
|
|
2108
2368
|
JS-written spot is not lost); (3) a channel nothing plays leaves the node's pose
|
|
2109
|
-
alone;
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
palettes and picking are unaffected) - nudge the scene with any
|
|
2113
|
-
setTransform if it shows, until the core-side transparent sort lands.
|
|
2369
|
+
alone; the draw sort follows native pose writes like any move (the core
|
|
2370
|
+
re-keys what it recomputed), so an animated transparent sorts as it
|
|
2371
|
+
animates.
|
|
2114
2372
|
|
|
2115
2373
|
### Root motion
|
|
2116
2374
|
|
|
@@ -2256,6 +2514,21 @@ with `srt tool 3d/model`.
|
|
|
2256
2514
|
half-blend of EXTREME targets can show a ragged self-shadow
|
|
2257
2515
|
terminator where the lit and shadowed halves disagree; a modeller's
|
|
2258
2516
|
blend shapes are far too small for it to show.
|
|
2517
|
+
- A shared part (`part.instances` set) has ONE mesh for every copy, at
|
|
2518
|
+
identity under the model root: hiding it hides all copies, swapping its
|
|
2519
|
+
material restyles all, moving it is wrong (its records are anchored on
|
|
2520
|
+
the root - see the instancing traps). Per-copy identity lives on the
|
|
2521
|
+
placement nodes in `model.nodes` and on `part.instances` - hide, move,
|
|
2522
|
+
animate and pick those. A file that references one mesh from nodes on
|
|
2523
|
+
both sides of a mirror yields two parts (one per winding sign), so
|
|
2524
|
+
`model.parts.find` by name may find the unmirrored one only; the
|
|
2525
|
+
mirrored copy carries the mirroring node's name.
|
|
2526
|
+
- `createModel` refuses a skinned or morphed part with `placements`
|
|
2527
|
+
(thrown, not drawn wrong): parseGltf never folds those, so this only
|
|
2528
|
+
comes from a hand-built ModelData - give each copy its own part.
|
|
2529
|
+
- `extras` fields exist only where the file HAD data (an empty object
|
|
2530
|
+
is dropped), so read them with `?.` - `model.extras?.spawn`, not
|
|
2531
|
+
`model.extras.spawn`.
|
|
2259
2532
|
|
|
2260
2533
|
### Materials and color
|
|
2261
2534
|
|
|
@@ -2282,15 +2555,32 @@ with `srt tool 3d/model`.
|
|
|
2282
2555
|
behind opaques without occluding other translucents). The one inference:
|
|
2283
2556
|
a stock material or `shaderMaterial` with any `blend` but "none" is transparent
|
|
2284
2557
|
unless told `transparent: false` - every blended draw belongs after the opaques, and
|
|
2285
|
-
back-to-front is harmless for add/multiply. The
|
|
2286
|
-
order
|
|
2287
|
-
|
|
2288
|
-
the
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2558
|
+
back-to-front is harmless for add/multiply. The spatial core owns the
|
|
2559
|
+
order of every scene and view target (`setDrawSort`; each mesh's bind
|
|
2560
|
+
carries its queue and renderOrder): four queues - the background,
|
|
2561
|
+
bound to the scene root and pinned first whatever the camera does,
|
|
2562
|
+
opaque meshes front-to-back by their distance to the camera, cutout
|
|
2563
|
+
meshes (any `alphaTest`, a `shaderMaterialClass({ cutout: true })`)
|
|
2564
|
+
after them the same way because their discard defeats early-z, and
|
|
2565
|
+
transparent meshes back-to-front by their view-space depth -
|
|
2566
|
+
`renderOrder` above depth inside each queue, never across queues,
|
|
2567
|
+
all measured at the CENTER of the mesh's world bounds (not the origin:
|
|
2568
|
+
off-origin geometry sorts by where it is; not the nearest bounds point:
|
|
2569
|
+
a big translucent ground plane would cover the small translucents on
|
|
2570
|
+
it), add order breaking ties. The opaque and cutout key is coarse on
|
|
2571
|
+
purpose - a logarithmic distance bucket, four per doubling - because
|
|
2572
|
+
early-z only needs near layers before far ones, and a coarse key keeps
|
|
2573
|
+
the order (and the meshes' add-order material grouping within a bucket)
|
|
2574
|
+
stable while the camera moves: a look-around changes no distance, and a
|
|
2575
|
+
step shorter than 16% of the nearest bucketed center's distance can
|
|
2576
|
+
cross no bucket edge, so neither re-sorts. Dense geometry added
|
|
2577
|
+
base-first therefore no longer needs the app to sort it (Godot and
|
|
2578
|
+
Unity sort opaques the same way, Three exactly). The core issues one
|
|
2579
|
+
`setDrawOrder` per target from the flush whenever a mesh was added,
|
|
2580
|
+
removed or re-keyed, a node moved, or the camera moved (with a
|
|
2581
|
+
transparent bound, or past that bucketed step), and skips it when the
|
|
2582
|
+
resort lands on the permutation already issued. Per-mesh sort only:
|
|
2583
|
+
one non-convex translucent
|
|
2294
2584
|
mesh still overlaps itself in vertex order, and two large interpenetrating
|
|
2295
2585
|
translucents can sort wrong (center distance, not per-pixel) - that is the
|
|
2296
2586
|
engine contract, no OIT. A `shaderMaterial({ transparent: true })`
|
|
@@ -2493,6 +2783,13 @@ with `srt tool 3d/model`.
|
|
|
2493
2783
|
core before freeing the buffer, so the destroyed instances' hiding
|
|
2494
2784
|
writes land in a live buffer; keep that order if you ever free a
|
|
2495
2785
|
record buffer by hand.
|
|
2786
|
+
- An anchored population (`createInstancedMesh` with `anchor`) draws
|
|
2787
|
+
`uModel * record`, and the record is relative to the ANCHOR: the
|
|
2788
|
+
copies land right only while the mesh's world equals the anchor's.
|
|
2789
|
+
Moving the mesh itself (or any group between it and the anchor) shifts
|
|
2790
|
+
every copy twice; move the anchor, the instances or their parents
|
|
2791
|
+
instead. A model's shared parts are anchored this way - `setTransform`
|
|
2792
|
+
on `model.parts[i].mesh` of a part with `instances` is the mistake.
|
|
2496
2793
|
|
|
2497
2794
|
### Shadows
|
|
2498
2795
|
|
|
@@ -2602,11 +2899,13 @@ with `srt tool 3d/model`.
|
|
|
2602
2899
|
|
|
2603
2900
|
### Picking, pointer and collision
|
|
2604
2901
|
|
|
2605
|
-
- Picking is triangle-accurate for ordinary meshes
|
|
2606
|
-
point, hits carry `face`/`uv`/`normal
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
`
|
|
2902
|
+
- Picking is triangle-accurate for ordinary meshes and for instances
|
|
2903
|
+
(`point` is a surface point, hits carry `face`/`uv`/`normal`; an
|
|
2904
|
+
instanced mesh's own node is never struck, its `bounds` only cull)
|
|
2905
|
+
but box-only for record meshes and for lines/points geometry: there
|
|
2906
|
+
`point` is where the ray meets the population `bounds` box (the
|
|
2907
|
+
geometry's box for lines), `normal` that face's, and `face`/`uv` are
|
|
2908
|
+
absent. Never present an
|
|
2610
2909
|
instanced hit as a surface hit. Both tiers run in the spatial core (Rust); never
|
|
2611
2910
|
add a per-triangle path in JS - rays at mesh scale are
|
|
2612
2911
|
interpreter-hostile, and the core already does it.
|
|
@@ -2642,10 +2941,8 @@ with `srt tool 3d/model`.
|
|
|
2642
2941
|
lies flat with the hole on y, discs and cylinder caps get a PLANAR disc
|
|
2643
2942
|
map inscribed in the unit square) but the doc comment is the source.
|
|
2644
2943
|
- Entry rebuild order: `setGeometry`/`setMaterial` re-add the entry at the
|
|
2645
|
-
list END and
|
|
2646
|
-
|
|
2647
|
-
(setMaterial swaps `mesh.material` before the rebuild, so _detach must
|
|
2648
|
-
not read the new material's flag).
|
|
2944
|
+
list END and rebind it with the new material's key, so the core's next
|
|
2945
|
+
flush re-sorts and the mesh keeps its place.
|
|
2649
2946
|
- `lathe` takes a CLOSED profile (a cross-section with thickness, or run
|
|
2650
2947
|
to the axis at x = 0) - it is a solid of revolution, NOT Three's open
|
|
2651
2948
|
polyline shell. An "open" outline must be closed by the author;
|