scene_processor.py 22 KB

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