Files
osmWorkflow/blender/tools/ingest_tree.py
2026-07-30 09:20:40 +08:00

144 lines
4.5 KiB
Python

"""Import Poly Haven island_tree_01 and export decimated GLBs.
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
import os
import sys
import bpy
TREE_BLEND = os.path.join(
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")]
def cli_args():
values = {"out": None}
argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
i = 0
while i < len(argv):
if argv[i].startswith("--") and i + 1 < len(argv):
values[argv[i][2:]] = argv[i + 1]
i += 2
else:
i += 1
if not values.get("out"):
raise RuntimeError("--out is required")
return values
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()
depsgraph = bpy.context.evaluated_depsgraph_get()
evaluated = obj.evaluated_get(depsgraph)
result = bpy.data.meshes.new_from_object(evaluated)
result.calc_loop_triangles()
return result
def export_one(out_path, b_mesh, l_mesh, materials):
"""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")
scene.collection.children.link(col)
b_obj = bpy.data.objects.new("Branch", b_mesh)
l_obj = bpy.data.objects.new("Leaf", l_mesh)
col.objects.link(b_obj)
col.objects.link(l_obj)
b_obj.data.materials.append(materials["branch"])
l_obj.data.materials.append(materials["leaf"])
l_obj.data.materials.append(materials["branch"])
scene.view_layers[0].update()
bpy.ops.export_scene.gltf(
filepath=out_path,
export_format="GLB",
use_selection=False,
export_apply=False,
export_texcoords=True,
export_normals=True,
export_materials="EXPORT",
export_image_format="JPEG",
)
bpy.data.scenes.remove(scene, do_unlink=True)
def main():
args = cli_args()
out_dir = args["out"]
os.makedirs(out_dir, exist_ok=True)
if not os.path.exists(TREE_BLEND):
print(f"Missing: {TREE_BLEND}")
return 1
bpy.ops.wm.open_mainfile(filepath=TREE_BLEND)
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 = []
for vi, (branch_id, leaf_id) in enumerate(PAIRS):
branch_obj = bpy.data.objects.get(
f"island_tree_01_branches_{branch_id}_LOD1")
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)
l_mesh = decimate_to_target(leaf_obj, TARGET_LEAF_TRIS)
decimated.extend([b_mesh, l_mesh])
out_path = os.path.join(out_dir, f"tree_variant_{vi:02d}.glb")
export_one(out_path, b_mesh, l_mesh,
{"branch": branch_mat, "leaf": leaf_mat})
kb = os.path.getsize(out_path) // 1024
print(f" variant_{vi:02d}: branch={len(b_mesh.loop_triangles)}tris "
f"leaf={len(l_mesh.loop_triangles)}tris ({kb}KB)")
for mesh in decimated:
if mesh and mesh.name in bpy.data.meshes:
bpy.data.meshes.remove(mesh, do_unlink=True)
print(f"\nDone — {len(PAIRS)} variants → {out_dir}")
return 0
if __name__ == "__main__":
sys.exit(main())