feat(picking): ray-pick Models via _worldBounds + linear scan#112
Merged
Conversation
Adds @adobe/data-graphics and data-graphics-samples packages with a full PBR IBL renderer (split-sum, BRDF LUT, prefiltered env, irradiance). Key fix: BRDF LUT was all-zeros because importance_sample_ggx degenerates when N=(0,0,1) — cross product produces (0,0,0) whose normalization is NaN, so every sample was silently rejected. Fixed by computing H directly in tangent space in brdf-lut.ts, bypassing the TBN rotation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace imperative loadGltfModel() calls with a declarative ECS pattern. Users insert a Geometry entity with pbrModelUrl; the pbrModelLoader system fetches the GLTF, uploads to GPU, and writes pbrModelBounds when done. A Model entity references a Geometry via pbrGeometryRef and carries node transforms + visibility — renderers only draw primitives whose Geometry has at least one visible Model. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Split the combined GPU mesh+material entity into two: PbrMaterial holds the bind group, PbrPrimitive holds vertex/index buffers and a pbrMaterialRef pointing to its material. Renderers build a materialMap per frame and skip redundant setBindGroup calls by comparing bind group object identity. This is the foundation for material sorting (group draws by bind group to minimize GPU state changes) and future instanced rendering where materials and geometry vary independently across draw calls. Fix: use GPUBindGroup object reference as the redundancy sentinel instead of an integer ID; ephemeral entity IDs are negative starting at -1, which collided with the -1 integer sentinel and silently skipped setBindGroup on the first primitive every frame. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Both IBL and direct renderers now issue drawIndexed with instanceCount derived from the ECS Model entities. Each visible Model contributes a mat4x4 (built from position/rotation/scale) to a per-Geometry GPU storage buffer uploaded each frame. The vertex shader reads the matrix via @Builtin(instance_index) and applies a cofactor normal transform. Adds pbr-ibl-instanced sample: 4×4 grid of DamagedHelmet sharing one Geometry entity, rendered with a single drawIndexed per primitive. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extracts createCubemap, cubeFaceView, cubemapSampleView from the private ibl/ folder into a public GPU namespace (src/gpu/) following the domain namespace pattern for types we don't own. render-helpers.ts re-exports from there so existing ibl/ imports are unchanged. Documents the domain namespace pattern in namespace.md: purpose-driven name (GPU not GPUDevice), no type re-export, static-access tree-shaking. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a parent/world-transform system and a solar system demo that exercises nested model hierarchies with orbital animation over PBR IBL-lit spheres. - node plugin gains `parent: number` (default 0 = root) on all entities, keeping roots and children in the same archetype so a single forward pass computes world matrices in insertion order - new transform plugin computes `worldMatrices: Map<number, Mat4x4>` each preRender; renderers (pbr-ibl, pbr-direct) read it instead of computing TRS inline - pbrShapes plugin: `insertSphere` + procedural UV-sphere geometry with `createColorMaterial` supporting emissive/metallic/roughness overrides - solar-system sample: Sun (emissive), Mercury, Venus, Earth, Mars + Moon whose transform parents to Earth, demonstrating the hierarchy Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Vertices no longer have the glTF node hierarchy baked in at load time. Each PbrPrimitive now carries pbrNodeLocalMatrix (the node's local-to- model-root matrix); renderers pre-multiply modelWorldMatrix × nodeLocalMatrix per primitive to build effective GPU instance matrices. - pack-vertex-buffer: remove worldMatrix param; vertices stay in node-local space, making them compatible with future GPU skinning (joint matrices replace the rigid node transform) - load-gltf-model: store worldMatrix as pbrNodeLocalMatrix per primitive; transform node-local AABB corners by world matrix for correct model bounds - pbr-core: add pbrNodeLocalMatrix component to PbrPrimitive archetype - pbr-ibl + pbr-direct: collect modelMatsByGeo as Mat4x4[], then per- primitive compute effective = modelMat × nodeMatrix, write to per- primitive instance buffer (keyed by primitive entity ID) - pbr-shapes: pass Mat4x4.identity as pbrNodeLocalMatrix (sphere vertices are already in object-local space) - gltf-types: add GltfSkin type, skin? on GltfNode, JOINTS_0/WEIGHTS_0 on GltfPrimitive, skins? on GltfAsset (type-level skinning prep) Single-mesh and instanced rendering unchanged in output; multi-mesh glTF models now correctly separate per-node transforms from vertex data. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PBR is just a collection of plugins and types. src/pbr/ was an
unnecessary intermediate layer that mirrored the top-level structure.
- pbr/types/{pbr-material,standard-vertex} → src/types/
- pbr/plugins/pbr-core, pbr-shapes → src/plugins/ (flat, simple files)
- pbr/plugins/pbr-model-loader + pbr/gltf/ → src/plugins/pbr-model-loader/
(cohesion rule: plugin promoted to folder, owns its gltf/ sub-files)
- pbr/plugins/pbr-ibl + pbr/ibl/ + IBL shaders → src/plugins/pbr-ibl/
- pbr/plugins/pbr-direct + direct shader → src/plugins/pbr-direct/
- pbr/bind-group-layouts.ts → src/plugins/ (private shared file)
- src/pbr/index.ts removed; its exports folded into src/index.ts
- All import paths updated; zero type errors
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ansforms Demonstrates pbrNodeLocalMatrix working correctly across 6 glTF nodes (truck body + 2 wheel groups at translated positions, all under a Y-up convention rotation root). The wheels render at their correct offsets relative to the truck body — proving the per-primitive node matrix path. Model: CesiumMilkTruck © 2017 Cesium, CC-BY 4.0 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…antique-camera All model and HDRI references now point to Khronos Sample Assets and Poly Haven CDN respectively — no binaries committed to the repo. CesiumMilkTruck sample replaced by AntiqueCamera (richer PBR materials). public/models/ and public/env/ added to .gitignore. Binary assets removed from branch history via git filter-branch (they existed only on this branch, never in main). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…d config Reflects the broader ambition of the package — not just rendering but general WebGPU compute as well. packages/data-graphics/ → packages/data-gpu/ packages/data-graphics-samples/ → packages/data-gpu-samples/ @adobe/data-graphics → @adobe/data-gpu data-graphics-samples → data-gpu-samples dev-graphics script → dev-gpu CI deploy path → gpu-samples Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Animation plugin: AnimationClip + AnimationPlayer archetypes, sample system, schema-driven interpolation dispatch (no type-specific switches in the animation system). Quat declares slerp in its schema. - Per-component interpolators via optional Schema.interpolators field. componentwiseLerp is the default for arrays of numbers/scalars. - Solar-system: orbit animation now driven by AnimationClip keyframes + AnimationPlayer instead of bespoke orbitSystem + Map closure. - All sample plugins gained an initializeScene transaction that bundles setIblEnvironmentUrl + setLight + insertGeometry + insertModel and registers an autoFitOrbit system so the camera fits to bounds automatically once the asset loads. - All sample elements switched to DatabaseElement<typeof plugin> + hooks (useElement, useEffect, useOrbitDragCamera). No more @State fields, pointer handlers, firstUpdated overrides, or service-helper constructors. Lit hooks subsystem is now the primary lifecycle path. - Drag-to-orbit gesture extracted into a shared useOrbitDragCamera hook. - type-casts.md rule expanded to clearly enumerate the two valid uses of `as` (proving validity TS cannot see; widening literals/defaults). All new code reviewed for compliance; identity casts removed. - Misc: Quat type defined directly as readonly [n,n,n,n] tuple rather than via Schema.ToType<typeof schema> so the schema can reference slerp without a type-level cycle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Composes the four-layer pipeline:
1. animation: animationSampleSystem advances clip time, samples each
track at the current time, writes the sampled value to the joint
entity's component (position/rotation/scale). Schema-declared
interpolators dispatch slerp automatically for Quat tracks.
2. transform: existing transformSystem walks the joint hierarchy and
produces a worldMatrices map for all node-archetype entities,
including the new joint entities.
3. pbrSkinningMatrixSystem (new): for each Skeleton, computes
inverse(modelWorld) × jointWorld × IBM per joint and uploads
N×Mat4x4 to the skeleton's storage buffer; also writes the model
world matrix to the skeleton's per-Model instance buffer. Runs in
preRender so the IBL renderer sees fresh joint matrices.
4. pbrIbl renderer: builds a second pipeline using a skinned WGSL
variant. Skinned VS attributes are a separate vertex buffer
(uint32×4 joints + float32×4 weights) so static meshes pay no
per-vertex cost. Bind group 3 has two storage entries — instance
matrix + joint matrices — staying within WebGPU's default
maxBindGroups = 4.
glTF parsing:
- parse-skin.ts builds the joint template (per-joint local TRS +
parent-joint index) and reads inverseBindMatrices into a flat
Float32Array.
- parse-animations.ts converts channels/samplers to AnimationTrack[],
resolving target glTF node indices to joint indices for clip
portability across instances. linear / step / cubicSpline modes
are passed through (cubicSpline TBD on the consumer side).
- pack-vertex-buffer tolerates missing NORMAL / TEXCOORD_0 (Fox lacks
both) and packs an optional secondary skinning attribute buffer when
JOINTS_0 + WEIGHTS_0 are present.
- load-gltf-model synthesises sequential indices for non-indexed
primitives (Fox is non-indexed).
- pbrInsertLoadedPrimitives also inserts AnimationClip entities for
each parsed animation and writes their ids onto the Geometry as
animationClipRefs.
pbr-skinning plugin (new): owns the Skeleton archetype + a lazy init
system that, for each Model whose Geometry has skinJointTemplate,
spawns one joint entity per template entry (reusing the node plugin's
TRS components), allocates two GPU buffers + a bind group for the
skeleton, sets animationSkeletonRef on the Model, and — if the
Geometry came with clips — also inserts an AnimationPlayer whose
animationTargets are the new joint entity ids. The same generic
animation plugin drives the joints; no skinning-specific animation
code.
Sample: skinned-fox loads Khronos Fox.glb and plays the walk-cycle
animation. Camera plane range is widened (orbitRadius × 4) because
Fox is authored in cm — the default 0.1/100 planes clip it.
Misc:
- node plugin gains a Node archetype so joints can be inserted as
plain TRS entities.
- pbrCore gains pbrSkinVertexBuffer + skeletonModelRef +
skeletonJointMatrixBindGroup + animationSkeletonRef so renderers
can query for them without depending on pbr-skinning.
- All sample Model.insert call sites now pass animationSkeletonRef: 0.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ugin Every sample had its own orbit resources, transactions, and camera system. The pattern was nearly identical: orbitCenter / Radius / Height / Angle / AutoSpin resources, a setOrbit / addOrbitAngle / resumeAutoSpin transaction trio, an orbitCamera system that wrote the scene-uniforms `camera` resource each frame, and (for the asset-loading samples) an autoFitOrbit system that read pbrModelBounds and called setOrbit. Extracts all of that into `orbitCamera` in @adobe/data-gpu. Each sample plugin now just extends it and writes the auto-fit factors (or, for solar-system, the fixed orbit values) inside its `initializeScene` transaction. The near/far planes derive from the orbit radius so big models like Fox no longer get clipped by the default 0.1/100 planes. Adds a `useOrbitCameraControl(service)` hook on the samples side that wires drag-to-rotate + drag-end-resumes-auto-spin to any service that extends orbit-camera. The previous useOrbitDragCamera (lower-level, callback-shaped) stays for one-off uses; useOrbitCameraControl is the default for samples. Net: -172 lines across the samples, 14 files changed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Records the rule and the two public CDNs we use (Khronos Sample Assets for glTF, Poly Haven for HDRIs), so future agent runs don't re-add the binaries we already had to scrub from git history once. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Records the work we discussed but deferred: tests for the animation and skinning code, shadow mapping, multi-clip blending, cubicSpline interpolation, asset URL dedup, skinned-mesh instancing, camera improvements, and smaller items. So none of these get lost while we focus on GPU compute next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
50k–1M boid flock simulation written entirely against WebGPU compute.
Architecture:
Per frame, six command-buffer ops, zero CPU↔GPU sync:
1. clear_cells — zero the 32³ uniform-grid cell-count atomics
2. populate_grid — atomicAdd cell counters per boid
3. prefix_sum — single-thread exclusive scan to cellOffsets
(also seeds cellWriteCursors)
4. bin_boids — atomic-scatter boid indices into sortedIndices
5. update_boids — read sorted neighbours from current cell and
26 adjacent cells, apply cohesion / alignment
/ separation, write new state to the ping-pong
target. Boid 0 also writes the indirect-draw
args (5 × u32).
6. drawIndexedIndirect — vertex shader indexes into the just-written
state buffer; orientation derives from velocity.
No per-boid entities: state lives entirely in GPU storage buffers
referenced from the database as resources. Two state buffers form a
ping-pong; bind-group swap is the only per-frame state change. Render
pass reads the buffer the compute pass just wrote, so the rendered
flock is always one frame fresh, never stale.
Performance on this machine (M-series Apple Silicon, browser):
50k boids 120 fps (vsync-capped)
100k boids 120 fps (vsync-capped)
250k boids 49 fps 20 ms/frame
500k boids 17 fps 60 ms/frame
1M boids 4 fps 260 ms/frame
Reference points: the WebGPU canonical sample baselines at 1.5k boids;
Sebastian Lague's Unity flocking demo runs 200k at 60 fps natively.
Bottleneck above 250k is the update pass's neighbour-walk cost — uniform
grid is fine up through 100k+, spatial-hash + workgroup-shared-memory
caching would push it further.
Future generalisation: the resources-as-buffers pattern, the indirect-
draw flow, and the compute-pass orchestration are all candidates to
lift into a `gpuCompute` plugin in @adobe/data-gpu — but we'll
generalise from the sample, not before.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
50k boids in a 20-unit world filled the volume so uniformly that no emergent group behaviour was visible — every boid had ~45 neighbours inside its view sphere, so cohesion/alignment averaged out to nothing. Two changes: - Lowered DEFAULT_BOIDS to 4 000, made the arrowhead 3× larger, brought the camera closer, and bumped alignment / max speed. Now distinct flocks form, drift, and split. - Replaced toroidal wrap with an aquarium: a linear inward force ramps up over the last 2.5 units of each axis (`wallGain = 12`), and a hard clamp at ±worldExtent is the last-resort backstop. Flocks make smooth U-turns at the walls instead of teleporting across. Grid was tightened: 1000 cells (10³, cellSize = 2.0) which matches the boid view radius so the 3×3×3 neighbour scan covers every possible neighbour. The previous 32³ grid had cellSize = 0.625 — smaller than the view radius — so neighbours outside the 3×3×3 window were silently missed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three changes to push the sim from a jiggling fog into visible
schooling:
1. Cut DEFAULT_BOIDS to 2 500. With viewR ≈ 1.6 in a 20-unit cube
this gives ~8 neighbours per boid — the Reynolds sweet spot.
Above that, force averaging smooths every group out to nothing.
2. Seeded positions via rejection sampling against a 3D sum-of-sines
density function ('densityNoise'). Boids start clumped into a few
blobs at ~6-unit feature scale instead of a uniform cloud. The
dynamics inherit that structure on frame 1.
3. Seeded velocities from a curl-noise-like flow field. Boids in the
same blob start moving in the same direction, so alignment locks
them into a coherent flock immediately rather than spending the
first few seconds finding each other.
Also tightened the view radius (separationDist 0.8 → viewR 1.6) and
softened cohesion (gain 0.6) so flocks can break apart and reform
instead of clumping into one ball.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Aquarium walls were trapping flocks against faces — the wall force + the flock's alignment kept everyone pointed at the boundary, where they jiggled. Wrap-around lets schools migrate freely across the volume; they reappear on the opposite face and continue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Mouse moves are unprojected onto the plane through the orbit center perpendicular to the view, then fed into the compute shader as a world-space scare point. Boids within scareRadius (3 units) feel an outward force that ramps linearly from full at distance 0 to zero at the radius — strong enough (gain 18) that the swarm carves a clear void around the cursor and flocks visibly stream out of the way. Params struct grows from 48 to 80 bytes: + scareData : vec4f xyz = position, w = active flag + scareTuning : vec4f x = radius, y = gain, zw unused Element listens for pointermove/leave on the canvas and calls setScareFromNdc / disableScare. The unprojection lives in the transaction since it needs the camera resource. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Auto-loads whenever a plugin file is touched (path patterns match
plugins/** and *-service.ts). Captures the framing we worked out:
- Two orthogonal questions: what's authored (human surface) vs.
what's derived (system implementation, AI's land).
- Two tiers of model plugins: a small fixed core (node, camera,
light, model+geometry) and an open set of authoring abstractions
(orbit, shape, animation, ...) that systems expand into core
state.
- A concise visualisation format for sketching plugins: indent
components/resources/archetypes; archetypes compose via
[field, ...OtherArchetype]; entity references typed as EntityId
drop the 'Ref' suffix; drop implementation prefixes (pbr/ibl)
from the conceptual view.
Includes a worked example showing what `corePlugin` looks like under
this format — 20 fields total covers the whole renderable-scene
surface.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The previous draft baked graphics vocabulary into the framing (renderer,
camera, light, IBL) which made it look graphics-specific even though
the model/system + authored/derived split applies to any ECS plugin.
Changes:
- Worked example is now a chat domain (user, channel, message) which
exercises every part of the format (components, resources, archetypes,
spread composition, optional fields) without any rendering vocabulary.
- "Core model" framed as "primary record types of the domain" rather
than "what every renderer needs."
- Path patterns broadened to **/*plugin* and **/*plugin*/** so the
rule auto-loads on any file or folder containing the word plugin,
not just files matching the data-gpu samples conventions.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…convention
Records the conventions we settled on:
- System plugin format (query / read / write outline) alongside the
existing model plugin format.
- Query DSL: Archetype+component-component, comma-separated for
multiple independent queries.
- Underscore prefix marks ephemeral / derived / not-part-of-data-model
in both docs and code. Applies to component, resource, and archetype
names.
- Inline ': Type' annotations appear only on _-prefixed names; the
type of authored items is read from the data plugin where they
are declared.
Rule trimmed for a Sonnet-or-better reader — no extended worked
examples, just the format spec and conventions. The conventions
generalise the model format too (shared section), so both formats
stay consistent.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…refix from plugins, suffix plugin files Nest every feature folder (node, camera, light, model, animation, orbit, scene-uniforms, pbr, vertices) plus the scene aggregator under graphics/, leaving the package root open for a future compute/ sibling. Strip the _ prefix from plugin and system names — folder grouping now carries the "implementation vs. authored" signal. The _ prefix stays on components, archetypes, and ephemeral resources where it still marks derived state inside the data model. Every plugin file ends in -plugin.ts (node-plugin.ts, pbr-core-plugin.ts, ...) so cursor rules and grep can match plugins by filename without inspecting contents. Export names stay short (node, pbrCore, ...). Type relocations: - types/* folders move next to the feature that owns them - pbr-material renamed to visible-material; archetype _PbrMaterial → _VisibleMaterial - animation-types.ts split into proper namespaces: interpolation-mode/ and animation-track/ (with AnimationTrack.sample) - gltf-types.ts renamed to gltf-schema.ts (external-schema lump) - ColorMaterialOptions moved into visible-material/ where it belongs Animation performance: split the plugin into a data half plus the systems half, declare advanceAnimations' t parameter as the store type via Database.Plugin.ToStore, and run the loop twice each frame — once through db.transactions (observable players, observers fire) and once directly against db.store (non-observable players, no notification overhead). Two archetypes (Animation, AnimationObservable) so the choice happens at insert time without a row migration. Transform: split _worldMatrix creation from world-matrix computation so the second system can write into an existing column without migration. Rules updated: - namespace.md: multi-declaration files section, never *-types.ts, selective -plugin file suffix - plugin-modelling.md: _ prefix scoped to components/archetypes/ resources; schemas-over-defaults for components; archetypes rule - archetypes.md (new): declarative include/exclude queries, tail→head migration Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Scare is now a ray from the camera eye through the cursor toward the far plane; boids within a perpendicular radius of that line are pushed outward regardless of depth. Camera rotation is honored by virtue of the ray being computed from cam.forward/right/up each frame. - Default boid count up to 4000 for a denser flock. - Boid color: hue from facing direction (forward unit vector remapped to [0,1] per channel), intensity scaled 0.35→1.0 by speed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… + API cleanup
## Structure
- `scene/` — authored content: `node/`, `model/`, `light/`, `scene-uniforms/`
- `node/`: `node-data-plugin` + `transform-plugin` + `node-plugin` (combiner)
- `model/`: `model-plugin` (data), `model-loader-plugin`, `world-bounds-plugin`, `gltf/`
- `light/`: `light-plugin` (data)
- `scene-uniforms/`: type + plugin (camera + light → GPU uniform buffer)
- `scene-plugin.ts`: `scene = combine(Node.plugin, model, SceneUniforms.plugin)`
- `rendering/` (was `pbr/`): `pbr-core-plugin`, `ibl-render/`, `skinning/`,
`standard-vertex/`, `visible-material/`, `rendering-plugin.ts` (aggregator)
- `camera/orbit/` (was `orbit/`): `orbit-data-plugin`, `orbit-system-plugin`,
`orbit-plugin` (combiner), `attach-orbit-drag.ts`
- `animation/`: `animation-data-plugin` + `animation-plugin` (full)
- `gpu/` helpers moved into `rendering/ibl-render/ibl/` (only consumer)
- `vertices/` deleted (PositionColorNormalVertex was unused)
## Plugins
- `scene` now includes `SceneUniforms.plugin` — one import gives node + model +
light + camera + GPU uniform packing
- `rendering` export added as intent-forward alias for `pbrIblRender`
- `pbrDirectRender` removed — `pbrIblRender` already has a procedural fallback
when no `environmentUrl` is set
- `picking-plugin` split into `pickingBase` + `picking`; `PickingDB` derived via
`Database.Plugin.ToStore` — no `any` in the ray scan impl
- `pickFromNdc` → `pickFromScreen({ x, y })` — takes pixel coords directly;
NDC conversion happens internally with inline comments
- `setScareFromNdc` → `setScareFromScreen({ x, y, width, height })` — same
- `attachOrbitDrag` exported from `@adobe/data-gpu` — framework-agnostic DOM
helper replaces the lit-specific orbit hook impl
## Type safety
- `pbr-core-plugin`: `null as unknown as GPU*` → `null as GPU* | null`
(surfaced two latent null-safety bugs in ibl-render, now guarded)
- `model-loader-plugin`: `_bounds` default is `null as Aabb | null`
- `sample.ts`: return type `any` → `number | number[]`
- `compute-world-matrices`: invariant comments added to remaining casts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bridging physics bodies to the renderer is exactly when you want render-rate interpolation, so the bridge now provides it — samples get smooth decoupled physics with no extra plugin to remember (rigid-stack reverts to the plain combine). interpolation stays exported for custom non-bridge render paths. Also fix the clock/interpolation tests' store access to the loose-cast pattern the solver benchmark uses (a created DB's writable store isn't on the public Database type), so tsc -b passes alongside vitest. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Kinematic bodies (bodyType "kinematic") are now mirrored as engine-kinematic (Rapier kinematicPositionBased / Jolt EMotionType_Kinematic in the dynamic layer) and driven to their authored position/rotation each step (setNextKinematic* / MoveKinematic), so they push dynamics without being pushed back. They render at the live authored pose (no prev-snapshot), and the kinematic-drive query keys on that absence — distinguishing kinematic from dynamic and static by archetype shape, no per-row value test. rigid-stack gains a steel bar that sweeps through the bin, plowing the stack — a visible side-by-side demo of kinematic→dynamic push on both solvers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add "capsule" to ColliderShape (Y-aligned: halfExtents.x = radius, .y = cylinder half-height). Both solvers build it (Rapier capsule(halfHeight,radius) / Jolt CapsuleShape) and ColliderShape.massProperties gains the exact cylinder + two-hemisphere inertia. Rendering: a capsule can't be non-uniformly scaled without distorting its caps, so it's built at real size (capsuleMesh) and drawn at unit scale — the bridge caches one mesh per distinct (radius, half-height). Shared uploadShapeMesh helper. rigid-stack now drops boxes, spheres, and tumbling capsules. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…dering) Incremental 3D convex hull (convexHullMesh / hullFaces): seed a tetrahedron, fold each point in by deleting visible faces and bridging the horizon, with every face oriented outward via a fixed interior point (seed centroid) — robust without half-edge bookkeeping. Produces a flat-shaded StandardVertex mesh so an authored point-cloud convex collider can be rendered (Phase-2 auto-hull will reuse it). Tested: tetra→4, octahedron→8, cube→12 (Euler), interior points discarded, degenerate/coplanar→null. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add colliderShape "hull" + a runtime convexPoints component and a ConvexBody archetype (dynamic/kinematic hulls; halfExtents unused). Both solvers build the collision hull from the points (Rapier convexHull; Jolt ConvexHullShapeSettings), the bridge builds the render mesh via convexHullMesh and caches it per point-array reference (shared clouds share one mesh + instanced draw), drawn at unit scale. rigid-stack now drops random convex polyhedra alongside boxes/capsules/spheres. Authored point-cloud path (the chosen "points now" model); the "auto-hull from a render Geometry" convenience is the deferred Phase 2. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add colliderShape "mesh" + a ColliderMesh type, a runtime colliderMesh component, and a MeshCollider archetype (static only — a triangle soup has no interior). Both solvers build the engine trimesh from the authored verts/indices (Rapier trimesh; Jolt MeshShapeSettings via VertexList + IndexedTriangleList), the bridge builds a flat-shaded render mesh (flatShadedMesh) cached per colliderMesh ref. rigid-stack gains a static mesh ramp that dropped bodies land on and slide down. Completes K3 Phase 1 (authored hull + mesh). Note: flat-shaded render meshes use uint16 indices — fine for authored ramps/props, not yet dense terrain. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…the stack Enlarge the bin (BIN 7→12) and split it: the stable wood tower sits in the right zone at +6.5; the sweeping kinematic bar + the mesh ramp are confined to the left (sweep centre −4.5 ± 4.5), so the bar churns the dynamic pile without ever reaching the stack. Drops now rain across the whole bin, so some still land on and top the tower. Camera pulled back to frame the larger scene. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Retain model-space CPU positions+indices on Geometry (_cpuPositions/_cpuIndices, aggregated over non-skinned primitives with node matrices baked). New modelCollider plugin: a ModelBody / StaticModelCollider carries a geometry + colliderShape hull|mesh but no collision data, and a postUpdate system fills it once the mesh loads — hull → simplified hullVertices (engine rebuilds the hull), mesh → the triangles verbatim — sourced from collisionGeometry ?? geometry, with the model's scale baked in, cached per (geometry, shape, scale). Hand-authored colliders still work (generation only runs when the data is absent). Also link interpolation into the model render path: interpolateWorldMatrix recomposes _worldMatrix from the interpolated pose (after transformSystem) so a physics-driven model renders smoothly instead of at the stepped sim pose. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…l body Both solvers now skip mirroring a hull/mesh body until its collision data exists, so an auto-collider body whose source model is still loading isn't mirrored with a placeholder shape — it's picked up the frame the data lands. rigid-stack drops a downloaded DamagedHelmet (CC-BY 4.0) as three dynamic bodies: rendered in full detail, colliding as a convex hull auto-generated from the mesh, on both solvers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a solver-agnostic jointData model (Joint archetype: type, two bodies, local anchors, hinge axis + angle limits) and a JointType enum. Both solvers mirror joints once both bodies exist (tag + exclude, like body sync): Rapier fixed/revolute/spherical impulse joints; Jolt Fixed/Hinge/Point constraints (world-space anchors mapped from the bodies' spawn pose). rigid-stack hangs a chain of capsule links joined by point joints, anchored to a static box, that the sweeper knocks around. Add physics/README.md: a checkboxed feature roadmap — what's shipped (solvers, body types, shapes, clock+interpolation, auto-colliders) and what's next, including the single-hull-is-convex-approximation limitation and the ragdoll path (per-bone colliders + this joints layer + a controller). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ities Add a `cone` joint type: the bone axis is bound to a swing cone (half-angle jointSwingLimit) around the reference axis, with a twist range (jointMinLimit/jointMaxLimit) — the anatomical shoulder/hip limit ragdolls need. Full on Jolt (SwingTwistConstraint); the Rapier compat binding has no cone constraint, so it approximates `cone` as a free spherical (documented — use Jolt for ragdoll limits). rigid-stack adds a cone-limited arm in a clear corner: on Jolt it droops to the cone limit and leans; on Rapier it hangs straight down. Also fix both solvers to apply a body's authored linearVelocity AND angularVelocity at creation (Rapier set only linear before; Jolt set neither) — correct behaviour, and needed for spun/launched bodies. Roadmap: cone-twist limits checked off. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…admap Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tep 1) fitBoneCapsules: assigns each skinned vertex to its dominant bone, pushes that bone's vertices into bind-local (via the joint's inverse-bind matrix), and fits a capsule — longest local axis as the capsule axis, perpendicular spread as the radius. Output is a bone-local offset (position + a rotation orienting the Y-aligned capsule onto the fitted axis) + dims, so the capsule's world pose each frame is jointWorldMatrix · offset (tracking the animated skeleton). Pure + tested (X/Y-elongated boxes, per-bone grouping, sparse-bone skip). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…oll step 1) Retain CPU skin (mesh-bind positions + 4 joints + 4 weights) on skinned Geometries. New boneColliders plugin: once a skeleton's skin loads it fits one capsule per bone (fitBoneCapsules) and spawns a kinematic capsule body per bone; each frame trackBoneColliders places it at jointWorldMatrix · offset, so the capsules follow the animated skeleton (rendered over the character as a debug proxy). Flipping them to dynamic + joints is the next step (the controller). New `ragdoll` sample: CesiumMan (CC-BY 4.0) plays its walk clip while 19 auto-fitted capsules track its bones. Verified in-browser (capsules on the limbs, animation cycling). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…olver Add a collisionGroup component: bodies sharing the same non-zero group don't collide with each other (they still hit the world) — honored by rapierSolver via per-collider groups (Jolt support is a follow-up). Bone capsules get group 1, so a ragdoll's bones never self-collide. The ragdoll sample now runs on rapierSolver with a ground slab, and the model is lifted so the (coming) ragdoll drops onto it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…econcile triggerRagdoll (on boneColliders): joints each bone capsule to its nearest capsule-bearing ancestor with a point joint (anchored at the shared joint, from the current pose), flips every capsule kinematic→dynamic, and stops the animation. The Rapier solver performs the engine flip (setBodyType Dynamic + prev-pose snapshot) when it sees a kinematic body's bodyType become dynamic. reconcileRagdoll then writes each dynamic capsule's pose back onto its skeleton joint (capsuleWorld · offset⁻¹, relative to the parent's capsule-derived world), so the skinned mesh goes limp and flops; trackBoneColliders skips dynamic bones. ragdoll sample: CesiumMan walks, then auto-ragdolls after 4s — 19 capsules flip, 18 joints hold it together (collisionGroup 1 prevents self-collision), it drops onto the floor and the skin collapses with it. Verified in-browser. Cone limits + Jolt flip/collision-groups are follow-ups (point joints + Rapier for v1; see physics/README.md). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…on Jolt) The v1 ragdoll used point (ball) joints — no angular limits — so the limbs folded freely and the body curled into a tight spiral. Fix it properly: - joltSolver gains the kinematic→dynamic flip (SetMotionType) and a no-self- collide RAGDOLL object layer, so collisionGroup>0 bodies (a ragdoll's bones) collide with the world but not each other. - the controller now jointing each bone with a cone (swing-twist) joint whose reference axis is the bone's current direction, limiting it to ~52° swing / ~±29° twist from rest — anatomical, not a free ball. - the ragdoll sample runs on joltSolver (cone limits are Jolt-only; Rapier's binding has no cone constraint). Verified in-browser: CesiumMan walks then collapses to a *spread* sprawl on the floor (z-span ~1.4) instead of a tight spiral; no self-collision explosion, no spinning, no errors. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ollTrigger Split the ragdoll sample into two panels (like rigid-stack): a shared base scene (model + floor + autoplay + auto-ragdoll, backend-agnostic) combined per solver. Jolt shows the cone (swing-twist) limits; Rapier the free-ball ragdoll — an honest side-by-side through the same skeleton. Extract ragdollTrigger (the _ragdollTrigger flag + triggerRagdoll transaction) into its own plugin so the base scene drives whichever backend is combined in — setting up a Jolt-native Ragdoll backend to replace our generic one on the Jolt panel. boneColliders now extends it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e with ours Add joltRagdoll: a Jolt-native ragdoll using Skeleton / RagdollSettings / Ragdoll / SkeletonPose. The joltSolver now publishes a _joltContext (jolt module + PhysicsSystem + the no-self-collide ragdoll layer) so the ragdoll is built in the same world as the floor. Once the skin loads it builds one dynamic body per bone (capsule from fitBoneCapsules, tiny sphere otherwise) + a swing-twist to-parent constraint + DisableParentChildCollisions; while alive it DriveToPoseUsingKinematics toward the animated pose, and on triggerRagdoll it stops driving (bodies fall) and reads the pose back — moving the model to the ragdoll root + applying local joint states so the whole skin flops. The ragdoll sample now runs the two backends side by side: Jolt-native Ragdoll vs our generic boneColliders (Rapier). ECS-opt: the SkeletonPose + Vec3/Quat scratch are built once and reused each frame (no per-frame WASM allocation). Verified in-browser: both walk then collapse onto the floor; the Jolt skeleton settles at y~0.03-0.14; no errors. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…eric) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts: # packages/data-react-hello/package.json # packages/data-solid-dashboard/package.json # packages/data/src/schema/index.ts
…tabase vite-plugin-checker now type-checks the samples, surfacing pre-existing errors in the p2p sample: the negotiation/presence bootstrap containers fed the UI-restricted service to a controller and a streaming async-generator transaction (both need the full database), and read a non-existent `.sync` view for the peer id. Cast `this.service` to the full database (it is the live database at runtime; the UIService restriction only narrows the type for pure widgets) and read the peer mark from `concurrency.userId`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… casts Bootstrap containers (those that own a controller or drive a streaming async-generator transaction) need the full transactional surface, not the UI-restricted view. Previously DatabaseElement stored the live database typed only as UIService.FromService, forcing every such container — and the p2p sample — to cast (`as unknown as`, `as any`) to recover the real type. Fix at the source: DatabaseElement now stores the live `database` with its true `ToDatabase<P>` type (the DI target), and derives the restricted `service` view through a new `UIService.restrict` helper. `restrict` performs the sole full→restricted narrowing with zero casts via the overload- implementation pattern (precise mapped return on the overload, broadened `Service` implementation signature for the identity body). The narrowing is sound by construction — `T` is always assignable to `FromService<T>` — TS just cannot prove it for a deferred generic conditional. Consumers are now cast-free: the negotiation/presence bootstrap containers read `this.database`, the DI wrappers bind `.database`, and the peer id comes from `concurrency.userId` (there is no `.sync` view). The restricted `service` getter is unchanged for pure widgets; database-element.type-test still holds. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds ray-against-Model picking as a foundation for selection, hover, and (later) physics. Linear-scan today; a comment block in `picking-plugin.ts` sketches a `Broadphase` resource interface so a uniform-grid / BVH / GPU-compute impl can slot in without changing the public API.
Changes
Test plan
🤖 Generated with Claude Code