版权声明 © 2026 梦帮集团(DREAMVFIA)保留所有权利。本文为《三界械堂》项目组原创技术文章,全部代码来自项目实际管线并经过运行验证,可自由用于学习与非商业项目;商业转载或引用请注明出处「2026 梦帮集团(DREAMVFIA)」。文中出现的世界观与美术设定(劫灭量子太刀、王天劫、三界械堂等)均为梦帮集团原创 IP。
从一张 AI 原画到 UE5 资产:程序化武器建模管线全解(Python × Blender × UE5)
关键词:程序化建模、Blender bpy、Python、FBX、UE5 自动化导入、资产管线、DCC 无头模式
环境:Python 3.10+ / Blender 3.3~4.2 / Unreal Engine 5.2+ | 难度:中级 | 阅读预计:45 分钟
写在前面
《三界械堂》的美术资产库里躺着十三张武器原画------都是概念设计阶段用 AI 工具批量产出的:主角王天劫的「劫灭量子太刀」、八卦护手、太极刀镡、缠柄、刀刃上缠绕的青色雷电。原画很漂亮,但它们只是图片。要进引擎,就得变成带材质槽、带正确法线、带合理面数的三维网格。
传统路径是找建模师照图手工建。这条路没有任何问题------除了它贵、慢、且不可复现。对一个小团队来说,十三把武器排进外包日程表,往往意味着一个月的等待和一轮轮的返工沟通。
于是我们走了另一条路:用代码把武器「写」出来。刀身的弧度是一条二次曲线,护手是一个八棱柱,缠柄是两条反向螺旋------这些几何特征全部可以参数化。把参数和生成逻辑写成纯 Python,让 Blender 在无头(headless)模式下执行,产物直接是 UE5 可导入的 FBX。改一个参数,重跑一次脚本,一把新变体就出来了。
这篇文章完整拆解这条管线的每一环。所有代码都在项目里真实运行过,文中出现的顶点数、面数、部件数都是实测值,不是示意。游戏内容只作背景,重点是这条管线本身------它适用于任何需要批量生产硬表面道具的项目。
本文覆盖:
- 管线的分层设计:为什么几何内核必须与 Blender 解耦
- 从原画提取结构参数的方法论
- 扫掠(sweep):程序化硬表面建模唯一需要的原语
- 刀身截面设计、弧度曲线与收尖处理的数学
- 螺旋缠柄的正交标架构造
- 不开 Blender 的快速预览:OBJ + matplotlib
- bpy 落地:网格构建、法线修复、Blender 3.x/4.x 材质 API 兼容
- FBX 导出参数逐项讲解(UE5 视角)
- UE5 Python 自动化导入
- 参数化变体量产与管线的适用边界
一、管线总览:三层结构与一个原则
整条管线分三层,每层一个可执行文件,职责严格分离:
┌────────────────────────────────────────────────────────┐
│ 第一层:几何内核 katana_core.py │
│ 纯 Python,零依赖。所有形体参数、网格生成函数。 │
│ 输出:部件列表 [(名称, 材质名, 顶点, 面), ...] │
└──────────────┬─────────────────────┬───────────────────┘
│ │
┌──────────────▼──────────┐ ┌───────▼───────────────────┐
│ 第二层A:预览器 │ │ 第二层B:Blender 脚本 │
│ gen_obj.py │ │ jiemie_katana_blender.py │
│ 输出 OBJ 白模 + │ │ bpy 建网格 + PBR 材质 + │
│ matplotlib 四视图 PNG │ │ 导出 FBX(无头模式运行) │
└─────────────────────────┘ └───────┬───────────────────┘
│ SM_JieMie_Katana.fbx
┌───────▼───────────────────┐
│ 第三层:UE5 导入 │
│ ue5_import_katana.py │
│ AssetImportTask 自动导入 + │
│ LightmapUV + 碰撞 │
└────────────────────────────┘
这个结构背后只有一个原则:几何内核不允许 import bpy。
为什么这条红线如此重要?因为 bpy(Blender 的 Python API)只在 Blender 进程内可用。一旦几何逻辑和 bpy 纠缠在一起,你就失去了三样东西:
- 可测试性。纯 Python 的几何函数可以在任何环境跑单元测试(「刀身应该有 45 圈截面环」「顶点数应该等于环数×截面点数」),CI 服务器上不用装 Blender。
- 快速迭代。改一个弧度参数想看效果,走 Blender 全流程要十几秒;纯 Python 直接出 OBJ + matplotlib 预览图只要一秒。调形阶段 90% 的迭代都发生在预览层。
- 多端复用 。同一份几何数据,预览器拿去写 OBJ,Blender 脚本拿去建 bpy 网格,未来还可以直接喂给 UE5 的 Python
unreal.EditorStaticMeshLibrary或者转 glTF 给网页端展示。生成逻辑只写一遍。
这其实就是老生常谈的「数据与表示分离」,只是在 DCC 工具链的语境下,它经常被忽视------太多人打开 Blender 就直接往脚本编辑器里写建模代码,最后整个资产逻辑被锁死在一个 .blend 文件里。
二、从原画提取参数:建模前的「读图」功课
程序化建模的第一步不是写代码,是把原画翻译成参数表。对着劫灭量子太刀的原画,我们逐个部件记录它的几何特征与估算尺寸:
| 部件 | 原画特征 | 几何抽象 | 关键参数 |
|---|---|---|---|
| 刀身 | 单刃、微弧、刀尖上扬 | 五边截面沿弧线扫掠 | 刃长 0.76m,弧度 0.036m,厚 7.8mm |
| 能量刃口 | 沿刃口的青色发光雷电 | 菱形截面细条贴刃扫掠 | 宽 3.6mm,自发光强度 30 |
| 刀面电路 | 面上的符文式发光细线 | 三角截面浅浮雕条 ×4 | 长 0.42~0.58m |
| 护手 | 八边形金属盘 | 八棱柱 + 同心八棱环 | 外径 0.060m,厚 15mm |
| 太极盘 | 护手中心的阴阳图 | 圆柱盘 + 两粒发光珠 | 半径 19.5mm |
| 发光槽 | 护手正面八个蓝色槽 | 小长方体 ×8 环形阵列 | 分布半径 50.5mm |
| 科技护套 | 刀根的方形机械块 | 倒角长方体 + 侧面灯条 ×6 | 长 60mm |
| 缠柄 | 黑色斜十字缠绕 | 椭圆柱 + 双反向螺旋带 | 柄长 0.26m,9 圈 |
| 柄头 | 金属帽 + 宝石 + 尖刺 | 锥台 + 球 + 圆锥 | 刺长 52mm |
两个实践心得。第一,尺寸按真实世界估 :武士刀刃长典型值 60~80cm,全刀约一米出头。程序化建模最大的隐形福利就是尺寸从第一天起就是物理正确的,进 UE5 不需要「目测缩放」。第二,先数结构再看细节:原画上的战损、贴纸、污渍属于贴图层,不进几何参数表;几何层只记录「有几个体块、什么拓扑、怎么连接」。分不清这两层,是程序化建模新手最容易陷入的泥潭------试图用几何还原每一道划痕,面数爆炸且毫无必要。
三、几何内核:扫掠是唯一需要的原语
打开任何建模软件,工具栏里有几百个按钮;但做硬表面道具的程序化生成,你真正需要的原语只有一个:扫掠(sweep)------把一个二维截面(profile)沿着一串空间标架(frames)移动,连接相邻截面成四边面。圆柱、圆锥、棱柱、圆环、刀身、螺旋带,全部是扫掠的特例。
这是我们几何内核的核心函数,不到四十行:
python
def sweep(profile, frames, close_profile=True, cap_start=True, cap_end=True):
"""把 2D 截面 profile [(u,v)...] 沿 frames [(origin,U,V)...] 扫掠成网格。
返回 (verts, faces)。"""
verts, faces = [], []
n = len(profile)
# 1. 生成顶点:每个标架处放一圈截面点
# 世界坐标 = origin + u*U + v*V
for (o, u, v) in frames:
for (pu, pv) in profile:
verts.append(vadd(o, vadd(vscale(u, pu), vscale(v, pv))))
# 2. 连接相邻两圈成四边面
rings = len(frames)
m = n if close_profile else n - 1
for r in range(rings - 1):
for i in range(m):
j = (i + 1) % n
a = r*n + i; b = r*n + j
c = (r+1)*n + j; d = (r+1)*n + i
faces.append((a, b, c, d))
# 3. 两端封口(N 边形面,交给下游三角化)
if cap_start:
faces.append(tuple(range(n - 1, -1, -1)))
if cap_end:
base = (rings - 1) * n
faces.append(tuple(range(base, base + n)))
return verts, faces
理解它的关键是 frame(标架) 这个概念:一个标架是三元组 (origin, U, V)------截面所在平面的原点和两个基向量。截面点 (pu, pv) 变换到世界空间就是 origin + pu*U + pv*V。这个设计把「截面长什么样」和「截面怎么摆」彻底分开:
- 直圆柱:截面是圆,标架沿直线平移;
- 锥体:标架平移的同时,把 U、V 逐渐缩短(截面缩小);
- 弯曲的刀身:标架的 origin 沿弧线走;
- 螺旋缠带:标架一边前进一边绕轴旋转。
有了 sweep,其他基元都是几行代码的封装。比如八棱柱(护手)就是「正八边形截面 + 两个标架」:
python
def ngon_prism_x(x0, x1, radius, nsides=8, rot=0.0):
prof = [(radius*math.cos(2*math.pi*i/nsides + rot),
radius*math.sin(2*math.pi*i/nsides + rot)) for i in range(nsides)]
frames = [((x0,0,0),(0,1,0),(0,0,1)), ((x1,0,0),(0,1,0),(0,0,1))]
return sweep(prof, frames)
四、刀身:截面设计、弧度曲线与收尖
刀身是全刀的灵魂,也是最能体现「参数化思维」的部件。
4.1 五边截面
真实刀剑的截面不是简单的扁菱形。观察原画(以及真实的打刀),我们设计了一个五边形截面:最下方是刃口尖点,两侧是最宽的「镐线」肩部,上方是带倒角的平背:
python
prof = [
(0.0, -EDGE_DROP), # 刃口尖(下方)
(THICK/2, -0.004), # 右肩(最宽处)
(THICK/2*0.62, SPINE_RISE*0.86), # 右背倒角
(0.0, SPINE_RISE), # 背脊(上方)
(-THICK/2*0.62, SPINE_RISE*0.86), # 左背倒角
(-THICK/2, -0.004), # 左肩
]
其中 EDGE_DROP = 0.030(刃口到中线 30mm)、SPINE_RISE = 0.016(刀背到中线 16mm)、THICK = 0.0078(最大厚度 7.8mm)。刃口在截面上是一个真正的「尖点」------两条边在此汇聚成零厚度,渲染时这条棱会自然接收高光,视觉上就是开过刃的感觉。背部的两级倒角则让刀背在受光时有一条细窄的亮面,避免「铁片感」。
4.2 弧度(反り):一条二次曲线
日本刀的「反り」(sori)是刀身向刀背方向的弧弯。我们用二次曲线描述中线的抬升:
python
BLADE_LEN = 0.76 # 刃长
SORI = 0.036 # 最大弧高
def blade_center_z(x):
t = max(x, 0.0) / BLADE_LEN
return SORI * t * t # 二次曲线:刀根平缓、越往刀尖弯得越快
为什么用 t² 而不是圆弧或正弦?因为二次曲线在 x=0(刀根)处斜率为零------刀身从护手里「笔直地」长出来,然后逐渐弯曲,这正是打刀「先反」造型的观感。生成刀身时,44 段标架沿 X 轴排布,每段的 origin 按这条曲线抬升,同时在最后 20% 长度上把截面高度逐渐压缩(k 系数从 1.0 降到 0.45),刀身便向刀尖自然收窄:
python
def blade_frames(x_from=0.0, x_to=BLADE_LEN, steps=44):
frames = []
for s in range(steps + 1):
t = s / steps
x = x_from + (x_to - x_from) * t
tt = x / BLADE_LEN
k = 1.0 if tt < 0.80 else 1.0 - 0.55 * ((tt - 0.80) / 0.20) ** 1.5
frames.append(((x, 0, blade_center_z(x)), (0, 1, 0), (0, 0, k)))
return frames
注意第三个基向量 (0, 0, k)------把缩放编码进标架的基向量长度,sweep 函数完全不用改,截面就自动变小了。这是标架抽象的又一次胜利。
4.3 切先:把最后一圈收成一个点
刀尖(切先)不能用「截面缩到无限小」来做------那会产生一圈退化的微小面片,导出 FBX 后法线计算会出噪点。正确做法是:扫掠在距刀尖 2cm 处停止,追加一个单独的顶点,把最后一圈截面的每条边与这个尖点连成三角形扇:
python
verts, faces = sweep(prof, frames, cap_start=True, cap_end=False)
tip = (BLADE_LEN + 0.022, 0.0, blade_center_z(BLADE_LEN) + TIP_UP)
verts.append(tip)
ti = len(verts) - 1
base = (len(frames) - 1) * n # 最后一圈的起始索引
for i in range(n):
j = (i + 1) % n
faces.append((base + i, base + j, ti)) # 三角扇收尖
TIP_UP = 0.006 让刀尖相对中线再上扬 6mm,配合弧度曲线,形成打刀特有的「帽子切先」上挑轮廓。
五、螺旋缠柄:正交标架的构造
柄上的斜十字缠绳(柄巻き)是最有「手工感」的部件,也是数学上最有趣的:它是两条互为镜像的螺旋带。每条带是一个矩形截面沿螺旋线的扫掠------难点在于,螺旋线上每一点的标架怎么算?
标架需要两个互相垂直、且垂直于前进方向的基向量。设螺旋参数 t ∈ [0,1],角度 θ = θ₀ ± turns·2π·t(正负号决定左旋右旋),柄面是椭圆截面(半径 ry、rz),则:
python
def build_wrap_helix(handed=1, theta0=0.0):
prof = [(-0.0052, -0.0012), (0.0052, -0.0012),
(0.0052, 0.0022), (-0.0052, 0.0022)] # 10.4mm 宽扁矩形
frames = []
steps, turns = 96, 9.0
x0, x1 = GRIP_X0 - 0.004, GRIP_X1 + 0.006
for s in range(steps + 1):
t = s / steps
x = x0 + (x1 - x0) * t
th = theta0 + handed * turns * 2*math.pi * t
ry, rz = _grip_r(t, 0.0145), _grip_r(t, 0.0192) # 柄向尾部收细
cy, cz = ry*math.cos(th), rz*math.sin(th) # 螺旋线位置
# R:径向朝外(椭圆面的近似法向)
R = vnorm((0.0, math.cos(th)*rz, math.sin(th)*ry))
# T:前进方向 = 位置对 t 的导数
T = vnorm((x1 - x0,
handed*turns*2*math.pi*(-math.sin(th))*ry,
handed*turns*2*math.pi*( math.cos(th))*rz))
# B:横向 = T × R,三者构成正交标架
B = vnorm(vcross(T, R))
frames.append(((x, cy, cz), B, R))
return sweep(prof, frames)
三个向量的分工:R(径向)让缠带贴着柄面「躺平」,T(切向)是螺旋的前进方向,B = T × R 是缠带的宽度方向。矩形截面的宽沿 B、厚沿 R,截面 v 坐标从 -1.2mm 开始------故意嵌进柄体 1.2mm ,保证缠带与柄面之间绝无缝隙。两条带 handed=+1, θ₀=0 和 handed=-1, θ₀=π 反向缠绕,交叉处自然形成菱形网格,正是原画里的斜十字纹。
椭圆截面还有个小陷阱:椭圆上一点的外法线不是 从中心指向该点的方向(圆才是)。近似法线是 (cosθ·rz, sinθ·ry)------两个半径交换了位置。用错的话缠带会在椭圆长短轴处「陷进」柄体。这类三维小知识,程序化建模会逼着你真正搞懂。
六、材质即数据:一个字典喂三端
几何内核的最后一件事是定义材质表。注意它依然是纯数据------没有任何渲染器绑定:
python
MATERIALS = {
"steel_dark": {"color": (0.13,0.14,0.16), "metallic": 0.9,
"rough": 0.35, "emit": None},
"guard_bronze": {"color": (0.45,0.36,0.18), "metallic": 1.0,
"rough": 0.45, "emit": None},
"wrap_black": {"color": (0.05,0.05,0.06), "metallic": 0.0,
"rough": 0.9, "emit": None},
"emissive_core": {"color": (0.21,0.88,1.0), "metallic": 0.0,
"rough": 0.3, "emit": ((0.21,0.88,1.0), 30.0)},
# ...共 6 种
}
同一份数据被三端消费:预览器把它写成 OBJ 的 .mtl 伴随文件(Kd 漫反射色、Ke 自发光);Blender 脚本把它翻译成 Principled BSDF 节点参数;UE5 侧则按材质槽名称对号入座换成引擎材质实例。emissive_core 的自发光强度 30 是给 UE5 Bloom 准备的------刀刃的青色雷电要在夜之城式的暗调场景里「炸」出来,强度必须远超 1.0。
每个部件在注册时声明自己用哪个材质:
python
def build_parts():
parts = []
parts.append(("blade", "steel_dark", *build_blade()))
parts.append(("energy_edge", "emissive_core", *build_energy_edge()))
parts.append(("guard_plate", "guard_bronze", *build_guard_plate()))
# ...共 34 个部件
return parts
实测整刀数据:34 个部件、2640 个顶点、2481 个面(四边面为主)。对一把要怼到镜头前的英雄武器来说,这个面数低得奢侈------但别忘了,硬表面的观感大头在法线和材质,而不是密度。后续需要更圆润的倒角,在 Blender 层加一个 Bevel 修改器即可,几何内核不用动。
七、不开 Blender 的快速预览:OBJ + matplotlib
调形阶段最怕的是「改参数 → 开 Blender → 跑脚本 → 看结果」这个循环太长。我们的解法是给几何内核配一个零门槛预览器:直接写 OBJ 文件,再用 matplotlib 的 3D 模块渲四张示意图。
写 OBJ 出乎意料地简单------它就是文本:
python
def write_obj(parts, path_obj, path_mtl):
with open(path_mtl, "w", encoding="utf-8") as m:
for name, mat in K.MATERIALS.items():
r, g, b = mat["color"]
m.write(f"newmtl {name}\nKd {r:.3f} {g:.3f} {b:.3f}\n")
if mat["emit"]:
(er, eg, eb), s = mat["emit"]
m.write(f"Ke {er*s/10:.3f} {eg*s/10:.3f} {eb*s/10:.3f}\n")
m.write("\n")
with open(path_obj, "w", encoding="utf-8") as f:
f.write(f"mtllib {os.path.basename(path_mtl)}\n")
off = 1 # OBJ 索引从 1 开始!
for pname, mat, verts, faces in parts:
f.write(f"o {pname}\nusemtl {mat}\n")
for v in verts:
f.write(f"v {v[0]:.6f} {v[1]:.6f} {v[2]:.6f}\n")
for face in faces:
f.write("f " + " ".join(str(i + off) for i in face) + "\n")
off += len(verts)
唯一的坑是 off:OBJ 的顶点索引是全局的且从 1 开始,每写完一个部件要把偏移量累加上去。这个格式几乎所有三维软件都认------Windows 自带的 3D 查看器、Blender、MeshLab 都能直接打开,团队里非技术成员也能双击看模型。
matplotlib 渲染用 Poly3DCollection 把所有面画成多边形集合,四个机位(侧视、俯视、透视、护手特写)各出一张 PNG。它没有光照、没有正确的遮挡排序(zsort="average" 只是近似),但检查「护手和刀根有没有穿插」「缠柄圈数密不密」绰绰有余。从改参数到看见图,实测一秒出头------这就是几何内核与 Blender 解耦换来的迭代速度。
在我们的实际调形过程中,这个预览层抓住了两个问题:第一版科技护套(habaki)比护手还宽,视觉头重脚轻,把半长从 36mm 收到 30mm;第一版缠柄只有 6 圈,间隙大得能看见柄体,加密到 9 圈、缠带加宽到 10.4mm 后才有原画里紧实的手工感。两次修改都只动了一行参数。
八、bpy 落地:把纯数据变成 Blender 网格
预览满意后,进入 Blender 层。脚本要在无头模式下运行:
blender --background --python jiemie_katana_blender.py -- --out "SM_JieMie_Katana.fbx"
--background 不开 GUI,--python 指定脚本,-- 之后的参数留给脚本自己解析(sys.argv 里 -- 后面的部分)。这是 DCC 自动化的标准姿势,CI 服务器上也能跑。
8.1 from_pydata:最直接的建网格方式
bpy 提供了多种建网格的途径(bmesh 逐面构造、逐顶点 operator......),但当你手里已经有完整的顶点/面数据时,from_pydata 是最快的:
python
def part_to_object(pname, matname, verts, faces, mats):
mesh = bpy.data.meshes.new(pname)
mesh.from_pydata([list(v) for v in verts], [], [list(f) for f in faces])
mesh.update()
obj = bpy.data.objects.new(pname, mesh)
bpy.context.collection.objects.link(obj)
obj.data.materials.append(mats[matname])
from_pydata(顶点, 边, 面) 直接吞下 Python 列表,面可以是任意 N 边形(我们的封口面就是 6 边形和 22 边形)。必须调用 mesh.update() 让 Blender 计算内部缓存,否则后续操作会崩。
8.2 法线修复:程序生成网格的必修课
手写几何最常见的问题是面的环绕方向(winding)不一致------有的面法线朝外、有的朝内,渲染时表面会出现「补丁状」的明暗错乱。与其在生成端小心翼翼地保证每个 face 的顶点顺序,不如交给 bmesh 一键重算:
python
bpy.ops.object.mode_set(mode="EDIT")
bm = bmesh.from_edit_mesh(mesh)
bmesh.ops.recalc_face_normals(bm, faces=bm.faces) # 法线统一朝外
bmesh.update_edit_mesh(mesh)
bpy.ops.object.mode_set(mode="OBJECT")
recalc_face_normals 按封闭体积的启发式把所有法线翻向外侧。配合平滑标记(曲面部件 use_smooth=True,硬表面保持平直),着色立刻正常。我们还在合并后的物体上加了一个 WEIGHTED_NORMAL 修改器(keep_sharp=True),它按面积加权平均法线,让大平面主导过渡区的着色------硬表面建模在 UE 里「看起来贵」的小秘诀。
8.3 材质 API 的 3.x/4.x 兼容陷阱
这是全篇最「血泪」的一段。Blender 4.0 重构了 Principled BSDF 的输入槽命名:3.x 的自发光槽叫 Emission,4.x 改名为 Emission Color。直接按名字索引,脚本在另一个大版本上必崩。防御式写法:
python
if spec["emit"]:
(er, eg, eb), strength = spec["emit"]
if "Emission Color" in bsdf.inputs: # Blender 4.x
bsdf.inputs["Emission Color"].default_value = (er, eg, eb, 1.0)
bsdf.inputs["Emission Strength"].default_value = strength / 10.0
elif "Emission" in bsdf.inputs: # Blender 3.x
bsdf.inputs["Emission"].default_value = (er, eg, eb, 1.0)
用 in bsdf.inputs 探测槽位是否存在,两个版本各走各的分支。类似的改名还有 Specular → Specular IOR Level、Transmission → Transmission Weight。写要分发给别人的 bpy 脚本,这种探测式兼容是基本素养------你无法控制用户装的是哪个 Blender。
8.4 合并与命名规范
34 个部件最终 bpy.ops.object.join() 合并成一个 物体,命名 SM_JieMie_Katana。两个理由:UE5 里一把武器就该是一个 StaticMesh(挂接、物理、流送都简单);材质自动去重后剩 6 个材质槽,正好对应六种材质。SM_ 前缀遵循 UE 社区资产命名规范(StaticMesh),从源头贯彻命名规范,省去导入后改名的麻烦。
九、FBX 导出:每个参数都有讲究
bpy.ops.export_scene.fbx 的参数少说几十个,对 UE5 而言关键的是这几个:
python
bpy.ops.export_scene.fbx(
filepath=path,
use_selection=True,
object_types={"MESH"}, # 只要网格,不带灯光相机
mesh_smooth_type="FACE", # 按面导出平滑组 → UE 不再警告
use_tspace=True, # 导出切线空间 → 法线贴图正确
add_leaf_bones=False, # 骨骼末端不加叶子骨(静态网格无所谓,习惯)
axis_forward="-Z", axis_up="Y", # 坐标系转换(见下)
apply_unit_scale=True,
global_scale=1.0,
path_mode="COPY", # 贴图打包进 FBX 目录
)
三个值得展开的点:
mesh_smooth_type="FACE" 。默认值导出的 FBX 不带平滑组信息,UE5 导入时会弹那个著名的警告 "No smoothing group information was found",并把整个模型硬着色。设成 FACE 后,我们在 Blender 里标记的平滑/平直信息完整传递。
use_tspace=True。导出预计算的切线与副切线。没有它,UE5 会自己重算切线空间,与烘焙法线贴图时的切线空间不一致,斜面上的法线细节会出现「交叉阴影」伪影。做武器这种要怼脸看的道具,必须开。
坐标系 。Blender 是右手系 Z 朝上,UE5 是左手系 Z 朝上、X 朝前。axis_forward="-Z", axis_up="Y" 是社区验证过的标准组合,配合 UE 导入侧的 convert_scene,模型进引擎后朝向正确、不需要额外旋转。单位方面 Blender 用米、UE 用厘米,FBX 的 unit scale 机制会自动换算------但如果你的模型进 UE 后只有指甲盖大,就在导入侧补一个 import_uniform_scale=100。
十、UE5 侧:Python 自动化导入
UE5 内置 Python(需在插件里启用 Python Editor Script Plugin)。与其每次手动拖 FBX 再逐项勾选项,不如把导入也写成脚本,配置永远一致:
python
import unreal, os
def build_task():
options = unreal.FbxImportUI()
options.import_mesh = True
options.import_as_skeletal = False # 静态网格
options.import_materials = True # 先带占位材质进来
options.import_textures = False
sm = options.static_mesh_import_data
sm.combine_meshes = True
sm.generate_lightmap_u_vs = True # 自动生成 Lightmap UV1
sm.auto_generate_collision = True # 自动凸包碰撞
sm.convert_scene = True # 处理 Blender→UE 坐标差异
if hasattr(sm, "build_nanite"):
sm.build_nanite = False # 2.5k 面,Nanite 纯属浪费
task = unreal.AssetImportTask()
task.filename = FBX_PATH
task.destination_path = "/Game/SanJie/Weapons/JieMie"
task.destination_name = "SM_JieMie_Katana"
task.automated = True # 不弹导入对话框
task.replace_existing = True # 重导入覆盖,支持迭代
task.save = True
task.options = options
return task
unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([build_task()])
几个配置的理由:Nanite 不开 ------Nanite 为百万面网格设计,2481 面的武器开它反而增加存储与集群开销;replace_existing=True 是管线的灵魂 ------参数改了、重跑 Blender、重跑导入,UE 里的资产原地更新,所有引用它的蓝图、关卡不受影响,这才叫「管线」而不是「一次性脚本」;材质用占位 ------FBX 里的简单材质导入后,手动或用脚本把六个槽替换成项目的主材质实例(MI_JieMie_Steel、MI_JieMie_Emissive......),自发光槽接上 Niagara 顶点动画或 Panner 节点,刀刃的雷电就会流动起来。
导入脚本最后会打印每个材质槽的名称清单,方便核对:
[JieMie] LOD0 共 6 个材质槽:
槽 0: steel_dark ← 建议替换为 MI_JieMie_Steel
槽 1: emissive_core ← 建议替换为 MI_JieMie_Lightning
...
运行方式两种:编辑器里 Tools → Execute Python Script;或者命令行无头执行 UnrealEditor-Cmd.exe 工程.uproject -run=pythonscript -script=路径------后者可以接进 CI,做到「提交参数改动 → 自动出 FBX → 自动进引擎 → 自动截图对比」的全自动回归。
十一、一键串联:Windows 批处理的自动发现
管线各层都是命令行程序,最后用一个 bat 把「找 Blender → 无头建模 → 导出 FBX」串成双击即用:
bat
@echo off
chcp 65001 >nul
setlocal enabledelayedexpansion
cd /d "%~dp0"
set BLENDER=
if exist "D:\Blender\blender.exe" set BLENDER=D:\Blender\blender.exe
if "!BLENDER!"=="" (
for %%D in ("D:\Program Files\Blender Foundation" "D:\Blender Foundation" "D:\") do (
if exist "%%~D" (
for /f "delims=" %%F in ('dir /b /s /a-d "%%~D\blender.exe" 2^>nul') do (
if "!BLENDER!"=="" set "BLENDER=%%F"
)
)
)
)
if "!BLENDER!"=="" ( where blender >nul 2>&1 && set BLENDER=blender )
"!BLENDER!" --background --python "%~dp0jiemie_katana_blender.py" -- --out "%~dp0SM_JieMie_Katana.fbx"
细节:chcp 65001 切 UTF-8 防止中文路径乱码;enabledelayedexpansion + !VAR! 才能在 for 循环里正确读写变量(batch 的经典陷阱);查找顺序「显式路径 → 常见安装目录递归 → PATH」,兼顾速度与覆盖面。团队里任何人拿到这个文件夹,双击 bat 就能出 FBX,不需要知道任何命令行知识。
十二、参数化的回报:变体量产
管线搭好后,真正的红利开始显现。刀的所有形体特征都是 katana_core.py 顶部的具名常量:
python
BLADE_LEN = 0.76 # 刃长
SORI = 0.036 # 弧度
THICK = 0.0078 # 厚度
BLADE_LEN=0.95, SORI=0.05→ 一把野太刀;SORI=0, SPINE_RISE=THICK/2→ 直刃唐刀;BLADE_LEN=0.45→ 胁差(短刀);- 改
MATERIALS["emissive_core"]["color"]→ 雷属性青色、火属性橙红、混沌紫------对应《三界械堂》里主角混元灵根切换灵气属性的设定,一套几何配 N 套发光配色,皮肤系统的资产成本近乎为零。
再往前一步是批量生成:写一个循环,遍历参数组合字典,每组调用一次 Blender 无头导出,一夜之间产出整个武器库的白模基底。这正是「程序化」三个字的复利:第一把刀花三天搭管线,第二把刀开始只按分钟计。
python
VARIANTS = {
"SM_JieMie_Katana": dict(BLADE_LEN=0.76, SORI=0.036),
"SM_JieMie_Nodachi": dict(BLADE_LEN=0.95, SORI=0.050),
"SM_JieMie_TangDao": dict(BLADE_LEN=0.78, SORI=0.0),
"SM_JieMie_Wakizashi":dict(BLADE_LEN=0.45, SORI=0.022),
}
for name, params in VARIANTS.items():
apply_params(params) # 覆写 katana_core 模块常量
build_and_export(f"{name}.fbx")
十三、边界与反思:这条管线不适合什么
诚实地划清边界,比吹嘘全能更有价值。
适合:硬表面道具(武器、义体部件、机械、建筑构件)、kitbash 零件库、白模与中模、需要大量参数变体的资产、需要进 CI 的资产回归。
不适合 :有机体(角色、生物------去用雕刻)、布料毛发(去用模拟)、需要极高艺术自由度的英雄资产终稿(程序化出中模,最后 20% 的灵魂仍然要美术手调)。我们对劫灭太刀的定位也很清楚:这是可以直接用于远中景与预告片的中模,如果它要出现在过场动画的极限特写里,还需要美术在这版基底上做高模雕刻与贴图绘制。程序化管线的价值不是取代美术,而是把美术从「搭基本形」的机械劳动里解放出来,把时间花在真正需要人类审美的最后一公里。
另一个反思是 UV。本文的管线止步于「自动 Lightmap UV」,贴图 UV 仍是程序生成的简单投影。对纯色 + 自发光的赛博风格资产够用;要画手绘贴图,还得在 Blender 里认真展一次 UV。程序化展 UV(xatlas 等方案)是这条管线的下一站。
十四、全流程清单(拿走即用)
最后把整条管线浓缩成一张可执行的清单:
- 读图:对着原画列部件参数表(体块、拓扑、真实尺寸)。
- 内核:纯 Python 写几何生成,sweep 一个原语打天下;材质做成数据字典。禁止 import bpy。
- 预览:OBJ + matplotlib,秒级迭代调形;尺寸穿插问题在这层全部解决。
- Blender 层 :
from_pydata建网格 →recalc_face_normals修法线 → 版本兼容的 Principled 材质 →join合并 → 加权法线修改器。 - 导出 :
mesh_smooth_type="FACE"、use_tspace=True、axis_forward="-Z"/axis_up="Y"。 - UE5 :
AssetImportTask自动导入,replace_existing=True支持无限重导,Lightmap UV 与碰撞自动生成,材质槽换项目 MI。 - 串联:bat/shell 一键脚本 + (可选)CI 无头跑 UE 导入。
- 量产:参数字典 × 循环 = 武器库。
从一张 AI 概念图,到 UE5 里一把 2481 面、六材质槽、刀刃流着青色雷电的量子太刀------整条路上没有一次手动建模操作。这就是 2026 年的小团队该有的资产生产力。
版权与免责声明
© 2026 梦帮集团(DREAMVFIA)。本文全部技术内容与示例代码为梦帮集团《三界械堂》项目组原创,代码可用于学习与非商业用途;商业使用请注明出处。
《三界械堂》世界观与全部美术设定(劫灭量子太刀、王天劫、八卦护手造型等)为梦帮集团原创知识产权。文中提及的 Blender 为 Blender 基金会开源软件,Unreal Engine 为 Epic Games 商标,请遵循各自许可协议。
本文描述的管线为项目当前实践,随工具版本演进可能调整;文中数据(顶点数、面数等)为写作时的实测值。
------ 梦帮集团 · 《三界械堂》研发组 2026 年