Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion bindings/pycvc/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ set_property(SOURCE pycvc.i PROPERTY DEPENDS
${CMAKE_CURRENT_SOURCE_DIR}/pycvc_algorithm.h
${CMAKE_CURRENT_SOURCE_DIR}/pycvc_state.h
${CMAKE_CURRENT_SOURCE_DIR}/pycvc_exec.h
${CMAKE_CURRENT_SOURCE_DIR}/pycvc_image.i)
${CMAKE_CURRENT_SOURCE_DIR}/pycvc_image.i
${CMAKE_CURRENT_SOURCE_DIR}/pycvc_model.i)

# Shared by both modules — defined before either option block so the scene
# module can install/test even when the core module isn't built in this run.
Expand Down Expand Up @@ -79,6 +80,7 @@ if(CVC_BUILD_PYCVC_CORE)
pycvc_app:test_pycvc_app.py pycvc_filters:test_pycvc_filters.py
pycvc_algorithm:test_pycvc_algorithm.py
pycvc_image:test_pycvc_image.py
pycvc_model:test_pycvc_model.py
pycvc_state:test_pycvc_state.py
pycvc_async:test_pycvc_async.py
pycvc_exec:test_pycvc_exec.py
Expand Down
7 changes: 7 additions & 0 deletions bindings/pycvc/pycvc.i
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,13 @@ namespace cvc {
// machinery + capsule dtor defined above.
%include "pycvc_image.i"

// ── Phase 3 (Phase-6 binding): cvc::model + pycvc.load_model ─────────────
// The multi-mesh scene value type (meshes + materials + textures) and the native
// loader pycvc.load_model(path) → pycvc.model. %include'd HERE, after geometry
// AND image, because model::mesh holds a cvc::geometry and material holds a
// cvc::image — both must already be wrapped for model's accessors to marshal.
%include "pycvc_model.i"

// ── Phase 2: compute layer (SDF / meshing / quality / generators) ───────
// Module-level free functions + enum constants + QualityStats, taking/returning
// the real wrapped cvc::geometry/cvc::volume (declared above). Comes last so
Expand Down
123 changes: 123 additions & 0 deletions bindings/pycvc/pycvc_model.i
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// pycvc_model.i — SWIG surface for cvc::model (Phase-3 mesh/model value type).
//
// NOT a standalone module: this file is %include'd by pycvc.i (AFTER geometry and
// image are defined, since model::mesh holds a cvc::geometry and material holds a
// cvc::image) so `model` / `material` land in the `pycvc` module and
// `pycvc.load_model(path)` returns a pycvc.model. It relies on the DoubleVector /
// boost::uint64_t typemaps + the %exception block set up in pycvc.i, and on the
// already-wrapped cvc::geometry / cvc::image.
//
// SWIG APPROACH (documented per the phase spec):
// * DIRECT WRAP of the real header (mirrors pycvc_image.i): %include model.h,
// curated with %ignore for the members that don't marshal.
// * The vectors model::meshes / model::materials are re-surfaced as PROPERTIES
// (model.meshes / model.materials) backed by %template'd std::vector proxies
// (MeshVector / MaterialVector) — indexable + len(). The raw members are
// %ignore'd because their std::vector type isn't %template'd yet at the point
// SWIG wraps the class body, so a %extend accessor placed AFTER the %template
// is what yields the real sequence proxy.
// * material's base_color / emissive are boost::array<double,N> and DON'T
// marshal; base_color_texture is a by-value cvc::image member. All three are
// %ignore'd and re-exposed via a %extend with a DIFFERENT name (a same-named
// %extend is swallowed by the %ignore — the documented pycvc.i gotcha), then
// aliased to the natural name in a %pythoncode block (the pixel_format_* alias
// trick pycvc_image.i already uses). base_color()/emissive() return plain
// tuples; base_color_texture() returns a pycvc.image COPY (empty if none).
// * model::extents() returns the opaque bounding_box (as in pycvc.i, where every
// bounding_box return is ignored), so it is re-exposed as a 6-tuple
// (minx..maxz) the same way (extents_bbox %extend aliased to extents).
// * model::mesh::geom is re-exposed as geometry() (COW copy) — the raw member is
// %ignore'd and a distinct-named %extend is aliased back, same pattern.
// * load_model(path) is a %inline free function wrapping cvc::read_model — it
// takes ONLY a path (read_model needs no app; the returned geometries are bare
// value types with a null context, which every read-only accessor the bindings
// expose is fine with), so no app is threaded through.

%{
#include <cvc/model/model.h>
#include <cvc/model/model_file_io.h> // read_model (used by the %inline load_model below)
%}

// model::mesh is a NESTED struct; SWIG (4.x) ignores nested classes by default
// (Warning 325), which would leave MeshVector's elements opaque. flatnested lifts
// it to a normal wrapped proxy so model.meshes[i].geometry()/.material/.name work.
%feature("flatnested", "1") cvc::model::mesh;

// The vectors and the opaque bounding_box extents() are re-surfaced by %extend
// after the %template's below; %ignore the raw members so they don't wrap as
// opaque std::vector pointers.
%ignore cvc::model::meshes;
%ignore cvc::model::materials;
%ignore cvc::model::extents;
%ignore cvc::model::mesh::geom;

// material: boost::array + by-value image members don't marshal cleanly; exposed
// via the renamed-%extend trick below.
%ignore cvc::material::base_color;
%ignore cvc::material::emissive;
%ignore cvc::material::base_color_texture;

%include "cvc/model/model.h"

// Sequence proxies for the mesh/material vectors (indexable + len()). Must come
// AFTER the %include so cvc::model::mesh / cvc::material are fully declared.
%template(MeshVector) std::vector<cvc::model::mesh>;
%template(MaterialVector) std::vector<cvc::material>;

%extend cvc::model::mesh {
// The mesh's geometry as a pycvc.geometry (a COW copy of the mesh's cvc::geometry).
cvc::geometry mesh_geometry() const { return $self->geom; }
%pythoncode %{
geometry = mesh_geometry
%}
}

%extend cvc::material {
// base_color RGBA multiplier as a 4-tuple (the boost::array<double,4> is ignored).
PyObject *base_color_rgba() const {
return Py_BuildValue("(dddd)", $self->base_color[0], $self->base_color[1],
$self->base_color[2], $self->base_color[3]);
}
// emissive RGB as a 3-tuple (the boost::array<double,3> is ignored).
PyObject *emissive_rgb() const {
return Py_BuildValue("(ddd)", $self->emissive[0], $self->emissive[1], $self->emissive[2]);
}
// The LOADED base-color texture as a pycvc.image COPY (image.empty() if none /
// unresolved). Zero-copy sharing isn't offered here — the material owns the
// decoded pixels and a copy keeps the surface a plain value.
cvc::image base_color_texture_image() const { return $self->base_color_texture; }
%pythoncode %{
base_color = base_color_rgba
emissive = emissive_rgb
base_color_texture = base_color_texture_image
%}
}

%extend cvc::model {
// meshes / materials as real sequence proxies (the raw members are ignored;
// their std::vector type isn't templated at class-wrap time). Exposed as
// PROPERTIES below so `model.meshes[i]` / `model.materials[i]` read naturally.
std::vector<cvc::model::mesh> mesh_list() const { return $self->meshes; }
std::vector<cvc::material> material_list() const { return $self->materials; }
// extents() as a (minx, miny, minz, maxx, maxy, maxz) 6-tuple — the bounding_box
// return is opaque here exactly as in pycvc.i.
std::vector<double> extents_bbox() const {
cvc::bounding_box b = $self->extents();
return {b.minx, b.miny, b.minz, b.maxx, b.maxy, b.maxz};
}
%pythoncode %{
meshes = property(lambda self: self.mesh_list())
materials = property(lambda self: self.material_list())
extents = extents_bbox
%}
}

// pycvc.load_model(path) — the core deliverable. Wraps cvc::read_model (which
// dispatches by extension to the Assimp handler for obj/ply/stl/fbx/gltf/glb/...).
// No app is needed; an unsupported extension / missing handler raises the same
// cvc::exception the %exception block maps to a Python RuntimeError.
%inline %{
namespace pycvc {
cvc::model load_model(const std::string &path) { return cvc::read_model(path); }
} // namespace pycvc
%}
91 changes: 50 additions & 41 deletions bindings/pycvc/pymod_gl/scenes.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@

These are generic loaders for a ``geometry_bundle`` export — a ``terrain.json``
heightfield plus a ``buildings.glb`` (glTF 2.0) city mesh, as produced by the
CVC-DBG ``geometry-scene-gen`` tool (e.g. the Austin bundle). The glTF is read
with VTK's ``vtkGLTFReader`` (no trimesh/pygltflib needed) and added as a single
VTK prop; the terrain becomes a draped surface mesh; a bilinear ``sampler`` lets
you drape an agent onto the terrain.
CVC-DBG ``geometry-scene-gen`` tool (e.g. the Austin bundle). The glTF is loaded
NATIVELY via libcvc (``pycvc.load_model`` → ``cvc::model``, the Assimp-backed
mesh loader — no ``vtkGLTFReader``, no trimesh/pygltflib) and added as a single
native geometry node; the terrain becomes a draped surface mesh; a bilinear
``sampler`` lets you drape an agent onto the terrain. (``building_occupancy``
below still rasterizes the mesh through VTK offscreen — a separate concern.)

ATTRIBUTION: bundles generated from OpenStreetMap are © OpenStreetMap
contributors and licensed under the Open Database License (ODbL,
Expand Down Expand Up @@ -77,41 +79,39 @@ def add_terrain_json(lab, path: str, name: str = "terrain", color=(0.34, 0.40, 0
def add_gltf(
lab, path: str, name: str, color=(0.74, 0.74, 0.78), opacity: float = 1.0, parent: str = ""
):
"""Load a glTF/GLB mesh with VTK and add it to ``lab`` as one named prop node.
``parent`` (default the root) makes it a CHILD of that node so it stays aligned
to and moves with it (e.g. buildings under the terrain). Returns the
``vtkActor``. Needs the vtk-python wrappers (vtkmodules)."""
from vtkmodules.vtkIOGeometry import vtkGLTFReader
from vtkmodules.vtkFiltersGeometry import vtkCompositeDataGeometryFilter
from vtkmodules.vtkRenderingCore import vtkActor, vtkPolyDataMapper

reader = vtkGLTFReader()
reader.SetFileName(path)
reader.Update()
# glTF comes back as a multiblock; flatten to one polydata.
geom = vtkCompositeDataGeometryFilter()
geom.SetInputConnection(reader.GetOutputPort())
geom.Update()
pd = geom.GetOutput()

mapper = vtkPolyDataMapper()
mapper.SetInputData(pd)
mapper.SetStatic(1) # geometry never changes -> VTK caches the VBO, no per-frame rebuild
mapper.ScalarVisibilityOff() # use the single material color, not any glTF scalars
actor = vtkActor()
actor.SetMapper(mapper)
prop = actor.GetProperty()
prop.SetColor(*color)
prop.SetOpacity(opacity)
# Ground-level views leave many building faces facing away from the light; a
# strong ambient term keeps them from going black so the city reads clearly.
prop.SetAmbient(0.45)
prop.SetDiffuse(0.7)
prop.SetSpecular(0.05)

b = pd.GetBounds() # (xmin,xmax, ymin,ymax, zmin,zmax)
lab.add_prop(name, actor, (b[0], b[2], b[4], b[1], b[3], b[5]), parent=parent)
return actor
"""Load a glTF/GLB (or any Assimp-supported) mesh NATIVELY via libcvc and add it
to ``lab`` as one named ``GeometryNode``. ``parent`` (default the root) makes it a
CHILD of that node so it stays aligned to and moves with it (e.g. buildings under
the terrain). Returns the live ``GeometryNode``.

The whole file is flattened to a single ``cvc::geometry``
(``pycvc.load_model(path).merged()``) and rendered with a UNIFORM
``color``/``opacity`` — the demo deliberately renders a single-color city, not
per-glTF materials — matching the previous VTK path's look (ambient 0.45 /
diffuse 0.7 / specular 0.05). Imports NO VTK glTF reader; the scene node itself
still renders through cvcGL/VTK as usual."""
pycvc = lab._pycvc
# Native load: one flattened geometry (single-color, one-node city mesh).
g = pycvc.load_model(path).merged()

# Add via the native scene path, honoring parent (child inherits its transform).
scene = lab._scene
if parent:
node = scene.add_child_geometry(parent, name, g)
else:
node = scene.addGraphics(name, g) # downcast to the GeometryNode proxy

# Uniform single-color material through the actor property (per-vertex colors go
# through VTK's LUT and mangle channels — see Lab.recolor). Ground-level views
# leave many building faces facing away from the light; a strong ambient term
# keeps them from going black so the city reads clearly.
node.setUseSingleColor(True)
node.setColor(*[float(c) for c in color])
node.setOpacity(float(opacity))
node.setAmbient(0.45)
node.setDiffuse(0.7)
node.setSpecular(0.05)
return node


# ── grounded routing: keep a vehicle ON THE STREETS, out of the buildings ────
Expand All @@ -123,7 +123,12 @@ def add_gltf(


def building_occupancy(
glb_path: str, bounds2d, nx: int = 512, ny: int = 512, inflate_m: float = 10.0, cache: bool = True
glb_path: str,
bounds2d,
nx: int = 512,
ny: int = 512,
inflate_m: float = 10.0,
cache: bool = True,
):
"""Rasterize ``buildings.glb`` into a SOLID boolean occupancy grid (``True`` =
inside a building footprint) over ``bounds2d`` = ``(min_x, min_y, max_x, max_y)``.
Expand All @@ -148,7 +153,11 @@ def building_occupancy(
import numpy as np

cache_path = "%s.occ_%dx%d_i%d.npy" % (glb_path, nx, ny, int(round(inflate_m)))
if cache and os.path.exists(cache_path) and os.path.getmtime(cache_path) >= os.path.getmtime(glb_path):
if (
cache
and os.path.exists(cache_path)
and os.path.getmtime(cache_path) >= os.path.getmtime(glb_path)
):
return np.load(cache_path)

# Register VTK's OpenGL2 render factory — in a raw Python interpreter (unlike the
Expand Down
Loading
Loading