using System.Collections.Generic; using System.IO; using UnityEngine; #if UNITY_EDITOR using UnityEditor; #endif namespace URDF { /// /// Main URDF importer that creates Unity GameObjects from URDF data /// public static class URDFImporter { /// /// Import a URDF file and create a GameObject hierarchy in the scene /// /// Path to the URDF file /// Parent transform to attach the robot to (null = scene root) /// If true, ignores URDF scale values (useful if models are already scaled to meters) /// Scale factor to apply to joint positions (default 1.0). Use 1000 if models were scaled from mm to m. /// Root GameObject of the imported robot public static GameObject ImportURDF(string urdfFilePath, Transform parentTransform = null, bool ignoreURDFScale = false, float jointPositionScale = 1.0f) { // Parse URDF URDFRobot robot = URDFParser.ParseURDF(urdfFilePath); if (robot == null) { Debug.LogError("Failed to parse URDF file"); return null; } // Create root GameObject GameObject rootObject = new GameObject(robot.name); if (parentTransform != null) { rootObject.transform.SetParent(parentTransform); } // Create a dictionary to store link GameObjects Dictionary linkGameObjects = new Dictionary(); // First pass: Create all link GameObjects foreach (var linkPair in robot.links) { string linkName = linkPair.Key; URDFLink link = linkPair.Value; GameObject linkObject = new GameObject(linkName); linkGameObjects[linkName] = linkObject; // Set as child of root for now (will be reparented based on joints) linkObject.transform.SetParent(rootObject.transform); } // Second pass: Set up joint hierarchy and apply transforms foreach (var jointPair in robot.joints) { URDFJoint joint = jointPair.Value; if (!linkGameObjects.ContainsKey(joint.parentLink) || !linkGameObjects.ContainsKey(joint.childLink)) { Debug.LogWarning($"Joint {joint.name} references missing links: parent={joint.parentLink}, child={joint.childLink}"); continue; } GameObject parentObject = linkGameObjects[joint.parentLink]; GameObject childObject = linkGameObjects[joint.childLink]; // Parent child to parent childObject.transform.SetParent(parentObject.transform); // Apply joint origin as local transform Vector3 unityPosition = URDFCoordinateConverter.ConvertPosition(joint.origin.xyz); Quaternion unityRotation = URDFCoordinateConverter.ConvertRotation(joint.origin.rpy); // Scale joint positions if needed (e.g., if models were scaled from mm to m) unityPosition *= jointPositionScale; childObject.transform.localPosition = unityPosition; childObject.transform.localRotation = unityRotation; } // Third pass: Add visual meshes to links foreach (var linkPair in robot.links) { string linkName = linkPair.Key; URDFLink link = linkPair.Value; GameObject linkObject = linkGameObjects[linkName]; // Add visual meshes foreach (URDFVisual visual in link.visuals) { if (visual.geometry != null && visual.geometry.type == URDFGeometry.GeometryType.Mesh) { string meshPath = URDFMeshResolver.ResolveMeshPath(visual.geometry.meshFilename, urdfFilePath); if (!string.IsNullOrEmpty(meshPath)) { #if UNITY_EDITOR Debug.Log($"Loading mesh for link {linkName}: {meshPath}"); // Ensure path uses forward slashes for Unity meshPath = meshPath.Replace('\\', '/'); // Load the mesh/model - OBJ files are imported as GameObjects in Unity GameObject meshPrefab = AssetDatabase.LoadAssetAtPath(meshPath); // Fallback: Try to find by filename if direct path fails if (meshPrefab == null) { string fileName = Path.GetFileNameWithoutExtension(meshPath); string[] guids = AssetDatabase.FindAssets(fileName + " t:GameObject"); foreach (string guid in guids) { string foundPath = AssetDatabase.GUIDToAssetPath(guid); if (foundPath.Contains(fileName) && (foundPath.EndsWith(".obj") || foundPath.EndsWith(".fbx") || foundPath.EndsWith(".glb"))) { Debug.Log($"Found mesh by search: {foundPath}"); meshPrefab = AssetDatabase.LoadAssetAtPath(foundPath); if (meshPrefab != null) break; } } } if (meshPrefab != null) { Debug.Log($"Successfully loaded mesh prefab: {meshPath}"); // Instantiate the mesh as a child GameObject meshInstance; // Try to instantiate as prefab first (preserves prefab connection) if (PrefabUtility.IsPartOfPrefabAsset(meshPrefab)) { meshInstance = PrefabUtility.InstantiatePrefab(meshPrefab) as GameObject; } else { meshInstance = Object.Instantiate(meshPrefab); } if (meshInstance != null) { meshInstance.name = Path.GetFileNameWithoutExtension(meshPath); meshInstance.transform.SetParent(linkObject.transform); // Apply visual origin transform Vector3 visualPos = URDFCoordinateConverter.ConvertPosition(visual.origin.xyz); Quaternion visualRot = URDFCoordinateConverter.ConvertRotation(visual.origin.rpy); meshInstance.transform.localPosition = visualPos; meshInstance.transform.localRotation = visualRot; // Apply mesh scale from URDF // Note: URDF scale="0.001" typically means models are in millimeters // If models are already scaled to meters, ignoreURDFScale should be true if (!ignoreURDFScale && visual.geometry.scale != Vector3.one) { meshInstance.transform.localScale = visual.geometry.scale; } else if (ignoreURDFScale) { // Models are already in meters, use unity scale meshInstance.transform.localScale = Vector3.one; } // Apply materials to all renderers MeshRenderer[] renderers = meshInstance.GetComponentsInChildren(true); if (renderers != null && renderers.Length > 0) { Debug.Log($"Found {renderers.Length} renderer(s) on mesh {meshInstance.name}"); foreach (MeshRenderer renderer in renderers) { if (renderer == null) continue; // Check if the renderer already has materials (from .mtl file import) bool hasValidMaterials = renderer.sharedMaterials != null && renderer.sharedMaterials.Length > 0 && renderer.sharedMaterials[0] != null; if (hasValidMaterials) { Debug.Log($"Renderer on {renderer.gameObject.name} already has materials from .mtl file, preserving them"); // Only apply URDF color override if specified and different from default if (visual.color != Color.white && visual.color.a > 0) { // Create a copy of the existing material and apply color tint Material[] materials = new Material[renderer.sharedMaterials.Length]; for (int i = 0; i < renderer.sharedMaterials.Length; i++) { if (renderer.sharedMaterials[i] != null) { materials[i] = new Material(renderer.sharedMaterials[i]); materials[i].color = visual.color; } else { materials[i] = renderer.sharedMaterials[i]; } } renderer.materials = materials; Debug.Log($"Applied URDF color override to existing materials"); } // Otherwise, keep the imported materials as-is } else { // No materials from .mtl file, create new ones Debug.Log($"No materials found on {renderer.gameObject.name}, creating new material"); Material mat; // Try URP shaders first, then fallback to built-in shaders Shader shader = Shader.Find("Universal Render Pipeline/Lit"); if (shader == null) { shader = Shader.Find("Universal Render Pipeline/Simple Lit"); } if (shader == null) { shader = Shader.Find("Standard"); } if (shader == null) { shader = Shader.Find("Diffuse"); } if (shader == null) { Debug.LogError("Could not find any suitable shader for material!"); continue; } mat = new Material(shader); // Use color from URDF if specified, otherwise use default grey if (visual.color != Color.white && visual.color.a > 0) { mat.color = visual.color; } else { // Default grey material mat.color = new Color(0.7f, 0.7f, 0.7f, 1f); // Light grey } if (mat != null) { renderer.material = mat; Debug.Log($"Applied new material to renderer on {renderer.gameObject.name}"); } else { Debug.LogWarning($"Failed to create material for renderer on {renderer.gameObject.name}"); } } } } else { Debug.LogWarning($"No renderers found on mesh instance: {meshInstance.name}"); } Debug.Log($"Successfully added mesh to link {linkName}: {meshInstance.name}"); } else { Debug.LogError($"Failed to instantiate mesh prefab: {meshPath}"); } } else { Debug.LogWarning($"Could not load mesh prefab from: {meshPath}. File may not be imported in Unity yet."); // Try to refresh the asset database and reload AssetDatabase.Refresh(); meshPrefab = AssetDatabase.LoadAssetAtPath(meshPath); if (meshPrefab != null) { Debug.Log($"Mesh loaded after refresh: {meshPath}"); GameObject meshInstance = Object.Instantiate(meshPrefab); meshInstance.name = Path.GetFileNameWithoutExtension(meshPath); meshInstance.transform.SetParent(linkObject.transform); Vector3 visualPos = URDFCoordinateConverter.ConvertPosition(visual.origin.xyz); Quaternion visualRot = URDFCoordinateConverter.ConvertRotation(visual.origin.rpy); meshInstance.transform.localPosition = visualPos; meshInstance.transform.localRotation = visualRot; if (visual.geometry.scale != Vector3.one) { meshInstance.transform.localScale = visual.geometry.scale; } } } #endif } else { Debug.LogWarning($"Could not resolve mesh path for link {linkName}: {visual.geometry.meshFilename}"); } } } } Debug.Log($"Successfully imported URDF robot: {robot.name}"); return rootObject; } } }