feat: 树渲染改用 apple/fattree 模型,修复 Cesium alpha 抠图丢失
tree_style 新增 apple(SpeedTree Red Delicious,4475 tris,alpha 抠图叶片 + 法线贴图)和 fattree(低面数卡通树,2238 tris,实体几何)。 高度改为按模型自身包围盒归一化到目标高度,树根落在 z=0,不再用魔数; 每棵树按序号做确定性抖动(缩放 ±14%、黄金角偏航、±3° 倾斜), tree_row 采样高度全部相同,不抖动就是同一棵树盖章 156 次。 移除 polyhaven 样式和 island_tree_01 资产(76MB)+ ingest_tree.py: 该样式 append 的 *_LOD1 并不是完整的树,枝干只有 0.41 单位高,叶片是 挂在原点下方的平面簇,本是给源文件几何节点散布用的碎片。 Cesium 侧修三处: - make_export_material 把 Alpha 恒定写死为 1.0,叶片卡片整块导出,而 SpeedTree 图集抠掉的区域是纯黑,在 Cesium 里就是黑色色块。 - Blender 4.2 起 glTF 导出不再读 blend_method(仍可写但已失效,写 CLIP 读回来是 HASHED),改为从节点树推断 alpha 模式,alpha 直连 BSDF 会落到 BLEND。新增 materials.link_alpha_clip() 构造导出器识别的 1 - (alpha < cutoff) 结构,得到 alphaMode=MASK。 - 新增 alpha_dilated_image():把不透明像素颜色向抠图区域外扩 8 圈, 避免 mipmap 把黑色平均进叶缘。叶缘相邻的纯黑像素 10.3% → 0.6%。 另修两个既有 bug: - 材质槽在 mesh 上,181 棵树共享一个 datablock,第一棵替换后其余会把结果 再包一层,产生 Cesium Cesium Cesium... 的材质名;烘焙图缓存按材质名索引, 每轮再嵌一份同样的贴图。GLB 22.46MB → 20.76MB,materials 270 → 23。 - shrub_02 从 glTF 带进来一个 Math 节点接在 Alpha 上,但其 JPEG 贴图 alpha 全是 1.0,只判断「连了 Alpha」会误判为抠图材质。 模型资产为第三方素材,已 gitignore,缺失时回落到 natural; 来源见 assets/models/SOURCES.md。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,143 +0,0 @@
|
||||
"""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())
|
||||
Reference in New Issue
Block a user