public override void OnImportAsset(AssetImportContext ctx)
        {
            try
            {
                if (Utils.IsUsingADBV1())
                {
                    ctx.LogImportWarning("Search indexes are not supported with the Asset Database V1");
                    return;
                }

                var db = ScriptableObject.CreateInstance <SearchDatabase>();
                db.Import(ctx.assetPath);
                ctx.AddObjectToAsset("index", db);
                ctx.SetMainObject(db);

                #if UNITY_2020_1_OR_NEWER
                ctx.DependsOnCustomDependency(nameof(CustomObjectIndexerAttribute));
                #endif

                hideFlags |= HideFlags.HideInInspector;
            }
            catch (SearchDatabaseException ex)
            {
                ctx.LogImportError(ex.Message, AssetDatabase.LoadMainAssetAtPath(AssetDatabase.GUIDToAssetPath(ex.guid)));
            }
        }
        public static UdsTexture FromNode(AssetImportContext ctx, XmlNode node)
        {
            UdsTexture tex = new UdsTexture();

            /*
             * <Texture name="concrete_cast-in-place_formwork_wood_boards_bump_0" texturemode="0" texturefilter="3" textureaddressx="0" textureaddressy="0" rgbcurve="-1.000000"
             *          srgb="0" file="rac_basic_sample_project_local_copy-3DView-UE4_Assets/concrete.cast-in-place.formwork.wood.boards.bump.jpg">
             *   <Hash value="c99e25a6f94199ce085a6e78e56639f2"/>
             * </Texture>
             */

            tex.name     = node.Attributes["name"].Value;
            tex.filePath = node.Attributes["file"].Value;

            if (Regex.Match(tex.filePath, @"\.ies$", RegexOptions.IgnoreCase).Success)
            {
                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));
                return(null);
            }


            tex.fullyQualifiedPath = Path.Combine(Path.GetDirectoryName(ctx.assetPath), tex.filePath);

            var texAssetObj = AssetDatabase.LoadAssetAtPath(tex.fullyQualifiedPath, typeof(Texture));

            if (texAssetObj != null)
            {
                tex.assetRef = (Texture)texAssetObj;
                var texImporterObj = AssetImporter.GetAtPath(tex.fullyQualifiedPath); // load import settings for possible later adjustment once we know what this will be used for
                if (texImporterObj != null)
                {
                    tex.importer = (TextureImporter)texImporterObj;
                }
            }

            if (tex.assetRef == null || tex.importer == null)
            {
                ctx.LogImportError(String.Format("UdsTexture::FromNode: Asset does not exist at path \"{0}\"", tex.fullyQualifiedPath));
            }

            return(tex);
        }
        /// <summary>
        /// Generates a 2D texture from an image file
        /// </summary>
        /// <param name="imagePath">The path to the png or jpg file</param>
        /// <param name="name">The name of the texture object. Defaults to the ase file name + Texture</param>
        /// <returns></returns>
        public Texture2D GenerateTexture(string imagePath, string name = null)
        {
            if (TextureCreationOptions == null)
            {
                TextureCreationOptions = TextureCreationOptions.Default;
            }

            var texture = new Texture2D(2, 2, TextureFormat.ARGB32, false);

            if (!texture.LoadImage(File.ReadAllBytes(imagePath)))
            {
                ctx.LogImportWarning("Texture loading not successful");
            }

            texture.wrapMode            = TextureCreationOptions.textureWrapMode;
            texture.filterMode          = TextureCreationOptions.filterMode;
            texture.alphaIsTransparency = TextureCreationOptions.alphaIsTransparency;
            texture.name = string.IsNullOrWhiteSpace(name) ? AseFileNoExt + "Texture" : name;
            return(texture);
        }
Esempio n. 4
0
        /// <summary>
        /// Common method performing the import of the asset
        /// </summary>
        /// <param name="ctx">Asset importer context.</param>
        public override void OnImportAsset(AssetImportContext ctx)
        {
            engine.TextureGenerationType = TextureImporterType.Default;

            Texture cookieTextureCube = null;
            Texture cookieTexture2D   = null;

            string iesFilePath  = Path.Combine(Path.GetDirectoryName(Application.dataPath), ctx.assetPath);
            string errorMessage = engine.ReadFile(iesFilePath);

            if (string.IsNullOrEmpty(errorMessage))
            {
                iesMetaData.FileFormatVersion      = engine.FileFormatVersion;
                iesMetaData.IESPhotometricType     = engine.GetPhotometricType();
                iesMetaData.Manufacturer           = engine.GetKeywordValue("MANUFAC");
                iesMetaData.LuminaireCatalogNumber = engine.GetKeywordValue("LUMCAT");
                iesMetaData.LuminaireDescription   = engine.GetKeywordValue("LUMINAIRE");
                iesMetaData.LampCatalogNumber      = engine.GetKeywordValue("LAMPCAT");
                iesMetaData.LampDescription        = engine.GetKeywordValue("LAMP");

                (iesMetaData.IESMaximumIntensity, iesMetaData.IESMaximumIntensityUnit) = engine.GetMaximumIntensity();

                string warningMessage;

                (warningMessage, cookieTextureCube) = engine.GenerateCubeCookie(iesMetaData.CookieCompression, (int)iesMetaData.iesSize);
                if (!string.IsNullOrEmpty(warningMessage))
                {
                    ctx.LogImportWarning($"Cannot properly generate IES Cube texture: {warningMessage}");
                }
                cookieTextureCube.IncrementUpdateCount();

                (warningMessage, cookieTexture2D) = engine.Generate2DCookie(iesMetaData.CookieCompression, iesMetaData.SpotAngle, (int)iesMetaData.iesSize, iesMetaData.ApplyLightAttenuation);
                if (!string.IsNullOrEmpty(warningMessage))
                {
                    ctx.LogImportWarning($"Cannot properly generate IES 2D texture: {warningMessage}");
                }
                cookieTexture2D.IncrementUpdateCount();
            }
            else
            {
                ctx.LogImportError($"Cannot read IES file '{iesFilePath}': {errorMessage}");
            }

            string iesFileName = Path.GetFileNameWithoutExtension(ctx.assetPath);

            var iesObject = ScriptableObject.CreateInstance <IESObject>();

            iesObject.iesMetaData = iesMetaData;
            GameObject lightObject = new GameObject(iesFileName);

            lightObject.transform.localEulerAngles = new Vector3(90f, 0f, iesMetaData.LightAimAxisRotation);

            Light light = lightObject.AddComponent <Light>();

            light.type      = (iesMetaData.PrefabLightType == IESLightType.Point) ? LightType.Point : LightType.Spot;
            light.intensity = 1f;  // would need a better intensity value formula
            light.range     = 10f; // would need a better range value formula
            light.spotAngle = iesMetaData.SpotAngle;

            ctx.AddObjectToAsset("IES", iesObject);
            ctx.SetMainObject(iesObject);

            IESImporter.createRenderPipelinePrefabLight?.Invoke(ctx, iesFileName, iesMetaData.UseIESMaximumIntensity, iesMetaData.IESMaximumIntensityUnit, iesMetaData.IESMaximumIntensity, light, (iesMetaData.PrefabLightType == IESLightType.Point) ? cookieTextureCube : cookieTexture2D);

            if (cookieTextureCube != null)
            {
                cookieTextureCube.name = iesFileName + "-Cube-IES";
                ctx.AddObjectToAsset(cookieTextureCube.name, cookieTextureCube);
            }
            if (cookieTexture2D != null)
            {
                cookieTexture2D.name = iesFileName + "-2D-IES";
                ctx.AddObjectToAsset(cookieTexture2D.name, cookieTexture2D);
            }
        }
Esempio n. 5
0
    public override void OnImportAsset(AssetImportContext ctx)
    {
        Tools.EnsureDirectoryExists(PREFAB_DESTINATION_DIRECTORY);
        int idx = ctx.assetPath.LastIndexOf("geobin");

        if (-1 == idx)
        {
            ctx.LogImportWarning("File is not located under 'geobin', skipping");
            return;
        }

        if (null == s_runtime_data) //!m_RuntimeReady
        {
            s_runtime_data = new RuntimeData();
            string basepath = ctx.assetPath.Substring(0, idx - 1);
            if (!basepath.EndsWith("/") && basepath[basepath.Length - 1] != Path.DirectorySeparatorChar)
            {
                basepath += Path.DirectorySeparatorChar;
            }
            s_runtime_data.prepare(basepath);
        }

        if (m_previous_asset == null || m_previous == null)
        {
            sg = SceneGraph.loadWholeMap(ctx.assetPath);
        }
        else
        {
            Debug.Log("Reusing previously loaded graph");
            sg = m_previous;
        }

        m_previous       = sg;
        m_previous_asset = ctx.assetPath;
        var top_nodes = sg.calculateUsages();

        if (top_nodes.Count != 0)
        {
            bool some_prefabs_were_missing = false;
            foreach (var pair in top_nodes)
            {
                GameObject top_level = convertFromRoot(pair.Value);
                if (top_level == null)
                {
                    some_prefabs_were_missing = true;
                }
                else
                {
                    GameObject.DestroyImmediate(top_level);
                }
            }

            foreach (KeyValuePair <SceneNode, GameObject> pair in m_imported_prefabs)
            {
                if (!top_nodes.ContainsValue(pair.Key))
                {
                    GameObject.DestroyImmediate(pair.Value); // remove all non-top-level prefabs from scene.
                }
            }

            if (some_prefabs_were_missing == false)
            {
                createTopNodesInstances(top_nodes);
            }
            else
            {
                string saved_ones = String.Join("\n", m_created_prefabs);
                ctx.LogImportWarning(String.Format(
                                         "The following prefab assets were missing and were created, please retry: {0}",
                                         saved_ones));
            }
        }
    }
        public override void OnImportAsset(AssetImportContext ctx)
        {
            var stringsMap = new Dictionary <string, string>();

            try {
                using (var stringsFile = File.OpenText(ctx.assetPath)) {
                    string entry;
                    string category = null;

                    while ((entry = this.ReadEntry(stringsFile)) != null)
                    {
                        entry = entry.TrimEnd();

                        if (entry.StartsWith("[") && entry.EndsWith("]"))
                        {
                            category = entry.Substring(1, entry.Length - 2);
                            continue;
                        }

                        // comments can be escaped with \;
                        var commentPos = entry.IndexOf(';');
                        if (commentPos > 0 && entry[commentPos - 1] != '\\')
                        {
                            commentPos = -1;
                        }

                        if (commentPos >= 0)
                        {
                            entry = entry.Substring(0, commentPos);
                        }

                        // ignore whitespace or completely commented lines
                        if (entry.All(char.IsWhiteSpace))
                        {
                            continue;
                        }

                        var splitPos = entry.IndexOf('=');
                        if (splitPos == -1)
                        {
                            ctx.LogImportWarning($"missing delimited in line '{entry}'");
                            continue;
                        }

                        var key   = entry.Substring(0, splitPos);
                        var value = entry.Substring(splitPos + 1).Trim();

                        if (category != null)
                        {
                            key = $"{category}.{key}";
                        }

                        if (stringsMap.ContainsKey(key))
                        {
                            ctx.LogImportWarning($"duplicate key '{key}'");
                        }

                        stringsMap.Add(key, value);
                    }
                }
            } catch (Exception e) {
                stringsMap.Clear();
                ctx.LogImportError(e.ToString());
            }

            var strings = Strings.Create(stringsMap);

            ctx.AddObjectToAsset(Path.GetFileName(ctx.assetPath), strings);
            ctx.SetMainObject(strings);
        }
    public override void OnImportAsset(AssetImportContext ctx)
    {
        var mesh = new Mesh();

        ctx.AddObjectToAsset("mesh0", mesh);
        ctx.SetMainObject(mesh);

        using (BinaryReader reader = new BinaryReader(File.Open(ctx.assetPath, FileMode.Open))) {
            byte[] searchBuf = new byte[32];
            var    markerStr = "DatasmithMeshSourceModel";

            byte[] marker       = System.Text.Encoding.ASCII.GetBytes(markerStr);
            uint   markerLength = (uint)markerStr.Length;

            bool didFindMarker = false;
            while (reader.BaseStream.Position < (reader.BaseStream.Length - markerLength))
            {
                reader.BaseStream.Read(searchBuf, 0, (int)markerLength);
                if (0 == memcmp(searchBuf, marker, markerLength))
                {
                    reader.BaseStream.Position += 2; // Skip 2 extra null bytes after the marker
                    didFindMarker = true;
                    break;
                }

                // rewind to 1 byte after the previous read position
                reader.BaseStream.Position -= (markerLength - 1);
            }

            if (!didFindMarker)
            {
                throw new Exception("Couldn't find marker " + markerStr + " in file " + ctx.assetPath);
            }

            for (uint i = 0; i < 6; ++i)
            {
                if (reader.ReadUInt32() != 0)
                {
                    Console.Out.WriteLine("Warning: expected all zeros between marker and start of material index array");
                }
            }

            reader.ReadUInt32(); // length1
            reader.ReadUInt32(); // length2
            reader.ReadUInt32(); // unknown 9c 00 00 00
            reader.ReadUInt32(); // unknown 00 00 00 00
            reader.ReadUInt32(); // unknown 01 00 00 00
            reader.ReadUInt32(); // unknown 00 00 00 00


            uint   materialIndexCount = reader.ReadUInt32();
            uint[] materialIndices    = new uint[materialIndexCount];
            for (uint i = 0; i < materialIndexCount; ++i)
            {
                materialIndices[i] = reader.ReadUInt32();
            }

            uint   unknownCount = reader.ReadUInt32();
            uint[] unknownData  = new uint[unknownCount];
            for (uint i = 0; i < unknownCount; ++i)
            {
                unknownData[i] = reader.ReadUInt32();
            }

            List <Vector3> vertices = new List <Vector3>();

            Dictionary <int, int> indexRemap = new Dictionary <int, int>();
            {
                // Collapse vertices and generate an index remapping table
                Dictionary <Vector3, int> uniqueVertices = new Dictionary <Vector3, int>();


                int fileVertexCount = (int)reader.ReadUInt32();
                int vertexLimit     = 524288;

                if (fileVertexCount > vertexLimit)
                {
                    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));
                    return;
                }

                for (int i = 0; i < fileVertexCount; ++i)
                {
                    Vector3 v = new Vector3();
                    // Adjust scale from cm -> meters
                    v.x = reader.ReadSingle() * 0.01f;
                    v.y = reader.ReadSingle() * 0.01f;
                    v.z = reader.ReadSingle() * 0.01f;

                    if (!uniqueVertices.ContainsKey(v))
                    {
                        vertices.Add(v);
                        uniqueVertices.Add(v, vertices.Count - 1);
                    }

                    indexRemap.Add(i, uniqueVertices[v]);
                }

                /*
                 * if (vertices.Count < fileVertexCount) {
                 * Debug.Log(String.Format("Vertex position remapping removed {0} nonunique positions", fileVertexCount - vertices.Count));
                 * }
                 */
            }


            uint  indexCount      = reader.ReadUInt32();
            int[] triangleIndices = new int[indexCount];
            for (uint triIdx = 0; triIdx < (indexCount / 3); ++triIdx)
            {
                triangleIndices[(triIdx * 3) + 0] = indexRemap[(int)reader.ReadUInt32()];
                triangleIndices[(triIdx * 3) + 1] = indexRemap[(int)reader.ReadUInt32()];
                triangleIndices[(triIdx * 3) + 2] = indexRemap[(int)reader.ReadUInt32()];
            }

            reader.ReadUInt32(); // unknown-zero, maybe a count of an unused field
            reader.ReadUInt32(); // unknown-zero, maybe a count of an unused field
            uint      normalCount = reader.ReadUInt32();
            Vector3[] normals     = new Vector3[normalCount];
            for (uint i = 0; i < normalCount; ++i)
            {
                normals[i].x = reader.ReadSingle();
                normals[i].y = reader.ReadSingle();
                normals[i].z = reader.ReadSingle();
            }


            uint      uvCount = reader.ReadUInt32();
            Vector2[] uvs     = new Vector2[uvCount];
            for (uint i = 0; i < uvCount; ++i)
            {
                uvs[i].x = reader.ReadSingle();
                uvs[i].y = reader.ReadSingle();
            }

            // Datasmith hands us per-face-vertex normals and UVs, which Unity can't handle.
            // Use the Datasmith-supplied index array to write new per-submesh (material group) position/normal/UV buffers.

            {
                var  materialToSubmesh = new SortedDictionary <uint, uint>();
                uint submeshCount      = 0;
                for (uint triIdx = 0; triIdx < materialIndexCount; ++triIdx)
                {
                    uint midx = materialIndices[triIdx];
                    if (!materialToSubmesh.ContainsKey(midx))
                    {
                        materialToSubmesh[midx] = submeshCount;
                        submeshCount           += 1;
                    }
                }


                List <Vector3>     cookedPositions      = new List <Vector3>();
                List <Vector2>     cookedUVs            = new List <Vector2>();
                List <Vector3>     cookedNormals        = new List <Vector3>();
                List <List <int> > cookedSubmeshIndices = new List <List <int> >();

                List <Dictionary <Hash128, int> > vertexCollapseData = new List <Dictionary <Hash128, int> >(vertices.Count);
                // Prepopulate the vertex-collapse list with empty dicts
                for (int vIdx = 0; vIdx < vertices.Count; ++vIdx)
                {
                    vertexCollapseData.Add(new Dictionary <Hash128, int>());
                }


                for (uint submeshIndex = 0; submeshIndex < submeshCount; ++submeshIndex)
                {
                    List <int> thisSubmeshIndices = new List <int>();

                    for (uint triIdx = 0; triIdx < materialIndexCount; ++triIdx)
                    {
                        if (materialToSubmesh[materialIndices[triIdx]] != submeshIndex)
                        {
                            continue; // this triangle is not relevant in this submesh.
                        }
                        for (uint triVIdx = 0; triVIdx < 3; ++triVIdx)
                        {
                            uint triVIdx_adj = 2 - triVIdx; // Adjusted to swap winding order

                            int     positionIndex = triangleIndices[(triIdx * 3) + triVIdx_adj];
                            Vector3 fvP           = vertices[positionIndex];
                            Vector2 fvUV          = uvs[(triIdx * 3) + triVIdx_adj];
                            Vector3 fvN           = normals[(triIdx * 3) + triVIdx_adj];

                            // Try and find an existing vertex/normal/UV set to reuse
                            // We already collapsed coincident positions while reading the vertex and index buffers, so we can partition our search by position index.
                            Dictionary <Hash128, int> collapseData = vertexCollapseData[positionIndex];
                            Hash128 targetHash = NUVHash(fvN, fvUV);
                            int     targetVIdx;

                            if (collapseData.ContainsKey(targetHash))
                            {
                                // Match found, reuse the previous vertex
                                targetVIdx = collapseData[targetHash];
                            }
                            else
                            {
                                // No match found, so we add it
                                cookedPositions.Add(fvP);
                                cookedUVs.Add(fvUV);
                                cookedNormals.Add(fvN);

                                targetVIdx = cookedPositions.Count - 1;
                                collapseData.Add(targetHash, targetVIdx);
                            }


                            thisSubmeshIndices.Add(targetVIdx);
                        }
                    }
                    cookedSubmeshIndices.Add(thisSubmeshIndices);
                }

                mesh.Clear();
                if (cookedPositions.Count > 65535)
                {
                    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));
                    mesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32;
                }

                mesh.SetVertices(cookedPositions);
                mesh.SetUVs(0, cookedUVs);
                mesh.SetNormals(cookedNormals);
                mesh.subMeshCount = (int)submeshCount;
                for (uint submeshIndex = 0; submeshIndex < submeshCount; ++submeshIndex)
                {
                    mesh.SetIndices(cookedSubmeshIndices[(int)submeshIndex].ToArray(), MeshTopology.Triangles, (int)submeshIndex);
                }

                // Generate lightmap UVs
                if (materialIndexCount > 50000 /*triangles*/)
                {
                    if (m_ForceLightmapUVGeneration)
                    {
                        UnityEditor.Unwrapping.GenerateSecondaryUVSet(mesh);
                    }
                    else
                    {
                        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));
                    }
                }

                mesh.RecalculateBounds();
                mesh.RecalculateTangents();
            }
        }
    }
Esempio n. 8
0
 public void LogWarning(string message, UnityEngine.Object context = null)
 {
     ctx.LogImportWarning(message, context);
 }
Esempio n. 9
0
        public override void OnImportAsset(AssetImportContext ctx)
        {
            byte[]    bytes         = File.ReadAllBytes(ctx.assetPath);
            Texture2D sourceTexture = new Texture2D(1, 1, TextureFormat.ARGB32, false);

            sourceTexture.LoadImage(bytes);

            originalWidth  = sourceTexture.width;
            originalHeight = sourceTexture.height;

            cols = sourceTexture.width / pageSize;
            if (cols == 0)
            {
                cols = 1;
            }

            rows = sourceTexture.height / pageSize;
            if (rows == 0)
            {
                rows = 1;
            }

            bool changedPageSize = false;

            while (cols * rows > 2048)
            {
                pageSize       *= 2;
                cols           /= 2;
                rows           /= 2;
                changedPageSize = true;
            }

            if (changedPageSize)
            {
                ctx.LogImportWarning("Texture Width * Texture Height * Page Size must be less or equal to 2048, so the Page Size is increased to " + pageSize);
            }

            int resizeX = sourceTexture.width;
            int resizeY = sourceTexture.height;

            if (sourceTexture.width % pageSize != 0)
            {
                resizeX = cols * pageSize;
            }
            if (sourceTexture.height % pageSize != 0)
            {
                resizeY = rows * pageSize;
            }

            if (resizeX != sourceTexture.width || resizeY != sourceTexture.height)
            {
                TextureScaler.Scale(sourceTexture, resizeX, resizeY);
                ctx.LogImportWarning("The width and height of the texture must be equal to Page Size * N, so the texture size is changed to " + resizeX + "x" + resizeY);
            }

            Texture2DArray array = InitTexture2DArray(sourceTexture, ctx);

            ctx.AddObjectToAsset("_MainTex", array);
            ctx.SetMainObject(array);

            DestroyImmediate(sourceTexture);
        }
    private void ImportActorChildren(AssetImportContext ctx, GameObject parentObject, XmlNode containerNode)
    {
        foreach (XmlNode node in containerNode.ChildNodes)
        {
            if (!(node.Name == "Actor" || node.Name == "ActorMesh" || node.Name == "Light"))
            {
                continue; // Only examine supported nodes
            }
            String     objName = node.Attributes["label"].Value + "_" + node.Attributes["name"].Value;
            GameObject obj     = new GameObject(objName);
            obj.transform.SetParent(parentObject.transform, /*worldPositionStays=*/ false);
            //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.)
            // Calling ctx.AddObjectToAsset on the child GameObjects will cause Unity to crash when changing importer settings.

            {
                XmlNode xfNode = node.SelectSingleNode("child::Transform");
                if (xfNode != null)
                {
                    // Transform position and rotation are stored in an absolute coordinate space. TODO: not sure if Scale will be applied correctly
                    // Datasmith units are cm, while Unity units are m; we adjust the scale of mesh vertices and of incoming Transform nodes to match.

                    obj.transform.position = new Vector3(
                        Single.Parse(xfNode.Attributes["tx"].Value) * 0.01f,
                        Single.Parse(xfNode.Attributes["ty"].Value) * 0.01f,
                        Single.Parse(xfNode.Attributes["tz"].Value) * 0.01f);
                    obj.transform.localScale = new Vector3(
                        Single.Parse(xfNode.Attributes["sx"].Value),
                        Single.Parse(xfNode.Attributes["sy"].Value),
                        Single.Parse(xfNode.Attributes["sz"].Value));
                    obj.transform.rotation = new Quaternion(
                        Single.Parse(xfNode.Attributes["qx"].Value),
                        Single.Parse(xfNode.Attributes["qy"].Value),
                        Single.Parse(xfNode.Attributes["qz"].Value),
                        Single.Parse(xfNode.Attributes["qw"].Value));
                }
            } // transform processing

            if (node.Name == "ActorMesh")
            {
                XmlNode meshNode = node.SelectSingleNode("child::mesh");
                if (meshNode != null)
                {
                    String        meshName = meshNode.Attributes["name"].Value;
                    UdsStaticMesh mesh;

                    staticMeshElements.TryGetValue(meshName, out mesh);
                    Debug.Assert(mesh != null, String.Format("Missing StaticMesh node for \"{0}\" referenced from ActorMesh node \"{1}\"", meshName, node.Attributes["name"].Value));
                    if (mesh.assetRef)
                    {
                        MeshFilter mf = obj.AddComponent <MeshFilter>();
                        mf.sharedMesh = mesh.assetRef;

                        Material[] mats = new Material[mesh.assetRef.subMeshCount];
                        for (int materialIdx = 0; materialIdx < mesh.assetRef.subMeshCount; ++materialIdx)
                        {
                            mats[materialIdx] = mesh.materialRefs[materialIdx].assetRef;
                        }

                        MeshRenderer mr = obj.AddComponent <MeshRenderer>();
                        mr.sharedMaterials = mats;

                        String RevitLayer = node.Attributes["layer"].Value;
                        //Dictionary<String, String> metadata = new Dictionary<string, string>();
                        //actorMetadata.TryGetValue(node.Attributes["name"].Value, out metadata);

                        // Process imported metadata and try to do something reasonable with it
                        {
                            //String RevitCategory;
                            //metadata.TryGetValue("Element_Category", out RevitCategory);
                            if (RevitLayer != null)
                            {
                                if (Regex.Match(RevitLayer, @"Ceilings", RegexOptions.IgnoreCase).Success)
                                {
                                    // Apply ceiling height offset
                                    Vector3 p = obj.transform.position;
                                    p.z += m_CeilingHeightOffset;
                                    obj.transform.position = p;
                                }

                                if (Regex.Match(RevitLayer, @"Floors", RegexOptions.IgnoreCase).Success)
                                {
                                    // Apply floor height offset
                                    Vector3 p = obj.transform.position;
                                    p.z += m_FloorHeightOffset;
                                    obj.transform.position = p;
                                }


                                if (Regex.Match(RevitLayer, m_IgnoredLayerRegex, RegexOptions.IgnoreCase).Success)
                                {
                                    // Default-hidden objects. For example, "Entourage" and "Planting" objects are not exported correctly by Datasmith (no materials/textures), so we hide them.
                                    obj.SetActive(false);
                                }
                                else if (Regex.Match(RevitLayer, m_StaticTangibleLayerRegex, RegexOptions.IgnoreCase).Success)
                                {
                                    // Completely static objects that should be lightmapped and have collision enabled
                                    GameObjectUtility.SetStaticEditorFlags(obj, StaticEditorFlags.LightmapStatic | StaticEditorFlags.OccludeeStatic | StaticEditorFlags.OccluderStatic | StaticEditorFlags.BatchingStatic | StaticEditorFlags.ReflectionProbeStatic);

                                    // Collision
                                    MeshCollider collider = obj.AddComponent <MeshCollider>();
                                    collider.cookingOptions = (MeshColliderCookingOptions.CookForFasterSimulation | MeshColliderCookingOptions.EnableMeshCleaning | MeshColliderCookingOptions.WeldColocatedVertices);
                                }
                                else if (Regex.Match(RevitLayer, m_StaticIntangibleLayerRegex, RegexOptions.IgnoreCase).Success)
                                {
                                    // Completely static objects that should be lightmapped, but don't need collision
                                    GameObjectUtility.SetStaticEditorFlags(obj, StaticEditorFlags.LightmapStatic | StaticEditorFlags.OccludeeStatic | StaticEditorFlags.OccluderStatic | StaticEditorFlags.BatchingStatic | StaticEditorFlags.ReflectionProbeStatic);
                                }
                                else if (Regex.Match(RevitLayer, m_PhysicsPropsLayerRegex).Success)
                                {
                                    // Clutter that can be physics-enabled

                                    MeshCollider collider = obj.AddComponent <MeshCollider>();
                                    collider.cookingOptions = (MeshColliderCookingOptions.CookForFasterSimulation | MeshColliderCookingOptions.EnableMeshCleaning | MeshColliderCookingOptions.WeldColocatedVertices);

#if USE_VRC_SDK3
                                    if (m_SetupPhysicsProps)
                                    {
                                        collider.convex = true;

                                        Rigidbody rb = obj.AddComponent <Rigidbody>();
                                        // rb.collisionDetectionMode = CollisionDetectionMode.Continuous; // Higher quality collision detection, but slower.

                                        // Add VRCPickup component to make the object interactable
                                        VRC.SDK3.Components.VRCPickup pickup = obj.AddComponent <VRC.SDK3.Components.VRCPickup>();

                                        pickup.pickupable = true;
                                        pickup.allowManipulationWhenEquipped = true;

                                        // Add UdonBehaviour component to replicate the object's position
                                        VRC.Udon.UdonBehaviour udon = obj.AddComponent <VRC.Udon.UdonBehaviour>();
                                        udon.SynchronizePosition = true;

                                        // 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)
                                    }
#endif
                                }
                                else
                                {
                                    ctx.LogImportWarning(String.Format("Unhandled Layer \"{0}\" -- ActorMesh \"{1}\" will not have physics and lighting behaviours automatically mapped", RevitLayer, objName));
                                }

                                if (Regex.Match(RevitLayer, @"Lighting").Success)
                                {
                                    // 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.
                                    mr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
                                }
                            }
                        }
                    }
                    else
                    {
                        ctx.LogImportError(String.Format("ActorMesh {0} mesh {1} assetRef is NULL", obj.name, meshName));
                    }
                }
            } // ActorMesh

            if (node.Name == "Light" && m_ImportLights)
            {
                Light light = obj.AddComponent <Light>();
                if (node.Attributes["type"].Value == "PointLight")
                {
                    light.type = LightType.Point;
                }
                else
                {
                    ctx.LogImportWarning(String.Format("Light {0}: Unhandled \"type\" \"{1}\", defaulting to Point", objName, node.Attributes["type"].Value));
                }

                { // Color temperature or RGB color
                    XmlNode colorNode = node.SelectSingleNode("child::Color");
                    if (colorNode != null)
                    {
                        if (Int32.Parse(colorNode.Attributes["usetemp"].Value) != 0)
                        {
                            float colorTemperature = Single.Parse(colorNode.Attributes["temperature"].Value);
                            // There doesn't appear to be a way to turn on color temperature mode on the Light programmatically (why?)
                            // Convert it to RGB; algorithm borrowed from https://tannerhelland.com/2012/09/18/convert-temperature-rgb-algorithm-code.html

                            float tmpKelvin = Mathf.Clamp(colorTemperature, 1000.0f, 40000.0f) / 100.0f;
                            // Note: The R-squared values for each approximation follow each calculation
                            float r = tmpKelvin <= 66.0f ? 255.0f :
                                      Mathf.Clamp(329.698727446f * (Mathf.Pow(tmpKelvin - 60.0f, -0.1332047592f)), 0.0f, 255.0f); // .988

                            float g = tmpKelvin <= 66 ?
                                      Mathf.Clamp(99.4708025861f * Mathf.Log(tmpKelvin) - 161.1195681661f, 0.0f, 255.0f) :         // .996
                                      Mathf.Clamp(288.1221695283f * (Mathf.Pow(tmpKelvin - 60.0f, -0.0755148492f)), 0.0f, 255.0f); // .987

                            float b = tmpKelvin >= 66 ? 255 :
                                      tmpKelvin <= 19 ? 0 :
                                      Mathf.Clamp(138.5177312231f * Mathf.Log(tmpKelvin - 10.0f) - 305.0447927307f, 0.0f, 255.0f); // .998

                            light.color = new Color(r / 255.0f, g / 255.0f, b / 255.0f);
                        }
                        else
                        {
                            float r = Single.Parse(colorNode.Attributes["R"].Value);
                            float g = Single.Parse(colorNode.Attributes["G"].Value);
                            float b = Single.Parse(colorNode.Attributes["B"].Value);
                            light.color = new Color(r, g, b);
                        }
                    }
                }

                // Common light parameters
                if (m_SetupLightmapBaking)
                {
                    light.lightmapBakeType = LightmapBakeType.Baked;
                }

                GameObjectUtility.SetStaticEditorFlags(obj, StaticEditorFlags.LightmapStatic | StaticEditorFlags.OccludeeStatic | StaticEditorFlags.OccluderStatic | StaticEditorFlags.BatchingStatic | StaticEditorFlags.ReflectionProbeStatic);
            }

            { // children node processing
                XmlNode childrenNode = node.SelectSingleNode("child::children");
                if (childrenNode != null)
                {
                    // TODO obey visible="true" / visible="false" attribute on children node
                    ImportActorChildren(ctx, obj, childrenNode);
                }
            } // children node processing
        }     // child loop
    }