安装BlenderMMD导入插件

方法1 Github下载

https://github.com/UuuNyaa/blender_mmd_tools/tree/v4.2.2?tab=readme-ov-file

方法2 Blender内置下载

内置下载或者到Blender插件社区下载 社区地址:https://extensions.blender.org/

模型处理

导入

删除无用

joints(关节点)和rigidbodies(刚体)

多余材质合并

面,齿,舌,口合并,编辑模式下材质面板点一下直接选择就行,最后选择面,点击指定

合并后物体模式下删除材质槽

眼睛合并到一起

眉毛合并到一起

眼睛高光和透贴不要删除

头发(刘海)需要拆下来,做眼睛透过头发效果

不需要分离,选出来之后直接新建一个材质头发指定上

将头发,肌肤,黑丝都合并到饰品里,然后删掉其他的

生成一套平滑法线用于描边

可以存在UV里面或者在定点色里面

新建python脚本

注意:mesh = bpy.data.meshes[‘星見雅’],这里的名称是网格体名称

因为是UV存值,所以需要将点位置从3维转换到2维,使用八面体映射

import bpy  
from mathutils import *
from math import *
import numpy as np

#创建空字典列表list
dict = {}
#获取模型Mesh
mesh = bpy.data.meshes['星見雅']

#calc_tangents:
#在网格中计算每个顶点的切线(tangent)、双切线(bitangent)和法线(normal),以便后续的方向相关计算。
mesh.calc_tangents(uvmap = 'UVMap')

#计算两个向量之间的夹角
# a·b = |a||b|cosθ
# θ = arccos(a·b / (|a||b|))
# 这里返回的弧度值  θ * (π/180) ≈ xxx弧度
def included_angle(v0, v1):
    return np.arccos(v0.dot(v1)/(v0.length * v1.length))

# 3维降为2维
def unitVectorToOct(v):
    # 步骤1:计算L1范数(曼哈顿距离)
    d = abs(v.x) + abs(v.y) + abs(v.z)
    # 步骤2:通过除以L1范数进行投影到八面体表面
    # o 是八面体的坐标
    o = Vector((v.x / d, v.y / d))
    # 步骤3:如果z是负数,需要进行特殊处理以保持连续性
    if v.z <= 0:
        # 使用折叠技术处理八面体的下半部分
        o.x = (1 - abs(o.y)) * (1 if o.x >= 0 else -1)
        o.y = (1 - abs(o.x)) * (1 if o.y >= 0 else -1)
    return o

#.co是coordinate的缩写,表示顶点的坐标。
# 取出模型的每一个顶点坐标,然后清空置空列表,后续存入新的坐标数据
for vertex in mesh.vertices:
    # 初始化 "<Vector (1.0, 2.0, 3.0)>": []
    dict[str(vertex.co)] = []

# 获取模型Mesh的每一个面
# l0,l1,l2分别表示模型Mesh的每一个面中的每一个顶点数据
# l0,l1,l2这样的写法是Blender的写法,表示loop0,loop1,loop2的循环体
# mesh.loops 存储了多边形顶点的循环信息
# poly.loop_start 表示多边形顶点的循环开始索引
for poly in mesh.polygons:
    #获取模型Mesh的每一个三角面中的每一个顶点数据
    l0 = mesh.loops[poly.loop_start] 
    l1 = mesh.loops[poly.loop_start + 1]
    l2 = mesh.loops[poly.loop_start + 2]
    
    #获取模型Mesh的每一个面中的每一个顶点数据
    ## 顶点的主要属性:
    #vertex.co           顶点的3D坐标 (Vector类型,包含x,y,z)
    #vertex.normal       顶点的法线方向
    #vertex.index        顶点在mesh.vertices数组中的索引号
    #vertex.groups       顶点组信息(用于骨骼绑定等)
    v0 = mesh.vertices[l0.vertex_index]
    v1 = mesh.vertices[l1.vertex_index]
    v2 = mesh.vertices[l2.vertex_index]

    #计算向量,三角形两条边向量
    vec0 = v1.co - v0.co
    vec1 = v2.co - v0.co
    
    #计算向量叉积,三角形两条边向量叉乘,得到法线
    n = vec0.cross(vec1)
    n = n.normalized()

    #v0.co 是一个 Vector 类型,包含 (x,y,z) 坐标值
    #Python的字典要求键(key)必须是"可哈希的"(hashable)
    #Vector 类型不能直接用作字典的键
    #这里将顶点坐标转换为字符串,便于后续使用
    #k0,k1,k2是字典的Key
    k0 = str(v0.co)     # 将顶点0的坐标转换为字符串
    k1 = str(v1.co)     # 将顶点1的坐标转换为字符串
    k2 = str(v2.co)     # 将顶点2的坐标转换为字符串
    
    # 计算三角形三个顶点的权重
    if k0 in dict:
        #计算顶点0的权重,w实际上是两向量夹角角度
        w = included_angle(v2.co - v0.co, v1.co - v0.co)
        # 添加数据
        dict[k0].append({"n" : n, "w" : w})
        
    if k1 in dict:
        w = included_angle(v0.co - v1.co, v2.co - v1.co)
        dict[k1].append({"n" : n, "w" : w})
        
    if k2 in dict:
        w = included_angle(v1.co - v2.co, v0.co - v2.co)
        dict[k2].append({"n" : n, "w" : w})
        
for poly in mesh.polygons:
    # 获取每一个三角面数据 
    #range(poly.loop_start, poly.loop_start + 3)遍历3个点
    for loop_index in range(poly.loop_start, poly.loop_start + 3):
        l = mesh.loops[loop_index]
        vertex_index = l.vertex_index
        v = mesh.vertices[vertex_index]
        #smoothNormal初始化
        smoothNormal = Vector((0,0,0))
        weightSum = 0
        k = str(v.co)
        if k in dict:
            a = dict[k]
            for d in a:
                n = d['n']
                w = d['w']
                #加权平均法线的计算公式:
                #例如:smoothNormal = (n1 * w1 + n2 * w2 + n3 * w3) / (w1 + w2 + w3)
                smoothNormal += n * w
                weightSum += w
        if smoothNormal.length != 0:
            smoothNormal /= weightSum
            smoothNormal = smoothNormal.normalized()
        else:
            # 如果计算出的平滑法线无效(长度为0),
            # 使用原始顶点法线作为后备方案
            smoothNormal = l.normal
        
        normal = l.normal           # 获取顶点的原始法线
        tangent = l.tangent         # 获取顶点的原始切线
        bitangent = l.bitangent     # 获取顶点的原始副切线

        # 计算投影
        # 使用原始的切线空间基底,计算平滑法线在这个空间中的表示
        x = tangent.dot(smoothNormal)       # 平滑法线在原始切线方向上的投影
        y = bitangent.dot(smoothNormal)     # 平滑法线在原始副切线方向上的投影
        z = normal.dot(smoothNormal)        # 平滑法线在原始法线方向上的投影
        
        #UV数组的大小总是等于loops的数量
        uv1 = mesh.uv_layers['UVMap.001'].uv[loop_index]
        
        uv1.vector = unitVectorToOct(Vector((x,y,z)))

如果使用顶点色传入的话不需要进行维度转换

# 存入顶点色而不是UV
vertex_color_layer = mesh.vertex_colors.active.data[loop_index]
# 直接将平滑法线的分量存入顶点色
vertex_color_layer.color = (smoothNormal.x, smoothNormal.y, smoothNormal.z)

模型拆分

将有形态键的面部单独拆一个网格,包含下面的目影一起拆

清理材质

面部一样清理一下

去除原网格体形态键

给虚幻引擎需要将模型拆分多一些,刘海拆分下来

饰品和衣服也拆分下来

因为涉及到半透明排序还有覆层材质,眼影等全部都要拆分

拆完记得重命名和清理材质

Blender内模型可以不需要拆

合并之前的模型,刘海记得单独一个材质插槽