Blender 之修改器代码分析

2023-11-19

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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

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

随机推荐

  • 图像恢复(加噪与去噪)

    人工智能导论实验导航 实验一 斑马问题 https blog csdn net weixin 46291251 article details 122246347 实验二 图像恢复 https blog csdn net weixin 46
  • tar命令笔记

    作用 tar 可以保存文件属性 本身不具备压缩能力 配合gzip或者bzip 进行压缩解压缩 参数 相关参数如下 来自百度百科 c create 创建新的tar文件 x extract get 解开tar文件 t list 列出tar文件中
  • 火狐浏览器文本两端对齐无效text-align: justify

    找了很多地方 尝试很多办法都不好使 直到看到这篇 只需要设置了text align justify时加设一个white space pre line就可以了
  • [Docker]使用Docker部署Kafka

    Kafka 是一个分布式流处理平台 它依赖于 ZooKeeper 作为其协调服务 在 Kafka 集群中 ZooKeeper 负责管理和协调 Kafka 的各个节点 因此 要在 Docker 容器中启动 Kafka 通常需要同时启动一个 Z
  • 对数器的简单使用

    对数器 1 前言 2 内容 简介对数器 以排序算法的检测为实例 3 总结 4 更新日志 1 前言 学习左神的数据结构的过程中 推荐使用对数器检验自己的算法是否正确 2 内容 简介对数器 1 对数器的作用 在一个题目未OJ的时候 可以通过对数
  • Transformer学习笔记

    一 Transformer诞生背景 Transformer模型是解决序列转录问题的一大创新 在Transformer模型之前 序列转录模型都或多或少的基于复杂的循环或卷积神经网络 循环神经网络的计算是时序性的 位置的计算必须基于之前所有位置
  • 微信小程序数据 \n 换行符失效解决办法

    最近遇到一个问题 使用uni app写小程序时 拿到一个字符串 后台返回的 需要在 n 处换行 但是直接使用 let title 黄鹤楼送 n孟浩然之广陵
  • 使用python对银行信息管理系统的简单实现

    一 首先是用户属性的类 class account object 储存用户信息的类 def init self id1 name tel money self id id1 账户 self name name 姓名 self tel tel
  • mo管理器java_Android开发之通过包管理器获取安装应用信息

    最近在自己写一个APP 有一个模块需要获取手机应用的一些信息 坑还是有 但都基本踩过了 自己把他实现了出来 实现方法还是很需要掌握的 底部弹出的对话框中四个选项的实现不多做说明 主要讲讲如何获取这些安装的应用信息 好了 不多说 看看效果图
  • 1024,干程序才懂得节日!

    1024程序员节 1024程序员节是广大程序员的共同节日 1024是2的十次方 二进制计数的基本计量单位之一 针对程序员经常周末加班与工作日熬夜的情况 部分互联网机构倡议每年的10月24日为1024程序员节 在这一天建议程序员拒绝加班 程序
  • 【C/C++】报错问题积累

    1 出现Deprecated declaration XXX give arg types c文件中 有没有参数的函数时 声明需要加void即 main c void fun main h void fun void
  • androidX 在AndroidMainfest里面加入provider后编译不通过

  • 【three.js练习程序】创建简单物理地形

  • ubuntu 18.04 双系统安装

    下载镜像 Ubuntu 18 04 6 LTS Bionic Beaver 磁盘分区用于ubuntu存储 在C盘中分出200M用于ubuntu的引导启动 C盘已经分出200M空间 D盘分配出160G用于存储文件 U盘制作系统盘 刻录软件 推
  • linux下TUN或TAP虚拟网卡的使用

    tun tap 驱动程序实现了虚拟网卡的功能 tun表示虚拟的是点对点设备 tap表示虚拟的是以太网设备 这两种设备针对网络包实施不同的封装 利用tun tap 驱动 可以将tcp ip协议栈处理好的网络分包传给任何一个使用tun tap驱
  • ClickHouse安装(集群版)

    ClickHouse安装 集群版 一 准备工作 1 设置hostname 2 hosts映射 3 关闭防火墙 4 同步时间 5 关闭selinux 6 安装好zookeeper 7 重启 二 搭建ClickHouse集群 1 下载安装包 2
  • c++类模板与继承详解

    c 类模板 继承 详解 类模板和类模板之间 类模板和类之间可以互相继承 它们之间的派生关系有以下四种情况 1 类模板继承类模板 2 类模板继承模板类 3 类模板继承普通类 4 普通类继承模板类 include
  • 【linux】shell 编程之字符串与数组

    前言 对字符串的操作在众多的编程语言中可以说是最基础的了 字符串 String 就是一系列字符的组合 字符串是 Shell 编程中最常用的数据类型之一 除了数字和字符串 也没有其他类型了 一 shell 中字符串的几种格式 在shell中
  • 吃透Chisel语言.18.Chisel模块详解(五)——Chisel中使用Verilog模块

    Chisel模块详解 五 Chisel中使用Verilog模块 上一篇文章讲述了用函数实现轻量级模块的方法 可以大幅度提升编码效率 Chisel中也提供了一些好用的函数 方便我们编写代码 也方便Chisel编译器优化生成的硬件电路 在Chi
  • Blender 之修改器代码分析

    转载 Blender 之修改器代码分析 KAlO2 博客园 Blender 之修改器代码分析 Blender的修改器 modifier 模块 默认界面右下块 Property 面板的扳手 分类 修改 生成 形变 模拟 列出所有的修改器 也可