├── .gitignore ├── README.md ├── UDatasmithImporter.cs └── UdsMeshImporter.cs /.gitignore: -------------------------------------------------------------------------------- 1 | *.meta 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UnityDatasmithImporter 2 | 3 | ### About 4 | This is a set of Unity ScriptedImporter scripts to import Unreal Datasmith bundles into Unity. It's designed to quickly set up VRChat worlds using the output of the Autodesk Revit Datasmith plugin, but could potentially be improved or serve as a starting point for other use cases. 5 | 6 | ### Quick Start (Revit) 7 | * Download and install the Revit Datasmith plugin (version 4.26 at the time of this writing) and its prerequisites package from https://www.unrealengine.com/en-US/datasmith/plugins 8 | * Open your Revit project 9 | * Open a 3D View that displays the geometry you want to export 10 | * Select the Datasmith tab and press "Export 3D View" 11 | * Collect the generated udatasmith file and Assets folder -- you'll move those into your Unity project in the next phase. 12 | 13 | ### Quick Start (Unity) 14 | * Create a new Unity project using the appropriate Unity version for the VRChat SDK (2018.4.20f1 at the time of this writing) 15 | * Import VRChat's World SDK3 16 | * Checkout this repository to your Assets folder (or otherwise copy these scripts into your project) 17 | * Copy a udatasmith file and its associated Data folder into your Assets 18 | * Drop the prefab generated from the udatasmith file into the Scene. 19 | * Import the VRCWorld prefab from the SDK and move it to set up the spawn point 20 | * Bake lightmaps (or turn off "Setup Lightmap Baking" on the udatasmith asset settings) 21 | * Build and test! 22 | 23 | If you get errors about missing assets on the first attempt, right-clicking the udatasmith asset and selecting 'reimport' will usually fix that. The udatasmith importer currently assumes that the udsmesh and texture files have already been processed, which is not guaranteed when adding all of the files at once. 24 | 25 | ### Supported datatypes 26 | * The world transform hierarchy 27 | * StaticMesh actors and material assignments 28 | * Common Material properties (Diffuse and Bump maps) 29 | * Lights -- early WIP, most Light properties are not yet translated. 30 | 31 | 32 | -------------------------------------------------------------------------------- /UDatasmithImporter.cs: -------------------------------------------------------------------------------- 1 | #if UNITY_EDITOR // Importer scripts only work in the Editor 2 | 3 | #define USE_VRC_SDK3 // Enable VRChat SDK3 integrations 4 | 5 | using UnityEngine; 6 | using UnityEditor; 7 | using UnityEditor.Experimental.AssetImporters; 8 | using System; 9 | using System.IO; 10 | using System.Xml; 11 | using System.Collections.Generic; 12 | using System.Text.RegularExpressions; 13 | 14 | 15 | [ScriptedImporter(1, "udatasmith")] 16 | public class UDatasmithImporter : ScriptedImporter { 17 | 18 | [Tooltip("Height offset for elements in the 'Ceiling' category; may be used to eliminate Z-fighting of coincident ceiling and floor surfaces. Should probably be negative (move ceiling down)")] 19 | public float m_CeilingHeightOffset = -0.001f; 20 | [Tooltip("Height offset for elements in the 'Floor' category; may be used to eliminate Z-fighting of coincident ceiling and floor surfaces. Should probably be positive (move floor up)")] 21 | public float m_FloorHeightOffset = 0.0f; 22 | [Tooltip("Import lights. Not all Light types and properties are supported.")] 23 | public bool m_ImportLights = true; 24 | [Tooltip("Set up translated lights for static lightmap baking")] 25 | public bool m_SetupLightmapBaking = true; 26 | [Tooltip("Set up VRChat networked physics props for translated actors based on Revit Layer attribute")] 27 | public bool m_SetupPhysicsProps = false; 28 | 29 | [Tooltip("Revit Layers to be mapped as static objects with Mesh colliders. (Regular expression, case-insensitive)")] 30 | public string m_StaticTangibleLayerRegex = @"Roof|Topography|Furniture|Stairs|Levels|Walls|Structural|Floors|Site|Pads|Ramps|Ceilings|Casework|Parking|Parts|Pipes|Pipe Fitting|Gutters|Fascia"; 31 | [Tooltip("Revit Layers to be mapped as static, intangible (no collision) objects. (Regular expression, case-insensitive)")] 32 | public string m_StaticIntangibleLayerRegex = @"Windows|Doors|Specialty Equipment|Plumbing|Electrical|Curtain|Railings|Top Rails|Lighting Fixtures|Mechanical|Equipment"; 33 | [Tooltip("Revit Layers to be mapped as physics props. (Regular expression, case-insensitive)")] 34 | public string m_PhysicsPropsLayerRegex = @"Generic Models"; 35 | [Tooltip("Revit Layers to import as disabled GameObjects. (Regular expression, case-insensitive)")] 36 | public string m_IgnoredLayerRegex = @"Entourage|Planting"; 37 | 38 | 39 | class UdsStaticMesh { 40 | UdsStaticMesh() { 41 | materialNames = new List(); 42 | materialRefs = new List(); 43 | } 44 | 45 | public String name; 46 | public String filePath; 47 | public List materialNames; 48 | public List materialRefs; 49 | public Mesh assetRef; 50 | 51 | public static UdsStaticMesh FromNode(AssetImportContext ctx, XmlNode node) { 52 | Debug.Assert(node.Name == "StaticMesh"); 53 | UdsStaticMesh m = new UdsStaticMesh(); 54 | 55 | m.name = node.Attributes["name"].Value; 56 | 57 | SortedDictionary materialIdtoName = new SortedDictionary(); 58 | foreach (XmlNode cn in node.ChildNodes) { 59 | if (cn.Name == "file") { 60 | m.filePath = cn.Attributes["path"].Value; 61 | } else if (cn.Name == "Material") { 62 | materialIdtoName.Add(Int32.Parse(cn.Attributes["id"].Value), cn.Attributes["name"].Value); 63 | } 64 | } 65 | 66 | foreach (var iter in materialIdtoName) { 67 | // We use sorted material IDs for submesh mapping, same as the udsmesh importer 68 | m.materialNames.Add(iter.Value); 69 | } 70 | 71 | //Debug.Log(String.Format("StaticMesh: \"{0}\" => file \"{1}\", {2} materials", m.name, m.filePath, m.materialNames.Count)); 72 | String fullMeshPath = Path.Combine(Path.GetDirectoryName(ctx.assetPath), m.filePath); 73 | m.assetRef = (Mesh)AssetDatabase.LoadAssetAtPath(fullMeshPath, typeof(Mesh)); 74 | if (!m.assetRef) { 75 | Debug.Log(String.Format("StaticMesh: AssetDatabase.LoadAssetAtPath(\"{0}\") failed to return a mesh object", fullMeshPath)); 76 | } 77 | 78 | 79 | return m; 80 | } 81 | }; 82 | 83 | class UdsTexture { 84 | public String name; 85 | public String filePath; 86 | public String fullyQualifiedPath; 87 | public Texture assetRef; 88 | public TextureImporter importer; 89 | public static UdsTexture FromNode(AssetImportContext ctx, XmlNode node) { 90 | UdsTexture tex = new UdsTexture(); 91 | /* 92 | * 94 | * 95 | * 96 | */ 97 | 98 | tex.name = node.Attributes["name"].Value; 99 | tex.filePath = node.Attributes["file"].Value; 100 | 101 | if (Regex.Match(tex.filePath, @"\.ies$", RegexOptions.IgnoreCase).Success) { 102 | ctx.LogImportWarning(String.Format("Texture Reference \"{0}\" to IES light profile \"{1}\" cannot be resolved: IES light profile import is not implemented.", tex.name, tex.filePath)); 103 | return null; 104 | } 105 | 106 | 107 | tex.fullyQualifiedPath = Path.Combine(Path.GetDirectoryName(ctx.assetPath), tex.filePath); 108 | 109 | var texAssetObj = AssetDatabase.LoadAssetAtPath(tex.fullyQualifiedPath, typeof(Texture)); 110 | if (texAssetObj != null) { 111 | tex.assetRef = (Texture)texAssetObj; 112 | var texImporterObj = AssetImporter.GetAtPath(tex.fullyQualifiedPath); // load import settings for possible later adjustment once we know what this will be used for 113 | if (texImporterObj != null) { 114 | tex.importer = (TextureImporter)texImporterObj; 115 | } 116 | } 117 | 118 | if (tex.assetRef == null || tex.importer == null) { 119 | ctx.LogImportError(String.Format("UdsTexture::FromNode: Asset does not exist at path \"{0}\"", tex.fullyQualifiedPath)); 120 | } 121 | 122 | return tex; 123 | } 124 | }; 125 | 126 | class UdsMaterial { 127 | 128 | public String name; 129 | public Material assetRef; 130 | 131 | public static Color ParseKVPColor(XmlNode node) { 132 | Debug.Assert(node.Attributes["type"].Value == "Color"); 133 | float r, g, b, a; 134 | 135 | Match m = Regex.Match(node.Attributes["val"].Value, @"[rR]\s*=\s*([0-9.+-]+)\s*,\s*[gG]\s*=\s*([0-9.+-]+)\s*,\s*[bB]\s*=\s*([0-9.+-]+)\s*,\s*[aA]\s*=\s*([0-9.+-]+)"); 136 | Debug.Assert(m.Success); 137 | r = Single.Parse(m.Groups[1].ToString()); 138 | g = Single.Parse(m.Groups[2].ToString()); 139 | b = Single.Parse(m.Groups[3].ToString()); 140 | a = Single.Parse(m.Groups[4].ToString()); 141 | 142 | return new Color(r, g, b, a); 143 | } 144 | 145 | public static void FillUVOffsetScaleParameters(XmlNode node, String attrBasename, out Vector2 uvOffset, out Vector2 uvScale) { 146 | 147 | uvOffset = new Vector2(0.0f, 0.0f); 148 | uvScale = new Vector2(1.0f, 1.0f); 149 | 150 | XmlNode pn; 151 | pn = node.SelectSingleNode("child::KeyValueProperty[@name='" + attrBasename + "_UVOffsetX']"); 152 | if (pn != null) uvOffset.x = Single.Parse(pn.Attributes["val"].Value); 153 | pn = node.SelectSingleNode("child::KeyValueProperty[@name='" + attrBasename + "_UVOffsetY']"); 154 | if (pn != null) uvOffset.y = Single.Parse(pn.Attributes["val"].Value); 155 | 156 | pn = node.SelectSingleNode("child::KeyValueProperty[@name='" + attrBasename + "_UVScaleX']"); 157 | if (pn != null) uvScale.x = Single.Parse(pn.Attributes["val"].Value); 158 | pn = node.SelectSingleNode("child::KeyValueProperty[@name='" + attrBasename + "_UVScaleY']"); 159 | if (pn != null) uvScale.y = Single.Parse(pn.Attributes["val"].Value); 160 | } 161 | 162 | public static UdsMaterial FromNode(AssetImportContext ctx, XmlNode node, Dictionary textureElements) { 163 | Debug.Assert(node.Name == "MasterMaterial"); 164 | UdsMaterial m = new UdsMaterial(); 165 | 166 | m.assetRef = new Material(Shader.Find("Standard")); 167 | m.assetRef.name = node.Attributes["label"].Value; 168 | m.name = node.Attributes["name"].Value; 169 | 170 | /* 171 | * 172 | // mix between DiffuseColor and DiffuseMap? 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | if (Type == 2) { 195 | 196 | 197 | 198 | } 199 | 200 | */ 201 | 202 | foreach (XmlNode kvpNode in node.ChildNodes) { 203 | if (kvpNode.Name != "KeyValueProperty") continue; 204 | String keyName = kvpNode.Attributes["name"].Value; 205 | if (keyName == "DiffuseColor") { 206 | Color kvpColor = ParseKVPColor(kvpNode); 207 | m.assetRef.color = new Color(kvpColor.r, kvpColor.g, kvpColor.b, m.assetRef.color.a); // Alpha channel is preserved since we set it from the "Transparency" key 208 | 209 | } else if (keyName == "DiffuseMap") { 210 | Debug.Assert(kvpNode.Attributes["type"].Value == "Texture"); 211 | UdsTexture texRef; 212 | textureElements.TryGetValue(kvpNode.Attributes["val"].Value, out texRef); 213 | if (texRef == null) { 214 | ctx.LogImportError(String.Format("Missing diffuse texref \"{0}\" while assembling material node \"{1}\"", kvpNode.Attributes["val"].Value, m.name)); 215 | } else { 216 | Vector2 uvOffset, uvScale; 217 | FillUVOffsetScaleParameters(node, "DiffuseMap", out uvOffset, out uvScale); 218 | 219 | m.assetRef.SetTexture("_MainTex", texRef.assetRef); 220 | m.assetRef.SetTextureOffset("_MainTex", uvOffset); 221 | m.assetRef.SetTextureScale("_MainTex", uvScale); 222 | } 223 | } else if (keyName == "BumpMap") { 224 | Debug.Assert(kvpNode.Attributes["type"].Value == "Texture"); 225 | UdsTexture texRef; 226 | textureElements.TryGetValue(kvpNode.Attributes["val"].Value, out texRef); 227 | if (texRef == null) { 228 | ctx.LogImportError(String.Format("Missing bump texref \"{0}\" while assembling material node \"{1}\"", kvpNode.Attributes["val"].Value, m.name)); 229 | } else { 230 | Vector2 uvOffset, uvScale; 231 | FillUVOffsetScaleParameters(node, "BumpMap", out uvOffset, out uvScale); 232 | 233 | if (!texRef.importer.convertToNormalmap) { 234 | // Update importer config for this texture to convert it to a normal map 235 | texRef.importer.textureType = TextureImporterType.NormalMap; 236 | texRef.importer.convertToNormalmap = true; 237 | texRef.importer.SaveAndReimport(); 238 | } 239 | 240 | m.assetRef.SetTexture("_BumpMap", texRef.assetRef); 241 | m.assetRef.SetTextureOffset("_BumpMap", uvOffset); 242 | m.assetRef.SetTextureScale("_BumpMap", uvScale); 243 | m.assetRef.EnableKeyword("_NORMALMAP"); // ref: https://docs.unity3d.com/Manual/materials-scripting-standard-shader.html 244 | } 245 | } else if (keyName == "BumpAmount") { 246 | m.assetRef.SetFloat("_BumpScale", Math.Min(Single.Parse(kvpNode.Attributes["val"].Value), 1.0f)); 247 | } else if (keyName == "Transparency") { 248 | float transparency = Single.Parse(kvpNode.Attributes["val"].Value); 249 | if (transparency > 0.0f) { 250 | 251 | // Set up the material for the "Transparent" transparency mode. 252 | // This code is borrowed from StandardShaderGUI.cs:365 in the Unity 2018.4.20 builtin shaders package 253 | m.assetRef.SetInt("_Mode", 3); // StandardShader.BlendMode enum: {Opaque=0, Cutout=1, Fade=2, Transparent=3} 254 | m.assetRef.SetOverrideTag("RenderType", "Transparent"); 255 | m.assetRef.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.One); 256 | m.assetRef.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha); 257 | m.assetRef.SetInt("_ZWrite", 0); 258 | m.assetRef.DisableKeyword("_ALPHATEST_ON"); 259 | m.assetRef.DisableKeyword("_ALPHABLEND_ON"); 260 | m.assetRef.EnableKeyword("_ALPHAPREMULTIPLY_ON"); 261 | m.assetRef.renderQueue = (int)UnityEngine.Rendering.RenderQueue.Transparent; 262 | // End borrowed setup code 263 | 264 | Color c = m.assetRef.color; 265 | c.a = 1.0f - transparency; 266 | m.assetRef.color = c; 267 | 268 | } 269 | } else if (keyName == "IsMetal") { 270 | m.assetRef.SetFloat("_Metallic", Boolean.Parse(kvpNode.Attributes["val"].Value) ? 1.0f : 0.0f); 271 | } else if (keyName == "Glossiness") { 272 | m.assetRef.SetFloat("_Glossiness", Single.Parse(kvpNode.Attributes["val"].Value)); 273 | } else if (keyName == "SelfIlluminationLuminance") { 274 | float emissiveLuma = Single.Parse(kvpNode.Attributes["val"].Value); 275 | if (emissiveLuma > 0.0f) { 276 | // Emissive surfaces seem to mostly be used for light fixture glass. Those components also have lights attached, 277 | // so we don't need the emissive component to contribute to global illumination. 278 | m.assetRef.EnableKeyword("_EMISSION"); 279 | m.assetRef.globalIlluminationFlags = MaterialGlobalIlluminationFlags.None; 280 | m.assetRef.SetColor("_EmissionColor", new Color(emissiveLuma, emissiveLuma, emissiveLuma, emissiveLuma)); 281 | } 282 | } 283 | 284 | 285 | } // Kvp node loop 286 | 287 | ctx.AddObjectToAsset(m.name, m.assetRef); 288 | 289 | return m; 290 | } 291 | }; 292 | 293 | private Dictionary staticMeshElements; 294 | private Dictionary materialElements; 295 | private Dictionary textureElements; 296 | private Dictionary > actorMetadata; 297 | 298 | private float clamp(float x, float a, float b) { 299 | return Math.Min(Math.Max(x, a), b); 300 | } 301 | 302 | private void ImportActorChildren(AssetImportContext ctx, GameObject parentObject, XmlNode containerNode) { 303 | 304 | foreach (XmlNode node in containerNode.ChildNodes) { 305 | if (!(node.Name == "Actor" || node.Name == "ActorMesh" || node.Name == "Light")) 306 | continue; // Only examine supported nodes 307 | 308 | String objName = node.Attributes["label"].Value + "_" + node.Attributes["name"].Value; 309 | GameObject obj = new GameObject(objName); 310 | obj.transform.SetParent(parentObject.transform, /*worldPositionStays=*/false); 311 | //Do NOT call ctx.AddObjectToAsset on these GameObjects. It should only be called on the root GameObject of the hierarchy (which the Unity manual fails to mention, of course.) 312 | // Calling ctx.AddObjectToAsset on the child GameObjects will cause Unity to crash when changing importer settings. 313 | 314 | { 315 | XmlNode xfNode = node.SelectSingleNode("child::Transform"); 316 | if (xfNode != null) { 317 | // Transform position and rotation are stored in an absolute coordinate space. TODO: not sure if Scale will be applied correctly 318 | // Datasmith units are cm, while Unity units are m; we adjust the scale of mesh vertices and of incoming Transform nodes to match. 319 | 320 | obj.transform.position = new Vector3( 321 | Single.Parse(xfNode.Attributes["tx"].Value) * 0.01f, 322 | Single.Parse(xfNode.Attributes["ty"].Value) * 0.01f, 323 | Single.Parse(xfNode.Attributes["tz"].Value) * 0.01f); 324 | obj.transform.localScale = new Vector3( 325 | Single.Parse(xfNode.Attributes["sx"].Value), 326 | Single.Parse(xfNode.Attributes["sy"].Value), 327 | Single.Parse(xfNode.Attributes["sz"].Value)); 328 | obj.transform.rotation = new Quaternion( 329 | Single.Parse(xfNode.Attributes["qx"].Value), 330 | Single.Parse(xfNode.Attributes["qy"].Value), 331 | Single.Parse(xfNode.Attributes["qz"].Value), 332 | Single.Parse(xfNode.Attributes["qw"].Value)); 333 | } 334 | } // transform processing 335 | 336 | if (node.Name == "ActorMesh") { 337 | XmlNode meshNode = node.SelectSingleNode("child::mesh"); 338 | if (meshNode != null) { 339 | String meshName = meshNode.Attributes["name"].Value; 340 | UdsStaticMesh mesh; 341 | 342 | staticMeshElements.TryGetValue(meshName, out mesh); 343 | Debug.Assert(mesh != null, String.Format("Missing StaticMesh node for \"{0}\" referenced from ActorMesh node \"{1}\"", meshName, node.Attributes["name"].Value)); 344 | if (mesh.assetRef) { 345 | MeshFilter mf = obj.AddComponent(); 346 | mf.sharedMesh = mesh.assetRef; 347 | 348 | Material[] mats = new Material[mesh.assetRef.subMeshCount]; 349 | for (int materialIdx = 0; materialIdx < mesh.assetRef.subMeshCount; ++materialIdx) { 350 | mats[materialIdx] = mesh.materialRefs[materialIdx].assetRef; 351 | } 352 | 353 | MeshRenderer mr = obj.AddComponent(); 354 | mr.sharedMaterials = mats; 355 | 356 | String RevitLayer = node.Attributes["layer"].Value; 357 | //Dictionary metadata = new Dictionary(); 358 | //actorMetadata.TryGetValue(node.Attributes["name"].Value, out metadata); 359 | 360 | // Process imported metadata and try to do something reasonable with it 361 | { 362 | //String RevitCategory; 363 | //metadata.TryGetValue("Element_Category", out RevitCategory); 364 | if (RevitLayer != null) { 365 | 366 | if (Regex.Match(RevitLayer, @"Ceilings", RegexOptions.IgnoreCase).Success) { 367 | // Apply ceiling height offset 368 | Vector3 p = obj.transform.position; 369 | p.z += m_CeilingHeightOffset; 370 | obj.transform.position = p; 371 | } 372 | 373 | if (Regex.Match(RevitLayer, @"Floors", RegexOptions.IgnoreCase).Success) { 374 | // Apply floor height offset 375 | Vector3 p = obj.transform.position; 376 | p.z += m_FloorHeightOffset; 377 | obj.transform.position = p; 378 | } 379 | 380 | 381 | if (Regex.Match(RevitLayer, m_IgnoredLayerRegex, RegexOptions.IgnoreCase).Success) { 382 | // Default-hidden objects. For example, "Entourage" and "Planting" objects are not exported correctly by Datasmith (no materials/textures), so we hide them. 383 | obj.SetActive(false); 384 | } else if (Regex.Match(RevitLayer, m_StaticTangibleLayerRegex, RegexOptions.IgnoreCase).Success) { 385 | // Completely static objects that should be lightmapped and have collision enabled 386 | GameObjectUtility.SetStaticEditorFlags(obj, StaticEditorFlags.LightmapStatic | StaticEditorFlags.OccludeeStatic | StaticEditorFlags.OccluderStatic | StaticEditorFlags.BatchingStatic | StaticEditorFlags.ReflectionProbeStatic); 387 | 388 | // Collision 389 | MeshCollider collider = obj.AddComponent(); 390 | collider.cookingOptions = (MeshColliderCookingOptions.CookForFasterSimulation | MeshColliderCookingOptions.EnableMeshCleaning | MeshColliderCookingOptions.WeldColocatedVertices); 391 | 392 | } else if (Regex.Match(RevitLayer, m_StaticIntangibleLayerRegex, RegexOptions.IgnoreCase).Success) { 393 | // Completely static objects that should be lightmapped, but don't need collision 394 | GameObjectUtility.SetStaticEditorFlags(obj, StaticEditorFlags.LightmapStatic | StaticEditorFlags.OccludeeStatic | StaticEditorFlags.OccluderStatic | StaticEditorFlags.BatchingStatic | StaticEditorFlags.ReflectionProbeStatic); 395 | 396 | } else if (Regex.Match(RevitLayer, m_PhysicsPropsLayerRegex).Success) { 397 | // Clutter that can be physics-enabled 398 | 399 | MeshCollider collider = obj.AddComponent(); 400 | collider.cookingOptions = (MeshColliderCookingOptions.CookForFasterSimulation | MeshColliderCookingOptions.EnableMeshCleaning | MeshColliderCookingOptions.WeldColocatedVertices); 401 | 402 | #if USE_VRC_SDK3 403 | if (m_SetupPhysicsProps) { 404 | 405 | collider.convex = true; 406 | 407 | Rigidbody rb = obj.AddComponent(); 408 | // rb.collisionDetectionMode = CollisionDetectionMode.Continuous; // Higher quality collision detection, but slower. 409 | 410 | // Add VRCPickup component to make the object interactable 411 | VRC.SDK3.Components.VRCPickup pickup = obj.AddComponent(); 412 | 413 | pickup.pickupable = true; 414 | pickup.allowManipulationWhenEquipped = true; 415 | 416 | // Add UdonBehaviour component to replicate the object's position 417 | VRC.Udon.UdonBehaviour udon = obj.AddComponent(); 418 | udon.SynchronizePosition = true; 419 | 420 | // TODO see if it's possible to only enable gravity on objects the first time they're picked up (so wall/ceiling fixtures can remain in place until grabbed) 421 | } 422 | 423 | #endif 424 | 425 | 426 | } else { 427 | ctx.LogImportWarning(String.Format("Unhandled Layer \"{0}\" -- ActorMesh \"{1}\" will not have physics and lighting behaviours automatically mapped", RevitLayer, objName)); 428 | } 429 | 430 | if (Regex.Match(RevitLayer, @"Lighting").Success) { 431 | // Turn off shadow casting on light fixtures. Light sources are usually placed inside the fixture body and we don't want the fixture geometry to block them. 432 | mr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off; 433 | } 434 | 435 | } 436 | } 437 | 438 | 439 | 440 | 441 | } else { 442 | ctx.LogImportError(String.Format("ActorMesh {0} mesh {1} assetRef is NULL", obj.name, meshName)); 443 | } 444 | } 445 | } // ActorMesh 446 | 447 | if (node.Name == "Light" && m_ImportLights) { 448 | Light light = obj.AddComponent(); 449 | if (node.Attributes["type"].Value == "PointLight") { 450 | light.type = LightType.Point; 451 | } else { 452 | ctx.LogImportWarning(String.Format("Light {0}: Unhandled \"type\" \"{1}\", defaulting to Point", objName, node.Attributes["type"].Value)); 453 | } 454 | 455 | { // Color temperature or RGB color 456 | XmlNode colorNode = node.SelectSingleNode("child::Color"); 457 | if (colorNode != null) { 458 | if (Int32.Parse(colorNode.Attributes["usetemp"].Value) != 0) { 459 | float colorTemperature = Single.Parse(colorNode.Attributes["temperature"].Value); 460 | // There doesn't appear to be a way to turn on color temperature mode on the Light programmatically (why?) 461 | // Convert it to RGB; algorithm borrowed from https://tannerhelland.com/2012/09/18/convert-temperature-rgb-algorithm-code.html 462 | 463 | float tmpKelvin = Mathf.Clamp(colorTemperature, 1000.0f, 40000.0f) / 100.0f; 464 | // Note: The R-squared values for each approximation follow each calculation 465 | float r = tmpKelvin <= 66.0f ? 255.0f : 466 | Mathf.Clamp(329.698727446f * (Mathf.Pow(tmpKelvin - 60.0f, -0.1332047592f)), 0.0f, 255.0f); // .988 467 | 468 | float g = tmpKelvin <= 66 ? 469 | Mathf.Clamp(99.4708025861f * Mathf.Log(tmpKelvin) - 161.1195681661f, 0.0f, 255.0f) : // .996 470 | Mathf.Clamp(288.1221695283f * (Mathf.Pow(tmpKelvin - 60.0f, -0.0755148492f)), 0.0f, 255.0f); // .987 471 | 472 | float b = tmpKelvin >= 66 ? 255 : 473 | tmpKelvin <= 19 ? 0 : 474 | Mathf.Clamp(138.5177312231f * Mathf.Log(tmpKelvin - 10.0f) - 305.0447927307f, 0.0f, 255.0f); // .998 475 | 476 | light.color = new Color(r/255.0f, g/255.0f, b/255.0f); 477 | } else { 478 | float r = Single.Parse(colorNode.Attributes["R"].Value); 479 | float g = Single.Parse(colorNode.Attributes["G"].Value); 480 | float b = Single.Parse(colorNode.Attributes["B"].Value); 481 | light.color = new Color(r, g, b); 482 | } 483 | } 484 | } 485 | 486 | // Common light parameters 487 | if (m_SetupLightmapBaking) { 488 | light.lightmapBakeType = LightmapBakeType.Baked; 489 | } 490 | 491 | GameObjectUtility.SetStaticEditorFlags(obj, StaticEditorFlags.LightmapStatic | StaticEditorFlags.OccludeeStatic | StaticEditorFlags.OccluderStatic | StaticEditorFlags.BatchingStatic | StaticEditorFlags.ReflectionProbeStatic); 492 | } 493 | 494 | { // children node processing 495 | XmlNode childrenNode = node.SelectSingleNode("child::children"); 496 | if (childrenNode != null) { 497 | // TODO obey visible="true" / visible="false" attribute on children node 498 | ImportActorChildren(ctx, obj, childrenNode); 499 | } 500 | } // children node processing 501 | 502 | } // child loop 503 | } 504 | 505 | 506 | 507 | public override void OnImportAsset(AssetImportContext ctx) { 508 | 509 | XmlDocument doc = new XmlDocument(); 510 | { 511 | String wholeDoc = File.ReadAllText(ctx.assetPath); 512 | // Datasmith doesn't properly escape & in texture filename XML elements, so we need to do that before we can pass the document to XmlDocument or it will throw a parse error. 513 | doc.LoadXml(System.Text.RegularExpressions.Regex.Replace(wholeDoc, @"&([^;]{8}?)", "&$1")); 514 | } 515 | 516 | XmlElement rootElement = doc.DocumentElement; 517 | Debug.Assert(rootElement.Name == "DatasmithUnrealScene"); 518 | 519 | staticMeshElements = new Dictionary(); 520 | materialElements = new Dictionary(); 521 | textureElements = new Dictionary(); 522 | actorMetadata = new Dictionary>(); 523 | 524 | // Populate actor metadata dictionaries 525 | foreach (XmlNode metadataNode in rootElement.SelectNodes("child::MetaData")) { 526 | var m = Regex.Match(metadataNode.Attributes["reference"].Value, @"Actor\.(.*)$"); 527 | if (!m.Success) continue; 528 | String actorId = m.Groups[1].Value; 529 | 530 | Dictionary metadata = new Dictionary(); 531 | foreach (XmlNode kvpNode in metadataNode.SelectNodes("child::KeyValueProperty")) { 532 | metadata.Add(kvpNode.Attributes["name"].Value, kvpNode.Attributes["val"].Value); 533 | } 534 | actorMetadata.Add(actorId, metadata); 535 | } 536 | 537 | // Import textures 538 | foreach (XmlNode node in rootElement.SelectNodes("child::Texture")) { 539 | UdsTexture tex = UdsTexture.FromNode(ctx, node); 540 | if (tex != null) { 541 | textureElements.Add(tex.name, tex); 542 | } 543 | } 544 | 545 | // Import materials 546 | foreach (XmlNode node in rootElement.SelectNodes("child::MasterMaterial")) { 547 | UdsMaterial m = UdsMaterial.FromNode(ctx, node, textureElements); 548 | materialElements.Add(m.name, m); 549 | } 550 | 551 | // Import StaticMesh nodes and crossreference materials 552 | foreach (XmlNode node in rootElement.SelectNodes("child::StaticMesh")) { 553 | UdsStaticMesh m = UdsStaticMesh.FromNode(ctx, node); 554 | 555 | for (int materialIdx = 0; materialIdx < m.materialNames.Count; ++materialIdx) { 556 | UdsMaterial mat; 557 | if (!materialElements.TryGetValue(m.materialNames[materialIdx], out mat)) { 558 | ctx.LogImportError(String.Format("Can't resolve Material ref \"{0}\"", m.materialNames[materialIdx])); 559 | } 560 | m.materialRefs.Add(mat); 561 | } 562 | 563 | staticMeshElements.Add(m.name, m); 564 | 565 | } 566 | 567 | 568 | GameObject sceneRoot = new GameObject("datasmithRoot"); 569 | sceneRoot.transform.rotation = Quaternion.Euler(90.0f, 0.0f, 0.0f); // Convert Revit's Z-up orientation to Unity's Y-up orientation 570 | ctx.AddObjectToAsset("datasmithRoot", sceneRoot); 571 | ctx.SetMainObject(sceneRoot); 572 | 573 | ImportActorChildren(ctx, sceneRoot, rootElement); 574 | 575 | // cleanup (TODO not sure if this is required / what the lifecycle of this object looks like) 576 | staticMeshElements = null; 577 | materialElements = null; 578 | textureElements = null; 579 | actorMetadata = null; 580 | } 581 | 582 | }; 583 | 584 | #endif // UNITY_EDITOR -------------------------------------------------------------------------------- /UdsMeshImporter.cs: -------------------------------------------------------------------------------- 1 | #if UNITY_EDITOR // Importer scripts only work in the Editor 2 | 3 | using UnityEngine; 4 | using UnityEditor.Experimental.AssetImporters; 5 | using System; 6 | using System.IO; 7 | using System.Collections.Generic; 8 | 9 | 10 | [ScriptedImporter(1, "udsmesh")] 11 | public class UdsMeshImporter : ScriptedImporter { 12 | 13 | [Tooltip("Force lightmap UV2 generation during import on large/complex meshes. Note that this can take a significant amount of CPU time.")] 14 | public bool m_ForceLightmapUVGeneration = false; 15 | 16 | private static int memcmp(byte[] left, byte[] right, uint length) { 17 | for (int i = 0; i < length; ++i) { 18 | if (left[i] < right[i]) 19 | return -1; 20 | else if (left[i] > right[i]) 21 | return 1; 22 | } 23 | return 0; 24 | } 25 | 26 | public static Hash128 NUVHash(Vector3 n, Vector2 uv) { 27 | Hash128 res = new Hash128(); 28 | HashUtilities.QuantisedVectorHash(ref n, ref res); 29 | Hash128 uvHash = new Hash128(); 30 | Vector3 uv3 = new Vector3(uv.x, uv.y, 0.0f); 31 | HashUtilities.QuantisedVectorHash(ref uv3, ref uvHash); 32 | HashUtilities.AppendHash(ref uvHash, ref res); 33 | return res; 34 | } 35 | 36 | public override void OnImportAsset(AssetImportContext ctx) { 37 | var mesh = new Mesh(); 38 | ctx.AddObjectToAsset("mesh0", mesh); 39 | ctx.SetMainObject(mesh); 40 | 41 | using (BinaryReader reader = new BinaryReader(File.Open(ctx.assetPath, FileMode.Open))) { 42 | 43 | 44 | byte[] searchBuf = new byte[32]; 45 | var markerStr = "DatasmithMeshSourceModel"; 46 | 47 | byte[] marker = System.Text.Encoding.ASCII.GetBytes(markerStr); 48 | uint markerLength = (uint) markerStr.Length; 49 | 50 | bool didFindMarker = false; 51 | while (reader.BaseStream.Position < (reader.BaseStream.Length - markerLength)) { 52 | 53 | reader.BaseStream.Read(searchBuf, 0, (int) markerLength); 54 | if (0 == memcmp(searchBuf, marker, markerLength)) { 55 | reader.BaseStream.Position += 2; // Skip 2 extra null bytes after the marker 56 | didFindMarker = true; 57 | break; 58 | } 59 | 60 | // rewind to 1 byte after the previous read position 61 | reader.BaseStream.Position -= (markerLength - 1); 62 | } 63 | 64 | if (!didFindMarker) { 65 | throw new Exception("Couldn't find marker " + markerStr + " in file " + ctx.assetPath); 66 | } 67 | 68 | for (uint i = 0; i < 6; ++i) { 69 | if (reader.ReadUInt32() != 0) { 70 | Console.Out.WriteLine("Warning: expected all zeros between marker and start of material index array"); 71 | } 72 | } 73 | 74 | reader.ReadUInt32(); // length1 75 | reader.ReadUInt32(); // length2 76 | reader.ReadUInt32(); // unknown 9c 00 00 00 77 | reader.ReadUInt32(); // unknown 00 00 00 00 78 | reader.ReadUInt32(); // unknown 01 00 00 00 79 | reader.ReadUInt32(); // unknown 00 00 00 00 80 | 81 | 82 | uint materialIndexCount = reader.ReadUInt32(); 83 | uint[] materialIndices = new uint[materialIndexCount]; 84 | for (uint i = 0; i < materialIndexCount; ++i) { 85 | materialIndices[i] = reader.ReadUInt32(); 86 | } 87 | 88 | uint unknownCount = reader.ReadUInt32(); 89 | uint[] unknownData = new uint[unknownCount]; 90 | for (uint i = 0; i < unknownCount; ++i) { 91 | unknownData[i] = reader.ReadUInt32(); 92 | } 93 | 94 | List vertices = new List(); 95 | 96 | Dictionary indexRemap = new Dictionary(); 97 | { 98 | // Collapse vertices and generate an index remapping table 99 | Dictionary uniqueVertices = new Dictionary(); 100 | 101 | 102 | int fileVertexCount = (int) reader.ReadUInt32(); 103 | int vertexLimit = 524288; 104 | 105 | if (fileVertexCount > vertexLimit) { 106 | ctx.LogImportError(String.Format("UdsMeshImporter: Sanity check failed: File {0} has too many vertices ({1}, limit is {2}) -- returning empty mesh.", ctx.assetPath, fileVertexCount, vertexLimit)); 107 | return; 108 | } 109 | 110 | for (int i = 0; i < fileVertexCount; ++i) { 111 | Vector3 v = new Vector3(); 112 | // Adjust scale from cm -> meters 113 | v.x = reader.ReadSingle() * 0.01f; 114 | v.y = reader.ReadSingle() * 0.01f; 115 | v.z = reader.ReadSingle() * 0.01f; 116 | 117 | if (!uniqueVertices.ContainsKey(v)) { 118 | vertices.Add(v); 119 | uniqueVertices.Add(v, vertices.Count - 1); 120 | } 121 | 122 | indexRemap.Add(i, uniqueVertices[v]); 123 | } 124 | /* 125 | if (vertices.Count < fileVertexCount) { 126 | Debug.Log(String.Format("Vertex position remapping removed {0} nonunique positions", fileVertexCount - vertices.Count)); 127 | } 128 | */ 129 | } 130 | 131 | 132 | uint indexCount = reader.ReadUInt32(); 133 | int[] triangleIndices = new int[indexCount]; 134 | for (uint triIdx = 0; triIdx < (indexCount/3); ++triIdx) { 135 | triangleIndices[(triIdx * 3) + 0] = indexRemap[(int)reader.ReadUInt32()]; 136 | triangleIndices[(triIdx * 3) + 1] = indexRemap[(int)reader.ReadUInt32()]; 137 | triangleIndices[(triIdx * 3) + 2] = indexRemap[(int)reader.ReadUInt32()]; 138 | } 139 | 140 | reader.ReadUInt32(); // unknown-zero, maybe a count of an unused field 141 | reader.ReadUInt32(); // unknown-zero, maybe a count of an unused field 142 | uint normalCount = reader.ReadUInt32(); 143 | Vector3[] normals = new Vector3[normalCount]; 144 | for (uint i = 0; i < normalCount; ++i) { 145 | normals[i].x = reader.ReadSingle(); 146 | normals[i].y = reader.ReadSingle(); 147 | normals[i].z = reader.ReadSingle(); 148 | } 149 | 150 | 151 | uint uvCount = reader.ReadUInt32(); 152 | Vector2[] uvs = new Vector2[uvCount]; 153 | for (uint i = 0; i < uvCount; ++i) { 154 | uvs[i].x = reader.ReadSingle(); 155 | uvs[i].y = reader.ReadSingle(); 156 | } 157 | 158 | // Datasmith hands us per-face-vertex normals and UVs, which Unity can't handle. 159 | // Use the Datasmith-supplied index array to write new per-submesh (material group) position/normal/UV buffers. 160 | 161 | { 162 | var materialToSubmesh = new SortedDictionary(); 163 | uint submeshCount = 0; 164 | for (uint triIdx = 0; triIdx < materialIndexCount; ++triIdx) { 165 | uint midx = materialIndices[triIdx]; 166 | if (!materialToSubmesh.ContainsKey(midx)) { 167 | materialToSubmesh[midx] = submeshCount; 168 | submeshCount += 1; 169 | } 170 | } 171 | 172 | 173 | List cookedPositions = new List(); 174 | List cookedUVs = new List(); 175 | List cookedNormals = new List(); 176 | List > cookedSubmeshIndices = new List >(); 177 | 178 | List> vertexCollapseData = new List>(vertices.Count); 179 | // Prepopulate the vertex-collapse list with empty dicts 180 | for (int vIdx = 0; vIdx < vertices.Count; ++vIdx) { 181 | vertexCollapseData.Add(new Dictionary()); 182 | } 183 | 184 | 185 | for (uint submeshIndex = 0; submeshIndex < submeshCount; ++submeshIndex) { 186 | List thisSubmeshIndices = new List(); 187 | 188 | for (uint triIdx = 0; triIdx < materialIndexCount; ++triIdx) { 189 | 190 | if (materialToSubmesh[materialIndices[triIdx]] != submeshIndex) 191 | continue; // this triangle is not relevant in this submesh. 192 | 193 | for (uint triVIdx = 0; triVIdx < 3; ++triVIdx) { 194 | uint triVIdx_adj = 2 - triVIdx; // Adjusted to swap winding order 195 | 196 | int positionIndex = triangleIndices[(triIdx * 3) + triVIdx_adj]; 197 | Vector3 fvP = vertices[positionIndex]; 198 | Vector2 fvUV = uvs[(triIdx * 3) + triVIdx_adj]; 199 | Vector3 fvN = normals[(triIdx * 3) + triVIdx_adj]; 200 | 201 | // Try and find an existing vertex/normal/UV set to reuse 202 | // We already collapsed coincident positions while reading the vertex and index buffers, so we can partition our search by position index. 203 | Dictionary collapseData = vertexCollapseData[positionIndex]; 204 | Hash128 targetHash = NUVHash(fvN, fvUV); 205 | int targetVIdx; 206 | 207 | if (collapseData.ContainsKey(targetHash)) { 208 | // Match found, reuse the previous vertex 209 | targetVIdx = collapseData[targetHash]; 210 | } else { 211 | // No match found, so we add it 212 | cookedPositions.Add(fvP); 213 | cookedUVs.Add(fvUV); 214 | cookedNormals.Add(fvN); 215 | 216 | targetVIdx = cookedPositions.Count - 1; 217 | collapseData.Add(targetHash, targetVIdx); 218 | 219 | } 220 | 221 | 222 | thisSubmeshIndices.Add(targetVIdx); 223 | } 224 | 225 | } 226 | cookedSubmeshIndices.Add(thisSubmeshIndices); 227 | } 228 | 229 | mesh.Clear(); 230 | if (cookedPositions.Count > 65535) { 231 | ctx.LogImportWarning(String.Format("Mesh \"{0}\" has more than 65535 vertices ({1}) and requires a 32-bit index buffer. This mesh may not render correctly on all platforms.", ctx.assetPath, cookedPositions.Count)); 232 | mesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32; 233 | } 234 | 235 | mesh.SetVertices(cookedPositions); 236 | mesh.SetUVs(0, cookedUVs); 237 | mesh.SetNormals(cookedNormals); 238 | mesh.subMeshCount = (int) submeshCount; 239 | for (uint submeshIndex = 0; submeshIndex < submeshCount; ++submeshIndex) { 240 | mesh.SetIndices(cookedSubmeshIndices[(int) submeshIndex].ToArray(), MeshTopology.Triangles, (int) submeshIndex); 241 | } 242 | 243 | // Generate lightmap UVs 244 | if (materialIndexCount > 50000 /*triangles*/) { 245 | if (m_ForceLightmapUVGeneration) { 246 | UnityEditor.Unwrapping.GenerateSecondaryUVSet(mesh); 247 | } else { 248 | ctx.LogImportWarning(String.Format("Mesh \"{0}\": lightmap UVs won't automatically be generated due to complexity limits. Turn on \"Force Lightmap UV generation\" to override.", ctx.assetPath)); 249 | } 250 | } 251 | 252 | mesh.RecalculateBounds(); 253 | mesh.RecalculateTangents(); 254 | } 255 | 256 | } 257 | } 258 | } 259 | 260 | #endif --------------------------------------------------------------------------------