docs: tree.py + ingest_tree.py 补充文档注释
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user