Assemble a Scene
This page shows the recommended way to build a scene from:
plants imported from files (
.opfor.gwa)plants generated in Julia with the growth API
optional ground geometry
TLDR: Use the make_scene function. It creates the scene root, places objects, relabels node ids, and returns a prepared SceneGeometry.
Recommended API
using PlantGeom
using MultiScaleTreeGraph
using CairoMakie
include(joinpath(pkgdir(PlantGeom), "docs", "src", "getting_started", "tree_demo_helpers.jl"))
files_dir = joinpath(pkgdir(PlantGeom), "test", "files")
imported = read_opf(joinpath(files_dir, "simple_plant.opf"); mtg_type=NodeMTG)
generated = build_demo_tree_with_growth_api()
scene = make_scene(domain=(0.0, 0.0, 8.0, 4.0)) do sc
add_plant!(
sc,
imported;
group="imported",
id=1,
at=(1.0, 1.0, 0.0),
rotation=0.25,
)
add_plant!(
sc,
generated;
group="generated",
id=2,
at=(4.7, 1.4, 0.0),
scale=1.15,
rotation=-0.35,
inclination_angle=0.12,
)
add_ground!(sc; nx=8, ny=4, group="ground", type="Ground")
end
f, ax, p = plantviz(scene.mtg, figure=(size=(920, 620),))
f
The domain is (xmin, ymin, xmax, ymax) in scene coordinates. It is also used as the default ground extent when you call add_ground! inside the builder block.
Add Objects
Use add_plant! for plant-like MTGs and add_object! for standalone objects (like solar panels, or buildings).
oak = read_opf("plants/oak.opf"; mtg_type=NodeMTG)
bench = read_gwa("objects/bench.gwa"; mtg_type=NodeMTG)
scene = make_scene(domain=(0.0, 0.0, 10.0, 6.0)) do sc
add_plant!(
sc,
oak;
group="trees",
id=1,
at=(2.0, 2.0, 0.0),
rotate=(z=20.0,),
deg=true,
)
add_object!(
sc,
bench;
group="furniture",
id=10,
type="Bench",
at=(6.0, 1.5, 0.0),
scale=0.8,
)
endIn mixed scenes, use the same MTG encoding type for the scene root and imported objects. By default, make_scene creates a NodeMTG scene root. If your objects use MutableNodeMTG, pass the same type to make_scene:
oak = read_opf("plants/oak.opf"; mtg_type=MutableNodeMTG)
scene = make_scene(domain=(0.0, 0.0, 10.0, 6.0); mtg_type=MutableNodeMTG) do sc
add_plant!(sc, oak; group="trees", id=1)
endPlacement (e.g. add_plant!) can use the simple transform API:
at=(x, y, z)scale=sorscale=(sx, sy, sz)rotate=(x=..., y=..., z=...)deg=truewhen rotation angles are in degrees
It can also use OPS-style placement:
rotation=...inclination_azimut=...inclination_angle=...
Do not mix rotate= with OPS-style rotation= / inclination_*= placement in the same object call.
Reusing the Same Object More Than Once
add_plant! and add_object! copy MTG inputs before attaching them, so the same loaded object can be reused safely:
base_plant = read_opf("myplant.opf"; mtg_type=NodeMTG)
scene = make_scene(domain=(0.0, 0.0, 5.0, 3.0)) do sc
add_plant!(sc, base_plant; group="plants", id=1, at=(0.0, 0.0, 0.0))
add_plant!(sc, base_plant; group="plants", id=2, at=(2.0, 0.0, 0.0))
endExport to OPS
make_scene returns a SceneGeometry. The MTG scene root is stored in scene.mtg:
write_ops("mixed_scene.ops", scene.mtg)This will:
write the OPS scene table
emit one object file per child of the scene root
preserve the final placed geometry when you read the OPS back with
read_ops
Inspect Prepared Scene Geometry
The returned SceneGeometry also contains a merged mesh and per-node geometry summaries:
scene.merged_mesh
scene_node_ids(scene)
node_areas(scene)
node_barycenters(scene)This representation is useful when a downstream model needs one mesh plus a map from faces back to MTG node ids.
Advanced: Manual Scene Roots
A lower-level API is also available when you need explicit control over the scene root or want to work directly with OPS placement metadata. In that case, create a :Scene root yourself and attach objects with place_in_scene!.
using PlantGeom
using MultiScaleTreeGraph
using GeometryBasics
scene = Node(NodeMTG(:/, :Scene, 1, 0))
scene.scene_dimensions = (
Point{3,Float64}(0.0, 0.0, 0.0),
Point{3,Float64}(8.0, 4.0, 0.0),
)
imported = read_opf("simple_plant.opf"; mtg_type=NodeMTG)
place_in_scene!(
imported;
scene=scene,
scene_id=1,
plant_id=1,
functional_group="imported",
at=(1.0, 1.0, 0.0),
rotation=0.25,
)For each object root, place_in_scene! writes scene metadata compatible with OPS:
sceneIDplantIDfunctional_groupposscalerotationinclinationAzimutinclinationAngleoptionally
filePath
And by default it also:
computes the same placement transform used by
read_opsapplies it to all geometry nodes in the object subtree
stores that transform as
scene_transformationrelabels node ids when attaching the object to a scene so independent trees do not collide
Two separate objects often both start at node id 1, so relabeling matters when you attach multiple roots under one scene.
MTG Type Constraint
Scene assembly attaches multiple independent MTG roots under one :Scene root. Those roots must use the same MTG encoding type.
The simplest mixed-scene workflow is:
create or let
make_scenecreate aNodeMTGscene rootread imported OPF/GWA objects with
mtg_type=NodeMTGbuild generated plants with the growth API, which already uses
NodeMTG
If your input objects use MutableNodeMTG, call make_scene(...; mtg_type=MutableNodeMTG) and load all imported objects with mtg_type=MutableNodeMTG.
If you mix NodeMTG and MutableNodeMTG roots in the same manual scene, addchild! will fail.
When Not To Build A Scene
If you only want a single standalone plant/object, keep it as an object-local MTG and write it directly with write_opf or write_gwa.
Build a scene only when the object is meant to live inside a larger spatial assembly.