From ecc42e59a57f2f0dfe69f021e87d0ad1829cb623 Mon Sep 17 00:00:00 2001 From: Joe Rivera Date: Fri, 2 Oct 2026 16:42:35 -0500 Subject: [PATCH] ariadne: port the volume bunny demos to DSL + mesh-SDF volume source + reusable controls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cvcGL volume-raycast (volren_bunny) and volume-slice (volslice_bunny) demos had no Ariadne .ari version because the realizer could only load a volume FILE, while the demos synthesize the volume as the signed distance field of the bunny MESH at runtime. Enabler — a MESH-SDF volume source in the scene schema: `source: { sdf: { mesh: , dim: N } }` on a volren/volslice node. The realizer loads the mesh (optionally applying the node's `fit:` to stand/scale it, for framing parity with a geometry node) and runs cvc::sdf over an N³ grid — so any demo can turn a mesh into a volume with no asset file, pure-DSL on the stock ariadne_hello host. Guarded on CVC_ENABLE_SDF (warns + skips without it). The file-source path is unchanged (realize regression test still passes). Ports — volren_bunny.ari (distance-field isosurface) and volslice_bunny.ari (a transfer function around the zero level set), both verified rendering headless via EGL. Reusable components — they import the existing stage_lighting + camera_controls components (shared with bunny_shadow), plus two new catalog components, volren_controls and volslice_controls, that bind the renderer's two-way cvc::volren/volslice::state_settings keys (steps/ambient/supersample/shadows, quality/near-plane/max-planes) so the panels are declarative — no host C++. Added both to the component INDEX. Tests: AriadneScene.VolumeSdfMeshSource (loader parses source: { sdf }); the existing cvcgl_ariadne_realize volren/volslice path is unchanged and green. --- inc/cvc/ariadne/scene.h | 9 +++ src/cvc/ariadne/components/INDEX.ari | 12 ++++ .../ariadne/components/volren_controls.ari | 35 +++++++++++ .../ariadne/components/volslice_controls.ari | 33 ++++++++++ src/cvc/ariadne/loader.cpp | 5 ++ src/cvc/tests/ariadne_loader_test.cpp | 28 +++++++++ src/cvcGL/ariadne/scene_realize.cpp | 60 +++++++++++++++---- src/cvcGL/examples/volren_bunny.ari | 51 ++++++++++++++++ src/cvcGL/examples/volslice_bunny.ari | 58 ++++++++++++++++++ 9 files changed, 279 insertions(+), 12 deletions(-) create mode 100644 src/cvc/ariadne/components/volren_controls.ari create mode 100644 src/cvc/ariadne/components/volslice_controls.ari create mode 100644 src/cvcGL/examples/volren_bunny.ari create mode 100644 src/cvcGL/examples/volslice_bunny.ari diff --git a/inc/cvc/ariadne/scene.h b/inc/cvc/ariadne/scene.h index da8b13300..eeead7ac2 100644 --- a/inc/cvc/ariadne/scene.h +++ b/inc/cvc/ariadne/scene.h @@ -86,6 +86,15 @@ struct SceneNode { std::string source_primitive; float plane_size = 1.0f; // "plane": full edge length + // A VOLUME computed as a signed distance field from a MESH (for volren/volslice), instead of + // loading a pre-baked volume with source: { file }. `source: { sdf: { mesh: , dim: N } }` — + // the realizer loads the mesh and runs cvc::sdf over an N³ grid framed to the mesh extents. Empty + // = not an SDF source. Needs a CVC_ENABLE_SDF build (the realizer warns and skips the node + // without it). This is how a demo turns a mesh (e.g. stanford.bunny) into a volume with no asset + // file. + std::string source_sdf_mesh; + int sdf_dim = 64; + bool has_material = false; float color[3] = {0.8f, 0.8f, 0.9f}; float ambient = 0.2f; diff --git a/src/cvc/ariadne/components/INDEX.ari b/src/cvc/ariadne/components/INDEX.ari index 1730e438d..64f4d2f65 100644 --- a/src/cvc/ariadne/components/INDEX.ari +++ b/src/cvc/ariadne/components/INDEX.ari @@ -58,3 +58,15 @@ catalog: for: "a live image/raster viewer (nav grids, textures) with a colormap menu" args: [] note: "needs a host that publishes named cvc::images (ImGuiBackend::set_image) each frame" + - component: volren_controls + file: volren_controls.ari + unit: volren_controls + binds: ".graphics.root.children.bunny_volume.volren.*" # cvc::volren::state_settings + for: "a `type: volren` node (steps / ambient / supersample / volumetric shadows)" + args: [] # convention: node id `bunny_volume`; copy + rename for another + - component: volslice_controls + file: volslice_controls.ari + unit: volslice_controls + binds: ".graphics.root.children.bunny_volume.volslice.*" # cvc::volslice::state_settings + for: "a `type: volslice` node (quality / near-plane peel / max planes)" + args: [] # convention: node id `bunny_volume`; copy + rename for another diff --git a/src/cvc/ariadne/components/volren_controls.ari b/src/cvc/ariadne/components/volren_controls.ari new file mode 100644 index 000000000..5f3de1fa1 --- /dev/null +++ b/src/cvc/ariadne/components/volren_controls.ari @@ -0,0 +1,35 @@ +# volren_controls.ari — a REUSABLE Ariadne component: live controls for a `type: volren` node. +# Binds the two-way cvc::volren::state_settings keys (state at +# ".graphics.root.children.bunny_volume.volren.*"), so a slider drives the software raycaster +# with NO host C++ — the declarative replacement for the volren_bunny.cpp "Volume raycast" panel. +# +# Convention: it binds a volren node named `bunny_volume`. To drive a differently-named node, copy +# this file and change the node id in the bind paths. +# +# Use it: +# import: components/volren_controls.ari +# windows: +# - include: volren_controls + +units: + volren_controls: + window: Volume raycast + id: volren_controls + children: + - slider_int: Steps + bind: graphics.root.children.bunny_volume.volren.steps + lo: 64 + hi: 1024 + def: 384 + - slider_float: Ambient + bind: graphics.root.children.bunny_volume.volren.ambient + lo: 0.0 + hi: 1.0 + def: 0.35 + - slider_int: Supersample (AA) + bind: graphics.root.children.bunny_volume.volren.supersample + lo: 1 + hi: 4 + def: 1 + - checkbox: Volumetric shadows + bind: graphics.root.children.bunny_volume.volren.shadows.enabled diff --git a/src/cvc/ariadne/components/volslice_controls.ari b/src/cvc/ariadne/components/volslice_controls.ari new file mode 100644 index 000000000..84aa11fe0 --- /dev/null +++ b/src/cvc/ariadne/components/volslice_controls.ari @@ -0,0 +1,33 @@ +# volslice_controls.ari — a REUSABLE Ariadne component: live controls for a `type: volslice` node. +# Binds the two-way cvc::volslice::state_settings keys (state at +# ".graphics.root.children.bunny_volume.volslice.*"), so a slider drives the slice renderer +# with NO host C++ — the declarative replacement for volslice_bunny.cpp's "Volume slices" panel. +# +# Convention: it binds a volslice node named `bunny_volume` (the id the volume demos use). To drive a +# differently-named node, copy this file and change the node id in the bind paths. +# +# Use it: +# import: components/volslice_controls.ari +# windows: +# - include: volslice_controls + +units: + volslice_controls: + window: Volume slices + id: volslice_controls + children: + - slider_float: Quality + bind: graphics.root.children.bunny_volume.volslice.quality + lo: 0.1 + hi: 1.0 + def: 0.6 + - slider_float: Near-plane peel + bind: graphics.root.children.bunny_volume.volslice.near_plane + lo: 0.0 + hi: 2.0 + def: 0.0 + - slider_int: Max planes + bind: graphics.root.children.bunny_volume.volslice.max_planes + lo: 32 + hi: 2000 + def: 1000 diff --git a/src/cvc/ariadne/loader.cpp b/src/cvc/ariadne/loader.cpp index 021cccbf5..a22adb968 100644 --- a/src/cvc/ariadne/loader.cpp +++ b/src/cvc/ariadne/loader.cpp @@ -1235,6 +1235,11 @@ SceneNode parse_scene_node(const YAML::Node &n) { sn.plane_size = static_cast(num(plane, "size", sn.plane_size)); else if (plane.IsScalar()) sn.plane_size = static_cast(plane.as()); + } else if (const YAML::Node sdf = src["sdf"]) { + // source: { sdf: { mesh: , dim: N } } — a volren/volslice volume computed as the + // signed distance field of a mesh (no pre-baked volume asset). + sn.source_sdf_mesh = str(sdf, "mesh", str(sdf, "file")); + sn.sdf_dim = static_cast(num(sdf, "dim", sn.sdf_dim)); } else { sn.source_file = str(src, "uri", str(src, "file")); } diff --git a/src/cvc/tests/ariadne_loader_test.cpp b/src/cvc/tests/ariadne_loader_test.cpp index f138e1ccc..469e349b7 100644 --- a/src/cvc/tests/ariadne_loader_test.cpp +++ b/src/cvc/tests/ariadne_loader_test.cpp @@ -826,6 +826,34 @@ meta: { min_libcvc: "0.0.0" } EXPECT_TRUE(b->fit_up_y); } +// A volren/volslice volume declared as the signed distance field of a MESH: +// source: { sdf: { mesh: , dim: N } } — no pre-baked volume asset. +TEST(AriadneScene, VolumeSdfMeshSource) { + SKIP_WITHOUT_YAML(); + LoadResult r = load_string(R"( +meta: { min_libcvc: "0.0.0" } +scene: + nodes: + - node: bunny_volume + type: volren + source: { sdf: { mesh: stanford.bunny, dim: 48 } } + fit: { height: 100, up: y } + volren: { distance_field: true, isosurfaces: [ { value: 0.0, color: [0.8, 0.8, 0.9] } ] } +)"); + ASSERT_TRUE(r.ok) << r.error; + ASSERT_EQ(r.scene.nodes.size(), 1u); + const SceneNode *v = find_scene_node(r.scene.nodes, "bunny_volume"); + ASSERT_NE(v, nullptr); + EXPECT_EQ(v->type, "volren"); + EXPECT_EQ(v->source_sdf_mesh, "stanford.bunny"); // the mesh the SDF is computed from + EXPECT_EQ(v->sdf_dim, 48); + EXPECT_TRUE(v->source_file.empty()); // an sdf source is not a file + ASSERT_TRUE(v->has_fit); // fit is applied to the mesh BEFORE the SDF (framing parity) + EXPECT_FLOAT_EQ(v->fit_height, 100.0f); + ASSERT_TRUE(v->has_volren); + ASSERT_EQ(v->volren.isosurfaces.size(), 1u); +} + // §9 enrichments: a tuned StageLighting rig and shadow resolution/update-interval. TEST(AriadneScene, RigTuningAndShadowResolution) { SKIP_WITHOUT_YAML(); diff --git a/src/cvcGL/ariadne/scene_realize.cpp b/src/cvcGL/ariadne/scene_realize.cpp index ebc25113c..397165646 100644 --- a/src/cvcGL/ariadne/scene_realize.cpp +++ b/src/cvcGL/ariadne/scene_realize.cpp @@ -15,6 +15,7 @@ #include #include #include +#include // cvc::sdf — source: { sdf: { mesh:, dim: } } volume source #include #include #include @@ -245,6 +246,47 @@ void configure_volslice(VolSliceNode &vn, const cvc::ariadne::SceneVolSlice &v, vn.setConfig(s); } +#if CVC_ENABLE_SDF +// A dim³ signed-distance-field volume of a mesh, framed like the volume demos' bunny_sdf: the grid +// spans the mesh extents padded 10% (cubic), SDF_V2. Lets a volren/volslice node declare +// `source: { sdf: { mesh:, dim: } }` instead of a pre-baked volume file. +cvc::volume sdf_volume_from_mesh(cvc::app &app, const std::string &mesh_uri, int dim, + bool apply_fit, bool fit_up_y, float fit_height) { + cvc::geometry mesh = cvc::read_geometry(mesh_uri); + if (apply_fit) + mesh = fit_to_ground(mesh, fit_up_y, fit_height); // stand/centre/scale the mesh before the SDF, + // so the volume is framed like the geometry + const cvc::bounding_box e = mesh.extents(); + const double ext = std::max({e.maxx - e.minx, e.maxy - e.miny, e.maxz - e.minz}); + const double half = ext * 1.1 * 0.5; + const double cx = 0.5 * (e.minx + e.maxx), cy = 0.5 * (e.miny + e.maxy), + cz = 0.5 * (e.minz + e.maxz); + const cvc::bounding_box bbox(cx - half, cy - half, cz - half, cx + half, cy + half, cz + half); + const int d = dim > 0 ? dim : 64; + return cvc::sdf(app, mesh, cvc::dimension(d, d, d), bbox, cvc::SDF_V2); +} +#endif + +// The volume a volren/volslice node draws: loaded from `source: { file }`, or computed as the SDF +// of a mesh (`source: { sdf: { mesh:, dim: } }`). Throws on failure — the caller's try/catch warns +// and skips the node. +cvc::volume load_node_volume(SceneGraph &sg, const cvc::ariadne::SceneNode &n, + std::vector *warnings) { + if (!n.source_sdf_mesh.empty()) { +#if CVC_ENABLE_SDF + return sdf_volume_from_mesh(sg.appContext(), n.source_sdf_mesh, n.sdf_dim, n.has_fit, + n.fit_up_y, n.fit_height); +#else + (void)warnings; + throw std::runtime_error("source: { sdf } needs a CVC_ENABLE_SDF build"); +#endif + } + cvc::ariadne::ResolvedFile src = resolve_source(n, warnings); + if (!src.ok) + throw std::runtime_error("unresolved source"); + return cvc::volume(sg.appContext(), src.path); +} + // Realize one node under `parent` (null = a top-level node parented to the graphics // root). A nested node is created via parent->createChild/addGraphicsChild, which // gives it the hierarchical state path `{parent}.children.{id}` AND makes its @@ -337,16 +379,13 @@ void realize_node(SceneGraph &sg, const cvc::ariadne::SceneNode &n, const std::s vnode->setDiffuse(n.diffuse); } } else if (n.type == "volren") { - if (n.source_file.empty()) { - warn(warnings, "ari: scene node '" + n.id + "' (volren) has no source.file"); + if (n.source_file.empty() && n.source_sdf_mesh.empty()) { + warn(warnings, "ari: scene node '" + n.id + "' (volren) has no source (file or sdf)"); return; } - cvc::ariadne::ResolvedFile src = resolve_source(n, warnings); - if (!src.ok) - return; std::shared_ptr vn; try { - cvc::volume vol(sg.appContext(), src.path); // reads on construct; throws on a bad file + cvc::volume vol = load_node_volume(sg, n, warnings); // file, or an SDF of a mesh // No sg.addGraphics overload for VolRenNode: create under the parent (or root). GraphicsNode *pr = parent ? parent : sg.getGraphicsRoot().get(); // Last-wins parity with sg.addGraphics: registerGraphics only reassigns the name @@ -365,16 +404,13 @@ void realize_node(SceneGraph &sg, const cvc::ariadne::SceneNode &n, const std::s out.volren_ticks.push_back(vn); // needs a per-frame tick() (tick_scene) node = vn; } else if (n.type == "volslice") { - if (n.source_file.empty()) { - warn(warnings, "ari: scene node '" + n.id + "' (volslice) has no source.file"); + if (n.source_file.empty() && n.source_sdf_mesh.empty()) { + warn(warnings, "ari: scene node '" + n.id + "' (volslice) has no source (file or sdf)"); return; } - cvc::ariadne::ResolvedFile src = resolve_source(n, warnings); - if (!src.ok) - return; std::shared_ptr vn; try { - cvc::volume vol(sg.appContext(), src.path); + cvc::volume vol = load_node_volume(sg, n, warnings); GraphicsNode *pr = parent ? parent : sg.getGraphicsRoot().get(); if (!parent && sg.hasGraphics(n.id)) // last-wins parity with sg.addGraphics (see volren) sg.removeGraphics(n.id); diff --git a/src/cvcGL/examples/volren_bunny.ari b/src/cvcGL/examples/volren_bunny.ari new file mode 100644 index 000000000..c65168fd0 --- /dev/null +++ b/src/cvcGL/examples/volren_bunny.ari @@ -0,0 +1,51 @@ +# volren_bunny.ari — the cvcGL volume-raycast bunny, ported to Ariadne. The volume is the signed +# distance field of the embedded Stanford bunny MESH, declared with `source: { sdf: { mesh:, dim } }` +# (no asset file). Run by the generic ariadne_hello host: +# +# ariadne_hello volren_bunny.ari --component-path src/cvc/ariadne/components +# ariadne_hello volren_bunny.ari --offscreen --png volren.png --frames 200 --component-path … + +meta: + name: Volren bunny (Ariadne) + author: cvc + min_libcvc: 3.4.0 + +# Reusable control components (shared with the other bunny demos): the lighting rig + camera +# navigation, plus the volume-raycast panel. All declarative — no host C++ panels. +import: + - cvc://components/stage_lighting.ari + - cvc://components/camera_controls.ari + - cvc://components/volren_controls.ari + +menubar: + - menu: Sim + items: + - menu_item: Quit + on: quit + +windows: + - include: stage_lighting + - include: camera_controls + - include: volren_controls + +scene: + chrome: false + background: { top: [0.10, 0.12, 0.16], bottom: [0.02, 0.02, 0.03] } + nodes: + - node: bunny_volume + type: volren + source: { sdf: { mesh: stanford.bunny, dim: 64 } } + fit: { height: 100, up: y } + volren: + distance_field: true + ambient: 0.35 + steps: 384 + resolution_scale: 0.6 + isosurfaces: + - { value: 0.0, color: [0.85, 0.82, 0.88], opacity: 1.0, shininess: 24 } + lights: + - { color: [1.0, 1.0, 1.0], direction: [-0.4, -0.3, -0.85] } + - node: ground + type: geometry + source: { plane: { size: 440 } } + material: { color: [0.18, 0.19, 0.22], ambient: 0.3, diffuse: 0.8 } diff --git a/src/cvcGL/examples/volslice_bunny.ari b/src/cvcGL/examples/volslice_bunny.ari new file mode 100644 index 000000000..52b8ea472 --- /dev/null +++ b/src/cvcGL/examples/volslice_bunny.ari @@ -0,0 +1,58 @@ +# volslice_bunny.ari — the cvcGL volume-slice bunny, ported to Ariadne. The volume is the signed +# distance field of the embedded Stanford bunny MESH (source: { sdf: { mesh:, dim } }); the transfer +# function paints a window around the zero level set (the surface). Run by the generic ariadne_hello: +# +# ariadne_hello volslice_bunny.ari --component-path src/cvc/ariadne/components +# ariadne_hello volslice_bunny.ari --offscreen --png volslice.png --frames 8 --component-path … + +meta: + name: Volslice bunny (Ariadne) + author: cvc + min_libcvc: 3.4.0 + +# Reusable control components (shared with the other bunny demos): the lighting rig + camera +# navigation, plus the volume-slice panel. All declarative — no host C++ panels. +import: + - cvc://components/stage_lighting.ari + - cvc://components/camera_controls.ari + - cvc://components/volslice_controls.ari + +menubar: + - menu: Sim + items: + - menu_item: Quit + on: quit + +windows: + - include: stage_lighting + - include: camera_controls + - include: volslice_controls + +scene: + chrome: false + background: { top: [0.10, 0.12, 0.16], bottom: [0.02, 0.02, 0.03] } + nodes: + # The bunny as a sliced volume. Slices are unlit by design — the transfer function supplies the + # colour; opacity is highest inside the surface (negative SDF) and fades to transparent air. + - node: bunny_volume + type: volslice + source: { sdf: { mesh: stanford.bunny, dim: 96 } } + fit: { height: 100, up: y } + volslice: + quality: 0.6 + opacity_correction: true + transfer_function: + window: [-6, 6] + points: + - { value: -6.0, color: [0.90, 0.60, 0.20, 0.85] } + - { value: -0.5, color: [1.00, 0.85, 0.60, 0.45] } + - { value: 0.5, color: [1.00, 0.95, 0.85, 0.12] } + - { value: 6.0, color: [1.00, 1.00, 1.00, 0.00] } + - node: ground + type: geometry + source: { plane: { size: 440 } } + material: { color: [0.18, 0.19, 0.22], ambient: 0.3, diffuse: 0.8 } + + lights: + - rig: three_point + key: { intensity: 1.9, azimuth: -38, elevation: 52 }