• 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。

    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, 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] |
    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

      

      

  • 相关阅读:
    P1030 求先序排列 P1305 新二叉树
    spfa
    Clairewd’s message ekmp
    Cyclic Nacklace hdu3746 kmp 最小循环节
    P1233 木棍加工 dp LIS
    P1052 过河 线性dp 路径压缩
    Best Reward 拓展kmp
    Period kmp
    Substrings kmp
    Count the string kmp
  • 原文地址:https://www.cnblogs.com/Martinium/p/blender_modifier.html
Copyright © 2020-2023  润新知