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[] = {
{, "", , N_("Modify"), ""},
{eModifierType_DataTransfer, "DATA_TRANSFER", ICON_MOD_DATA_TRANSFER, "Data Transfer", ""},
... {, "", , N_("Generate"), ""},
{eModifierType_Array, "ARRAY", ICON_MOD_ARRAY, "Array", ""},
... {, "", , N_("Deform"), ""},
{eModifierType_Armature, "ARMATURE", ICON_MOD_ARMATURE, "Armature", ""}, {, "", , N_("Simulate"), ""},
{eModifierType_Cloth, "CLOTH", ICON_MOD_CLOTH, "Cloth", ""}, {, NULL, , 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)[],
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) != ;
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。

s

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, );
}
if (mmd->flag & MOD_MIR_AXIS_Y) {
DerivedMesh *tmp = result;
result = doMirrorOnAxis(mmd, ob, result, );
if (tmp != dm) tmp->release(tmp); /* free intermediate results */
}
if (mmd->flag & MOD_MIR_AXIS_Z) {
DerivedMesh *tmp = result;
result = doMirrorOnAxis(mmd, ob, result, );
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 * , maxEdges * , , maxLoops * , maxPolys * );

  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[][] M[][] M[][] M[][] |
| M[][] M[][] M[][] M[][] |
| M[][] M[][] M[][] M[][] |
| M[][] M[][] M[][] M[][] |

matrix

  向量看做列向量。矩阵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

  

  

Blender 之修改器代码分析的更多相关文章

  1. Blender 之 Splash 代码分析

    注:以下内容基于 Blender 2.7x 版本工程,其它低版本可能有改动. Blender启动完成时,会出现一个画面,英文叫Splash.默认是打开的,可以在设置里关闭.在文件菜单里点击用户首选项( ...

  2. 防止apk反编译的技术分析浅谈--内存修改器篇

    声明: 1.本帖转载自http://jingyan.baidu.com/article/a24b33cd509eb719fe002b94.html,仅供自用,勿喷 Apk反编译修改器有很多.拿其中的比 ...

  3. Eclipse插件(导出UML图,打开文件资源管理器插件,静态代码分析工具PMD,在eclipse上安装插件)

    目录 能够导出UML图的Eclipse插件 打开文件资源管理器插件 Java静态代码分析工具PMD 如何在eclipse上安装插件 JProfiler性能分析工具 从更新站点安装EclEmma 能够导 ...

  4. C++内存修改器开源代码

    我们玩单机游戏时,游戏难度可能过大, 或者游戏已经比较熟练,想要增加游戏的玩法,这时候可以使用修改器. 内存式游戏修改器主要对游戏内存修改 修改时有两种方式,一是定时对内存数值进行修改.实现类似锁定的 ...

  5. M-Renamer方法名修改器,iOS项目方法名重构,Objective-C/Swift,代码模型预判,减少误改的机率,替换速度更快,可视化操作,傻瓜式操作,一键操作,引用处自动修改,马甲包的福音

    M-Renamer M-Renamer(Method-Name-Renamer)类方法名修改器,采用链式解析头文件,代码模型预判,减少误改的机率,替换速度更快:可以解析整个项目大多数类的方法,可视化操 ...

  6. 真香,撸一个SpringBoot在线代码修改器

    前言 项目上线之后,如果是后端报错,只能重新编译打包部署然后重启:如果仅仅是前端页面.样式.脚本修改,只需要替换到就可以了. 小公司的话可能比较自由,可以随意替换,但是有些公司权限设置的比较严格,需要 ...

  7. 关于rt-thread调度器实现的底层代码分析

      本文使用了rt-thread自带的钩子函数和显示函数进行了实验,从rt-thread自带的延时函数rt_thread_delay()函数入手,对rt-thread系统的调度器进行分析.主要参考资料 ...

  8. VSS源代码管理器运行代码分析工具的命令

    当你发现代码库总是报需要联系管理员运行代码分析工具时,你可以使用命令分析代码库代码解决: To fix the database problems, you can restart the analy ...

  9. 完整全面的Java资源库(包括构建、操作、代码分析、编译器、数据库、社区等等)

    构建 这里搜集了用来构建应用程序的工具. Apache Maven:Maven使用声明进行构建并进行依赖管理,偏向于使用约定而不是配置进行构建.Maven优于Apache Ant.后者采用了一种过程化 ...

随机推荐

  1. 接口返回值中数组中包含多个json对象形式

    返回数据Json: { "code": , "msg": "成功", "departmentlist": [ { &qu ...

  2. C++构造函数2

    一.构造函数分类 普通构造函数,复制(拷贝)构造函数,赋值构造函数, #include <iostream> using namespace std; class A { public: ...

  3. JavaScript 中 onload 事件绑定多个方法

    当需要调用的方法较多时,我们可以进一步优化,编写一个专门用于绑定 onload事件的方法: function addLoadEvent(func) { //把现有的 window.onload 事件处 ...

  4. sed awk 样例

    sed [options] '[action]' filename options: -n:一般sed命令会把所有数据都输出到屏幕,如果加入此选项,则只会把经过sed命令处理的行输出到屏幕. -e:允 ...

  5. Apache Shiro 学习记录3

    晚上看了教程的第三章....感觉Shiro字符串权限很好用....但是教程举的例子太少了.....而且有些地方讲的不是很清楚....所以我也自己测试了一下....记录一下测试的结果.... (1) * ...

  6. Rss 订阅:php动态生成xml格式的rss文件

    Rss 简介: 简易信息聚合(也 叫聚合内容)是一种描述和同步网站内容的格式.使用RSS订阅能更快地获取信息,网站提供RSS输出,有利于让用户获取网站内容的最新更新.网络用户可以在客户端借助于支持RS ...

  7. JS这些代码你都不会,你还有什么好说的!!!

    都说自己工资低的,先看看这些代码你能写出来不?这些都不会,你还嫌工资?

  8. 图解SQL的Join 转自coolshell

    对于SQL的Join,在学习起来可能是比较乱的.我们知道,SQL的Join语法有很多inner的,有outer的,有left的,有时候,对于Select出来的结果集是什么样子有点不是很清楚.Codin ...

  9. Linux 平台GCC使用小结

    gcc -Wall [-I search_headfile_path] [-L search_lib_path] sourcefile -lNAME -o exe-name -Wall选项打开所有最常 ...

  10. Linux 基础命令

    man      帮助命令   命令 --help   命令的简单帮助 help      命令的帮助(bash的内置命令) mkdir    创建目录  如makdir /data mkdir   ...