很多人第一次打开Blender源码的时候,心态基本是崩溃的。下载解压完,一个source目录丢过来,里面几百个文件夹,上万个头文件,完全不知道从哪下眼。这个“了解模块的大致分类”的标题,恰恰是进入Blender源码世界最应该先做的一件事——不是急着去读某段具体功能代码,而是先把整张代码地图铺开,搞清楚几大模块分别在哪、各自管什么、彼此之间怎么通讯。如果你是想参与Blender开发、或者想从源码层面理解Blender架构的读者,这篇内容就是给你的一张索引表。我会用实际阅读源码时的观察视角,把Blender源码里模块分类的逻辑讲清楚。
1. 为什么说模块分类是Blender源码的第一道门
Blender整个源码包解压之后,结构看起来非常规整,但这份规整是建立在长期演进之上的。最核心的代码基本全在source/blender目录下面,而source/blender里那十几个一级子目录,就构成了Blender的内部世界。很多人第一次进来,会被editors、windowmanager、blenkernel这些命名搞得一头雾水——它们不是随便起的名字,每个前缀背后都代表一层职责边界。
Blender的模块划分逻辑,一句话概括就是:底层数据与上层操作分离,核心算法与界面交互分离。这种分层设计,让Blender可以在不改变数据模型的前提下不断更换UI、增加功能插件,也能在命令行环境下脱离界面运行部分核心流程。理解了这一点,阅读源码时你就能区分出:哪些代码管“数据是什么”,哪些代码管“数据怎么变”,哪些代码只管“用户点了哪里”。
从宏观上看,Blender源码里的模块可以分为三大类方向:
- 数据与核心层:负责定义3D场景里的对象、网格、材质、动画骨骼等基础数据,以及操作这些数据的底层算法。典型代表是
blenkernel(平时常说的BKE)、blenlib、depsgraph、makesdna等。 - 交互与功能层:负责把用户的鼠标键盘操作翻译成具体的功能调用,也就是编辑器(Editor)、窗口管理(Window Manager)、操作符(Operators)所在的区域。典型代表是
editors、windowmanager、gpu、python等。 - 基础支撑层:像是内存管理、文件读写、数学库、第三方库封装,这些代码不直接参与3D建模逻辑,但没有它们整个大厦就会倒塌。典型代表是
blenlib、imbuf、extern等。
这个分类并不是绝对的,因为Blender源码里模块之间的耦合比想象中要高很多——BKE会调用BLI的基础函数,Editors又会调用BKE的核心数据结构,Depsgraph则横跨数据依赖和实时更新两个方向。所以“大致分类”这四个字非常精准,它就是先给你一张不保证绝对精确但能帮你定向的图,等你真正深入到具体模块内部时,再慢慢纠偏。
2. blenkernel:所有功能最终都要汇聚到这里的核心内核
如果要在Blender源码里选一个“心脏”,那一定是source/blender/blenkernel。平时在Blender社区和开发日志里经常看到“BKE”这个缩写,全称就是Blender Kernel。这个模块承载的是Blender最核心的数据结构定义和操作这些数据的算法函数。你在Blender里创建一个Cube,它在源码层面就是blenkernel里定义的Mesh和Object结构体的实例化。
打开blenkernel目录,你能看到一堆以BKE_开头的头文件和对应的.cc实现文件。比如BKE_mesh.h、BKE_object.h、BKE_scene.h、BKE_anim.h、BKE_modifier.h这些,基本上Blender的每一种核心对象类型,在BKE里都有一个对应的文件簇。阅读时你会发现一个非常鲜明的规律:BKE里的函数大多不关心UI,也不直接处理用户输入,它们接收的是纯粹的数据参数,返回的也是纯粹的数据结果。
举个例子,BKE里处理网格减面、法线重算、顶点合并这些操作的函数,输入的是一个Mesh指针和一些参数,计算完直接改网格数据本身或者返回新的网格。你在Blender里撤销一个操作时,实际上也是BKE在起作用——它把操作前的完整数据状态保存在撤销栈里,需要恢复时就调用BKE的拷贝或替换函数把旧状态塞回去。
BKE也是所有其他模块的“公共语言”。editors里的工具代码写操作逻辑时,最终会调用BKE_xxx里的函数;modifiers里的修改器实现,也需要BKE提供基础几何数据;python层的bpy API,本质上也是封装BKE函数供脚本层调用。所以如果阅读源码时遇到某个功能不知道从哪里看起,先猜BKE,大概率是对的。
BKE的体量也非常惊人,它是Blender源码中文件数最多、代码行数最多的模块之一。这也带来一个阅读上的建议:不要试图从头到尾通读BKE,正确的方式是先找到你关心的那个对象类型(比如Mesh),把它的头文件读完,再看对应的实现文件,最后通过搜索函数调用关系,追踪到其他模块。
3. blenlib与makesdna:容易被忽视的基础骨骼
很多人刚开始读Blender源码时,注意力都在功能模块上,blenlib这种“工具库”往往被直接跳过。但实际踩过坑之后会发现,不读blenlib,后面几乎每一步都会碰到障碍。blenlib(通常缩写为BLI)是Blender自己的通用基础库,里面包含了大量容器类型、字符串处理、数学运算、文件路径处理、链表操作等底层工具函数。它像是一套定制版STL,为了满足Blender内存管理和性能需求而专门打造的。
在blenlib里最应该优先认识的是几样东西:ListBase双向链表、BLI_memarena内存分配器、BLI_math_vector.h向量数学库、BLI_string.h字符串函数。Blender内部大量数据结构都是用ListBase组织起来的,比如场景里的对象列表、一个网格里的顶点列表,都是链表结构。这和很多用std::vector组织的现代C++项目风格不同,是Blender早期C代码风格遗留下来的产物。
为什么Blender不直接用标准库?一个重要原因是内存控制。Blender做的是交互式3D软件,对内存分配频率和碎片率都非常敏感,BLI_memarena可以一次性分配大块内存再按需切分,配合撤销系统能大幅提升效率。另一个原因是模块解耦,blenlib不依赖BKE、不依赖Editors,它是整个代码库最底层的那几块砖之一,几乎所有模块都会引用它。
makesdna这个模块名字看起来有点奇怪,但它在Blender代码生成体系里扮演的角色极其特殊。DNA在这里指的是Blender自己的“数据结构DNA”,也就是.blend文件里数据结构的二进制定义。makesdna会扫描BKE中声明了DNA_DEFINE_CUSTOM_PROPERTIES之类的结构体,生成一个结构体布局表,这个表决定了.blend文件如何序列化、如何跨版本兼容。可以简单理解为:makesdna是Blender存档格式的“编译器”。
读源码时你会看到很多结构体定义里带有DNA_标注,比如DNA_mesh_types.h、DNA_object_types.h,这些文件就是Blender核心数据结构的“标准答案”。如果你想加一个新属性到某个数据对象里,需要修改对应的DNA头文件,然后重新生成DNA,Blender的版本迁移机制才能识别它。明白了makesdna的职责,往后看.blend文件兼容问题、插件API数据结构变化,都会通透很多。
4. depsgraph:Blender实时更新能够成立的幕后调度员
Depsgraph(Dependency Graph),也就是依赖图,是Blender里最容易被忽略但同时又最影响性能与正确性的模块之一。它的核心作用是:确定场景中各种数据之间的依赖关系,并在数据发生变化时,精准计算出哪些部分需要更新、哪些部分可以跳过。
第一次接触这个模块的人,很容易把它理解成“一个简单的脏标记系统”。但Blender的依赖图远比脏标记复杂。比如一个物体被一个修改器处理,而修改器又引用了另一个物体的顶点位置;或者动画骨骼影响了网格变形,而变形结果又参与布料模拟——这些关系是网状的,不是线性的。Depsgraph会读取整个场景的关系,构建一张有向图,并按照拓扑顺序执行更新。在源码里,depsgraph的核心逻辑分布在source/blender/depsgraph目录下,其中DEG_depsgraph.h是主要对外接口。
Blender发展到今天的实时视口预览、物理模拟缓存、动画自动关键帧等特性,全都依赖Depsgraph来保证数据在正确的时机以正确的顺序更新。如果依赖图计算错误,会出现很经典的“动画结果滞后一帧”“修改器结果没刷新”之类的问题。看源码的时候,如果你想理解“为什么Blender知道这个物体移动了之后,需要重新计算哪些东西”,Depsgraph会给你答案。
Depsgraph的设计里还有一个概念值得单独提——IDNode与ComponentNode。每个需要被依赖图追踪的数据块(ID),在依赖图里就是一个节点;节点内部又按组件(比如几何、变换、动画)区分更新粒度。这种组件级拆分,让Blender可以只在某个组件的依赖关系变化时更新那一小块数据,而不是整个对象都重新计算,这也是Blender性能优化的重要基础。
5. windowmanager与editors:用户操作如何驱动数据变化
用户平时在Blender里做的所有操作,最终都会经过两个模块——windowmanager(缩写WM)和editors。这两个模块分工明确:WM负责窗口系统、事件分发、操作符注册与调度;Editors负责具体编辑器的UI面板和交互工具逻辑。
先说windowmanager。它是Blender的窗口管理层,对应代码目录是source/blender/windowmanager。WM内部维护了窗口、屏幕(Screen)、区域(Area)这些概念。你在Blender里切换编辑器布局、拖拽面板边缘、弹出菜单,这些事件从操作系统捕获之后,先进入WM的事件循环,WM根据鼠标位置判断事件落在哪个区域,再把事件分发给对应的编辑器处理。
WM里还有一个对插件开发极其重要的子系统:Operators(操作符)。Blender里的几乎所有命令式操作,比如移动物体、添加立方体、修改材质,都是通过操作符实现的。操作符是WM层连接UI和底层BKE函数的桥梁,操作符定义里会声明自己需要什么属性,执行时调用BKE完成实际数据变更。你按F3搜出来的那些可执行命令,本质上全是操作符。
再来看editors,这是Blender源码里代码量最大、功能最庞杂的模块之一。source/blender/editors下面按编辑器类型又分了很多子目录,比如mesh、object、animation、sculpt_paint、space_view3d等等。每个子目录大体上对应3D视口里的某种工具集。你在编辑模式里用到的挤出、倒角、环切,在雕刻模式下的各种笔刷,都分别在这几个子目录下实现。
Editors模块的实现风格和BKE区别很大。BKE里基本都是纯数据算法,而Editors里大量代码是在处理“用户意图如何转化为具体操作参数”。比如倒角工具:用户在UI上拖动鼠标时,Editors不断计算鼠标位置对应的倒角宽度,传递给BKE的倒角函数去更新预览结果;松开鼠标后,再调用一次正式的修改操作。这种“预览-确认”的模式,也是Blender交互反馈流畅的重要机制。
阅读Editors时要注意,它不是每个功能都直接调用BKE。很多时候Editors会先构造一个Operator,再通过WM触发它。这套间接机制看起来多了一层,但对撤销重做、快捷键绑定、宏录制来说极其关键。所以读到一个编辑器功能时,顺着“鼠标事件 -> WM分发 -> Editor处理 -> Operator调用 -> BKE执行”这条链路去追踪,会比零散地读文件高效得多。
6. makesrna与python:bpy API是如何连接底层C代码的
Blender最吸引人的地方之一,就是自带一套完整的Python API(bpy)。很多人在没看源码之前,以为bpy只是简单绑定了一下C函数。真正读完makesrna模块之后,你会发现Blender的Python绑定体系做得相当精巧,它不是一个函数一个函数手动绑定的,而是通过RNA这个中间层自动生成的。
source/blender/makesrna里的代码会扫描DNA结构体定义,为每个可暴露给用户或脚本的属性、方法生成对应的属性描述表。这些描述表在运行时成为Blender内部的“反射系统”,也就是RNA。简单来说,RNA就是Blender数据结构的元信息层:它告诉其他模块“这个对象有哪些属性,属性类型是什么,可读可写权限如何,显示名称叫什么”。Python的bpy模块,本质上就是对RNA这一层的包装。
读到这里你应该能理解,为什么Blender插件里写bpy.context.object.location.x = 1.0可以直接生效,而且修改完之后视口会立刻刷新——因为属性赋值背后走了“Python层 -> RNA层 -> DNA层 -> Depsgraph通知更新”这条完整链路。RNA层负责类型检查和属性访问控制,Depsgraph负责在数据变更后触发相关更新。
Python模块在源码里的位置是source/blender/python,这里面包含了BPY_开头的各种接口实现,以及Blender内置的mathutils数学模块、bpy模块的C扩展本体。如果你对插件开发感兴趣,或者想理解Blender脚本API到底能触达多深的底层功能,python目录和makesrna目录值得花时间交叉着读。尤其是自己做过插件的人,看懂RNA这一层之后,很多过去“试出来的API用法”会一下子变成“推导出来的必然结论”。
7. render与gpu:渲染管线和硬件加速的碰撞
Blender源码中,渲染相关的代码分布在好几个位置,理解这些模块的边界,对读源码的人尤其重要。大致有以下几块:source/blender/render是Blender内置渲染管线的核心调度模块;source/blender/gpu是GPU抽象层;source/blender/imbuf负责图像缓冲区的读写与转换;而实际渲染器(比如Cycles)的实现在intern/cycles目录下。
render模块在Blender里的角色,是“渲染任务的调度中枢”。它负责把场景中的数据收集起来,生成渲染任务,分发给具体的渲染引擎(可能是Cycles/EEVEE等),最后收集渲染结果写回图像缓冲区。这个模块里还包含了“渲染层”“合成节点”等环节。读代码时你会看到它和BKE、Depsgraph、IMBuf的交互非常频繁:BKE提供场景数据,Depsgraph计算最终的依赖图快照,IMBuf保存渲染出的像素,合成节点再对这些像素做最终处理。
gpu模块则是另一套逻辑。它不是一个具体的渲染引擎,而是Blender对各种GPU API(OpenGL、Vulkan、Metal)的统一封装层。从视口着色、EEVEE渲染器到几何节点预览,所有和GPU打交道的地方,最终都走gpu模块提供的接口。这样可以避免业务代码直接依赖某一款图形API,也为后续替换底层API留了空间。Blender未来如果全面转向Vulkan,业务层的很多代码都可以不动。
在Blender源码里,渲染相关模块还有一个特点:它们是最容易出现“版本差异巨大”的区域之一。因为渲染技术和GPU API迭代速度非常快,Blender几乎每两三个版本就会对渲染管线和GPU抽象层做大调整。阅读这一块源码时,建议先锁定一个具体版本的分支,避免用旧经验套新代码,差距会非常大。
8. 模块之间的调用关系:一次视口操作背后的完整旅程
前面几个部分都在单独介绍模块,但实际上Blender源码的精髓在于这些模块如何协作。把一个最简单的操作——比如在视口里拖动一个立方体——拆解开来,你能看到一整套模块链路。
用户按下鼠标并移动时,操作系统事件先被windowmanager捕获。WM会创建一个事件,记录鼠标所在的区域,判断当前应该由哪个编辑器处理。事件被转发到3D视口编辑器,也就是editors/space_view3d里的代码。视口编辑器根据当前工具模式(这里是移动工具),调用对应的操作符——移动操作符在WM里注册过,它的执行函数里会处理“鼠标偏移量如何映射为物体位置变化”的逻辑。位置变化的具体计算最终落到BKE的物体变换函数,BKE修改Object结构体里的位置矩阵,然后通知Depsgraph“这个对象的位置变了”。Depsgraph收到通知后,会检查有哪些节点依赖这个对象的位置,比如它的子物体、约束、修改器、着色器参数,然后按依赖顺序触发这些节点的更新。更新过程中可能会调用blenlib里的数学函数计算新矩阵,也可能会调用gpu模块更新视口里的显存缓冲。最后WM检测到当前渲染循环的时机,触发视口重新绘制,用户在屏幕上看到物体跟着鼠标移动了。
从源头到最终呈现,一共穿越了至少六个模块。每个模块各司其职,中间没有任何一个模块越界去“顺手”做别的事情。这就是Blender模块分类设计的最大价值:职责边界清晰,协作方式统一。读源码时保持这种“追踪跨模块调用链”的习惯,比单独背某个模块里的函数名有用得多。你不需要记住所有代码,但只要能快速判断每一个调用发生在哪个模块、负责什么事情,整个源码就不再是一团乱麻。
9. 阅读顺序建议:按照你自己的目标,选一条合适的路径
模块分类搞清楚了,接下来的问题就是“从哪开始读”。根据不同的阅读目标,我建议走三条不同的路径。
如果你对Blender数据结构和文件格式感兴趣,路径应该是:makesdna->blenkernel的对应类型头文件 ->blenloader(.blend文件读写与版本迁移)。沿着这条线,你能理解一个物体在内存里怎么存放、保存到硬盘时如何序列化、版本升级时老文件怎么兼容。
如果你对插件开发与Python API底层原理感兴趣,路径是:makesrna->python->windowmanager的操作符注册机制。看懂RNA之后,你会明白bpy里很多接口为什么这样设计,也能在遇到API不满足需求时,在C源码层面找到绕过的思路。
如果你对3D图形渲染与GPU感兴趣,路径是:gpu->render-> 具体的渲染引擎(比如Cycles)。但这里要提醒一句,GPU相关代码依赖较深的图形学背景,最好先熟悉OpenGL或Vulkan的基本概念再上手。
在具体阅读时,我自己的习惯是三遍式:第一遍只看头文件,搞清楚模块提供了哪些类型、哪些函数,构建概念地图;第二遍挑一个具体的操作函数,从头文件声明跟到实现文件,把数据流走通;第三遍才去看边缘细节和优化技巧。直接上来就啃实现文件,很容易迷失在几百行的矩阵计算或内存分配代码里。
Blender源码的体量对任何一个人来说都构成了阅读的阻力,但模块分类机制其实是它最友好的地方——你永远可以为某个需求锁定一个目录和一批相关文件,不需要知道全部,也能做出有效的贡献。