转载: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