scene_processor.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  1. import argparse
  2. import sys
  3. import json
  4. from pathlib import Path
  5. # Add the parent directory to the Python path to find utils
  6. utils_path = Path(__file__).parent.parent / 'utils'
  7. sys.path.append(str(utils_path))
  8. from yaml_utils import load_unity_yaml, convert_to_plain_python_types
  9. from hierarchy_utils import HierarchyParser
  10. class UnitySceneProcessor:
  11. def __init__(self, guid_map):
  12. self.guid_map = guid_map
  13. self.object_map = {}
  14. self.nodes = {}
  15. self.prefab_nodes = {}
  16. self.transform_to_gameobject = {}
  17. self.gameobject_to_transform = {}
  18. self.transform_children = {}
  19. self.stripped_gameobjects = {}
  20. self.processed_relationships = set()
  21. def load_prefab_data(self, prefab_guid):
  22. """Load and parse prefab data from GUID"""
  23. if not prefab_guid or prefab_guid not in self.guid_map:
  24. return {}
  25. prefab_path = self.guid_map[prefab_guid]
  26. try:
  27. documents = load_unity_yaml(prefab_path)
  28. if not documents:
  29. return {}
  30. raw_object_map = {int(doc.anchor.value): doc for doc in documents if hasattr(doc, 'anchor') and doc.anchor is not None}
  31. return {file_id: convert_to_plain_python_types(obj) for file_id, obj in raw_object_map.items()}
  32. except Exception:
  33. return {}
  34. def calculate_deep_sibling_index(self, scene_transform_id, prefab_guid):
  35. """Calculate deep sibling index for scene objects that are children of prefab objects"""
  36. scene_transform = self.object_map.get(scene_transform_id, {}).get('Transform', {})
  37. scene_root_order = scene_transform.get('m_RootOrder', 0)
  38. # Get parent transform (should be stripped)
  39. parent_transform_id = scene_transform.get('m_Father', {}).get('fileID')
  40. if not parent_transform_id:
  41. return str(scene_root_order)
  42. parent_transform = self.object_map.get(parent_transform_id, {}).get('Transform', {})
  43. corresponding_source = parent_transform.get('m_CorrespondingSourceObject', {})
  44. prefab_transform_id = corresponding_source.get('fileID')
  45. if not prefab_transform_id:
  46. return str(scene_root_order)
  47. # Load prefab data and traverse hierarchy
  48. prefab_data = self.load_prefab_data(prefab_guid)
  49. if not prefab_data:
  50. return str(scene_root_order)
  51. # Build sibling index by traversing up the prefab hierarchy
  52. sibling_indices = []
  53. current_transform_id = prefab_transform_id
  54. while current_transform_id and current_transform_id in prefab_data:
  55. transform_data = prefab_data.get(current_transform_id, {}).get('Transform', {})
  56. if not transform_data:
  57. break
  58. parent_id = transform_data.get('m_Father', {}).get('fileID')
  59. # Only add the index if this is NOT the root transform of the prefab
  60. if parent_id and parent_id != 0:
  61. root_order = transform_data.get('m_RootOrder', 0)
  62. sibling_indices.insert(0, str(root_order))
  63. # Break the loop if there's no parent
  64. if not parent_id or parent_id == 0:
  65. break
  66. current_transform_id = parent_id
  67. # Add the scene object's own root order at the end
  68. sibling_indices.append(str(scene_root_order))
  69. return '-'.join(sibling_indices)
  70. def process_first_pass(self):
  71. """First pass: Build relationship maps and create basic nodes"""
  72. stripped_transforms = {}
  73. stripped_gameobjects = {}
  74. prefab_instances = {}
  75. for file_id, obj_data in self.object_map.items():
  76. if 'GameObject' in obj_data:
  77. go_info = obj_data['GameObject']
  78. # Always create a node for a GameObject.
  79. # If it's part of a prefab, we'll link it later.
  80. self.nodes[file_id] = {
  81. 'fileID': str(file_id),
  82. 'm_Name': go_info.get('m_Name', 'Unknown'),
  83. 'm_IsActive': go_info.get('m_IsActive', 1),
  84. 'm_TagString': go_info.get('m_TagString', 'Untagged'),
  85. 'm_Layer': go_info.get('m_Layer', 0),
  86. 'components': [],
  87. 'children': []
  88. }
  89. # If it's a stripped GameObject, track it for component linking
  90. is_stripped = any('stripped' in str(key) for key in obj_data.keys() if hasattr(key, '__str__'))
  91. if is_stripped:
  92. prefab_instance_id = go_info.get('m_PrefabInstance', {}).get('fileID')
  93. if prefab_instance_id:
  94. stripped_gameobjects[file_id] = {
  95. 'prefab_instance_id': prefab_instance_id,
  96. 'm_CorrespondingSourceObject': go_info.get('m_CorrespondingSourceObject', {})
  97. }
  98. # If it's the root GameObject of a prefab instance, add a link for merging
  99. prefab_instance_id = go_info.get('m_PrefabInstance', {}).get('fileID')
  100. if prefab_instance_id and not is_stripped:
  101. self.nodes[file_id]['prefab_instance_id'] = prefab_instance_id
  102. elif 'PrefabInstance' in obj_data:
  103. prefab_info = obj_data['PrefabInstance']
  104. source_prefab = prefab_info.get('m_SourcePrefab', {})
  105. modifications = prefab_info.get('m_Modification', {})
  106. # Extract m_IsActive, m_Name, and m_RootOrder from modifications
  107. m_is_active = 1 # default
  108. m_name = None
  109. m_root_order = 999999 # default
  110. for mod in modifications.get('m_Modifications', []):
  111. if mod.get('propertyPath') == 'm_IsActive':
  112. m_is_active = mod.get('value', 1)
  113. elif mod.get('propertyPath') == 'm_Name':
  114. m_name = mod.get('value')
  115. elif mod.get('propertyPath') == 'm_RootOrder':
  116. m_root_order = mod.get('value', 999999)
  117. prefab_instances[file_id] = {
  118. 'source_prefab_guid': source_prefab.get('guid'),
  119. 'm_TransformParent': modifications.get('m_TransformParent', {}).get('fileID'),
  120. 'm_IsActive': m_is_active,
  121. 'm_Name': m_name,
  122. 'm_RootOrder': m_root_order
  123. }
  124. elif 'Transform' in obj_data:
  125. transform_info = obj_data['Transform']
  126. # Check if this is a stripped transform (has m_PrefabInstance)
  127. prefab_instance_id = transform_info.get('m_PrefabInstance', {}).get('fileID')
  128. if prefab_instance_id:
  129. stripped_transforms[file_id] = {
  130. 'prefab_instance_id': prefab_instance_id,
  131. 'm_CorrespondingSourceObject': transform_info.get('m_CorrespondingSourceObject', {})
  132. }
  133. # Build transform mappings
  134. gameobject_id = transform_info.get('m_GameObject', {}).get('fileID')
  135. if gameobject_id:
  136. self.transform_to_gameobject[file_id] = gameobject_id
  137. self.gameobject_to_transform[gameobject_id] = file_id
  138. # Build parent-child relationships
  139. parent_id = transform_info.get('m_Father', {}).get('fileID')
  140. if parent_id and parent_id != 0:
  141. if parent_id not in self.transform_children:
  142. self.transform_children[parent_id] = []
  143. self.transform_children[parent_id].append(file_id)
  144. # Create prefab nodes
  145. for prefab_id, prefab_info in prefab_instances.items():
  146. self.prefab_nodes[prefab_id] = {
  147. 'fileID': str(prefab_id),
  148. 'source_prefab_guid': prefab_info['source_prefab_guid'],
  149. 'm_IsActive': prefab_info['m_IsActive'],
  150. 'm_RootOrder': prefab_info.get('m_RootOrder', 999999),
  151. 'components': [], # Will be populated with override components
  152. 'children': []
  153. }
  154. if prefab_info.get('m_Name'):
  155. self.prefab_nodes[prefab_id]['m_Name'] = prefab_info['m_Name']
  156. # Store for second pass
  157. self.stripped_transforms = stripped_transforms
  158. self.stripped_gameobjects = stripped_gameobjects
  159. self.prefab_instances = prefab_instances
  160. def process_second_pass(self):
  161. """Second pass: Build hierarchy relationships"""
  162. # 1. Handle standard GameObject parent-child relationships
  163. for go_id, node in self.nodes.items():
  164. transform_id = self.gameobject_to_transform.get(go_id)
  165. if not transform_id:
  166. continue
  167. # Get child transforms
  168. child_transform_ids = self.transform_children.get(transform_id, [])
  169. for child_transform_id in child_transform_ids:
  170. # Check if child transform is stripped (part of prefab)
  171. if child_transform_id in self.stripped_transforms:
  172. stripped_info = self.stripped_transforms[child_transform_id]
  173. prefab_instance_id = stripped_info['prefab_instance_id']
  174. if prefab_instance_id in self.prefab_nodes:
  175. relationship_key = f"{go_id}->{prefab_instance_id}"
  176. if relationship_key not in self.processed_relationships:
  177. node['children'].append(self.prefab_nodes[prefab_instance_id])
  178. self.processed_relationships.add(relationship_key)
  179. else:
  180. # Regular GameObject child
  181. child_go_id = self.transform_to_gameobject.get(child_transform_id)
  182. if child_go_id and child_go_id in self.nodes:
  183. relationship_key = f"{go_id}->{child_go_id}"
  184. if relationship_key not in self.processed_relationships:
  185. node['children'].append(self.nodes[child_go_id])
  186. self.processed_relationships.add(relationship_key)
  187. # 2. Handle prefab-to-parent relationships
  188. for prefab_id, prefab_info in self.prefab_instances.items():
  189. parent_transform_id = prefab_info['m_TransformParent']
  190. if not parent_transform_id or parent_transform_id == 0:
  191. continue # Root prefab, will be handled in get_root_objects
  192. # Find parent GameObject or parent prefab
  193. parent_go_id = self.transform_to_gameobject.get(parent_transform_id)
  194. if parent_go_id and parent_go_id in self.nodes:
  195. # Prefab child of GameObject
  196. relationship_key = f"{parent_go_id}->{prefab_id}"
  197. if relationship_key not in self.processed_relationships:
  198. self.nodes[parent_go_id]['children'].append(self.prefab_nodes[prefab_id])
  199. self.processed_relationships.add(relationship_key)
  200. elif parent_transform_id in self.stripped_transforms:
  201. # Prefab child of another prefab
  202. stripped_info = self.stripped_transforms[parent_transform_id]
  203. parent_prefab_id = stripped_info['prefab_instance_id']
  204. if parent_prefab_id in self.prefab_nodes:
  205. relationship_key = f"{parent_prefab_id}->{prefab_id}"
  206. if relationship_key not in self.processed_relationships:
  207. self.prefab_nodes[parent_prefab_id]['children'].append(self.prefab_nodes[prefab_id])
  208. self.processed_relationships.add(relationship_key)
  209. # 3. Handle scene objects as children of prefabs (with deep sibling index)
  210. for go_id, node in self.nodes.items():
  211. transform_id = self.gameobject_to_transform.get(go_id)
  212. if not transform_id:
  213. continue
  214. # Find parent transform
  215. parent_transform_id = None
  216. for parent_id, children in self.transform_children.items():
  217. if transform_id in children:
  218. parent_transform_id = parent_id
  219. break
  220. if parent_transform_id and parent_transform_id in self.stripped_transforms:
  221. stripped_info = self.stripped_transforms[parent_transform_id]
  222. prefab_instance_id = stripped_info['prefab_instance_id']
  223. if prefab_instance_id in self.prefab_nodes:
  224. # Calculate deep sibling index
  225. prefab_guid = self.prefab_instances[prefab_instance_id]['source_prefab_guid']
  226. deep_sibling_index = self.calculate_deep_sibling_index(transform_id, prefab_guid)
  227. if deep_sibling_index:
  228. node['deep_sibling_index'] = deep_sibling_index
  229. # Add as child of prefab
  230. relationship_key = f"{prefab_instance_id}->{go_id}"
  231. if relationship_key not in self.processed_relationships:
  232. self.prefab_nodes[prefab_instance_id]['children'].append(node)
  233. self.processed_relationships.add(relationship_key)
  234. def process_third_pass(self):
  235. """Third pass: Extract components for both GameObjects and PrefabInstances"""
  236. for file_id, obj_data in self.object_map.items():
  237. # Skip GameObjects, Transforms, and PrefabInstances
  238. if any(key in obj_data for key in ['GameObject', 'Transform', 'PrefabInstance']):
  239. continue
  240. # Find the GameObject this component belongs to
  241. gameobject_id = None
  242. for key, data in obj_data.items():
  243. if isinstance(data, dict) and 'm_GameObject' in data:
  244. gameobject_id = data['m_GameObject'].get('fileID')
  245. break
  246. if not gameobject_id:
  247. continue
  248. component_name = next((key for key in obj_data if key not in ['m_ObjectHideFlags']), 'Unknown')
  249. # Handle MonoBehaviour with script GUID
  250. if component_name == 'MonoBehaviour':
  251. script_info = obj_data['MonoBehaviour'].get('m_Script', {})
  252. script_guid = script_info.get('guid')
  253. if script_guid:
  254. component_entry = script_guid
  255. else:
  256. component_entry = "MonoBehaviour"
  257. else:
  258. component_entry = component_name
  259. # Check if this component belongs to a regular GameObject
  260. if gameobject_id in self.nodes:
  261. if component_entry not in self.nodes[gameobject_id]['components']:
  262. self.nodes[gameobject_id]['components'].append(component_entry)
  263. # Check if this component belongs to a stripped GameObject (prefab override)
  264. elif gameobject_id in self.stripped_gameobjects:
  265. stripped_info = self.stripped_gameobjects[gameobject_id]
  266. prefab_instance_id = stripped_info['prefab_instance_id']
  267. if prefab_instance_id in self.prefab_nodes:
  268. if component_entry not in self.prefab_nodes[prefab_instance_id]['components']:
  269. self.prefab_nodes[prefab_instance_id]['components'].append(component_entry)
  270. # If neither, this component belongs to an unknown GameObject (potential orphan)
  271. else:
  272. print(f"Warning: Component {component_name} (ID: {file_id}) references unknown GameObject {gameobject_id}")
  273. def verification_pass(self):
  274. """
  275. Verifies and fixes parent-child relationships that might have been missed
  276. by using a direct child->parent lookup. This acts as a patch for the
  277. less reliable reverse-lookup used in process_second_pass.
  278. """
  279. for go_id, node in list(self.nodes.items()):
  280. transform_id = self.gameobject_to_transform.get(go_id)
  281. if not transform_id:
  282. continue
  283. transform_info = self.object_map.get(transform_id, {}).get('Transform', {})
  284. parent_transform_id = transform_info.get('m_Father', {}).get('fileID')
  285. if not parent_transform_id or parent_transform_id == 0:
  286. continue # This is a root object, no parent to verify.
  287. # Case 1: Parent is a PrefabInstance (via a stripped transform)
  288. if parent_transform_id in self.stripped_transforms:
  289. stripped_info = self.stripped_transforms[parent_transform_id]
  290. parent_prefab_id = stripped_info['prefab_instance_id']
  291. if parent_prefab_id in self.prefab_nodes:
  292. relationship_key = f"{parent_prefab_id}->{go_id}"
  293. if relationship_key not in self.processed_relationships:
  294. # Add deep sibling index if it's a child of a prefab
  295. prefab_guid = self.prefab_instances[parent_prefab_id]['source_prefab_guid']
  296. deep_sibling_index = self.calculate_deep_sibling_index(transform_id, prefab_guid)
  297. if deep_sibling_index:
  298. node['deep_sibling_index'] = deep_sibling_index
  299. self.prefab_nodes[parent_prefab_id]['children'].append(node)
  300. self.processed_relationships.add(relationship_key)
  301. # Case 2: Parent is a regular GameObject
  302. else:
  303. parent_go_id = self.transform_to_gameobject.get(parent_transform_id)
  304. if parent_go_id and parent_go_id in self.nodes:
  305. relationship_key = f"{parent_go_id}->{go_id}"
  306. if relationship_key not in self.processed_relationships:
  307. self.nodes[parent_go_id]['children'].append(node)
  308. self.processed_relationships.add(relationship_key)
  309. def merge_prefab_data_pass(self):
  310. """Fourth pass: Merge GameObject data into their corresponding PrefabInstance nodes"""
  311. nodes_to_delete = []
  312. for go_id, go_node in self.nodes.items():
  313. prefab_instance_id = go_node.get('prefab_instance_id')
  314. if not prefab_instance_id:
  315. continue
  316. if prefab_instance_id in self.prefab_nodes:
  317. # Merge the GameObject data into the PrefabInstance node
  318. # The prefab node's existing data takes precedence
  319. prefab_node = self.prefab_nodes[prefab_instance_id]
  320. # Create a copy of the gameobject node to avoid modifying during iteration
  321. go_node_copy = go_node.copy()
  322. # Remove keys that should not be merged or are already handled
  323. del go_node_copy['fileID']
  324. del go_node_copy['prefab_instance_id']
  325. # Update prefab_node with go_node_copy data
  326. # This will overwrite keys in prefab_node with values from go_node_copy
  327. # if they exist in both. We want the opposite.
  328. # Let's do it manually to ensure prefab data is kept
  329. for key, value in go_node_copy.items():
  330. if key not in prefab_node:
  331. prefab_node[key] = value
  332. # Specifically handle children and components to merge them
  333. prefab_node['children'].extend(go_node_copy.get('children', []))
  334. # Merge components, avoiding duplicates
  335. existing_components = set(str(c) for c in prefab_node.get('components', []))
  336. for comp in go_node_copy.get('components', []):
  337. if str(comp) not in existing_components:
  338. prefab_node['components'].append(comp)
  339. # Rename 'components' to 'addedComponents' for clarity
  340. if 'components' in prefab_node:
  341. prefab_node['addedComponents'] = prefab_node.pop('components')
  342. # Mark the original GameObject node for deletion
  343. nodes_to_delete.append(go_id)
  344. # Remove the now-redundant GameObject nodes
  345. for go_id in nodes_to_delete:
  346. del self.nodes[go_id]
  347. def cleanup_pass(self, nodes):
  348. """
  349. Recursively cleans up temporary or internal properties from the final node structure.
  350. """
  351. # List of properties to remove from the final output
  352. cleanup_keys = ['m_RootOrder']
  353. for node in nodes:
  354. for key in cleanup_keys:
  355. if key in node:
  356. del node[key]
  357. if 'children' in node and node['children']:
  358. self.cleanup_pass(node['children'])
  359. def load_documents(self, file_path):
  360. """Loads and parses the yaml documents from a scene/prefab file."""
  361. documents = load_unity_yaml(file_path)
  362. if not documents:
  363. self.object_map = {}
  364. return False
  365. raw_object_map = {int(doc.anchor.value): doc for doc in documents if hasattr(doc, 'anchor') and doc.anchor is not None}
  366. self.object_map = {file_id: convert_to_plain_python_types(obj) for file_id, obj in raw_object_map.items()}
  367. return True
  368. def get_hierarchy(self):
  369. """Builds and returns the final, cleaned-up hierarchy from the processed data."""
  370. # Use the centralized parser to get the final, sorted root objects
  371. parser = HierarchyParser(self.object_map)
  372. root_object_ids = parser.get_root_object_ids()
  373. # Build the final list of nodes from the identified roots
  374. root_nodes = []
  375. all_nodes = {**self.nodes, **self.prefab_nodes}
  376. for file_id, _ in root_object_ids:
  377. if file_id in all_nodes:
  378. root_nodes.append(all_nodes[file_id])
  379. # Run the final cleanup pass
  380. self.cleanup_pass(root_nodes)
  381. return root_nodes
  382. def process_file(self, file_path):
  383. """Main processing method"""
  384. if not self.load_documents(file_path):
  385. return []
  386. # Process in passes
  387. self.process_first_pass()
  388. self.process_second_pass()
  389. self.verification_pass()
  390. self.process_third_pass()
  391. self.merge_prefab_data_pass()
  392. return self.get_hierarchy()
  393. if __name__ == "__main__":
  394. # This script is intended to be used as a module.
  395. # For direct execution, see extract_mid_level.py.
  396. pass