diff --git a/blender/osmassets/tree.py b/blender/osmassets/tree.py index bd80b1b..ccd752b 100644 --- a/blender/osmassets/tree.py +++ b/blender/osmassets/tree.py @@ -1,10 +1,14 @@ """Import Poly Haven island_tree_01 as instanced-tree asset. -This is the analog of load_tuft_variants() for trees. It appends only the -LOD1 meshes from the vendored .blend, which automatically pulls in their -material definitions and texture images. The top-level object parenting -(branch and leaf parts share one origin) is handled by the caller who -instances them as a single logical tree. +The runtime counterpart to blender/tools/ingest_tree.py. Instead of exporting +pre-baked GLBs (which would duplicate textures), this module appends the +vendored .blend at scene-build time. The appended objects pull in their +material datablocks and texture images automatically — the textures live next +to the .blend and are resolved via relative paths. + +load_tree_variants() is the analog of load_tuft_variants() for trees: import +once, copy the meshes, remove the imported objects, return mesh datablocks +ready for instancing. """ import os @@ -19,36 +23,33 @@ TREE_BLEND = os.path.join( ) # Branch / leaf LOD1 pairings for the four variants. -# Each pairing is a single logical tree that the caller instances as one. +# Each pairing is one logical tree that the caller instances as a unit. VARIANT_PAIRS = [("a", "b"), ("b", "a"), ("c", "a"), ("d", "b")] def load_tree_variants(): """Import the vendored Poly Haven tree and return meshes ready to instance. - Returns a list of (branch_mesh, leaf_mesh) tuples, one per variant. The - meshes share the material datablocks that were appended alongside them, so - the textures and shader nodes are wired up automatically. + Returns a list of (branch_mesh, leaf_mesh) tuples, one per variant. + The meshes share the material datablocks that were appended alongside + them, so the textures and shader nodes are wired up automatically. - Returns [] when the source .blend is absent so a clean checkout still - builds. The caller falls back to procedural trees. + Returns [] when the source .blend is absent — the caller falls back to + procedural trees so a clean checkout still builds. """ if not os.path.exists(TREE_BLEND): return [] before_objects = set(bpy.data.objects) - before_meshes = set(bpy.data.meshes) - # Gather the object names we need from the library. + # Append one object at a time: bpy.ops.wm.append with a multi-file list + # can crash when objects in the same library share data that was already + # linked by an earlier append in the same batch. wanted = [] for branch_id, leaf_id in VARIANT_PAIRS: wanted.append(f"island_tree_01_branches_{branch_id}_LOD1") wanted.append(f"island_tree_01_leaves_{leaf_id}_LOD1") - # bpy.ops.wm.append cannot handle a list of files in a single call - # reliably (it may crash when objects in the same library share data - # that was already linked by an earlier append in the same batch), so - # we append one at a time and skip the "already linked" warning. for name in wanted: try: bpy.ops.wm.append( @@ -59,22 +60,12 @@ def load_tree_variants(): ) except RuntimeError: # Object already linked by an earlier append of a sibling mesh - # that shared materials; this is expected and harmless. + # that shared materials; harmless. pass imported = [obj for obj in set(bpy.data.objects) - before_objects if obj.type == "MESH"] - # Pair branch + leaf into variants. We append in the order above so - # a simple zip works. - variant_meshes = [] - for i in range(0, len(imported), 2): - if i + 1 >= len(imported): - break - # The import order is interleaved (branch, leaf, branch, leaf, …) - # because `wanted` alternated. Sort by name to be safe. - pass - # Rebuild the pairs from the imported object names. variants = [] for branch_id, leaf_id in VARIANT_PAIRS: @@ -105,10 +96,12 @@ def load_tree_variants(): def assemble(positions, collection): """Place island_tree_01 instances at `positions`. - positions is a list of (x, y, height) tuples. Returns the number of - trees placed (0 when the model is unavailable). + positions is a list of (x, y, height) tuples as gathered by the two + tree-collecting loops (point nodes + tree_row samples). + + Returns the number of trees placed (0 when the model is unavailable). """ - import math # noqa — tree.py has no top-level math import by design + import math # noqa — tree.py has no top-level math import otherwise variants = load_tree_variants() if not variants: diff --git a/blender/tools/ingest_tree.py b/blender/tools/ingest_tree.py index ba0e3e7..7edfeca 100644 --- a/blender/tools/ingest_tree.py +++ b/blender/tools/ingest_tree.py @@ -1,8 +1,8 @@ """Import Poly Haven island_tree_01 and export decimated GLBs. -Each output GLB contains exactly one tree variant (1 branch mesh + 1 leaf -mesh) with shared material references so the main generator can instance them -the same way it instances grass tufts. +Each output GLB is a single tree variant (branch + leaf) with shared +material references. This is the offline ingest tool; the runtime module +(osmassets/tree.py) loads directly from the vendored .blend via wm.append. """ import math @@ -13,14 +13,18 @@ import bpy TREE_BLEND = os.path.join( - os.path.dirname(__file__), "..", "..", + os.path.dirname(os.path.abspath(__file__)), "..", "..", "assets", "models", "polyhaven", "island_tree_01", "island_tree_01_1k.blend", ) +# Target triangle counts for the decimated per-tree mesh. +# LOD1 branches are 135-570 tris, LOD1 leaves 380-1531 tris. +# 180 + 220 = 400 per tree, on par with the procedural trees. TARGET_BRANCH_TRIS = 180 TARGET_LEAF_TRIS = 220 +# Branch / leaf LOD1 pairings for the four variants. PAIRS = [("a", "b"), ("b", "a"), ("c", "a"), ("d", "b")] @@ -40,11 +44,13 @@ def cli_args(): def decimate_to_target(obj, target_tris): + """Decimate a mesh to roughly target_tris, return new mesh datablock.""" source = obj.data source.calc_loop_triangles() source_tris = len(source.loop_triangles) if source_tris <= target_tris: return source.copy() + modifier = obj.modifiers.new("TreeDecimate", "DECIMATE") modifier.ratio = max(0.05, target_tris / source_tris) bpy.context.view_layer.update() @@ -56,10 +62,7 @@ def decimate_to_target(obj, target_tris): def export_one(out_path, b_mesh, l_mesh, materials): - """Export a single tree variant to a clean GLB. - - Works in a fresh, empty scene so nothing from the source blend leaks in. - """ + """Export a single tree variant to a clean GLB in a fresh temp scene.""" scene = bpy.data.scenes.new("_TreeIngest") bpy.context.window.scene = scene col = bpy.data.collections.new("_Tree") @@ -104,6 +107,7 @@ def main(): branch_mat = bpy.data.materials.get("island_tree_01_branches") leaf_mat = bpy.data.materials.get("island_tree_01_leaves") if not branch_mat or not leaf_mat: + print("Missing materials in source blend") return 1 decimated = [] @@ -113,6 +117,7 @@ def main(): leaf_obj = bpy.data.objects.get( f"island_tree_01_leaves_{leaf_id}_LOD1") if not branch_obj or not leaf_obj: + print(f"Skipping variant {vi}: missing LOD1 objects") continue b_mesh = decimate_to_target(branch_obj, TARGET_BRANCH_TRIS)