珠海市网站建设_网站建设公司_交互流畅度_seo优化
2026/1/21 18:55:13 网站建设 项目流程

import c4d from c4d import utils def GetClonerCache(cloner_obj, doc): # 强制更新文档缓存(确保克隆体已计算) doc.ExecutePasses(None, True, True, True, c4d.BUILDFLAGS_NONE) # 获取克隆对象的缓存根节点 cache_root = cloner_obj.GetCache() if cache_root is None: return [] # 递归遍历所有缓存子对象 def RecurseCache(obj, cache_list): if obj is None: return # 添加当前缓存对象到列表 cache_list.append(obj) # 遍历子对象 child = obj.GetDown() while child: RecurseCache(child, cache_list) child = child.GetNext() # 存储所有克隆体缓存 cloner_cache_list = [] RecurseCache(cache_root, cloner_cache_list) return cloner_cache_list def main(): # 获取当前文档 doc = c4d.documents.GetActiveDocument() # 获取选中的克隆对象(确保在C4D中选中了克隆对象) clone_obj = doc.SearchObject("草地") try: # 获取克隆体缓存 cache_objects = GetClonerCache(clone_obj, doc) # 输出每个克隆体的信息 print(f"找到 {len(cache_objects)} 个克隆体缓存:") for i, cache_obj in enumerate(cache_objects): # 获取克隆体的全局矩阵(位置、旋转、缩放) mg = cache_obj.GetMg() pos = mg.off scale = mg.GetScale() print(f"克隆体 {i+1}:") print(f" 位置: ({pos.x:.2f}, {pos.y:.2f}, {pos.z:.2f})") print(f" 缩放: ({scale.x:.2f}, {scale.y:.2f}, {scale.z:.2f})") print(f" 对象类型: {cache_obj.GetTypeName()}") print("-" * 30) except Exception as e: print(f"获取缓存失败: {str(e)}") if __name__ == "__main__": main()

反馈

割草代码:

import c4d import math # 克隆体固定类型ID CLONER_TYPE_ID = 1018544 def GetClonerCache(cloner_obj, doc): """复用你的稳定版缓存获取逻辑""" if not isinstance(cloner_obj, c4d.BaseObject) or cloner_obj.GetType() != CLONER_TYPE_ID: raise TypeError("传入的对象不是克隆对象(Cloner)") doc.ExecutePasses(None, True, True, True, c4d.BUILDFLAGS_NONE) cache_root = cloner_obj.GetCache() if cache_root is None: return [] def RecurseCache(obj, cache_list): if obj is None: return if obj.IsAlive() and obj.GetType() != c4d.Onull: cache_list.append(obj) child = obj.GetDown() while child: RecurseCache(child, cache_list) child = child.GetNext() cloner_cache_list = [] RecurseCache(cache_root, cloner_cache_list) return cloner_cache_list def IsPointInObjectByGetRad(point, target_obj): """用GetRad()判断重叠""" if target_obj is None or not target_obj.IsAlive(): return False target_mg = target_obj.GetMg() target_rad = target_obj.GetRad() local_point = ~target_mg * point return (abs(local_point.x) <= target_rad.x and abs(local_point.y) <= target_rad.y and abs(local_point.z) <= target_rad.z) def DeleteOverlappedCloners(cloner_cache, target_obj): """安全删除重叠的克隆体""" deleted_count = 0 for cache_obj in reversed(cloner_cache): try: if not cache_obj.IsAlive(): continue cloner_pos = cache_obj.GetMg().off if IsPointInObjectByGetRad(cloner_pos, target_obj): cache_obj.Remove() deleted_count += 1 except Exception as e: print(f"跳过删除错误: {str(e)}") continue return deleted_count def LockCacheAndRefresh(doc, cloner_obj): """ 核心修复:锁定克隆体缓存 + 强制持续刷新视图 """ # 1. 禁用克隆体的自动缓存刷新(关键:阻止C4D重新生成缓存) cloner_obj[c4d.CLONEROBJECT_DONT_REFRESH] = True # 2. 强制刷新文档和视图(多层刷新,确保效果持久) doc.SetDirty(c4d.DIRTYFLAGS_DATA) # 标记文档数据已修改 doc.ExecutePasses(None, True, True, True, c4d.BUILDFLAGS_0) # 重新计算但保留修改 # 3. 强制重绘所有视图(解决视图不刷新问题) c4d.DrawViews( c4d.DRAWFLAGS_ONLY_ACTIVE_VIEW | c4d.DRAWFLAGS_NO_THREAD | c4d.DRAWFLAGS_STATICBREAK ) c4d.EventAdd(c4d.EVENT_FORCEREDRAW) # 强制视图重绘 def main(): doc = c4d.documents.GetActiveDocument() if doc is None: return # 查找对象 cloner_obj = doc.SearchObject("草地") target_obj = doc.SearchObject("Sphere") if cloner_obj is None: print("❌ 未找到名为'草地'的克隆对象") return if target_obj is None: print("❌ 未找到名为'Cylinder'的目标物体") return # 开启撤销栈(可选,方便回滚) doc.StartUndo() try: # 1. 获取缓存 cache_objects = GetClonerCache(cloner_obj, doc) print(f"找到 {len(cache_objects)} 个克隆体缓存") # 2. 删除重叠克隆体 if cache_objects: deleted_count = DeleteOverlappedCloners(cache_objects, target_obj) print(f"\n✅ 删除了 {deleted_count} 个重叠的克隆体") # 3. 核心修复:锁定缓存 + 强制持续刷新 LockCacheAndRefresh(doc, cloner_obj) except Exception as e: print(f"执行失败: {str(e)}") finally: doc.EndUndo() # 结束撤销栈 c4d.EventAdd() #刷新界面 if __name__ == "__main__": main()

遇到的「仅播放状态有效果、渲染无效果」的核心问题,是因为我们修改的只是「实时预览缓存」,而非渲染管线使用的「渲染缓存」——C4D 的预览缓存和渲染缓存是两套独立的体系,播放时加载预览缓存,渲染时会重新计算渲染缓存,导致修改被覆盖。

问题根源

场景缓存类型是否保留修改原因
视图播放实时预览缓存代码修改的是这套缓存
渲染 / 渲染预览渲染管线缓存渲染时会重新生成缓存
停止播放静态缓存预览缓存被释放 / 重置

终极解决方案:选集隐藏

import c4d from c4d.modules import mograph def IsPointInObjectByGetRad(point, target_obj): """判断点是否在目标对象的包围盒内""" if target_obj is None or not target_obj.IsAlive(): return False target_mg = target_obj.GetMg() target_rad = target_obj.GetRad() # 将点转换到目标对象的局部坐标系 local_point = ~target_mg * point return (abs(local_point.x) <= target_rad.x and abs(local_point.y) <= target_rad.y and abs(local_point.z) <= target_rad.z) def main(): doc = c4d.documents.GetActiveDocument() if doc is None: return # 1. 查找对象 cloner_obj = doc.SearchObject("草地") # 查找目标物体逻辑:先找根节点 Control,再在其下递归查找 Cy_Collision root_name = "Control" target_name = "Cy_Collision" root_obj = doc.SearchObject(root_name) target_obj = None if root_obj: # 定义内部递归函数 def FindChildRecursive(obj, name): if obj.GetName() == name: return obj child = obj.GetDown() while child: res = FindChildRecursive(child, name) if res: return res child = child.GetNext() return None target_obj = FindChildRecursive(root_obj, target_name) # 如果没找到根节点,或者在根节点下没找到,可以尝试全局搜索兜底(可选) if target_obj is None: # 尝试直接全局搜索(保留一点容错性) target_obj = doc.SearchObject(target_name) if cloner_obj is None: c4d.gui.MessageDialog("❌ 未找到名为'草地'的克隆对象") return if target_obj is None: c4d.gui.MessageDialog(f"❌ 未找到目标物体: {root_name} -> ... -> {target_name}") return # 0. 检查当前帧是否为第 0 帧 (重置逻辑) current_frame = doc.GetTime().GetFrame(doc.GetFps()) TAG_NAME = "Deleted_Grass_Selection" if current_frame == 0: # 查找现有选集标签 sel_tag = None for tag in cloner_obj.GetTags(): if tag.CheckType(c4d.Tmgselection) and tag.GetName() == TAG_NAME: sel_tag = tag break if sel_tag: doc.StartUndo() # 清空选集 empty_bs = c4d.BaseSelect() mograph.GeSetMoDataSelection(sel_tag, empty_bs) doc.AddUndo(c4d.UNDOTYPE_CHANGE, sel_tag) doc.EndUndo() c4d.EventAdd() print("⏪ 第0帧:已重置草地状态 (选集已清空)") return # 检查是否为克隆对象 if cloner_obj.GetType() != 1018544: # Cloner ID c4d.gui.MessageDialog("❌ '草地'对象不是一个运动图形克隆器(Cloner)!") return # 2. 获取 MoData (运动图形数据) md = mograph.GeGetMoData(cloner_obj) if md is None: c4d.gui.MessageDialog("❌ 无法获取克隆数据,请确保克隆器已生成克隆。") return # 获取所有克隆的矩阵数组 (局部于克隆器,或者视情况而定,通常需要乘以前级矩阵) # GetArray(c4d.MODATA_MATRIX) 返回的是克隆相对于克隆器的局部矩阵列表 marr = md.GetArray(c4d.MODATA_MATRIX) count = len(marr) print(f"正在检测 {count} 个克隆体...") # 3. 计算需要隐藏的克隆索引 # 查找是否已有同名选集标签 TAG_NAME = "Deleted_Grass_Selection" sel_tag = None for tag in cloner_obj.GetTags(): if tag.CheckType(c4d.Tmgselection) and tag.GetName() == TAG_NAME: sel_tag = tag break # 获取现有选集(如果有) bs = c4d.BaseSelect() if sel_tag: # 从现有标签中读取之前的选集 existing_bs = mograph.GeGetMoDataSelection(sel_tag) if existing_bs: bs.Merge(existing_bs) # 保留之前的选择 cloner_mg = cloner_obj.GetMg() new_match_count = 0 for i in range(count): # 如果已经选中了,就跳过计算(优化性能) if bs.IsSelected(i): continue # 计算克隆体的世界坐标 # marr[i] 是克隆体相对于克隆器的矩阵 # 所以世界矩阵 = 克隆器世界矩阵 * 克隆体局部矩阵 clone_global_mg = cloner_mg * marr[i] clone_pos = clone_global_mg.off if IsPointInObjectByGetRad(clone_pos, target_obj): bs.Select(i) # 选中该索引 new_match_count += 1 total_selected = bs.GetCount() print(f"✅ 新增选中 {new_match_count} 个,总计 {total_selected} 个") if total_selected == 0: return doc.StartUndo() # 4. 创建或更新运动图形选集标签 (MoGraph Selection Tag) # 如果没有则创建 if sel_tag is None: sel_tag = c4d.BaseTag(c4d.Tmgselection) sel_tag.SetName(TAG_NAME) cloner_obj.InsertTag(sel_tag) doc.AddUndo(c4d.UNDOTYPE_NEW, sel_tag) # 更新选集数据 # 注意:MoGraph 选集标签的数据设置方式比较特殊 mograph.GeSetMoDataSelection(sel_tag, bs) doc.AddUndo(c4d.UNDOTYPE_CHANGE, sel_tag) # 5. 创建或配置简易效果器 (Plain Effector) 来隐藏这些克隆 EFFECTOR_NAME = "Mowing_Effector" effector = doc.SearchObject(EFFECTOR_NAME) if effector is None: effector = c4d.BaseObject(1018561) # Omgplain (Plain Effector ID) effector.SetName(EFFECTOR_NAME) # 将效果器放在克隆器旁边或归类 effector.InsertAfter(cloner_obj) doc.AddUndo(c4d.UNDOTYPE_NEW, effector) # 配置效果器参数 doc.AddUndo(c4d.UNDOTYPE_CHANGE, effector) # 关键:设置选集字符串,让效果器只对选集标签生效 # c4d.ID_MG_BASEEFFECTOR_SELECTION 是效果器选项卡里的"选集"字段ID effector[c4d.ID_MG_BASEEFFECTOR_SELECTION] = TAG_NAME # 开启"缩放"参数 (Scale),并将缩放值设为 -1 (实现缩放到0) effector[c4d.ID_MG_BASEEFFECTOR_SCALE_ACTIVE] = True effector[c4d.ID_MG_BASEEFFECTOR_UNIFORMSCALE] = True effector[c4d.ID_MG_BASEEFFECTOR_USCALE] = -1.0 # 关闭"可见"参数 (既然不起作用) effector[1106] = False # 确保不修改位置/旋转 effector[c4d.ID_MG_BASEEFFECTOR_POSITION_ACTIVE] = False # 6. 将效果器添加到克隆器的效果器列表中 effector_list = cloner_obj[c4d.ID_MG_MOTIONGENERATOR_EFFECTORLIST] if effector_list is None: effector_list = c4d.InExcludeData() # 检查列表中是否已存在该效果器 found = False for i in range(effector_list.GetObjectCount()): if effector_list.ObjectFromIndex(doc, i) == effector: found = True break if not found: effector_list.InsertObject(effector, 1) cloner_obj[c4d.ID_MG_MOTIONGENERATOR_EFFECTORLIST] = effector_list doc.AddUndo(c4d.UNDOTYPE_CHANGE, cloner_obj) doc.EndUndo() # 刷新场景 c4d.EventAdd() print(f"已更新选集标签 '{TAG_NAME}' 并应用隐藏效果器 '{EFFECTOR_NAME}'") if __name__ == "__main__": main()

在任何时间范围内返回到播放范围的开始释放所有选集:改59-83行。(不再是返回到0帧恢复选集)

# 0. 检查当前帧是否为起始帧 (重置逻辑) current_time = doc.GetTime() min_time = doc.GetLoopMinTime() TAG_NAME = "Deleted_Grass_Selection" # 提前定义 TAG_NAME # 判断当前时间是否 <= 预览范围起始时间 (允许一点点浮点误差) if current_time.Get() <= min_time.Get() + 0.001: # 查找现有选集标签 sel_tag = None for tag in cloner_obj.GetTags(): if tag.CheckType(c4d.Tmgselection) and tag.GetName() == TAG_NAME: sel_tag = tag break if sel_tag: doc.StartUndo() # 清空选集 empty_bs = c4d.BaseSelect() mograph.GeSetMoDataSelection(sel_tag, empty_bs) doc.AddUndo(c4d.UNDOTYPE_CHANGE, sel_tag) doc.EndUndo() c4d.EventAdd() print(f"⏪ 起始帧({min_time.GetFrame(doc.GetFps())}):已重置草地状态") return

完整代码

import c4d from c4d.modules import mograph def IsPointInObjectByGetRad(point, target_obj): """判断点是否在目标对象的包围盒内""" if target_obj is None or not target_obj.IsAlive(): return False target_mg = target_obj.GetMg() target_rad = target_obj.GetRad() # 将点转换到目标对象的局部坐标系 local_point = ~target_mg * point return (abs(local_point.x) <= target_rad.x and abs(local_point.y) <= target_rad.y and abs(local_point.z) <= target_rad.z) def main(): doc = c4d.documents.GetActiveDocument() if doc is None: return # 1. 查找对象 cloner_obj = doc.SearchObject("草地") # 查找目标物体逻辑:先找根节点 Control,再在其下递归查找 Cy_Collision root_name = "Control" target_name = "Cy_Collision" root_obj = doc.SearchObject(root_name) target_obj = None if root_obj: # 定义内部递归函数 def FindChildRecursive(obj, name): if obj.GetName() == name: return obj child = obj.GetDown() while child: res = FindChildRecursive(child, name) if res: return res child = child.GetNext() return None target_obj = FindChildRecursive(root_obj, target_name) # 如果没找到根节点,或者在根节点下没找到,可以尝试全局搜索兜底(可选) if target_obj is None: # 尝试直接全局搜索(保留一点容错性) target_obj = doc.SearchObject(target_name) if cloner_obj is None: c4d.gui.MessageDialog("❌ 未找到名为'草地'的克隆对象") return if target_obj is None: c4d.gui.MessageDialog(f"❌ 未找到目标物体: {root_name} -> ... -> {target_name}") return # 0. 检查当前帧是否为起始帧 (重置逻辑) current_time = doc.GetTime() min_time = doc.GetLoopMinTime() TAG_NAME = "Deleted_Grass_Selection" # 提前定义 TAG_NAME # 判断当前时间是否 <= 预览范围起始时间 (允许一点点浮点误差) if current_time.Get() <= min_time.Get() + 0.001: # 查找现有选集标签 sel_tag = None for tag in cloner_obj.GetTags(): if tag.CheckType(c4d.Tmgselection) and tag.GetName() == TAG_NAME: sel_tag = tag break if sel_tag: doc.StartUndo() # 清空选集 empty_bs = c4d.BaseSelect() mograph.GeSetMoDataSelection(sel_tag, empty_bs) doc.AddUndo(c4d.UNDOTYPE_CHANGE, sel_tag) doc.EndUndo() c4d.EventAdd() print(f"⏪ 起始帧({min_time.GetFrame(doc.GetFps())}):已重置草地状态") return # 检查是否为克隆对象 if cloner_obj.GetType() != 1018544: # Cloner ID c4d.gui.MessageDialog("❌ '草地'对象不是一个运动图形克隆器(Cloner)!") return # 2. 获取 MoData (运动图形数据) md = mograph.GeGetMoData(cloner_obj) if md is None: c4d.gui.MessageDialog("❌ 无法获取克隆数据,请确保克隆器已生成克隆。") return # 获取所有克隆的矩阵数组 (局部于克隆器,或者视情况而定,通常需要乘以前级矩阵) # GetArray(c4d.MODATA_MATRIX) 返回的是克隆相对于克隆器的局部矩阵列表 marr = md.GetArray(c4d.MODATA_MATRIX) count = len(marr) print(f"正在检测 {count} 个克隆体...") # 3. 计算需要隐藏的克隆索引 # 查找是否已有同名选集标签 TAG_NAME = "Deleted_Grass_Selection" sel_tag = None for tag in cloner_obj.GetTags(): if tag.CheckType(c4d.Tmgselection) and tag.GetName() == TAG_NAME: sel_tag = tag break # 获取现有选集(如果有) bs = c4d.BaseSelect() if sel_tag: # 从现有标签中读取之前的选集 existing_bs = mograph.GeGetMoDataSelection(sel_tag) if existing_bs: bs.Merge(existing_bs) # 保留之前的选择 cloner_mg = cloner_obj.GetMg() new_match_count = 0 for i in range(count): # 如果已经选中了,就跳过计算(优化性能) if bs.IsSelected(i): continue # 计算克隆体的世界坐标 # marr[i] 是克隆体相对于克隆器的矩阵 # 所以世界矩阵 = 克隆器世界矩阵 * 克隆体局部矩阵 clone_global_mg = cloner_mg * marr[i] clone_pos = clone_global_mg.off if IsPointInObjectByGetRad(clone_pos, target_obj): bs.Select(i) # 选中该索引 new_match_count += 1 total_selected = bs.GetCount() print(f"✅ 新增选中 {new_match_count} 个,总计 {total_selected} 个") if total_selected == 0: return doc.StartUndo() # 4. 创建或更新运动图形选集标签 (MoGraph Selection Tag) # 如果没有则创建 if sel_tag is None: sel_tag = c4d.BaseTag(c4d.Tmgselection) sel_tag.SetName(TAG_NAME) cloner_obj.InsertTag(sel_tag) doc.AddUndo(c4d.UNDOTYPE_NEW, sel_tag) # 更新选集数据 # 注意:MoGraph 选集标签的数据设置方式比较特殊 mograph.GeSetMoDataSelection(sel_tag, bs) doc.AddUndo(c4d.UNDOTYPE_CHANGE, sel_tag) # 5. 创建或配置简易效果器 (Plain Effector) 来隐藏这些克隆 EFFECTOR_NAME = "Mowing_Effector" effector = doc.SearchObject(EFFECTOR_NAME) if effector is None: effector = c4d.BaseObject(1018561) # Omgplain (Plain Effector ID) effector.SetName(EFFECTOR_NAME) # 将效果器放在克隆器旁边或归类 effector.InsertAfter(cloner_obj) doc.AddUndo(c4d.UNDOTYPE_NEW, effector) # 配置效果器参数 doc.AddUndo(c4d.UNDOTYPE_CHANGE, effector) # 关键:设置选集字符串,让效果器只对选集标签生效 # c4d.ID_MG_BASEEFFECTOR_SELECTION 是效果器选项卡里的"选集"字段ID effector[c4d.ID_MG_BASEEFFECTOR_SELECTION] = TAG_NAME # 开启"缩放"参数 (Scale),并将缩放值设为 -1 (实现缩放到0) effector[c4d.ID_MG_BASEEFFECTOR_SCALE_ACTIVE] = True effector[c4d.ID_MG_BASEEFFECTOR_UNIFORMSCALE] = True effector[c4d.ID_MG_BASEEFFECTOR_USCALE] = -1.0 # 关闭"可见"参数 (既然不起作用) effector[1106] = False # 确保不修改位置/旋转 effector[c4d.ID_MG_BASEEFFECTOR_POSITION_ACTIVE] = False # 6. 将效果器添加到克隆器的效果器列表中 effector_list = cloner_obj[c4d.ID_MG_MOTIONGENERATOR_EFFECTORLIST] if effector_list is None: effector_list = c4d.InExcludeData() # 检查列表中是否已存在该效果器 found = False for i in range(effector_list.GetObjectCount()): if effector_list.ObjectFromIndex(doc, i) == effector: found = True break if not found: effector_list.InsertObject(effector, 1) cloner_obj[c4d.ID_MG_MOTIONGENERATOR_EFFECTORLIST] = effector_list doc.AddUndo(c4d.UNDOTYPE_CHANGE, cloner_obj) doc.EndUndo() # 刷新场景 c4d.EventAdd() print(f"已更新选集标签 '{TAG_NAME}' 并应用隐藏效果器 '{EFFECTOR_NAME}'") if __name__ == "__main__": main()

问:贴在 Python 标签里,勾选了帧依赖

AI: 太好了,既然是在 Python 标签 环境下,我们就可以利用标签的 持久化数据 (BaseContainer) 来实现完美的回滚功能。

### 重构计划 (Python Tag 专用版)
我将为您重写代码,使其适应 Python Tag 的运行机制:

1.去除 Undo : 标签中严禁使用 StartUndo/AddUndo,否则会爆内存且卡顿。
2.去除 EventAdd : 标签会自动刷新,不需要手动触发。
3.实现历史记录 (History):
- 利用 op (即 Python 标签本身) 的 BaseContainer 来存储历史数据。
- 由于 BaseContainer 不能直接存复杂字典,我们将使用 HyperFile 序列化或者简单的 自定义数据ID 来存储每一帧的“收割记录”。
4. 回滚逻辑 :
- 检测 当前帧 < 上一帧 。
- 如果有回退,读取历史记录,把“未来”的草释放(取消隐藏)。
### 代码实现
这是一个完整的、可直接粘贴到 Python Tag 的代码。

我已为您创建了一个新文件 割草_PythonTag.py ,您可以直接将里面的内容复制粘贴到 C4D 的 Python 标签中。
核心功能说明:
1. 自动回滚 :脚本会在标签的 BaseContainer 中记录每一帧割掉了哪些草。当您拖动时间轴回退时,它会自动把那一帧之后割掉的草“种回去”(取消隐藏)。
2. 高性能 :
- 跳过已选 :如果草已经被割了,不会重复计算碰撞。
- 按需更新 :只有当确实有草被割掉或恢复时,才会写入标签,最大限度减少数据传输开销。
3. 无 Undo :移除了所有 Undo 操作,防止在播放动画时内存溢出。
4. 初始化检测 :会自动创建所需的选集标签和效果器(如果不存在)。

import c4d from c4d.modules import mograph def IsPointInObjectByGetRad(point, target_obj): """判断点是否在目标对象的包围盒内""" if target_obj is None or not target_obj.IsAlive(): return False target_mg = target_obj.GetMg() target_rad = target_obj.GetRad() # 将点转换到目标对象的局部坐标系 local_point = ~target_mg * point return (abs(local_point.x) <= target_rad.x and abs(local_point.y) <= target_rad.y and abs(local_point.z) <= target_rad.z) def GetHistory(op): """从对象获取历史记录""" bc = op.GetDataInstance() history = bc.GetContainerInstance(1000) if not history: history = c4d.BaseContainer() bc.SetContainer(1000, history) return history def SaveHistory(op, history): """保存历史记录""" bc = op.GetDataInstance() bc.SetContainer(1000, history) def main(): doc = c4d.documents.GetActiveDocument() if doc is None: return # 1. 查找对象 cloner_obj = doc.SearchObject("草地") # 查找目标物体逻辑:先找根节点 Control,再在其下递归查找 Cy_Collision root_name = "Control" target_name = "Cy_Collision" root_obj = doc.SearchObject(root_name) target_obj = None if root_obj: # 定义内部递归函数 def FindChildRecursive(obj, name): if obj.GetName() == name: return obj child = obj.GetDown() while child: res = FindChildRecursive(child, name) if res: return res child = child.GetNext() return None target_obj = FindChildRecursive(root_obj, target_name) # 如果没找到根节点,或者在根节点下没找到,可以尝试全局搜索兜底(可选) if target_obj is None: # 尝试直接全局搜索(保留一点容错性) target_obj = doc.SearchObject(target_name) if cloner_obj is None: c4d.gui.MessageDialog("❌ 未找到名为'草地'的克隆对象") return if target_obj is None: c4d.gui.MessageDialog(f"❌ 未找到目标物体: {root_name} -> ... -> {target_name}") return # 0. 获取当前帧 current_frame = doc.GetTime().GetFrame(doc.GetFps()) # 4. 创建或更新运动图形选集标签 (MoGraph Selection Tag) TAG_NAME = "Deleted_Grass_Selection" if current_frame == 0: # 查找现有选集标签 sel_tag = None for tag in cloner_obj.GetTags(): if tag.CheckType(c4d.Tmgselection) and tag.GetName() == TAG_NAME: sel_tag = tag break if sel_tag: doc.StartUndo() # 清空选集 empty_bs = c4d.BaseSelect() mograph.GeSetMoDataSelection(sel_tag, empty_bs) doc.AddUndo(c4d.UNDOTYPE_CHANGE, sel_tag) doc.EndUndo() c4d.EventAdd() print("⏪ 第0帧:已重置草地状态 (选集已清空)") return # 检查是否为克隆对象 if cloner_obj.GetType() != 1018544: # Cloner ID c4d.gui.MessageDialog("❌ '草地'对象不是一个运动图形克隆器(Cloner)!") return # 2. 获取 MoData (运动图形数据) md = mograph.GeGetMoData(cloner_obj) if md is None: c4d.gui.MessageDialog("❌ 无法获取克隆数据,请确保克隆器已生成克隆。") return # 获取所有克隆的矩阵数组 marr = md.GetArray(c4d.MODATA_MATRIX) if not marr: return count = len(marr) cloner_mg = cloner_obj.GetMg() print(f"正在检测 {count} 个克隆体... (Frame {current_frame})") # ========================================== # 核心逻辑:基于历史重建 + 新增检测 (移植自 Python Tag) # ========================================== history = GetHistory(cloner_obj) # 存储在克隆对象上 # A. 清理未来帧的历史 (支持回滚) frames_to_remove = [] for frame_id, _ in history: if frame_id > current_frame: frames_to_remove.append(frame_id) for fid in frames_to_remove: history.RemoveData(fid) # B. 重建当前帧之前的累积选集 rebuilt_bs = c4d.BaseSelect() # 遍历历史记录,恢复已知的删除 for frame_id, data in history: if frame_id <= current_frame: try: indices = [int(x) for x in data.split(",") if x] for idx in indices: rebuilt_bs.Select(idx) except: pass # C. 检测当前帧的新增删除 newly_deleted_indices = [] for i in range(count): if rebuilt_bs.IsSelected(i): continue # 已经删了,不用再算 # 计算位置 clone_global_mg = cloner_mg * marr[i] clone_pos = clone_global_mg.off if IsPointInObjectByGetRad(clone_pos, target_obj): rebuilt_bs.Select(i) newly_deleted_indices.append(i) # D. 如果有新增,写入历史 if newly_deleted_indices: s_indices = ",".join(str(x) for x in newly_deleted_indices) history.SetString(current_frame, s_indices) SaveHistory(cloner_obj, history) print(f"✅ Frame {current_frame}: 新增割草 {len(newly_deleted_indices)} 个") else: print(f"Frame {current_frame}: 无新增") sel_tag = None for tag in cloner_obj.GetTags(): if tag.CheckType(c4d.Tmgselection) and tag.GetName() == TAG_NAME: sel_tag = tag break #doc.StartUndo() # 如果没有则创建 if sel_tag is None: sel_tag = c4d.BaseTag(c4d.Tmgselection) sel_tag.SetName(TAG_NAME) cloner_obj.InsertTag(sel_tag) #doc.AddUndo(c4d.UNDOTYPE_NEW, sel_tag) # 更新选集数据 (使用重建后的完整选集) mograph.GeSetMoDataSelection(sel_tag, rebuilt_bs) # 强制刷新:通知标签和克隆器数据已改变 sel_tag.SetDirty(c4d.DIRTYFLAGS_DATA) sel_tag.Message(c4d.MSG_UPDATE) cloner_obj.Message(c4d.MSG_UPDATE) doc.AddUndo(c4d.UNDOTYPE_CHANGE, sel_tag) # 5. 创建或配置简易效果器 (Plain Effector) 来隐藏这些克隆 EFFECTOR_NAME = "Mowing_Effector" effector = doc.SearchObject(EFFECTOR_NAME) if effector is None: effector = c4d.BaseObject(1018561) # Omgplain (Plain Effector ID) effector.SetName(EFFECTOR_NAME) # 将效果器放在克隆器旁边或归类 effector.InsertAfter(cloner_obj) #(c4d.UNDOTYPE_NEW, effector) # 配置效果器参数 #doc.AddUndo(c4d.UNDOTYPE_CHANGE, effector) # 关键:设置选集字符串,让效果器只对选集标签生效 # c4d.ID_MG_BASEEFFECTOR_SELECTION 是效果器选项卡里的"选集"字段ID effector[c4d.ID_MG_BASEEFFECTOR_SELECTION] = TAG_NAME # 开启"缩放"参数 (Scale),并将缩放值设为 -1 (实现缩放到0) effector[c4d.ID_MG_BASEEFFECTOR_SCALE_ACTIVE] = True effector[c4d.ID_MG_BASEEFFECTOR_UNIFORMSCALE] = True effector[c4d.ID_MG_BASEEFFECTOR_USCALE] = -1.0 # 关闭"可见"参数 (既然不起作用) effector[1106] = False # 确保不修改位置/旋转 effector[c4d.ID_MG_BASEEFFECTOR_POSITION_ACTIVE] = False # 6. 将效果器添加到克隆器的效果器列表中 effector_list = cloner_obj[c4d.ID_MG_MOTIONGENERATOR_EFFECTORLIST] if effector_list is None: effector_list = c4d.InExcludeData() # 检查列表中是否已存在该效果器 found = False for i in range(effector_list.GetObjectCount()): if effector_list.ObjectFromIndex(doc, i) == effector: found = True break if not found: effector_list.InsertObject(effector, 1) cloner_obj[c4d.ID_MG_MOTIONGENERATOR_EFFECTORLIST] = effector_list #doc.AddUndo(c4d.UNDOTYPE_CHANGE, cloner_obj) #doc.EndUndo() # 刷新场景 #c4d.EventAdd() print(f"已更新选集标签 '{TAG_NAME}' 并应用隐藏效果器 '{EFFECTOR_NAME}'") if __name__ == "__main__": main()

### 自动回滚功能的实现原理

这个“自动回滚”功能的实现其实借鉴了**“事件溯源 (Event Sourcing)”**的设计思想。

简单来说,脚本不再只记录“现在的草地是什么样”,而是记录了“在每一帧发生了什么”。

以下是它的核心原理拆解:

### 1. 记忆存储 (The Memory)
我们利用 C4D 对象的 BaseContainer (一种类似字典的数据结构)在克隆器对象上开辟了一块“私有领地”(ID 1000),用来存储历史记录。

- 数据结构 : { 帧号: "被割掉的索引列表" }
- 例子 :
- 第 10 帧:割掉了索引 1, 2, 3
- 第 11 帧:割掉了索引 4, 5
- 第 12 帧:割掉了索引 6
代码参考:[GetHistory](割草.py#L18-L25)

### 2. 时光倒流 (The Rollback)
这是回滚的关键。每次脚本运行时(每一帧),它都会先检查这份“历史档案”。如果发现档案里有 比当前帧更晚 的记录,就说明“我们从未来回到了过去”。

为了保证因果律,必须把“未来”发生的事情抹除。

- 逻辑 :遍历所有历史记录,如果 记录帧号 > 当前帧号 ,则删除该条记录。
- 效果 :当你从第 100 帧拖回到第 50 帧时,第 51~100 帧的所有割草记录被瞬间销毁,就像从未发生过一样。
代码参考:[清理未来帧的历史](割草.py#L100-L106)

### 3. 现场重建 (The Reconstruction)
清理完“未来”后,脚本会把 剩下的、合法的历史记录 (即当前帧及以前的所有记录)全部读取出来,重新拼凑出一份完整的“死亡名单”(选集)。

- 逻辑 :创建一个空的选集 rebuilt_bs ,把历史记录里所有剩下的索引都加进去。
- 效果 :这样无论你是在播放、倒放还是跳帧,脚本计算出的选集永远是“截止到当前时刻”的正确状态。
代码参考:[重建当前帧之前的累积选集](割草.py#L108-L119)

### 4. 增量更新 (The New Events)
最后,脚本才会计算 当前这一帧 有没有新的草被割掉。如果有,就把它作为一条新记录写入历史。

- 逻辑 :如果检测到碰撞且该克隆体还没在“死亡名单”里,就把它加进去,并保存到当前帧的历史记录中。
代码参考:[检测当前帧的新增删除](割草.py#L121-L140)

### 总结
传统的做法是“走到哪改到哪”(直接修改结果),而这个做法是“记账本”。

- 回滚时 :把账本后面撕掉,重新算前面的总和。
- 前进时 :在账本后面接着记新的一笔。
这就是为什么它能完美支持时间轴的任意拖动和回滚!

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询