从一张 AI 原画到 UE5 资产:程序化武器建模管线全解(Python × Blender × UE5)

版权声明 © 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 纠缠在一起,你就失去了三样东西:

  1. 可测试性。纯 Python 的几何函数可以在任何环境跑单元测试(「刀身应该有 45 圈截面环」「顶点数应该等于环数×截面点数」),CI 服务器上不用装 Blender。
  2. 快速迭代。改一个弧度参数想看效果,走 Blender 全流程要十几秒;纯 Python 直接出 OBJ + matplotlib 预览图只要一秒。调形阶段 90% 的迭代都发生在预览层。
  3. 多端复用 。同一份几何数据,预览器拿去写 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     # 二次曲线:刀根平缓、越往刀尖弯得越快

为什么用 而不是圆弧或正弦?因为二次曲线在 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, θ₀=0handed=-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 探测槽位是否存在,两个版本各走各的分支。类似的改名还有 SpecularSpecular IOR LevelTransmissionTransmission 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_SteelMI_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 等方案)是这条管线的下一站。


十四、全流程清单(拿走即用)

最后把整条管线浓缩成一张可执行的清单:

  1. 读图:对着原画列部件参数表(体块、拓扑、真实尺寸)。
  2. 内核:纯 Python 写几何生成,sweep 一个原语打天下;材质做成数据字典。禁止 import bpy。
  3. 预览:OBJ + matplotlib,秒级迭代调形;尺寸穿插问题在这层全部解决。
  4. Blender 层from_pydata 建网格 → recalc_face_normals 修法线 → 版本兼容的 Principled 材质 → join 合并 → 加权法线修改器。
  5. 导出mesh_smooth_type="FACE"use_tspace=Trueaxis_forward="-Z"/axis_up="Y"
  6. UE5AssetImportTask 自动导入,replace_existing=True 支持无限重导,Lightmap UV 与碰撞自动生成,材质槽换项目 MI。
  7. 串联:bat/shell 一键脚本 + (可选)CI 无头跑 UE 导入。
  8. 量产:参数字典 × 循环 = 武器库。

从一张 AI 概念图,到 UE5 里一把 2481 面、六材质槽、刀刃流着青色雷电的量子太刀------整条路上没有一次手动建模操作。这就是 2026 年的小团队该有的资产生产力。


版权与免责声明

© 2026 梦帮集团(DREAMVFIA)。本文全部技术内容与示例代码为梦帮集团《三界械堂》项目组原创,代码可用于学习与非商业用途;商业使用请注明出处。

《三界械堂》世界观与全部美术设定(劫灭量子太刀、王天劫、八卦护手造型等)为梦帮集团原创知识产权。文中提及的 Blender 为 Blender 基金会开源软件,Unreal Engine 为 Epic Games 商标,请遵循各自许可协议。

本文描述的管线为项目当前实践,随工具版本演进可能调整;文中数据(顶点数、面数等)为写作时的实测值。

------ 梦帮集团 · 《三界械堂》研发组 2026 年