BlenderDev/blender architecture
Wikipedia,自由的百科全书
Blender Architecture
By Ton Roosendaal
赵正雄译
译者:本文介绍了blender的构架。细节方面并没有给出,请参照开发者文档。有什么问题可以问我。译者邮箱:typeError@gmail.com。欢迎大家复制并交流,复制务必标明出处,作者,译者。
-所见即所需,所做即所得-
Blender严格按照“面向数据”结构,就像一个资料库,但是加了一些的面向对象的观念。全部代码是用c写的。开发blender的时候,对于执行一些数据工作的所有的3D表示和所有的普通工具都试图一尘不变地定义结构。这种结构有意让开发者和用户使用blender快速和灵活。
这个图
表示了blender最基本的结构,”数据-显示-编辑”环是blender的核心,他们与GUI(图形使用者界面)分离并分别在不同级别上工作。
Data-select:一个用户选择了“数据”。这意味着用户指定了一个他想工作的3D资料库的一部分。它被组织成一个树形结构。 Blender的数据系统的底层寄存在文件中,而文件直接映射出它们是怎么在内存中组织的。Blender数据可以包括多种“场景”,每一个场景分别包括了3D物体,材质,动画系统和描绘系统。
Visual-select:被指定的数据可以用Zbuffer描绘出的3D线框,按纽或者是概要图标来显示。Blender提供一个非常灵活的允许任何不重叠,不阻塞层次类型的窗口系统。
Edit-select:基于选择的显示数据,用户可以选择许多工具。编辑通常直接地在数据上进行,而不是在可见的数据显示上。这一点很有逻辑性,但是很多的软件这样做在交互性和直觉性上做得不够而失败。反过来,速度是一个很重要的因素;当用3d的方式显示,用户在看见书如命令后的很长一段时间内才能看到效果。这也是一个“反向矫正(翻译的不对,请向后看)”的问题:用户只意识到了视觉,而不是实际的数据结构。因为是3d软件,视觉和实际的数据会偏差很大,往往会带来反效果。
-用户界面设计的决定
用户需要有很多而且可行的方法去进行3D建模,许多程序基于把如动画,材质编辑,模块化而把工作流程分开导致终止开发。决定如下:
1. 基于无重叠无覆盖的用户界面的窗口系统。
2. 允许blender中的每一个子窗口可以与任何一个显示选择的数据的“编辑器”相连。
3. 执行唯一的热键和鼠标动作,不会因为子窗口的不同而不同。
4. 因为是一个室内软件,使用速度因素大于学习难度因素。
用户界面的实现最初是用库去尝试,但是因为速度问题彻底失败。创建整个新的窗口管理器,完全基于IrisGL(openGL的前身)使得blender到现在还是很小巧。
数据结构
设计blender的大部分时间花在定义一个正确的数据组织的方式和数据之间有怎样的关系。牢记“实现根据设计”的概念,所有的设计决定都应该限制功能去使用。
基于我们的工程从内部需要3D的方式,决定在高的抽象级别设计数据结构,有以下几点需求:
1. 允许多人共同工作
2. 允许在一个工程内创建许多动画工程。
3. 允许数据被高效率地重用。
4. 允许模版。
这导致了设计不是标准的“数据资料库”,不像传统的“场景作图”(当时的基于理解/显示),但是基于创建通用的3d”数据世界”,在这你可以根据需要构建不同的场景绘画(显示,图形,动画)。这种数据结构的设计对初学者来说很不容易。
在blender中数据块是用户的建筑块:他们可以被拷贝,修改,根据需求连接到另一个上。
创建一个连接实际上上是建立了一种关系。这是一个块的用处。这可以制造一个快的实例,或者得到一个特殊的用处。
比方说一个“物体(object的数据结构在DNA_object_types.h文件中)”块类型使得一个“网状(mesh的数据结构在 DNA_mesh_types.h中)”块在3d场景中显示。“物体”表示决定了精确定位,旋转,和网状的大小。而网状只存储了节点的定位和朝向。可以有一个或者多个的“物体”连接同一个“网状”。其他的一些块类型比如材质可以连接到网状结构上去得到块属性。
每一个这样的快在blender中以一个ID结构(在DNA_ID.h中)开始,ID结构包括了这个块的唯一的名字,在某些情况下也包括了块所属的库(Library),ID结构使得blender的数据可以被唯一的表示和操作,而不需要知道具体的类型或者其他信息。
这些ID也使得blender以一种文件结构去在内部组织文件;它就像一些文件的目录。
当组合blender文件或者以“库”方式使用他们的时候这种组织变得很重要。
Blender中所有的数据储存在一个主结构(main tree 的数据结构在BKE_main.h中)中,它只不过是一块目录(a block of lists),数据都存储在这里,与用户是怎么样把他们连接起来的无关。图
你可以看出一个“场景”是怎样由一些在主结构的数据构建起来的。(译者主:在主结构中没有组织的方式!!)。
在主目录中存储的数据要进一步地由场景块(scene block在DNA_scene_types.h中)所显示出来。场景块有一个基结构(base structs在DNA_scene_types.h中)的列表,这可以使一个场景连接许多的物体而不把它们从主结构中移出。(基结构同样有一些属性,比如图层和选择,这样允许许多场景都能通过这样不同的属性来使用一个物体)
当使用blender的时候,库块(library block在DNA_ID.h中)经常不会自己释放,而是断开了连接。他们继续在主结构中。只有当用户计数为0时,它才会不被写入文件中。
图中黄色是表示两个主全局变量,当前场景(也表示活动场景)和活动物体。
数据块结构
所有在主结构中的块叫做库块(library blocks)(译者:物体,网状都叫做库块),有时候也叫做库数据(libdata)。库块以一个ID结构开始:
typedef struct ID {
void *next, *prev; /* for inserting in lists */
struct ID *newid; /* temporal data for finding new links when copying */
struct Library *lib; /* pointer to the optional Library */
char name[24]; /* unique name, starting with 2 bytes identifier */
short us; /* amount of users of the block */
short flag; /* bitwise flags to indicate special types */
} ID;
让我们进一步地看一个库块
- 网状结构。当然这样的块包括许多其他的块,以数组或者是链表存储。这里我们存储了节点(Mvert),朝向(faces),UV材质坐标(翻译不准)等在图中以蓝色表示的。这些块叫做直接数据(direct data)。这些数据经常与网状结构一同被写入或是一同从文件中读出。
除了这些永恒的数据,还有一些临时数据可以连接到网状结构上,比如显示列(display list)。这些数据即时生成,而不是写入文件,从文件读取数据的时候被设为null。因为所有的库数据和直接数据都存储在文件中,所以应该紧凑地设计它们,而临时数据留在外部。
现在我们得到了一条很重要的设计原则:blender的块之间指针的使用被限定为对库数据的指针。这意味着指向物体或者网状的指针可以在任何地方(比如在一个节点处),但是指向指定的节点的指针不能在任何地方存储。
这条原则使得有一个一致,可预知的结构,这种结构可以被blender代码的任何部分所使用(尤其是数据库管理和文件)。如果你想把一些东西变为库数据或者你想把它在blender的任何地方重用,它也可以使你做出决定。(译者:意思是:可以在任何地方使用库数据,对复制等操作很方便)
当一个文件被存储的时候,直接数据与库数据在一起。存储的数据读取的时候的指针需要被还原。这里的指针规则帮助我们快速地还原指针所指向的直接数据,因为这里的指针只存储了一些单独的有关内容。只有一些指向直接数据的指针被存储在一个全局列表中,对lib_link_***的函数调用中全部还原。
外部库数据
设计的一则需求是从别的文件中导入数据,或者以一类模版去动态连接它。在当时评价其他3d程序的时候我注意到他们使用操作系统的文件系统,以内部的一个目录树和每一个数据块对应的一个单独的文件去创建工程目录。除了复杂,笨拙,我怀疑速度也很慢(译者:作者也太嚣张了吧!)。
然而,blender用这种方法至少有一点好处。Blender数据名字的使用与文件名可以紧密结合。
当动态地使用别的文件的数据,很明显地只有库数据块可以被连接。Blender自动地读取与它有关的直接数据。可是为了使读取外部文件更有用,它同样要扩展它的所连接的树。比如说,读取一个被连接的物体,同样会自动读取它有关的网状结构,材料和材质。被扩展的数据叫做“间接数据”。在接口中你可以以红色的库图标去识别它。当存入到一个文件时,这样的扩展(间接数据)不被完全写入,这需要外部文件的编辑者改变对物体的连接,就像增加一个Ipo或者其他的材料。
一个种种特性的典型使用就是从别的文件动态链接一个场景。不管有什么在场景内都将被读取。
当存取链接库数据,只有它的ID组件被写入。这些ID包括一个指向被使用的“库”(=外部文件)。有唯一名字的ID被正确存储。
文件存取和读取
文件存取大体上是过一遍主结构,然后以raw binary dump格式存取用户的所有的块到磁盘上,每一个被存储的块有一个头(struct Bhead)它存储了其余的一些信息,比如内存中块的原始地址。
文件读取首先读取全部文件到内存中。Blender读取代码然后过一遍所有的Bhead,再处理库数据和间接数据,创建出一整份拷贝。最后所有的文件都可以从内存中删除。
写入动态链接的库数据:
因为blender的主结构只是另一种可列表结构,它使用多个主结构为了可以高效地存取和写动态链接数据。这个可以用split_main()这个函数的调用完成,创建每个文件所属的分离的数据的主结构。
对于存档,当前的主结构可以被正确地处理;对于其他的主结构,它只存入了所有的ID(就像ID_ID类型),用一个分离者表示它从哪一个文件而来
然后,它调用call_main()函数去合并成一个主结构。
读取动态链接库数据:
这可能有一点复杂,带有递归。
1. 当从文件读取数据,blender遇到一个库结构,它创建一个新的主结构去存储后来的ID_ID。这些ID被标记LIB_READ.
2. 然后检查所有新的主结构的被标记LIB_READ的块。
3. 如果LIB_READ块被找到:
3.1 它检查包含这个数据的文件是否已经被读取,如果没有则加载内存中的整个文件再把他们存储到主结构下。
3.2 按正常的步骤读取块,链接所有间接数据。新的数据块被链接上,旧的则被移出。
3.3 根据块的类型,调用expand_doit()函数,这个函数强制读取像3.2所说的块,或者检测已经正确读取得数据。注意:当这样的扩展数据再次从别的文件被读取,它只读取ID这一部分,把它连接到正确的主结构上并标记为LIB_READ。
4.只要LIB_READ被找到,回到第2步。
5.最后合并主结构并恢复所有正确的库数据指针。
正如你所看到的,这个系统需要很多指针魔法(不准确的翻译),指针必须映射到新的指针上面去,再到新的指针上面去,直到正确的指针指向的数据被分配。
结构DNA(SDNA)
在选择二进制存储文件,blender的结构会一直在变或者扩展,我想到设计一个系统可以照顾到低级别的版本的改变。这就是blender DNA。
基本上,SDNA是一种blender包含文件(include file)的精确二进制预处理版本,包括可以被存储进文件的数据。SDNA系统可以得到内容的结构信息:大小,元素类型和名称。
在每一个编译的blender有一个SDNA在内和一个保存的.blender文件内,都有SNDA被加入。而且,对于文件中的每一个Bhead,它保存着序列号表示结构类型。
这一切允许结构中的改动被检测和解决。举一个从int到short,char到float,数组或多或少的得到一些元素的例子。新的变量最好归 0,移除变量被简单地忽略。当做一个到endian系统的输出,最好有一些正确的类型转换。用SDNA使得即使输出到64位的系统照样成功。
此外,它还允许后退和前进兼容。1997年的二进制在现在依旧运行,反过来也是。
主要的限制是当真实版本需要改变时。比如加入一些新的变量需要给定值初始化,更糟的是,一些变量会改变值。
源代码层次
现在的代码层次仍然是当时还是一个公司时的制作时的一个快照。在图表中你可以找到目录名称
Blender的用户接口框架
在源代码的source/blender/src/目录下你可以找到与用户接口(UI)和blender的工具有关的文件。结构虽然明显,但是在目录结构上可以反映地更好。






