Add TownSquare scene assets, resource production docs, and model pipeline updates.
Bring in plaza props/prefabs, ZhaoYun source GLB, design originals, and Hunyuan/Blender tooling used to produce them. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -91,6 +91,7 @@ def _import_model_joined(path, warnings):
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
obj.select_set(True)
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
# 导入补偿(FBX +90°X)烤进顶点,烘焙在 Blender Z-up 下进行;导出前 restore_source_yup。
|
||||
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
|
||||
return obj
|
||||
|
||||
@@ -269,11 +270,13 @@ def main():
|
||||
|
||||
# 导出低模:剥掉烘焙 image 节点,材质名保留 stem(工具3 兜底识别用)
|
||||
bake_mat.node_tree.nodes.remove(target_node)
|
||||
from bl_decimate import export_fbx_source_yup, prepare_game_export
|
||||
prepare_game_export(low, restore_axis=True)
|
||||
out_fbx = os.path.join(outdir, stem + ".fbx")
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
low.select_set(True)
|
||||
bpy.ops.export_scene.fbx(filepath=out_fbx, use_selection=True,
|
||||
path_mode='STRIP', embed_textures=False)
|
||||
bpy.context.view_layer.objects.active = low
|
||||
export_fbx_source_yup(out_fbx)
|
||||
|
||||
print("MT_SUMMARY " + json.dumps(
|
||||
{"stem": stem, "fbx": os.path.basename(out_fbx), "outputs": outputs,
|
||||
|
||||
@@ -397,8 +397,17 @@ def main():
|
||||
for m in used_mats]
|
||||
|
||||
_strip_textures()
|
||||
from bl_decimate import export_fbx_source_yup, prepare_game_export
|
||||
meshes = [o for o in bpy.data.objects if o.type == "MESH"]
|
||||
for obj in meshes:
|
||||
prepare_game_export(obj)
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
for obj in meshes:
|
||||
obj.select_set(True)
|
||||
if meshes:
|
||||
bpy.context.view_layer.objects.active = meshes[0]
|
||||
fbx_out = os.path.join(outdir, model_name + ".fbx")
|
||||
bpy.ops.export_scene.fbx(filepath=fbx_out, path_mode='STRIP', embed_textures=False)
|
||||
export_fbx_source_yup(fbx_out)
|
||||
|
||||
print("MT_SUMMARY " + json.dumps(
|
||||
{"model": model_name, "fbx": os.path.basename(fbx_out), "materials": results},
|
||||
|
||||
@@ -32,6 +32,74 @@ def import_model_objects(path):
|
||||
bpy.ops.import_scene.gltf(filepath=path)
|
||||
return [obj for obj in bpy.data.objects if obj not in before]
|
||||
|
||||
|
||||
def restore_source_yup(obj):
|
||||
"""Blender Z-up 顶点还原为混元/glTF Y-up(-90°X)。须在 transform_apply 之后调用。"""
|
||||
import math
|
||||
import bpy
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
obj.select_set(True)
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
obj.rotation_mode = "XYZ"
|
||||
obj.rotation_euler = (-math.pi / 2.0, 0.0, 0.0)
|
||||
bpy.ops.object.transform_apply(location=False, rotation=True, scale=False)
|
||||
|
||||
|
||||
def snap_origin_bottom_center(obj):
|
||||
"""原点放到底面中点:X/Z 居中,Y = 最低点。须已是 Y-up 且 transform 已 apply。"""
|
||||
from mathutils import Vector
|
||||
verts = obj.data.vertices
|
||||
if not verts:
|
||||
return
|
||||
xs = [v.co.x for v in verts]
|
||||
ys = [v.co.y for v in verts]
|
||||
zs = [v.co.z for v in verts]
|
||||
origin = Vector((
|
||||
(min(xs) + max(xs)) * 0.5,
|
||||
min(ys),
|
||||
(min(zs) + max(zs)) * 0.5,
|
||||
))
|
||||
for v in verts:
|
||||
v.co -= origin
|
||||
obj.data.update()
|
||||
obj.location = (0.0, 0.0, 0.0)
|
||||
|
||||
|
||||
def prepare_game_export(obj, restore_axis=None):
|
||||
"""最终游戏 FBX:Y-up,原点在底面中点。
|
||||
|
||||
restore_axis: True 强制 -90°X;False 跳过;None 则导入后物体仍带约 +90°X 才还原。
|
||||
烘焙结束时顶点已是 Z-up、旋转已 apply,必须传 True。
|
||||
导出后:Y-up、原点底面中点、局部 -Z 为正面(与混元/glTF 一致)。
|
||||
"""
|
||||
import math
|
||||
import bpy
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
obj.select_set(True)
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
if restore_axis is None:
|
||||
obj.rotation_mode = "XYZ"
|
||||
restore_axis = abs(obj.rotation_euler.x) > math.radians(45.0)
|
||||
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
|
||||
if restore_axis:
|
||||
restore_source_yup(obj)
|
||||
snap_origin_bottom_center(obj)
|
||||
|
||||
|
||||
def export_fbx_source_yup(filepath):
|
||||
"""导出与混元 GLB 相同的 Y-up FBX。调用前顶点须已是 Y-up。"""
|
||||
import bpy
|
||||
bpy.ops.export_scene.fbx(
|
||||
filepath=filepath,
|
||||
use_selection=True,
|
||||
object_types={"MESH"},
|
||||
axis_forward="-Z",
|
||||
axis_up="Y",
|
||||
bake_space_transform=False,
|
||||
path_mode="STRIP",
|
||||
embed_textures=False,
|
||||
)
|
||||
|
||||
TOLERANCE = 0.03 # 面数相对误差容忍
|
||||
MAX_RETRY = 2 # ratio 修正轮数上限
|
||||
|
||||
@@ -159,6 +227,56 @@ def _decimate(obj, ratio):
|
||||
_apply_modifier(obj, mod)
|
||||
|
||||
|
||||
def _voxel_remesh(obj, voxel_size):
|
||||
import bpy
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
obj.select_set(True)
|
||||
mod = obj.modifiers.new("mt_voxel", "REMESH")
|
||||
mod.mode = "VOXEL"
|
||||
mod.voxel_size = max(float(voxel_size), 1e-4)
|
||||
_apply_modifier(obj, mod)
|
||||
|
||||
|
||||
def _voxel_fallback(obj, target, warnings):
|
||||
"""碎叶/非流形上 collapse 会卡在高面数;复制试体素尺寸后重网格,再 collapse。"""
|
||||
import bpy
|
||||
import mathutils
|
||||
|
||||
bb = [mathutils.Vector(c) for c in obj.bound_box]
|
||||
diag = (bb[0] - bb[6]).length
|
||||
if diag <= 0.0:
|
||||
return len(obj.data.polygons)
|
||||
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
obj.select_set(True)
|
||||
best_size = diag / 16.0
|
||||
best_diff = None
|
||||
for div in (12, 16, 20, 24, 32):
|
||||
size = diag / float(div)
|
||||
bpy.ops.object.duplicate()
|
||||
dup = bpy.context.view_layer.objects.active
|
||||
_voxel_remesh(dup, size)
|
||||
cur = len(dup.data.polygons)
|
||||
diff = abs(cur - target)
|
||||
if best_diff is None or diff < best_diff:
|
||||
best_diff = diff
|
||||
best_size = size
|
||||
bpy.data.objects.remove(dup, do_unlink=True)
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
obj.select_set(True)
|
||||
|
||||
_voxel_remesh(obj, best_size)
|
||||
cur = len(obj.data.polygons)
|
||||
warnings.append(
|
||||
"collapse 卡住后改用体素重网格 voxel=%.5f → %d 面" % (best_size, cur)
|
||||
)
|
||||
if cur > target:
|
||||
_decimate(obj, decimate_ratio(target, cur))
|
||||
_triangulate(obj)
|
||||
cur = len(obj.data.polygons)
|
||||
return cur
|
||||
|
||||
|
||||
def _clean_mesh(obj, warnings):
|
||||
"""减面前清理:按局部包围盒对角线相对焊接重合点 + 去退化面,给 collapse 更干净的流形,
|
||||
减少被迫加的 seam。各步失败降级不中断。"""
|
||||
@@ -474,6 +592,8 @@ def main():
|
||||
# 重试用尽仍超差且偏多:记录警告,交由上层决定是否可接受
|
||||
if not within_tolerance(target, cur) and cur > target:
|
||||
warnings.append("修正 %d 轮后仍超差:%d 面(目标 %d ±3%%)" % (MAX_RETRY, cur, target))
|
||||
if cur > max(target * 2, target + 500):
|
||||
cur = _voxel_fallback(obj, target, warnings)
|
||||
|
||||
uv_info = _do_unwrap(obj, unwrap_mode, warnings, overlap_tol)
|
||||
uv_png = os.path.splitext(os.path.abspath(out_fbx))[0] + "_uv.png"
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
"""把游戏 FBX 整理成混元 GLB 轴向(Y-up)且原点在底面中点。可重复执行。
|
||||
调用:blender -b --factory-startup --python bl_restore_yup.py -- <fbx> [fbx...]
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from bl_decimate import ( # noqa: E402
|
||||
export_fbx_source_yup,
|
||||
import_model_objects,
|
||||
prepare_game_export,
|
||||
)
|
||||
|
||||
|
||||
def restore_file(path):
|
||||
import bpy
|
||||
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
imported = import_model_objects(path)
|
||||
meshes = [o for o in imported if o.type == "MESH"]
|
||||
if not meshes:
|
||||
return {"path": path, "error": "no mesh"}
|
||||
for obj in meshes:
|
||||
prepare_game_export(obj)
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
for obj in meshes:
|
||||
obj.select_set(True)
|
||||
bpy.context.view_layer.objects.active = meshes[0]
|
||||
export_fbx_source_yup(path)
|
||||
verts = meshes[0].data.vertices
|
||||
xs = [v.co.x for v in verts]
|
||||
ys = [v.co.y for v in verts]
|
||||
zs = [v.co.z for v in verts]
|
||||
return {
|
||||
"path": os.path.basename(path),
|
||||
"meshes": len(meshes),
|
||||
"min_y": round(min(ys), 6),
|
||||
"mid_x": round((min(xs) + max(xs)) * 0.5, 6),
|
||||
"mid_z": round((min(zs) + max(zs)) * 0.5, 6),
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
argv = sys.argv[sys.argv.index("--") + 1 :]
|
||||
results = []
|
||||
for raw in argv:
|
||||
path = os.path.abspath(raw)
|
||||
if not os.path.isfile(path):
|
||||
results.append({"path": raw, "error": "missing"})
|
||||
continue
|
||||
results.append(restore_file(path))
|
||||
print("MT_SUMMARY " + json.dumps(results, ensure_ascii=False))
|
||||
|
||||
|
||||
if __name__ == "__main__" and "--" in sys.argv:
|
||||
main()
|
||||
Reference in New Issue
Block a user