Blender 之修改器代码分析

  • Post author:
  • Post category:其他


转载:

Blender 之修改器代码分析 – KAlO2 – 博客园


Blender 之修改器代码分析

Blender的修改器(modifier)模块,默认界面右下块(Property)面板的扳手,分类(修改、生成、形变、模拟)列出所有的修改器。也可以空格键输入modifier,出现”Add Modifier”后点击即可。我参与翻译了官方的

修改器文档

,也跟着

制作双螺旋结构的DNA教程

走了一遍,算是对修改器有个大致的了解。制作很简单,用上细分表面(Subsurf)、镜像(Mirror)、阵列(Array)、曲线(Curve)四个修改器。首先添加杆与球,用上

细分表面修改器

,得到更圆滑的效果。接着用

镜像修改器

得到一个碱基对,成哑铃状。然后用

阵列修改器

,生成梯子形状。最后用

曲线修改器

,扭转前面的梯子得到DNA的模型。如果对修改器不熟悉,可以照着教程走一遍,会有收获的。

修改器作为Blender的一个子系统,设计成栈模式。前一个修改器的输出作为后一个修改器的输入,达到最终的效果。修改器是一种以非破坏性(non constructive)的方式影响物体的操作。修改器可以添加或删除,栈上移上移下,应用会让更改生效(编辑模式下不可应用)。

修改器的工程 bf_modifiers.vcxproj ,源码路径在 source/blender/modifiers/ ,相关文件有:

source/blender/blenkernel/BKE_modifier.h

source/blender/blenkernel/intern/modifier.c

source/blender/editors/object/object_intern.h

source/blender/editors/object/object_modifier.c

source/blender/makesdna/DNA_modifier_types.h

source/blender/makesdna/intern/rna_modifier.c


Operator

把鼠标停在Array修改器上,会给出提示(tooltip):

Add a modifier to the active object: Array

Python: bpy.ops.object.modifier_add(type=”ARRAY”)

第一句是Operator 的 description 字段,第二句是对应的 Python 代码。直接在源码里工程搜索字符串 “Add a modifier” 就会指引你去往有关修改器的

Operator

字符串在 OBJECT_OT_modifier_add 函数里,找到 OBJECT_OT_modifier_add 函数名后,Visual Studio 里按下F12(或鼠标右键选择Go To definition)跳转到定义处。

从 object_intern.h 找到有关修改器 add / remove / move_up / move_down / apply / convert / copy 的 Operator:

void OBJECT_OT_modifier_add(struct wmOperatorType *ot);

void OBJECT_OT_modifier_remove(struct wmOperatorType *ot);

void OBJECT_OT_modifier_move_up(struct wmOperatorType *ot);

void OBJECT_OT_modifier_move_down(struct wmOperatorType *ot);

void OBJECT_OT_modifier_apply(struct wmOperatorType *ot);

void OBJECT_OT_modifier_convert(struct wmOperatorType *ot);

void OBJECT_OT_modifier_copy(struct wmOperatorType *ot);

void OBJECT_OT_modifier_add(wmOperatorType *ot)
{
    PropertyRNA *prop;

    /* identifiers */
    ot->name = "Add Modifier";
    ot->description = "Add a modifier to the active object";
    ot->idname = "OBJECT_OT_modifier_add";
    
    /* api callbacks */
    ot->invoke = WM_menu_invoke;
    ot->exec = modifier_add_exec;
    ot->poll = ED_operator_object_active_editable;
    
    /* flags */
    ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
    
    /* properties */
    prop = RNA_def_enum(ot->srna, "type", rna_enum_object_modifier_type_items, eModifierType_Subsurf, "Type", "");
    RNA_def_enum_funcs(prop, modifier_add_itemf);
    ot->prop = prop;
}

OBJECT_OT_modifier_add

关于Operator,

前面

介绍过,只不过这次多了 PropertyRNA,里面存储了修改器相关的属性数据。

添加修改器弹出的界面,分类列举了所有的修改器。绘制这个界面用到的数据:rna_enum_object_modifier_type_items。

// source/blender/makesdna/RNA_types.h
typedef struct EnumPropertyItem {
    int value;
    const char *identifier;
    int icon;
    const char *name;
    const char *description;
} EnumPropertyItem;

EnumPropertyItem
// source/blender/makesdna/intern/rna_modifier.c
EnumPropertyItem rna_enum_object_modifier_type_items[] = {
    {0, "", 0, N_("Modify"), ""},
    {eModifierType_DataTransfer, "DATA_TRANSFER", ICON_MOD_DATA_TRANSFER, "Data Transfer", ""},
    ...
    
    {0, "", 0, N_("Generate"), ""},
    {eModifierType_Array, "ARRAY", ICON_MOD_ARRAY, "Array", ""},
    ...
    
    {0, "", 0, N_("Deform"), ""},
    {eModifierType_Armature, "ARMATURE", ICON_MOD_ARMATURE, "Armature", ""},
    
    {0, "", 0, N_("Simulate"), ""},
    {eModifierType_Cloth, "CLOTH", ICON_MOD_CLOTH, "Cloth", ""},
    
    {0, NULL, 0, NULL, NULL}
};
  修改器是blender的一个模块,涉及到初始化。在看弹出Splash界面的代码时,已经发现了这一句 BKE_modifier_init(); ,在 blender 的 main函数(creator.c文件)里。

Modifier

source/blender/makesdna/DNA_object_types.h 里, struct Object 有 ListBase modifiers; 字段,即每个物体都挂着一个修改器链表。ListBase 是 Blender 的链表数据结构,在 source/blender/makesdna/DNA_listBase.h,很多结构体都会用到。由于修改器的顺序不能随意颠倒,所以用的是单向链表,而不是双向链表。Blender 是用 C/C++/Python 语言写的,开发于1990年代,当时C编译器流行而且免费,C++编译器很贵。

底层还是保留着大量C代码

。如果是C++,可以直接用标准模板库的std::list,而不用写大量的增删查找代码了。

所有的修改器的数据结构放在 DNA_modifier_types.h。修改器的数据结构 ModifierData 是双向链表。MOD_none.c 是修改器模板,功能置空。

enum ModifierType 是修改器的索引,可以通过 modifierType_getInfo 得到具体的修改器的信息。

const ModifierTypeInfo *modifierType_getInfo(ModifierType type);

ModifierTypeInfo 里面用到了很多函数指针,这在模拟C++的成员函数功能。

struct ModifierData* modifier_new(int type);

void modifier_free(struct ModifierData *md);

void modifier_copyData(struct ModifierData *md, struct ModifierData *target);

类似C++的构造函数、析构函数、复制构造函数。modifier_new 会调用initData,modifier_free会调用 freeData 函数,modifier_copyData 会调用 copyData。

ModifierData *modifier_new(int type)
{
    const ModifierTypeInfo *mti = modifierType_getInfo(type);
    ModifierData *md = MEM_callocN(mti->structSize, mti->structName);
    
    /* note, this name must be made unique later */
    BLI_strncpy(md->name, DATA_(mti->name), sizeof(md->name));

    md->type = type;
    md->mode = eModifierMode_Realtime | eModifierMode_Render | eModifierMode_Expanded;

    if (mti->flags & eModifierTypeFlag_EnableInEditmode)
        md->mode |= eModifierMode_Editmode;

    if (mti->initData) mti->initData(md);

    return md;
}

modifier_new

MEM_callocN 是Blender 自己的分配内存方法,具体修改器的长度信息会保存在 ModifierTypeInfo 的 structSize 字段。很多工程一般会用自己的一套 malloc/calloc/free 函数,并默认指向C语言的 malloc/calloc/free 函数。Blender 需要统计内存使用量,显示在整个窗口菜单(Info视图)一行的右边。

DATA_宏是对国际化的支持,从修改器的英文名找到对应语言的文字。

initData freeData 就像子类的构造函数,析构函数。以镜像修改器 MirrorModifierData 为例。

// source/blender/modifiers/intern/MOD_mirror.c

static void initData(ModifierData *md)
{
    MirrorModifierData *mmd = (MirrorModifierData *) md;

    mmd->flag |= (MOD_MIR_AXIS_X | MOD_MIR_VGROUP);
    mmd->tolerance = 0.001;
    mmd->mirror_ob = NULL;
}

这些赋给的初始值,会在UI界面添加修改器时显示出来。

我们看到,修改器文件命名都是以 MOD_ 开头的,这是 blender 工程的一种约定俗成。类似的缩写还有:

现在,我们在镜像修改器的 static 函数 initData copyData foreachObjectLink updateDepgraph updateDepsgraph applyModifier 上打断点,调试看函数何时被调用、以及调用的栈信息。这些都会作为 ModifierTypeInfo 类型里的函数指针而被调用,函数指针都标有注释。 函数在发现,任意修改修改器的参数信息(镜像轴、纹理、镜像物体等),会断在 applyModifier 函数上。看名字,还以为 applyModifier 仅仅在点击应用(Apply)按钮时才会调用。

// TaskScheduler *BLI_task_scheduler_create(int num_threads)

static void *task_scheduler_thread_run(void *thread_p)

task->run(pool, task->taskdata, thread_id);

static void scene_update_object_func(TaskPool * __restrict pool, void *taskdata, int threadid)

BKE_object_handle_update_ex(eval_ctx, scene_parent, object, scene->rigidbody_world, false);

BKE_object_handle_data_update(eval_ctx, scene, ob);

makeDerivedMesh(scene, ob, NULL, data_mask, false);

mesh_build_data(scene, ob, dataMask, build_shapekey_layers, need_mapping);

mesh_calc_modifiers(scene, ob, NULL, false, 1, need_mapping, dataMask, -1, true, build_shapekey_layers, true, &ob->derivedDeform, &ob->derivedFinal);

ndm = modwrap_applyModifier(md, ob, dm, app_flags);

mti->applyModifier(md, ob, dm, flag);

栈底不是main函数,applyModifier 原来是在其他线程被调用的。blender 用了操作系统都有实现的 pthread 库,移植性好。点击栈上各个函数,熟悉一下周围的代码。

mesh_calc_modifiers 可真是一个复杂的函数,传入的参数多,函数开头的参数也多得可怕。早先,为了简化C编译器的实现,要求变量声明在函数开始,方便计算开辟函数帧栈的大小。C++则一开始没有此要求。

/**
 * new value for useDeform -1  (hack for the gameengine):
 *
 * - apply only the modifier stack of the object, skipping the virtual modifiers,
 * - don't apply the key
 * - apply deform modifiers and input vertexco
 */
static void mesh_calc_modifiers(
        Scene *scene, Object *ob, float (*inputVertexCos)[3],
        const bool useRenderParams, int useDeform,
        const bool need_mapping, CustomDataMask dataMask,
        const int index, const bool useCache, const bool build_shapekey_layers,
        const bool allow_gpu,
        /* return args */
        DerivedMesh **r_deform, DerivedMesh **r_final)
{
    ...
    
    for (; md; md = md->next, curr = curr->next)
    {
        const ModifierTypeInfo *mti = modifierType_getInfo(md->type);

        md->scene = scene;
        if (!modifier_isEnabled(scene, md, required_mode))
            continue;
        
        ...
        
        ndm = modwrap_applyModifier(md, ob, dm, app_flags);
        ASSERT_IS_VALID_DM(ndm);
        
        if (ndm)
        {
            /* if the modifier returned a new dm, release the old one */
            if (dm && dm != ndm)
                dm->release(dm);

            dm = ndm;

            if (deformedVerts) {
                if (deformedVerts != inputVertexCos)
                    MEM_freeN(deformedVerts);

                deformedVerts = NULL;
            }
        }
        
        /* create an orco derivedmesh in parallel */
        if (nextmask & CD_MASK_ORCO)
        {
            ...
            ndm = modwrap_applyModifier(md, ob, orcodm, (app_flags & ~MOD_APPLY_USECACHE) | MOD_APPLY_ORCO);
            ASSERT_IS_VALID_DM(ndm);
            ...
        }
        
        /* create cloth orco derivedmesh in parallel */
        if (nextmask & CD_MASK_CLOTH_ORCO)
        {
            ...
            ndm = modwrap_applyModifier(md, ob, clothorcodm, (app_flags & ~MOD_APPLY_USECACHE) | MOD_APPLY_ORCO);
            ASSERT_IS_VALID_DM(ndm);
            ...
        }
        
    }
    
    for (md = firstmd; md; md = md->next)
        modifier_freeTemporaryData(md);
    
    ...
    
    const bool do_loop_normals = (me->flag & ME_AUTOSMOOTH) != 0;
    if (!do_loop_normals)
        dm_ensure_display_normals(finaldm);
        
    ...
}

mesh_calc_modifiers

上面是简化了的函数,方便说事。函数里面还用上了 OpenMP 并行编译指导语句 #pragma omp parallel。

for循环调用 modwrap_applyModifier,数据从修改器栈上的前一个修改器流向下一个修改器。modwrap_applyModifier 用来保证依赖法线的修改器(倒角修改器、数据转移修改器、位移修改器等)在 applyModifier 之前有着正确的法线,稍作调整法线后,就回到 applyModifier 上了。

applyModifier

这里需要引出 DerivedMesh,源码在 source/blender/blenkernel/BKE_DerivedMesh.h ,参考文档在

这里

。 DerivedMesh 作为一种重要的数据结构贯穿各修改器。可想象为修改器之间传递数据的介质,里面定义了很多很多的函数指针。数据从 Object 创建的 DerivedMesh 上操作,而不是直接在 Object 上操作。创建了新的 DerivedMesh 后,旧的 DerivedMesh 就会被释放掉。

applyModifier 调用了 mirrorModifier__doMirror 函数,如果输入与输出的 DerivedMesh 有变,则写入脏位 DM_DIRTY_NORMALS。因为物体镜像了,最终的法向量也需要跟着调整。在计算了所有的修改器后,会对 DerivedMesh 执行 dm_ensure_display_normals。

static DerivedMesh *mirrorModifier__doMirror(MirrorModifierData *mmd, Object *ob, DerivedMesh *dm)
{
    DerivedMesh *result = dm;

    /* check which axes have been toggled and mirror accordingly */
    if (mmd->flag & MOD_MIR_AXIS_X) {
        result = doMirrorOnAxis(mmd, ob, result, 0);
    }
    if (mmd->flag & MOD_MIR_AXIS_Y) {
        DerivedMesh *tmp = result;
        result = doMirrorOnAxis(mmd, ob, result, 1);
        if (tmp != dm) tmp->release(tmp);  /* free intermediate results */
    }
    if (mmd->flag & MOD_MIR_AXIS_Z) {
        DerivedMesh *tmp = result;
        result = doMirrorOnAxis(mmd, ob, result, 2);
        if (tmp != dm) tmp->release(tmp);  /* free intermediate results */
    }

    return result;
}

omeModifier_do()

mirrorModifier__doMirror 名字应该是多写了一个下划线,不过没关系。大多数修改器都会有一个 someModifier_do() 函数。

镜像修改器对建模对称的物体非常有用。镜像修改器可以选择性的在XYZ上作镜像,所以最多会有2*2*2 = 8个相同物体,分居在以原点为中心的八个象限。initData()里,默认仅对X轴镜像。对每个轴依次镜像,如果 DerivedMesh 数据有修改,则释放先前的 DerivedMesh 数据。

static DerivedMesh *doMirrorOnAxis(MirrorModifierData *mmd, Object *ob, DerivedMesh *dm, int axis)

读取栈上前一个修改器的顶点、边、面,细分等数据:

const int maxVerts = dm->getNumVerts(dm);
const int maxEdges = dm->getNumEdges(dm);
const int maxLoops = dm->getNumLoops(dm);
const int maxPolys = dm->getNumPolys(dm);

DerivedMesh* result = CDDM_from_template(dm, maxVerts * 2, maxEdges * 2, 0, maxLoops * 2, maxPolys * 2);

DerivedMesh *CDDM_from_template(DerivedMesh *source, int numVerts, int numEdges, int numTessFaces, int numLoops, int numPolys);  // cdderivedmesh.c

然后调用 CDDM_from_template 预分配内存,因为镜像后的顶点、边、面等数据会增一倍,所以系数都乘上2。对于

阵列修改器

而言,该数与阵列的数量成正比,外加上起始物体(Start Cap)和末端物体(End Cap)的数据,如果有的话。

镜像矩阵是 float mtx[4][4]; 。如果没有镜像物体,就以自己的原点作镜像(Ctrl + Alt + Shift + C 组合键可以用来修改物体的原点位置)。对 mtx 置一成单位矩阵后,如果对X轴镜像,mat[0][0] = -1,乘上后X坐标变成相反数。如果有镜像物体作为参考(通常是空物体),则用参考物体的局部坐标轴,而不是自己的局部坐标轴镜像。

mul_m4_v3(mtx, mv->co); 一句将原始顶点坐标变换到镜像坐标系中。const bool do_vtargetmap = (mmd->flag & MOD_MIR_NO_MERGE) == 0; 变量决定是否开启了合并选项。

Math

关于修改器模块,最重要的当属这种数据流入流出栈的架构思想,其次就是具体修改器的实现算法了。applyModifier 函数很长很长,少不了复杂的矩阵变换操作。Blender 采用了 OpenGL 里以列为主(column_major)的表示。

element = M[column][row];
| M[0][0] M[1][0] M[2][0] M[3][0] | 
| M[0][1] M[1][1] M[2][1] M[3][1] |
| M[0][2] M[1][2] M[2][2] M[3][2] |
| M[0][3] M[1][3] M[2][3] M[3][3] |

向量看做列向量。矩阵M与向量b的乘法 a = M*b; 可以写成:mul_v4_m4v4(a, M, b); 或 mul_v3_m3v3(a, M, b);,依矩阵的阶(Order)而选取。

矩阵乘法不满足交换律,但是满足结合律。(A * B) * v = A * (B * v); 合理地改变计算顺序,可以减少很多运算量。这涉及到矩阵连乘问题求解(动态规划)。

现在,我们需要给出点P关于平面对称的点R的公式,它们的距离之差为点在平面垂直线的两倍。R = P – 2*((P-V) dot N)*N

参考:


Blender 3D: Noob to Pro/Hacking Blender


如何添加一个修改器


Dev:Source/Modeling/DerivedMesh