trs_processor.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. import sys
  2. from pathlib import Path
  3. # Add the parent directory to the Python path to find utils
  4. utils_path = Path(__file__).parent.parent / 'utils'
  5. sys.path.append(str(utils_path))
  6. from yaml_utils import load_unity_yaml, convert_to_plain_python_types
  7. from hierarchy_utils import HierarchyParser
  8. class TRSSceneProcessor:
  9. """
  10. A specialized processor that uses the robust multi-pass system from the mid-level
  11. extractor to build a correct scene hierarchy, then extracts only TRS data.
  12. """
  13. def __init__(self, guid_map):
  14. self.guid_map = guid_map
  15. self.object_map = {}
  16. self.prefab_cache = {}
  17. self.nodes = {}
  18. self.prefab_nodes = {}
  19. self.transform_to_gameobject = {}
  20. self.gameobject_to_transform = {}
  21. self.transform_children = {}
  22. self.stripped_gameobjects = {}
  23. self.stripped_transforms = {}
  24. self.prefab_instances = {}
  25. self.processed_relationships = set()
  26. def load_documents(self, file_path):
  27. """Loads and parses the yaml documents from a scene/prefab file."""
  28. documents = load_unity_yaml(file_path)
  29. if not documents:
  30. self.object_map = {}
  31. return False
  32. raw_object_map = {int(doc.anchor.value): doc for doc in documents if hasattr(doc, 'anchor') and doc.anchor is not None}
  33. self.object_map = {file_id: convert_to_plain_python_types(obj) for file_id, obj in raw_object_map.items()}
  34. return True
  35. def _load_prefab_data(self, prefab_guid):
  36. """Load and cache prefab data from its GUID."""
  37. if prefab_guid in self.prefab_cache:
  38. return self.prefab_cache[prefab_guid]
  39. if not prefab_guid or prefab_guid not in self.guid_map:
  40. self.prefab_cache[prefab_guid] = None
  41. return None
  42. prefab_path = self.guid_map[prefab_guid]
  43. try:
  44. documents = load_unity_yaml(prefab_path)
  45. if not documents:
  46. self.prefab_cache[prefab_guid] = None
  47. return None
  48. raw_object_map = {int(doc.anchor.value): doc for doc in documents if hasattr(doc, 'anchor') and doc.anchor is not None}
  49. prefab_data = {file_id: convert_to_plain_python_types(obj) for file_id, obj in raw_object_map.items()}
  50. self.prefab_cache[prefab_guid] = prefab_data
  51. return prefab_data
  52. except Exception:
  53. self.prefab_cache[prefab_guid] = None
  54. return None
  55. def build_relationship_maps(self):
  56. """Pass 1: Build all necessary maps for hierarchy construction."""
  57. for file_id, obj_data in self.object_map.items():
  58. if 'GameObject' in obj_data:
  59. go_info = obj_data['GameObject']
  60. self.nodes[file_id] = {'fileID': str(file_id), 'm_Name': go_info.get('m_Name', 'Unknown'), 'children': []}
  61. if any('stripped' in str(k) for k in obj_data.keys()):
  62. self.stripped_gameobjects[file_id] = go_info
  63. prefab_instance_id = go_info.get('m_PrefabInstance', {}).get('fileID')
  64. if prefab_instance_id and not file_id in self.stripped_gameobjects:
  65. self.nodes[file_id]['prefab_instance_id'] = prefab_instance_id
  66. elif 'PrefabInstance' in obj_data:
  67. prefab_info = obj_data['PrefabInstance']
  68. self.prefab_instances[file_id] = prefab_info
  69. # Get name from modifications or fallback to prefab asset filename
  70. mod_name = next((mod['value'] for mod in prefab_info.get('m_Modification', {}).get('m_Modifications', []) if mod.get('propertyPath') == 'm_Name'), None)
  71. if not mod_name:
  72. guid = prefab_info.get('m_SourcePrefab', {}).get('guid')
  73. if guid and guid in self.guid_map:
  74. prefab_path = Path(self.guid_map[guid])
  75. mod_name = prefab_path.stem
  76. else:
  77. mod_name = 'Unknown'
  78. self.prefab_nodes[file_id] = {'fileID': str(file_id), 'm_Name': mod_name, 'children': []}
  79. elif 'Transform' in obj_data:
  80. transform_info = obj_data['Transform']
  81. gameobject_id = transform_info.get('m_GameObject', {}).get('fileID')
  82. if gameobject_id:
  83. self.transform_to_gameobject[file_id] = gameobject_id
  84. self.gameobject_to_transform[gameobject_id] = file_id
  85. parent_id = transform_info.get('m_Father', {}).get('fileID')
  86. if parent_id and parent_id != 0:
  87. if parent_id not in self.transform_children:
  88. self.transform_children[parent_id] = []
  89. self.transform_children[parent_id].append(file_id)
  90. if transform_info.get('m_PrefabInstance', {}).get('fileID'):
  91. self.stripped_transforms[file_id] = transform_info
  92. def build_hierarchy(self):
  93. """Pass 2: Connect nodes into a hierarchy."""
  94. # Link standard GameObjects
  95. for go_id, node in self.nodes.items():
  96. transform_id = self.gameobject_to_transform.get(go_id)
  97. if not transform_id: continue
  98. child_transform_ids = self.transform_children.get(transform_id, [])
  99. for child_transform_id in child_transform_ids:
  100. child_go_id = self.transform_to_gameobject.get(child_transform_id)
  101. if child_go_id and child_go_id in self.nodes:
  102. node['children'].append(self.nodes[child_go_id])
  103. # Link prefab instances to their parents
  104. for prefab_id, prefab_info in self.prefab_instances.items():
  105. parent_transform_id = prefab_info.get('m_Modification', {}).get('m_TransformParent', {}).get('fileID')
  106. if not parent_transform_id: continue
  107. parent_go_id = self.transform_to_gameobject.get(parent_transform_id)
  108. if parent_go_id and parent_go_id in self.nodes:
  109. self.nodes[parent_go_id]['children'].append(self.prefab_nodes[prefab_id])
  110. elif parent_transform_id in self.stripped_transforms:
  111. parent_prefab_instance_id = self.stripped_transforms[parent_transform_id].get('m_PrefabInstance', {}).get('fileID')
  112. if parent_prefab_instance_id in self.prefab_nodes:
  113. self.prefab_nodes[parent_prefab_instance_id]['children'].append(self.prefab_nodes[prefab_id])
  114. # Link scene objects that are children of prefabs
  115. for go_id, node in self.nodes.items():
  116. transform_id = self.gameobject_to_transform.get(go_id)
  117. if not transform_id: continue
  118. transform_info = self.object_map.get(transform_id, {}).get('Transform', {})
  119. parent_transform_id = transform_info.get('m_Father', {}).get('fileID')
  120. if parent_transform_id in self.stripped_transforms:
  121. parent_prefab_instance_id = self.stripped_transforms[parent_transform_id].get('m_PrefabInstance', {}).get('fileID')
  122. if parent_prefab_instance_id in self.prefab_nodes:
  123. self.prefab_nodes[parent_prefab_instance_id]['children'].append(node)
  124. def merge_prefab_info(self):
  125. """Pass 3: Merge root GameObject data into PrefabInstance nodes."""
  126. nodes_to_delete = []
  127. for go_id, go_node in self.nodes.items():
  128. prefab_instance_id = go_node.get('prefab_instance_id')
  129. if prefab_instance_id and prefab_instance_id in self.prefab_nodes:
  130. prefab_node = self.prefab_nodes[prefab_instance_id]
  131. prefab_info = self.prefab_instances[prefab_instance_id]
  132. # Name is now set in build_relationship_maps. We just merge the rest.
  133. prefab_node['source_prefab_guid'] = prefab_info.get('m_SourcePrefab', {}).get('guid')
  134. prefab_node['children'].extend(go_node.get('children', []))
  135. nodes_to_delete.append(go_id)
  136. for go_id in nodes_to_delete:
  137. del self.nodes[go_id]
  138. def extract_trs_data(self, nodes):
  139. """Pass 4: Recursively find and attach TRS data to each node."""
  140. for node in nodes:
  141. file_id = int(node['fileID'])
  142. # Default TRS values
  143. trs = {"position": {"x": 0, "y": 0, "z": 0}, "rotation": {"x": 0, "y": 0, "z": 0, "w": 1}, "scale": {"x": 1, "y": 1, "z": 1}}
  144. if 'source_prefab_guid' in node: # It's a prefab instance
  145. prefab_instance_info = self.prefab_instances.get(file_id, {})
  146. modifications = prefab_instance_info.get('m_Modification', {}).get('m_Modifications', [])
  147. # Find the root transform of the prefab to get base TRS
  148. prefab_data = self._load_prefab_data(node['source_prefab_guid'])
  149. if prefab_data:
  150. for obj in prefab_data.values():
  151. if 'Transform' in obj and obj['Transform'].get('m_Father', {}).get('fileID') == 0:
  152. base_transform = obj['Transform']
  153. trs['position'] = base_transform.get('m_LocalPosition', trs['position'])
  154. trs['rotation'] = base_transform.get('m_LocalRotation', trs['rotation'])
  155. trs['scale'] = base_transform.get('m_LocalScale', trs['scale'])
  156. break
  157. # Apply modifications
  158. for mod in modifications:
  159. prop = mod.get('propertyPath', '')
  160. # Skip any property that is not part of a Transform
  161. if not ('m_LocalPosition' in prop or 'm_LocalRotation' in prop or 'm_LocalScale' in prop):
  162. continue
  163. try:
  164. val = float(mod.get('value', 0))
  165. except (ValueError, TypeError):
  166. # If the value is not a valid float, skip this modification
  167. print(f"Warning: Could not convert value '{mod.get('value')}' for property '{prop}' to float. Skipping.", file=sys.stderr)
  168. continue
  169. if prop == 'm_LocalPosition.x': trs['position']['x'] = val
  170. elif prop == 'm_LocalPosition.y': trs['position']['y'] = val
  171. elif prop == 'm_LocalPosition.z': trs['position']['z'] = val
  172. elif prop == 'm_LocalRotation.x': trs['rotation']['x'] = val
  173. elif prop == 'm_LocalRotation.y': trs['rotation']['y'] = val
  174. elif prop == 'm_LocalRotation.z': trs['rotation']['z'] = val
  175. elif prop == 'm_LocalRotation.w': trs['rotation']['w'] = val
  176. elif prop == 'm_LocalScale.x': trs['scale']['x'] = val
  177. elif prop == 'm_LocalScale.y': trs['scale']['y'] = val
  178. elif prop == 'm_LocalScale.z': trs['scale']['z'] = val
  179. else: # It's a regular GameObject
  180. transform_id = self.gameobject_to_transform.get(file_id)
  181. if transform_id and transform_id in self.object_map:
  182. transform_info = self.object_map[transform_id].get('Transform', {})
  183. trs['position'] = transform_info.get('m_LocalPosition', trs['position'])
  184. trs['rotation'] = transform_info.get('m_LocalRotation', trs['rotation'])
  185. trs['scale'] = transform_info.get('m_LocalScale', trs['scale'])
  186. node['trs'] = trs
  187. if 'children' in node and node['children']:
  188. self.extract_trs_data(node['children'])
  189. def cleanup_hierarchy(self, nodes):
  190. """Pass 5: Remove intermediate data, leaving only the desired fields."""
  191. final_nodes = []
  192. for node in nodes:
  193. clean_node = {
  194. 'name': node.get('m_Name', 'Unknown'),
  195. 'trs': node.get('trs', {}),
  196. 'children': self.cleanup_hierarchy(node.get('children', []))
  197. }
  198. if 'source_prefab_guid' in node:
  199. clean_node['source_prefab_guid'] = node['source_prefab_guid']
  200. final_nodes.append(clean_node)
  201. return final_nodes
  202. def process(self):
  203. """Main processing pipeline."""
  204. if not self.object_map:
  205. return []
  206. # --- Run all passes ---
  207. self.build_relationship_maps()
  208. self.build_hierarchy()
  209. self.merge_prefab_info()
  210. # Get root nodes for the final passes
  211. parser = HierarchyParser(self.object_map)
  212. root_object_ids = parser.get_root_object_ids()
  213. all_nodes = {**self.nodes, **self.prefab_nodes}
  214. root_nodes = [all_nodes[file_id] for file_id, _ in root_object_ids if file_id in all_nodes]
  215. self.extract_trs_data(root_nodes)
  216. final_hierarchy = self.cleanup_hierarchy(root_nodes)
  217. return final_hierarchy