#修正骨骼数据 class FixSkeletonOperator(bpy.types.Operator): bl_idname = "character.fix_skeleton" bl_label = "Fix Skeleton Data" def execute(self, context): props = context.scene.example_props bpy.ops.mesh.primitive_cube_add(size=props.cube_size, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1)) obj = context.object obj.name = "Example_Cube" return {'FINISHED'} 这是一个blender插件开发的python脚本,现在帮我实现一个函数,我通过一个名称如:c1020,筛选场景中所有的名称中包含c1020字符的骨架对象,检测被找到的所有的骨架对象的直接子层级,对直接子层级其中名字不为’Master‘的骨架进行删除
时间: 2025-07-24 19:19:10 浏览: 11
<think>我们正在开发一个Blender插件,需要编写Python脚本实现以下功能:
1. 根据名称(如"c1020")筛选骨架对象(Armature)。
2. 对于每个找到的骨架对象,遍历其子层级(即该骨架对象的所有子孙对象),并删除其中名称不为"Master"的骨架(Armature)对象。
注意:Blender中的对象可能有复杂的层次结构,我们需要递归地检查子对象。
步骤:
1. 导入bpy模块,以便操作Blender数据。
2. 遍历场景中的所有对象,找到名称包含指定字符串(例如"c1020")的骨架对象(Armature)。
3. 对于每个匹配的骨架对象,我们需要遍历其所有子孙对象(即该对象下的整个子树)。
4. 在子孙对象中,检查每个对象是否为骨架(Armature)类型,并且名称不等于"Master"。
5. 删除符合条件的对象(注意:删除对象时,需要确保该对象没有子对象,或者我们处理好父子关系?实际上,Blender不允许直接删除有子对象的父对象,除非我们同时删除其子对象。但我们的需求是只删除名称不为"Master"的骨架,所以我们需要先收集所有要删除的对象,然后从叶子节点开始删除,或者先删除子对象再删除父对象?但是,我们这里只删除骨架对象,并且要求是子层级中的,所以通常这些骨架对象可能是叶子节点或者中间节点。然而,在Blender中,删除一个对象会自动解除它与子对象的父子关系,子对象会变成场景根层的对象。但这不是我们想要的效果,因为我们只想删除指定的骨架,而不影响其他对象。所以,我们需要确保只删除那些名称不为"Master"的骨架,并且这些骨架可能是其他对象的父对象?删除后,其子对象将变成根对象。这可能是可以接受的,因为我们的需求是删除这些骨架,而不管它们的子对象(这些子对象可能不是骨架,或者是我们想要保留的?)。但是,根据问题要求,我们只删除骨架对象,并且只根据名称判断。所以,我们只删除骨架对象,而不管它是否有子对象(非骨架子对象会保留,并提升到父级?)。
然而,这里有一个问题:如果我们要删除的骨架对象有子对象(比如网格对象或者其他),那么删除骨架后,这些子对象会变成场景根层的对象。这可能是我们想要的吗?题目要求是“删除其子层级中名称不为Master的骨架”,所以只删除骨架类型的对象,其他类型的对象不受影响。因此,即使该骨架有子对象,我们也要删除它,然后它的子对象会变成根对象。这符合要求。
但是,我们需要注意:删除对象时,如果直接在一个循环中删除,可能会影响遍历。因此,我们通常先收集所有要删除的对象,然后再逐个删除。
另外,遍历子层级:我们可以使用一个递归函数或者使用队列/栈来遍历所有子孙对象。
但是,Blender对象有一个children属性,我们可以使用它来递归。
然而,我们也可以使用`bpy.data.objects`来直接获取所有对象,然后检查其父级关系?但这样效率可能不高。我们可以从选定的骨架对象开始,递归遍历其所有子对象(包括子对象的子对象)。
具体步骤:
- 定义一个函数来收集一个对象的所有子孙对象(包括直接子对象和间接子对象)。
- 或者,我们可以使用一个循环,利用一个列表作为队列,将当前对象的子对象加入队列,然后继续处理队列中的对象,直到队列为空。
由于我们只关心骨架对象,并且只删除名称不为"Master"的骨架,所以我们在遍历时只关注骨架对象,但注意:子层级中可能有非骨架对象,我们不需要处理非骨架对象,只需要检查骨架对象。
但是,遍历子层级时,我们需要遍历所有类型的子对象,因为骨架对象的子对象可能是其他类型(比如网格),然后这些子对象又可能有子骨架对象?所以,我们遍历时不能只考虑骨架对象,而是要考虑所有对象,然后判断每个对象的类型。
因此,我们可以这样:
1. 对于每个匹配的骨架对象(父骨架),我们创建一个待遍历的集合,初始包含这个骨架对象的所有子对象(直接子对象)。
2. 然后,我们使用一个队列,将直接子对象加入队列,然后从队列中取出一个对象,检查它是否是骨架对象,如果是,并且名称不是"Master",则将其标记为待删除。然后,将该对象的子对象加入队列(无论它是什么类型,因为我们还要继续向下遍历)。
3. 注意:我们只遍历子层级,所以起始对象的子对象开始,不包括起始对象本身(因为起始对象是我们匹配到的对象,我们不想删除它,除非它也在子层级中?但题目要求是“其子层级”,所以起始对象本身不处理)。
但是,题目要求是“其子层级中”,所以起始对象(即匹配到的骨架对象)本身不处理,只处理它的子对象以及子对象的子对象等。
因此,我们设计一个函数来收集一个父对象的所有子孙对象(不包括自身)。
另外,注意:同一个对象可能被多个父对象引用?在Blender中,一个对象只能有一个父对象,所以不会重复。
实现步骤:
1. 遍历场景中的所有对象,找到名称包含"c1020"的骨架对象(Armature)。
2. 对于每个找到的骨架对象,我们收集它的所有子孙对象(递归子对象)。
3. 在子孙对象中,筛选出类型为骨架(Armature)并且名称不等于"Master"的对象。
4. 将这些对象收集到一个列表中,然后删除。
注意:删除对象时,我们使用`bpy.data.objects.remove(obj, do_unlink=True)`,但是注意,如果这个对象还在场景集合中,我们需要确保从场景中解除关联。通常,使用`bpy.data.objects.remove(obj)`会从整个Blender数据中删除该对象,并解除所有关联。
另外,为了避免在删除时影响其他操作,我们最好先收集所有要删除的对象,然后一次性删除?但是Blender的Python API中,删除对象可以在循环中进行。
但是,我们需要注意:删除一个对象后,其子对象会变成根对象,但我们已经收集了这些子对象(在遍历时已经收集了),所以我们在删除父对象之前,应该已经收集了它的子对象(因为我们是先收集再删除)。所以,我们可以先收集所有要删除的对象,然后循环删除。
但是,删除一个对象后,它的子对象的父对象会变成None,所以不会影响我们删除其他对象。因此,我们可以按任意顺序删除,因为我们已经收集了所有要删除的对象(在删除之前已经收集完毕)。
因此,代码结构如下:
```python
import bpy
# 定义要匹配的名称和要保留的名称
target_name = "c1020" # 可以是部分匹配,比如名称包含"c1020"
keep_name = "Master"
# 存储所有要删除的骨架对象
to_delete = []
# 获取场景中的所有对象
# 首先,找到所有名称包含target_name的骨架对象
base_armatures = [obj for obj in bpy.data.objects if obj.type == 'ARMATURE' and target_name in obj.name]
# 遍历每个基础骨架
for arm in base_armatures:
# 创建一个队列,用于遍历所有子孙对象(不包括arm本身)
queue = list(arm.children) # 直接子对象
visited = set() # 避免循环引用,但通常不会,不过为了安全
while queue:
obj = queue.pop(0)
if obj in visited:
continue
visited.add(obj)
# 检查该对象是否为骨架,并且名称不是keep_name
if obj.type == 'ARMATURE' and obj.name != keep_name:
# 注意:这里我们只删除骨架对象,并且名称不是Master的
# 但是,注意:我们可能已经将其加入待删除列表,但可能重复?因为同一个对象可能被多个父对象引用?但一个对象只有一个父对象,所以不会重复。
# 但是,同一个对象可能被多个父骨架的子层级包含?不会,因为一个对象只有一个父对象。
# 所以,我们可以直接加入待删除列表
to_delete.append(obj)
# 将该对象的子对象加入队列(继续向下遍历)
queue.extend(obj.children)
# 现在,我们得到了所有要删除的骨架对象列表to_delete
# 注意:同一个对象可能被多个基础骨架遍历到?因为一个对象只能有一个父对象,所以它只会属于一个基础骨架的子层级。但是,有可能在多个基础骨架的子层级中?不会,因为一个对象只有一个父对象,所以它只能属于一个父对象的子层级。所以不会重复。
# 但是,为了避免重复删除同一个对象(比如一个对象同时是两个基础骨架的子孙?不可能),所以我们可以用set来去重。
to_delete_set = set(to_delete)
# 删除这些对象
for obj in to_delete_set:
# 删除对象
bpy.data.objects.remove(obj, do_unlink=True)
# 注意:删除对象后,场景中可能还有一些空集合,但Blender会自动处理。
```
但是,上面的代码有一个问题:如果某个要删除的对象是另一个要删除对象的父对象,那么当我们删除父对象时,其子对象会变成根对象,但我们已经收集了这些子对象(并且也会被删除)。所以,删除顺序无所谓,因为即使先删除父对象,子对象变成根对象,然后我们删除子对象也是可以的。
但是,我们也可以考虑先删除子对象再删除父对象?但这里我们不需要,因为删除父对象时,子对象会解除父子关系,然后我们删除子对象时,它已经是一个独立的根对象,不影响删除。
另外,我们还可以使用递归函数来收集子孙对象,但这里用队列已经足够。
但是,注意:如果子对象非常多,使用队列遍历是可行的。
测试:在Blender中运行前,请确保备份文件。
另外,注意:我们只删除了对象本身,但可能还有与之关联的数据(例如骨架数据),这些数据在没有其他引用时会被Blender自动删除吗?不会,因为骨架数据(bpy.data.armatures)可能被多个对象共享?通常,一个骨架对象对应一个骨架数据。当我们删除骨架对象时,其对应的骨架数据如果没有其他对象使用,则会成为孤儿数据。我们可以选择也删除这些骨架数据,但题目没有要求,而且可能其他对象也在使用相同的骨架数据。所以,我们只删除对象,不删除数据块。这样,骨架数据块会保留,但不再被使用。如果希望清理未使用的数据块,可以调用`bpy.ops.outliner.orphans_purge()`,但这是可选操作,且需要在一个操作中调用,可能会影响性能。
因此,我们只删除对象。
改进:我们可以使用集合来存储待删除对象,避免重复(虽然理论上不会重复,但为了安全)。
另外,我们也可以考虑使用深度优先搜索,但这里广度优先或深度优先都可以。
另一种方法:使用递归函数收集子孙对象中的骨架对象(名称不为"Master"的)
例如:
```python
def collect_armatures_to_delete(obj, keep_name, collection):
# 遍历所有子对象
for child in obj.children:
# 如果是骨架且名称不是keep_name,则加入集合
if child.type == 'ARMATURE' and child.name != keep_name:
collection.add(child)
# 递归遍历子对象的子对象
collect_armatures_to_delete(child, keep_name, collection)
# 然后:
to_delete_set = set()
for arm in base_armatures:
collect_armatures_to_delete(arm, keep_name, to_delete_set)
```
这种递归方法更简洁,但可能遇到递归深度问题(如果层次非常深)。不过Blender中层次通常不会太深,所以可以使用。
我们选择递归方法,因为更清晰。
最终代码:
```python
import bpy
# 定义要匹配的名称和要保留的名称
target_name = "c1020" # 可以是部分匹配,比如名称包含"c1020"
keep_name = "Master"
# 递归收集要删除的骨架对象
def collect_armatures_to_delete(parent_obj, keep_name, collection):
for child in parent_obj.children:
# 检查该子对象
if child.type == 'ARMATURE' and child.name != keep_name:
collection.add(child)
# 递归遍历子对象的子对象
collect_armatures_to_delete(child, keep_name, collection)
# 找到所有基础骨架对象
base_armatures = [obj for obj in bpy.data.objects if obj.type == 'ARMATURE' and target_name in obj.name]
to_delete_set = set()
for arm in base_armatures:
# 从基础骨架开始,遍历其子层级(不包含基础骨架自身)
collect_armatures_to_delete(arm, keep_name, to_delete_set)
# 删除这些对象
for obj in to_delete_set:
bpy.data.objects.remove(obj, do_unlink=True)
print(f"Deleted {len(to_delete_set)} armature objects.")
```
注意:递归深度问题。如果层次太深,可能超出Python递归深度限制。我们可以使用迭代方式(队列)来避免递归深度问题。
修改为迭代方式(使用栈)的收集函数:
```python
def collect_armatures_to_delete_iter(parent_obj, keep_name):
collection = set()
stack = [parent_obj] # 我们将从parent_obj开始遍历其子对象,然后每个子对象再遍历其子对象
while stack:
current = stack.pop()
for child in current.children:
if child.type == 'ARMATURE' and child.name != keep_name:
collection.add(child)
# 将该子对象压入栈,以便继续遍历其子对象
stack.append(child)
return collection
# 然后:
to_delete_set = set()
for arm in base_armatures:
to_delete_set.update(collect_armatures_to_delete_iter(arm, keep_name))
```
但是,注意:上面的迭代函数中,我们使用栈,并且每次从栈中弹出一个对象,然后将其子对象压入栈。这样会遍历所有子孙对象。
我们使用栈(深度优先)或队列(广度优先)都可以,这里使用栈。
完整代码(迭代方式):
```python
import bpy
target_name = "c1020"
keep_name = "Master"
# 使用迭代方式(深度优先)收集子孙中的骨架对象(名称不为Master)
def collect_armatures_to_delete(parent_obj, keep_name):
collection = set()
stack = [parent_obj] # 注意:这里传入的parent_obj是基础骨架,但我们不检查它本身,因为只检查子层级
# 但是,我们传入的parent_obj是基础骨架,我们只想要它的子层级,所以第一步我们只处理它的子对象,然后子对象的子对象...
# 但是,上面的栈初始包含了parent_obj,然后我们处理parent_obj时,会遍历它的子对象,然后每个子对象会被加入栈。但是,我们并不想检查parent_obj本身(即基础骨架),所以我们应该从parent_obj的子对象开始。
# 修改:我们初始栈应该是parent_obj的子对象列表
stack = list(parent_obj.children) # 初始栈为直接子对象
while stack:
current = stack.pop()
# 检查current对象
if current.type == 'ARMATURE' and current.name != keep_name:
collection.add(current)
# 将current的子对象加入栈
stack.extend(current.children)
return collection
base_armatures = [obj for obj in bpy.data.objects if obj.type == 'ARMATURE' and target_name in obj.name]
to_delete_set = set()
for arm in base_armatures:
to_delete_set.update(collect_armatures_to_delete(arm, keep_name))
# 删除
for obj in to_delete_set:
bpy.data.objects.remove(obj, do_unlink=True)
print(f"Deleted {len(to_delete_set)} armature objects.")
```
这个版本避免了递归深度问题。
注意:如果场景中有大量对象,这个操作可能会比较慢,但通常骨架对象不会太多。
另外,我们也可以使用广度优先(使用队列),这里用栈(深度优先)也可以。
最后,将这段代码封装成一个Blender插件的操作符(Operator)以便使用。
例如:
```python
import bpy
class DeleteSubArmaturesOperator(bpy.types.Operator):
bl_idname = "object.delete_sub_armatures"
bl_label = "Delete Sub Armatures by Name"
def execute(self, context):
target_name = "c1020"
keep_name = "Master"
# 使用迭代方式收集
def collect_armatures_to_delete(parent_obj, keep_name):
collection = set()
stack = list(parent_obj.children)
while stack:
current = stack.pop()
if current.type == 'ARMATURE' and current.name != keep_name:
collection.add(current)
stack.extend(current.children)
return collection
base_armatures = [obj for obj in bpy.data.objects if obj.type == 'ARMATURE' and target_name in obj.name]
if not base_armatures:
self.report({'INFO'}, "No base armatures found.")
return {'CANCELLED'}
to_delete_set = set()
for arm in base_armatures:
to_delete_set.update(collect_armatures_to_delete(arm, keep_name))
if not to_delete_set:
self.report({'INFO'}, "No armatures to delete.")
return {'CANCELLED'}
# 删除
count = 0
for obj in to_delete_set:
# 确保对象没有被删除(可能在之前的操作中已经删除?但这里不会)
if obj.name in bpy.data.objects:
bpy.data.objects.remove(obj, do_unlink=True)
count += 1
self.report({'INFO'}, f"Deleted {count} armature objects.")
return {'FINISHED'}
# 注册
def register():
bpy.utils.register_class(DeleteSubArmaturesOperator)
def unregister():
bpy.utils.unregister_class(DeleteSubArmaturesOperator)
if __name__ == "__main__":
register()
```
这样,我们就可以在Blender中通过搜索该操作符来执行。
注意:上面的操作符中,我们固定了target_name和keep_name,实际应用中,我们可以将它们改为操作符的属性,以便在用户界面中调整。
例如:
```python
class DeleteSubArmaturesOperator(bpy.types.Operator):
bl_idname = "object.delete_sub_armatures"
bl_label = "Delete Sub Armatures by Name"
target_name: bpy.props.StringProperty(name="Target Name", default="c1020")
keep_name: bpy.props.StringProperty(name="Keep Name", default="Master")
def execute(self, context):
# 使用self.target_name和self.keep_name
...
```
然后在invoke方法中弹出对话框让用户输入,或者直接在重绘面板中显示。
但根据题目要求,我们只需要实现功能,所以这里提供基本实现。
注意:在删除对象时,如果对象被选中或者是活动对象,可能会引起问题。我们可以在删除前确保取消选中。
但是,Blender的remove函数会处理这些。
因此,以上代码可以满足需求。
注意:在Blender中运行脚本时,请确保在脚本模式下运行,或者将脚本保存为插件安装。
最后,我们输出一个信息,告知删除了多少个对象。
引用说明:
我们参考了Blender Python API的基本操作,如遍历对象、删除对象等[^1][^2][^3]。</think>在Blender插件开发中,根据名称筛选骨架对象并删除其子层级中不符合条件的骨架,可以通过以下Python脚本实现。该脚本会遍历场景中的所有骨架对象,筛选名称包含指定字符串(如"c1020")的对象,然后递归检查其子层级中的骨架对象,删除名称不为"Master"的骨架。
### 解决方案代码
```python
import bpy
from bpy.types import Operator
from bpy.props import StringProperty
def delete_non_master_armatures(parent_armature, keep_name="Master"):
"""递归删除子层级中名称不为'Master'的骨架对象"""
to_delete = []
# 使用栈实现非递归遍历
stack = list(parent_armature.children)
while stack:
obj = stack.pop()
# 收集需要删除的对象(先不删除,避免遍历中断)
if obj.type == 'ARMATURE' and obj.name != keep_name:
to_delete.append(obj)
# 将子对象加入栈中继续遍历
stack.extend(obj.children)
# 删除收集到的对象
for obj in to_delete:
if obj.name in bpy.data.objects:
bpy.data.objects.remove(obj, do_unlink=True)
return len(to_delete)
class OBJECT_OT_DeleteNonMasterArmatures(Operator):
bl_idname = "object.delete_non_master_armatures"
bl_label = "删除非Master骨架"
bl_description = "删除选定骨架子层级中名称不为Master的骨架对象"
bl_options = {'REGISTER', 'UNDO'}
filter_name: StringProperty(
name="筛选名称",
default="c1020",
description="需要筛选的骨架名称包含的字符串"
)
keep_name: StringProperty(
name="保留名称",
default="Master",
description="需要保留的骨架名称"
)
def execute(self, context):
# 查找所有包含指定名称的骨架对象
base_armatures = [
obj for obj in bpy.data.objects
if obj.type == 'ARMATURE' and self.filter_name in obj.name
]
if not base_armatures:
self.report({'WARNING'}, f"未找到包含'{self.filter_name}'的骨架对象")
return {'CANCELLED'}
total_deleted = 0
for arm in base_armatures:
deleted_count = delete_non_master_armatures(arm, self.keep_name)
total_deleted += deleted_count
self.report({'INFO'}, f"已删除 {total_deleted} 个非'{self.keep_name}'骨架")
return {'FINISHED'}
# 注册到Blender菜单
def menu_func(self, context):
self.layout.operator(OBJECT_OT_DeleteNonMasterArmatures.bl_idname)
def register():
bpy.utils.register_class(OBJECT_OT_DeleteNonMasterArmatures)
bpy.types.VIEW3D_MT_object.append(menu_func)
def unregister():
bpy.utils.unregister_class(OBJECT_OT_DeleteNonMasterArmatures)
bpy.types.VIEW3D_MT_object.remove(menu_func)
if __name__ == "__main__":
register()
```
### 功能说明
1. **筛选机制**:
- 查找所有名称包含`filter_name`(如"c1020")的骨架对象(Armature)
- 使用`bpy.data.objects`集合进行快速遍历
2. **层级处理**:
- 使用**栈(stack)实现非递归遍历**,避免递归深度限制
- 检查每个骨架对象的子对象(`obj.children`)
- 只处理骨架类型对象(`obj.type == 'ARMATURE'`)
3. **删除逻辑**:
- 先收集需要删除的对象,最后统一删除(避免遍历中断)
- 使用`bpy.data.objects.remove()`安全删除对象
- 支持撤销操作(`bl_options = {'REGISTER', 'UNDO'}`)
4. **用户交互**:
- 在3D视图的对象菜单中添加操作按钮
- 操作完成后显示统计信息
- 支持自定义筛选名称和保留名称
### 使用步骤
1. 将脚本保存为`.py`文件
2. 在Blender中通过`编辑 > 偏好设置 > 插件 > 安装`加载脚本
3. 在3D视图窗口:
- 选择`物体 > 删除非Master骨架`
- 在弹出对话框中设置筛选名称(如"c1020")
- 点击"确定"执行操作
### 注意事项
1. 删除操作不可逆,建议先保存文件
2. 脚本会跳过名称完全匹配"Master"的骨架
3. 支持批量处理多个匹配的父级骨架
4. 通过`bl_options = {'UNDO'}`支持Ctrl+Z撤销
此脚本通过直接操作Blender数据层实现高效处理,避免了场景更新带来的性能开销,适用于处理复杂骨架层级结构[^1][^2]。
阅读全文
相关推荐















