docs: tree.py + ingest_tree.py 补充文档注释

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-30 09:20:40 +08:00
parent 3c6c21ff2f
commit 365b158878
2 changed files with 37 additions and 39 deletions

View File

@@ -1,10 +1,14 @@
"""Import Poly Haven island_tree_01 as instanced-tree asset. """Import Poly Haven island_tree_01 as instanced-tree asset.
This is the analog of load_tuft_variants() for trees. It appends only the The runtime counterpart to blender/tools/ingest_tree.py. Instead of exporting
LOD1 meshes from the vendored .blend, which automatically pulls in their pre-baked GLBs (which would duplicate textures), this module appends the
material definitions and texture images. The top-level object parenting vendored .blend at scene-build time. The appended objects pull in their
(branch and leaf parts share one origin) is handled by the caller who material datablocks and texture images automatically — the textures live next
instances them as a single logical tree. 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 import os
@@ -19,36 +23,33 @@ TREE_BLEND = os.path.join(
) )
# Branch / leaf LOD1 pairings for the four variants. # 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")] VARIANT_PAIRS = [("a", "b"), ("b", "a"), ("c", "a"), ("d", "b")]
def load_tree_variants(): def load_tree_variants():
"""Import the vendored Poly Haven tree and return meshes ready to instance. """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 Returns a list of (branch_mesh, leaf_mesh) tuples, one per variant.
meshes share the material datablocks that were appended alongside them, so The meshes share the material datablocks that were appended alongside
the textures and shader nodes are wired up automatically. them, so the textures and shader nodes are wired up automatically.
Returns [] when the source .blend is absent so a clean checkout still Returns [] when the source .blend is absent — the caller falls back to
builds. The caller falls back to procedural trees. procedural trees so a clean checkout still builds.
""" """
if not os.path.exists(TREE_BLEND): if not os.path.exists(TREE_BLEND):
return [] return []
before_objects = set(bpy.data.objects) 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 = [] wanted = []
for branch_id, leaf_id in VARIANT_PAIRS: for branch_id, leaf_id in VARIANT_PAIRS:
wanted.append(f"island_tree_01_branches_{branch_id}_LOD1") wanted.append(f"island_tree_01_branches_{branch_id}_LOD1")
wanted.append(f"island_tree_01_leaves_{leaf_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: for name in wanted:
try: try:
bpy.ops.wm.append( bpy.ops.wm.append(
@@ -59,22 +60,12 @@ def load_tree_variants():
) )
except RuntimeError: except RuntimeError:
# Object already linked by an earlier append of a sibling mesh # Object already linked by an earlier append of a sibling mesh
# that shared materials; this is expected and harmless. # that shared materials; harmless.
pass pass
imported = [obj for obj in set(bpy.data.objects) - before_objects imported = [obj for obj in set(bpy.data.objects) - before_objects
if obj.type == "MESH"] 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. # Rebuild the pairs from the imported object names.
variants = [] variants = []
for branch_id, leaf_id in VARIANT_PAIRS: for branch_id, leaf_id in VARIANT_PAIRS:
@@ -105,10 +96,12 @@ def load_tree_variants():
def assemble(positions, collection): def assemble(positions, collection):
"""Place island_tree_01 instances at `positions`. """Place island_tree_01 instances at `positions`.
positions is a list of (x, y, height) tuples. Returns the number of positions is a list of (x, y, height) tuples as gathered by the two
trees placed (0 when the model is unavailable). 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() variants = load_tree_variants()
if not variants: if not variants:

View File

@@ -1,8 +1,8 @@
"""Import Poly Haven island_tree_01 and export decimated GLBs. """Import Poly Haven island_tree_01 and export decimated GLBs.
Each output GLB contains exactly one tree variant (1 branch mesh + 1 leaf Each output GLB is a single tree variant (branch + leaf) with shared
mesh) with shared material references so the main generator can instance them material references. This is the offline ingest tool; the runtime module
the same way it instances grass tufts. (osmassets/tree.py) loads directly from the vendored .blend via wm.append.
""" """
import math import math
@@ -13,14 +13,18 @@ import bpy
TREE_BLEND = os.path.join( TREE_BLEND = os.path.join(
os.path.dirname(__file__), "..", "..", os.path.dirname(os.path.abspath(__file__)), "..", "..",
"assets", "models", "polyhaven", "island_tree_01", "assets", "models", "polyhaven", "island_tree_01",
"island_tree_01_1k.blend", "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_BRANCH_TRIS = 180
TARGET_LEAF_TRIS = 220 TARGET_LEAF_TRIS = 220
# Branch / leaf LOD1 pairings for the four variants.
PAIRS = [("a", "b"), ("b", "a"), ("c", "a"), ("d", "b")] PAIRS = [("a", "b"), ("b", "a"), ("c", "a"), ("d", "b")]
@@ -40,11 +44,13 @@ def cli_args():
def decimate_to_target(obj, target_tris): def decimate_to_target(obj, target_tris):
"""Decimate a mesh to roughly target_tris, return new mesh datablock."""
source = obj.data source = obj.data
source.calc_loop_triangles() source.calc_loop_triangles()
source_tris = len(source.loop_triangles) source_tris = len(source.loop_triangles)
if source_tris <= target_tris: if source_tris <= target_tris:
return source.copy() return source.copy()
modifier = obj.modifiers.new("TreeDecimate", "DECIMATE") modifier = obj.modifiers.new("TreeDecimate", "DECIMATE")
modifier.ratio = max(0.05, target_tris / source_tris) modifier.ratio = max(0.05, target_tris / source_tris)
bpy.context.view_layer.update() 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): def export_one(out_path, b_mesh, l_mesh, materials):
"""Export a single tree variant to a clean GLB. """Export a single tree variant to a clean GLB in a fresh temp scene."""
Works in a fresh, empty scene so nothing from the source blend leaks in.
"""
scene = bpy.data.scenes.new("_TreeIngest") scene = bpy.data.scenes.new("_TreeIngest")
bpy.context.window.scene = scene bpy.context.window.scene = scene
col = bpy.data.collections.new("_Tree") col = bpy.data.collections.new("_Tree")
@@ -104,6 +107,7 @@ def main():
branch_mat = bpy.data.materials.get("island_tree_01_branches") branch_mat = bpy.data.materials.get("island_tree_01_branches")
leaf_mat = bpy.data.materials.get("island_tree_01_leaves") leaf_mat = bpy.data.materials.get("island_tree_01_leaves")
if not branch_mat or not leaf_mat: if not branch_mat or not leaf_mat:
print("Missing materials in source blend")
return 1 return 1
decimated = [] decimated = []
@@ -113,6 +117,7 @@ def main():
leaf_obj = bpy.data.objects.get( leaf_obj = bpy.data.objects.get(
f"island_tree_01_leaves_{leaf_id}_LOD1") f"island_tree_01_leaves_{leaf_id}_LOD1")
if not branch_obj or not leaf_obj: if not branch_obj or not leaf_obj:
print(f"Skipping variant {vi}: missing LOD1 objects")
continue continue
b_mesh = decimate_to_target(branch_obj, TARGET_BRANCH_TRIS) b_mesh = decimate_to_target(branch_obj, TARGET_BRANCH_TRIS)