Skip to content

threedim: new 3D asset, scene-graph and object processes (from #2109 stack) - #2122

Open
jcelerier wants to merge 12 commits into
plane/interopfrom
planf/objects
Open

threedim: new 3D asset, scene-graph and object processes (from #2109 stack)#2122
jcelerier wants to merge 12 commits into
plane/interopfrom
planf/objects

Conversation

@jcelerier

Copy link
Copy Markdown
Member

The first cluster of new 3D objects from split/threedim, on top of the interop stack (#2121). These processes are cross-coupled (they share loader/scene headers, so intermediate subsets don't build) and therefore land as one coherent unit — the same way the scene rework did.

Stacked on #2121; retargets to master as the stack merges.

What it adds

  • Asset & geometry loaders: glTF/FBX import (via new fastgltf and ufbx submodules), unified AssetLoader, image/buffer/texture helpers, ObjLoaderGeometryLoader rename.
  • Cameras, lights, transforms: Camera, CameraArray, CameraSwitch, Light, Transform3D.
  • Scene-graph family: scene preprocessor, filters, switch/selector/group, duplicator, from-meshes, inspector, resource routing.
  • Buffer/texture inject & extract into/out of scene resources.
  • Materials & rendering: PBRMesh, MaterialOverride, ConfigurePrimitive, Instancer, shadow-cascade setup.
  • Animation: AnimationPlayer, humanoid retargeting, inverse kinematics.
  • Text: text-to-mesh and text-to-texture.

The existing ModelDisplay, RenderPipeline and Splat nodes keep their current scene-aware versions (the old Splat/ node is intentionally preserved). The point-cloud PrimitiveCloud family lands in a follow-up PR (it needs the spz/zstd submodules).

Validation

  • Builds cleanly against the stack (all plugins).
  • ctest: 17/17.
  • Process + device factory sweeps under ASAN + UBSan — the process sweep now constructs and round-trips all the new 3D process models (Camera/Light/Transform/AssetLoader/Scene*/Instancer/PBR/Animation/IK/Text/Inject/Extract): 0 memory errors, 0 undefined-behavior in score's own code, 0 crashes.

🤖 Generated with Claude Code

@jcelerier
jcelerier force-pushed the plane/interop branch 2 times, most recently from 07a445e to bc681f8 Compare July 17, 2026 14:10
jcelerier added a commit that referenced this pull request Jul 17, 2026
FIX-GLTF (GltfParser.cpp): validate every accessor's byte range against its
bufferView/buffer BEFORE extract_primitive dereferences it — a crafted glTF
with an out-of-range accessor offset/count read past the buffer (heap OOB).
Adds accessor_within_bounds / range_within_view / buffer_source_byte_size
guards + an all-accessors sweep in validate().

Test: test_regression_splat_reload (Splat ports stable across save/reload).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
jcelerier added a commit that referenced this pull request Jul 17, 2026
FIX-GLTF (GltfParser.cpp): validate every accessor's byte range against its
bufferView/buffer BEFORE extract_primitive dereferences it — a crafted glTF
with an out-of-range accessor offset/count read past the buffer (heap OOB).
Adds accessor_within_bounds / range_within_view / buffer_source_byte_size
guards + an all-accessors sweep in validate().

Test: test_regression_splat_reload (Splat ports stable across save/reload).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
jcelerier added a commit that referenced this pull request Jul 17, 2026
FIX-GLTF (GltfParser.cpp): validate every accessor's byte range against its
bufferView/buffer BEFORE extract_primitive dereferences it — a crafted glTF
with an out-of-range accessor offset/count read past the buffer (heap OOB).
Adds accessor_within_bounds / range_within_view / buffer_source_byte_size
guards + an all-accessors sweep in validate().

Test: test_regression_splat_reload (Splat ports stable across save/reload).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
jcelerier added a commit that referenced this pull request Jul 17, 2026
test_unit_image_loader (22 cases / 881 assertions): score::gfx
decodeImageFrom{Path,Memory} (gfx TextureLoader) + Threedim LDR ImageLoader +
ArrayToTexture, across PNG8/16, JPEG, BMP, TIFF8/16, WebP, GIF, TGA, PPM/PGM,
SVG, ICO — exact dimensions, exact/toleranced pixels, RGBA byte order, straight
alpha, top-left origin, 16-bit precision, and graceful rejection of truncated /
garbage / empty / wrong-MIME inputs (ASAN-clean).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
@jcelerier
jcelerier force-pushed the planf/objects branch 2 times, most recently from 7c5c25d to f79c9cc Compare July 18, 2026 13:37
@jcelerier
jcelerier force-pushed the planf/objects branch 2 times, most recently from 3fe10b1 to 5f1b3e3 Compare July 18, 2026 20:38
@jcelerier
jcelerier force-pushed the planf/objects branch 2 times, most recently from 582ebb1 to df39bc1 Compare July 19, 2026 21:59
@jcelerier
jcelerier force-pushed the plane/interop branch 2 times, most recently from 76333c0 to ca08bb3 Compare July 20, 2026 23:09
@jcelerier
jcelerier force-pushed the planf/objects branch 2 times, most recently from d81056a to d8d137a Compare July 21, 2026 06:13
jcelerier and others added 12 commits August 17, 2026 14:31
(cherry picked from commit a012d1cbdc702366d16093fda112fb3bba4a78dd)
fastgltf's configure step downloads the simdjson single-header when no
simdjson::simdjson target or system package exists; network-isolated
builds (Flatpak, Nix) fail there. Vendor the pinned single-header copy
(v3.12.3, what fastgltf would download) so the build is self-contained.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
Adds the new 3D processes that build on the scene pipeline: geometry and
asset loaders (glTF/FBX via fastgltf/ufbx, image and buffer helpers),
cameras, lights and transforms, the scene-graph family (preprocessor,
filters, switch/selector/group, duplicator, from-meshes, inspector),
buffer/texture inject and extract, PBR materials and instancing, animation
with humanoid retargeting and inverse kinematics, and text-to-mesh/texture.

These are cross-coupled (they share loader/scene headers), so they land as
one coherent unit. The existing ModelDisplay, RenderPipeline and Splat nodes
keep their current scene-aware versions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
Brings the new 3D objects up to their final state, folding in the bug
fixes made after they were first added: Instancer GPU use-after-free and
instance-count bounds, OBJ and glTF index bounds-checks, GPU-resource
release on partial-init failure, Camera stale transform-slot, extraction
buffer sizing and SRB layout, cubemap face sizing and UBO slots, the
spherical and triplanar projection shader fixes, animation speed and
skinned-mesh deformation, and the review-round corrections.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
ObjectGallery --list [filter]     : print every registered process (name,
                                    uuid), optionally filtered by name.
ObjectGallery --filter <substr>   : open a window, instantiate each matching
              [--seconds N]         object, route its texture output to the
                                    window and play, so all the texture-
                                    producing objects can be watched at once.

Built on the MinimalGUIApplication + window-device path the gfx testers use,
so it drives the real engine. A handy way to try the whole object catalogue.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
GltfParser.cpp: validate every accessor's byte range against its
bufferView/buffer BEFORE extract_primitive dereferences it — a crafted glTF
with an out-of-range accessor offset/count read past the buffer (heap OOB).
Adds accessor_within_bounds / range_within_view / buffer_source_byte_size
guards + an all-accessors sweep in validate().

Test: test_regression_splat_reload (Splat ports stable across save/reload).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
test_unit_image_loader (22 cases / 881 assertions): score::gfx
decodeImageFrom{Path,Memory} (gfx TextureLoader) + Threedim LDR ImageLoader +
ArrayToTexture, across PNG8/16, JPEG, BMP, TIFF8/16, WebP, GIF, TGA, PPM/PGM,
SVG, ICO — exact dimensions, exact/toleranced pixels, RGBA byte order, straight
alpha, top-left origin, 16-bit precision, and graceful rejection of truncated /
garbage / empty / wrong-MIME inputs (ASAN-clean).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
Deduplicates the raw QImage handling and picks up decodeImageFromMemory's
RGBA8888 canonicalization and footer-less TGA fallback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
On glibc < 2.40 there is no _Float16, so the R16F/RGBA16F case compiled to
nothing and the texture was uploaded with uninitialized bytes. Convert with
a software IEEE 754 binary16 (round-to-nearest-even) fallback instead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
12 Catch2 suites pinning the fixes made while building out the 3D
processes: animation speed and skinned-mesh deformation, camera state
release, cubemap face clamping, compute-extraction SRB layout,
extraction index and buffer lifetime, HDR extension handling, instancer
clamping and stale buffers, OBJ parse crashes and shader string
generation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
Fulldome was one hardcoded mapping. A dome is not one projection: which
r = f(theta) a venue wants depends on what the content is for, and the four
classical fisheye laws are not interchangeable.

  equidistant   r = theta / (fov/2)           uniform angular resolution
  equisolid     r = sin(theta/2) / sin(fov/4) equal-area, what a fisheye lens does
  stereographic r = tan(theta/2) / tan(fov/4) conformal, the "little planet" look
  orthographic  r = sin(theta)  / sin(fov/2)  parallel-projection sphere

Each denominator is f evaluated at the half-FOV, so theta = fov/2 lands on
r = 1 and the image circle meets the frame wherever the user put the FOV. The
two half-angle families reduce to fov/4 for that reason and not by accident --
it is the same normalisation, not a different one.

Separate shader snippets rather than a uniform the shader branches on: the
projection is fixed for a whole frame, so branching would cost every vertex
what selecting a program costs once.

Equidistant is unchanged from the previous behaviour bit-for-bit -- its
half_fov_rad is the old proj_ratio spelled differently -- so existing domes
keep the picture they had.

Orthographic is only meaningful to 180 degrees: sin(theta) folds past the
pole, so beyond that the rear hemisphere mirrors onto the front. Left to the
user like every other framing decision here, and said so in the comment.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
The shaders were reachable only by writing an integer to the Camera inlet from
OSC or an automation, which is not a way anyone discovers a projection. All
four are baked for every draw regardless, so they cost nothing extra to list.

Indices follow the projections[] order in the renderer. 1 stays equidistant so
documents written when it was the only dome mode keep the picture they had.
"1-pass" stopped telling the user anything once the other three arrived --
they are all single-pass -- so the entry now names the projection instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants