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; }
关于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;
// 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; }
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); ... }
上面是简化了的函数,方便说事。函数里面还用上了 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; }
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