此文档致力于两个目的:帮助妳处理Blender 开发接口 中那些容易出错的地方;告诉 妳哪些行为 会导致不稳定。
Blender中,操作器是可被用户使用的工具, 同时它们也可通过python 来使用。 不过,操作器有某些限制, 以至于 用脚本控制它们时会有某些问题。
主要有这些限制...
•.无法直接传递那些要处理的数据,例如对象、网孔(meshes)和材质(操作 器从上下文( context )中取出数据 )
•.调用操作器时,返回值为成功(success)(如果它成功结束或者被取消的话)。然 而,在某些情况下,从编程接口的角度来看,如果返回 该操作的结果 的话将更符合逻辑。
•. 一个普通的编程接口函数出错 时,会抛出异常详细说明哪里出错 了,而操作器在这种情况下只会 让其 轮询(poll)函数失败。
在调用操作器时可能 会 出现 这样一个错误 :
>>> bpy.ops.action.clean(threshold=0.001)
RuntimeError: Operator bpy.ops.action.clean.poll() failed, context is incorrect
那么,什么样的上下文才是正确的上下文?
一般情况下,操作 器会检查当前 的活动区域类 型 、被选中或处于活动状态的 可操作对象 。但是,某些操作器对于调用时机的要求 是更苛刻的。
大部分情况下, 妳只需观察 一下某个操作器在Blender 中是怎么用的, 再想一下它做了哪些处理, 就能明白 这个操作器需要什么样的上下文了。
但是,假如妳这样做了之后还是不明白 它要求什么样的上下文的话 - 唯一一个 真正 能够得知真实情况 的方法就是 去 读 轮询函数的源代码,看看 它到底在检查些什么。
对于python 操作器来说, 要找到源代码并不难。因为 它是随 Blender 附带的 ,并且在操作 器的参考文档中标明了 源代码文件/行号。
下载并搜索C 语言代码 倒不是那么简单,尤其 是妳 并不熟悉C 语言 的情况下更是如此。但是 呢 ,可 以搜索操作 器的名字或描述信息, 这样,即使妳 不会C 语言,应当也能找到轮询函数了。
注意
Blender也提供了一个功能,让 轮询函数可以说明 它们为什么失败了,但是 这个功能目前并没被广泛运用。如果 妳 有兴趣改善我们的编程接口的话,那么 就在那些 轮询函数的失败原因并不明显的地方加上代码调用 CTX_wm_operator_poll_msg_set 函数。
>>> bpy.ops.gpencil.draw()
RuntimeError: Operator bpy.ops.gpencil.draw.poll() Failed to find Grease Pencil data to draw into
有些时候 , 妳希望 在通过python 修改某些 值之后立即获取 到新的值,例如:
在改变 了 对象 的位置( bpy.types.Object.location )之后, 妳可能想要立即通过 bpy.types.Object.matrix_world 访问 它的变换矩 阵(transformation) ,但是 妳会发现事情不是妳想的那样。
想一 下, 需要做哪些计算才能得到最终的变换矩阵 :
•.动画函数曲线。
•.驱动以及它们的python表达式。drivers and their expressions.
•.约束
•.亲代对象以及它们的 f 曲线(f-curves)、约束等等
为了避免在每次修改单个属性之后就进行费时 的计算,Blender 会将实际的计算过程推后,直到真正必要的时候才计算。
然而,妳 可能还是需要在脚本正在运行的时候就访问更新 后的值。
可如此实现这个目的: 在修改了一些值之后调用 bpy.types.Scene.update , 它会 将那些被标记为已更新的数据重新计算。
标准的回答是不可以,或者... “最好 不要那样做 ” 。
来说一下原因...
当某个脚本正被执行时,Blender 等待 它的结束 ,并且在这脚本结束之前处于锁定状态 。在这种状态下,Blender 不会重绘 界面,也不会 对用户的输入进行响应。 一般情况下,这不是问题,因为Blender 自带 的脚本 都不会运行狠长时间。然后 ,有些脚本 可能 会 长时间运行 ,并且 还可以观察 一下界面视图 里 的现象。
强烈建议不要写出那种锁定Blender 同时循环 重绘的工具。因为它们 与Blender 的以下能力相冲突: 同时运行多个操作器,并且更新界面 的多个不同部分。
所以,这个问题的最终解决方法就是写出一个 模态 ( modal ) 操作器 - 定义了一个modal()函数的操作器。参考文本编辑 器中的模态操作器模板。
模态操作器随着用户的输入而运行,或者设定了自带 的定时器来定时运行 。它们可以处理事件,或者将 事件传递给键盘映射 (keymap)或别的模态操作器。
模态操作器的例子:变形 (Transform)、绘制(Painting)、飞行模式(Fly-Mode)和文件选择。
编写模态操作器 比编写一个简单的 for 循环脚本需要更多的努力 ,但是 它更灵活并且能够更好地与Blender 的设计模式整合。
好好!我就是想要在python中绘制界面
如果妳坚持要这样做 - 确实可以做到 。但是 , 这样做了的脚本 将不会 被考虑 与Blender 一起发行 ,并且使用 该脚本带来的任何问题都不会被认为是漏洞 (bugs),另外 , 不保证未来的版本中仍然可以这样做。
bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
经常有用户抱怨 说 Blender 的矩阵乘法不正确 。造成 这一困扰的根源在于,mathutils 中 的矩阵 是按照 以列为主的方式存储的, 以便 与OpenGL 匹配, 而Blender 中其它部分的矩阵操作 和矩阵存储也是这样的。
这就与 numpy 不同,它是按照以行为主的形式来存储矩阵的 。那种方式 更符合 人们的 习 惯。
Blender的编辑网孔(EditMesh)是一种内部使用的数据结构(不会被保存,也不被暴露 给python), 这就导致一个烦人的事, 妳需要退出编辑模式之后才能使用python 来编辑网孔。
为什么我们没有去修复这个问题呢?因为 我们将要迁移 到使用BMesh 网孔编程接口,所以 为现在的编程接口做任何修复都是白费力气。
使用BMesh接口的话, 我们就可以将网孔数据暴露给python,然后 我们就 可以使用python 来编写有用而 又能在编辑模式快速执行的工具 了。
目前,这个限制只能 以别的方法绕过。当然,我们认为在这里说明一下这个问题是有必要的。
在Blender 中,有三种显著 不同的数据结构 都包含了盔甲骨架 (Armature Bones)。如果 你使用其中 的一个来访问骨架数据 的话, 妳可能访问 不到妳真正需要的属性。
注意
在以下的示例中,假设 bpy.context.object 是一个盔甲骨架。
bpy.context.object.data.edit_bones 中包含着一个编辑骨架; 要想访问它们,妳必须先将盔甲模式(armature mode)设置成编辑模式(编辑骨架 在对象( object )或姿势( pose )模式中是不存在的 )。使用 这些对象来创建骨架,设置它们的头(head)/尾(tail)或滚动值(roll),改变它们 与其它骨架之间的亲代关系,等等。
在盔甲编辑模式中使用 bpy.types.EditBone 的示例:
这只能在编辑模式做到。
>>> bpy.context.object.data.edit_bones["Bone"].head = Vector((1.0, 2.0, 3.0))
不处于编辑模式时,以下对象 将为空。
>>> mybones = bpy.context.selected_editable_bones
只会在编辑模式下返回一个编辑骨架。
>>> bpy.context.active_bone
bpy.context.object.data.bones 中包含着骨架。 这些对象 存在 于 对象模式 ,拥有多个 可由妳修改的属性, 不过要注意,头部 和尾部属性是只读的。
在对象模式或姿势模式使用 bpy.types.Bone 的示例:
在不处于编辑模式时,返回一个骨架(bone)(而不是一个编辑骨架(editbone))
>>> bpy.context.active_bone
以下代码有效,因为 在blender 中,设置选项 可在任何模式中编辑
>>> bpy.context.object.data.bones["Bone"].use_deform = True
可访问但是只读
>>> tail = myobj.data.bones["Bone"].tail
bpy.context.object.pose.bones 中包含着姿势骨架。动画数据 就储存于 这些对象中, 也就是说 ,可变成动画 的变换数据都是被应用到姿势骨架 上去的,例如约束 (constraints)和 ik 设置(ik-settings)。
在对象或姿势模式使用 bpy.types.PoseBone 的示例:
# 获取第一个约束(如果存在的话)的名字
bpy.context.object.pose.bones["Bone"].constraints[0].name
# 获取最近被选中的姿势骨架(仅可用于姿势模式)
bpy.context.active_pose_bone
注意
注意,姿势是通过对象访问,而不是通过对象数据访问的。blender 中 可以有多个对象 在不同的姿势中共享 同一个盔甲,就是因为这个。
注意
严格来讲,姿势骨架 (PoseBone)并非真正的骨架, 它们只是盔甲的状态,保存在 bpy.types.Object 中,而不是 bpy.types.Armature 中。然而 ,真正的骨架 是可以通过姿势骨架的接口来访问的 - bpy.types.PoseBone.bone
Python支持多种不同的字符编码,所以 妳完全可以 用latin1 或iso-8859-15 编码来写脚本。
参考pep-0263
然而,这导致python编程接口变得复杂 了,因为blend 文件本身 并 没有字符编码属性。
为了 将问题简单化, 我们已经决定了,blend 文件 中的所有内容都 必 须 兼容UTF-8 或ASCII。
这就意味着, 将 一个具有不同字符编码的字符串赋值 为 一个对象 的名字的话, 会产生错误。
文件路径是此规则的一个例外,因为 我们无法忽略用户 的系统中 那些 非utf-8 的路径的存在。 ( ☯ :例如某些使用 GB2312字符编码 的操作系统 )
这就意味着,那些看起来无害的表达式也会引起错误,例如
>>> print(bpy.data.filepath)
UnicodeEncodeError: 'ascii' codec can't encode characters in position 10-21: ordinal not in range(128)
>>> bpy.context.object.name = bpy.data.filepath
Traceback (most recent call last):
File "<blender_console>", line 1, in <module>
TypeError: bpy_struct: item.attr= val: Object.name expected a string type, not str
有两种处 理 文件系统编码问题的方法:
>>> print(repr(bpy.data.filepath))
>>> import os
>>> filepath_bytes = os.fsencode(bpy.data.filepath)
>>> filepath_utf8 = filepath_bytes.decode('utf-8', "replace")
>>> bpy.context.object.name = filepath_utf8
统一码的编码/解码是一个 狠大的课题,伴随 着这个课题产生了大量 的python 文档。 为了避免 在字符编码问题上 陷得太深 - 可按以下建议来做:
•. 当输入内容是未知的的时候,坚持使用utf-8 编码或转换成utf-8 编码。
•.避免直接将文件路径当成字符串使用,应当使用 os.path 函数 作为替代。
•. 对文件路径进行操作时,使用 os.fsencode() / os.fsdecode() ,而不是内置的字符串解码函数。
•.需要输出或者在用户界面上显示文件路径 的时候,使用 repr(path) 或 "%r" % path 。
•. 可能 的话 - 使用字节数组而不是python 字符串。 在读取输入数据时, 将它们当成 二进制数据来读取 将不会面临那么多的问题 ,尽管 妳仍然需要处理 妳在Blender 中需要使用的那些字符串。某些导入器是这样做的。
在Blender 中使用Python 多线程功能,只有在线程 于脚本之 前 结束时才会正常工作。例如使用 threading.join() 来确保这一点。
以下是在Blender 中使用多线程的一个示例:
import threading
import time
def prod():
print(threading.current_thread().name, "Starting")
# 做某些可能有用的事
import bpy
from mathutils import Vector
from random import random
prod_vec = Vector((random() - 0.5, random() - 0.5, random() - 0.5))
print("Prodding", prod_vec)
bpy.data.objects["Cube"].location += prod_vec
time.sleep(random() + 1.0)
# 结束
print(threading.current_thread().name, "Exiting")
threads = [threading.Thread(name="Prod %d" % i, target=prod) for i in range(10)]
print("Starting threads...")
for t in threads:
t.start()
print("Waiting for threads to finish...")
for t in threads:
t.join()
以下这个示例, 是一个定时器, 在Blender 运行的时候, 该定时器每秒运行多次,连续 地移动默认的立方体( 不支持 )。
def func():
print("Running...")
import bpy
bpy.data.objects['Cube'].location.x += 0.05
def my_timer():
from threading import Timer
t = Timer(0.1, my_timer)
t.start()
func()
my_timer()
像上面代码这样的示例, 在脚本结束之后 还让线程运行 着。它们可能 会 在短时间内还正常运行, 但最终 会引起Blender 自己 的绘制代码随机 地发生崩溃或出错 。
到目前为止, 我们没有尝试 让Blender 中整合的python 代码变成线程安全的。所以,除非 我们确定无疑 地支持了这一特性,否则最好不要这样做。
注意
Python的线程 只会提供并行 性,而不会 让妳 的脚本在 多处理器的系统中跑得快一些。 subprocess 和 multiprocess模块也可以配合blender 使用,它们 也可以用上 多处理器。
理想情况下, 是不可能从 python 中引起Blender 崩溃的。然而,由于编程接口 中有某些问题,所以 是可能引起崩溃的。
严格来讲, 这是编程接口中的一个漏洞。但是 要想修复 此漏洞的话, 就意味着需要 在每次访问数据 时进行内存验证 , 因为 大部分崩溃都是由于python 对象直接引 用Blender的内存而引起的 , 在内存被释放之后, 再通过python 访问该内存就会引起脚本崩溃。但是 ,要想真正修复的话, 将导致脚本运行得非常缓慢。或者我们需要设 计另一种完全 不同的编程接口,在那里并不直接引用内存。
以下是一些提示,如何避免陷入 这样的问题。
•.注意内存的限制,尤其 是处理 大的列表数据的时候。因为Blender 当内存 不足时直接 就崩溃了。
•.已知狠多 费了狠大力气才修复的问题都是引用 了已经释放过的数据而引起的。删除数据 时,注意不要 再保留对它的任何引用。
•. 在Blender 运行过程中保持活跃状态 的模块或类, 不应当保留任何可能 被用户删除的数据的引用。 而应当在每次脚本被激活时重新从上下文中获取数据 。
•. 不一定每次都会崩溃, 在某些配置文件/操作系统的组合情况下可能会崩溃得多一些。
撤销会使得所有 bpy.types.ID 实例(对象 、场景、网孔等等 )都变得无效。
以下示例演示了,撤销之后内存位置 的变化。
>>> hash(bpy.context.object)
-9223372036849950810
>>> hash(bpy.context.object)
-9223372036849950810
# ... 移动活跃对象,然后撤销
>>> hash(bpy.context.object)
-9223372036849951740
就像上面据说的建议那样, 当Blender 处于交互模式时 ,不要保留 对任何数据的引用 。这是唯一一个避免脚本变得 不稳定的方法。
使用bpy.ops.object.mode_set(mode='EDIT') / bpy.ops.object.mode_set(mode='OBJECT')改变编辑模式将 导致对象的数据被重新分配内存。 在切换了编辑模式之后, 对一个网孔的顶点/面/UV贴图 、盔甲骨架、曲线节点 等数据的任何引用 都无法再访问到数据。
只有对数据本身的引用可重复访问。以下示例 将导致崩溃。
mesh = bpy.context.active_object.data
faces = mesh.faces
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.object.mode_set(mode='OBJECT')
# 将会崩溃
print(faces)
所以,切换了编辑模式之后,妳需要重新访问任何 的对象数据变量。 以下示例演示了如何避免上面示例中的崩溃。
mesh = bpy.context.active_object.data
faces = mesh.faces
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.object.mode_set(mode='OBJECT')
# 面(faces)已被重新分配
faces = mesh.faces
print(faces)
这种问题可能 在碰到任何会重新分配对象数据的内存的函数时产生 ,不过 最常见的就是切换编辑模式 的情况了。
当向一个曲线添加 新节点、向一个网孔添加新顶点/边/面时, 在内部会对储存 这些数据的数组进行重新分配。
bpy.ops.curve.primitive_bezier_curve_add()
point = bpy.context.object.data.splines[0].bezier_points[0]
bpy.context.object.data.splines[0].bezier_points.add()
# 会崩溃的啊!
point.co = 1.0, 2.0, 3.0
可这样避免:添加 新节点之后,重新 给point 变量赋值;或者储存节点 的下标,而不是储存节点本身 。
避免此问题的最好方法就是 一次性 将所有节点都添加到曲线 中。 这就意味着妳不用担心数组的重新分配问题,而且 这样还会更快 ,因为每 当 添加 一个新节点就重新分配数组的方式 太低效了。
董文华
HxLauncher: Launch Android applications by voice commands