Revit转GLTF优化思路

421 阅读5分钟

参考地址:github.com/weiyu666/Re…

这里面的Revit转GLTF项目(RevitExportGltf)可以将Revit模型转成gltf模型,确实挺好用的,就是有很多小毛病,现在我把这些都优化了一下)

优化方式:

  1. OnMaterial(MaterialNode node) 方法调用的是最频繁的,为了避免反复使用RevitAPI读取材质文件,将材质基本信息做成全局缓存,如果是重复的,直接读取而不是再调RevitAPI;
  2. OnElementBegin(ElementId elementId)
    • 去掉string和double转换过程,全部都使用double类型来进行存储;
    • 不将需要旋转操作的模型视为相似模型,这些模型全部重新创建一次而不是使用matrix转换,这会让gltf体积小很多。

获取材质信息

  1. 判断材质是否有效,无效则创建默认材质
  2. 使用RevitAPI获取材质信息(重点是材质文件路径)并缓存
  3. 生成gltf材质数据文件,对于存在材质文件的,将材质文件复制到生成目录下,并生成samplertexture数据结构;不存在的根据节点盐酸直接设置 baseColorFactor属性即可
class MaterialTemp
{
    public string name { get; set; }
    public string path { get; set; }
    public string id { get; set; }
    public bool isFileExist { get; set; }
}
// 材质文件路径
readonly HashSet<string> txtPathList = new HashSet<string>();
readonly Dictionary<string, MaterialTemp> mTempDict = new Dictionary<string, MaterialTemp>();
public void OnMaterial(MaterialNode node)
{
    Output.AddMsg("OnMaterial Start");
    ElementId id = node.MaterialId;
    if (id == ElementId.InvalidElementId)
    {
        CreateDefaultMaterial(node);
        return;
    }
    Element m = doc.GetElement(id);
    // 避免重复查找
    MaterialTemp temp;
    if (!mTempDict.ContainsKey(m.UniqueId))
    {
        // 查找材质文件路径
        (bool b1, string path1) = FindTexturePath(node);
        temp = new MaterialTemp()
        {
            name = m.Name,
            isFileExist = b1,
            path = path1,
            id = m.UniqueId,
        };
        mTempDict.Add(m.UniqueId, temp);
    }
    else temp = mTempDict[m.UniqueId];


    string matName = temp.name;
    string uniqueId = temp.id;
    string path = temp.path;
    /* 上面是RevitAPI               
     * 下面都是数据处理模块
     */
    glTFMaterial gl_mat = new glTFMaterial()
    {
        name = matName,
    };
    glTFPBR pbr = new glTFPBR
    {
        metallicFactor = 0f,    //金属感强度
        roughnessFactor = 1f    //粗糙感强度
    };
    //第四个值是材料的Alpha覆盖率。该alphaMode属性指定如何解释alpha
    double alpha = Math.Round(node.Transparency, 2);
    if (alpha != 0)
    {
        gl_mat.alphaMode = "BLEND";
        gl_mat.doubleSided = "true";
        alpha = 1 - alpha;
    }
    if (temp.isFileExist)
    {
        string name = Path.GetFileName(path);
        /* 1. 添加本地文件
         * 2. 添加gltf本地文件信息
         */
        if (txtPathList.Add(path))
        {
            string dest = Path.Combine(directory, name);
            File.Copy(path, dest, true);
        }
        var bcr = CreateAndAddTextureInfo(uniqueId, name);
        pbr.baseColorTexture = bcr;
    }
    else
    {
        pbr.baseColorFactor = new List<float>() { node.Color.Red / 255f, node.Color.Green / 255f, node.Color.Blue / 255f, (float)alpha / 1f };
    }
    gl_mat.pbrMetallicRoughness = pbr;
    Materials.AddOrUpdateCurrent(uniqueId, gl_mat);

    Output.AddMsg("OnMaterial End");
}
 private (bool, string) FindTexturePath(MaterialNode node)
 {
     Asset currentAsset = node.HasOverriddenAppearance ? node.GetAppearanceOverride() : currentAsset = node.GetAppearance();
     if (currentAsset == null) return (false, null);
     Asset asset = FindTextureAsset(currentAsset);
     if (asset == null) return (false, null);

     if (asset["unifiedbitmap_Bitmap"] is AssetPropertyString propertyString)
     {
         string textureFile = propertyString.Value.Split('|')[0];
         if (!string.IsNullOrEmpty(textureFile))
         {
             // 用Asset中贴图信息和注册表里的材质库地址得到贴图文件所在位置
             string texturePath = Path.Combine(textureFolder, textureFile.Replace("/", "\\"));
             return (File.Exists(texturePath), texturePath);
         }

     }
     return (false, null);
 }
 /// <summary>
 /// 将纹理图片信息添加到gltf中
 /// </summary>
 /// <param name="id"></param>
 /// <param name="name"></param>
 /// <returns></returns>
 private glTFbaseColorTexture CreateAndAddTextureInfo(string id, string name)
 {
     glTFImage image = new glTFImage();
     //通过 uri定位到图片资源
     //image.uri = "./" + dirName + "/" + name;
     image.uri = "./" + name;
     Images.AddOrUpdateCurrent(id, image);
     //取样器,定义图片的采样和滤波方式
     glTFSampler sampler = new glTFSampler
     {
         name = "default_sampler",
         magFilter = 9729,           //线性mipmap取原图中相邻像素并使用线性插值获得中间值来填充新点的颜色
         minFilter = 9987,           //最邻近过滤mipmap过滤
         wrapS = 10497,              //wrapS 、wrapT纹理在水平、垂直方向上纹理包裹方式
         wrapT = 10497
     };
     Samplers.AddOrUpdateCurrent(id, sampler);
     //贴图信息,使用source和ssampler指向图片和采样器
     glTFTexture texture = new glTFTexture
     {
         source = Images.CurrentIndex,
         sampler = Samplers.CurrentIndex
     };
     Textures.AddOrUpdateCurrent(id, texture);
     //贴图索引
     glTFbaseColorTexture bct = new glTFbaseColorTexture();
     bct.index = Textures.CurrentIndex;
     //pbr.baseColorTexture = bct;
     return bct;
 }

 private void CreateDefaultMaterial(MaterialNode node)
 {
     glTFMaterial gl_mat = new glTFMaterial();
     string uuid = $"r{node.Color.Red}g{node.Color.Green}b{node.Color.Blue}";
     string matName = $"MaterialNode_{Util.ColorToInt(node.Color)}_{Util.RealString(node.Transparency * 100)}";
     gl_mat.name = matName;
     glTFPBR pbr = new glTFPBR();
     pbr.baseColorFactor = new List<float>() { node.Color.Red / 255f, node.Color.Green / 255f, node.Color.Blue / 255f, 1f };
     pbr.metallicFactor = 0f;
     pbr.roughnessFactor = 1f;
     gl_mat.pbrMetallicRoughness = pbr;
     Materials.AddOrUpdateCurrent(uuid, gl_mat);
 }

创建gltf模型

主要实现过程都在OnElementBegin(ElementId elementId)中。由于旋转矩阵会让gltf体积变大,而且最要命的在Revit中,很多模型的形状是一样的,但坐标系不一致,而插件是通过模型的坐标系来进行旋转变换的,如果视为相似模型,这会导致生成的gltf跟Revit有出入。 具体有两种解决思路:

  1. 放弃旋转矩阵,只要涉及到旋转的,统统排除掉相似模型之外,gltf中只有translation属性;
  2. 找到形状不一致或坐标系不一致的,仅将这些排除掉相似模型之外,gltf中有translation属性和matrix属性; 测试结果如下:第一种方法速度和体积均优于第二种;

image.png

优化后的代码如下:

 RenderNodeAction OnElementBegin(ElementId elementId)
 {
     Output.CurrentIndex++;
     index = 0; currentElem = doc.GetElement(elementId);
     string uniqueId = currentElem.UniqueId;
     string id = currentElem.Id.ToString();
     string name = currentElem.Name;
     Output.AddMsg($"{id},{name}:OnElementBegin");
     Category category = currentElem.Category;
     string categoryName = category?.Name;
     if (Nodes.Contains(uniqueId)) return RenderNodeAction.Skip;
     if (category != null)
     {
         if ((BuiltInCategory)category.Id.IntegerValue == BuiltInCategory.OST_Cameras ||
            category.CategoryType == CategoryType.AnalyticalModel)
         {
             Debug.WriteLine($"{currentElem.Name}该构件为相机或分析模型,跳过");
             return RenderNodeAction.Skip;
         }
     }
     isHasSimilar = false;
     // 涉及到RevitAPI操作 获取 模型所有信息;
     var info = GetElementGeoInfo(currentElem);
     currentData = new ObjectData
     {
         ElementId = id,
         ElementName = name,
         ElementArea = info.area,
         ElementVolume = info.volume,
         ElementLocation2 = new List<XYZ>(),
         handOri = info.handOri,
     };
     // 获取几何构造

     List<string> Names = new List<string>() { categoryName, info.familyName, name };
     InstanceNameData = null;
     // 创建树,设置InstanceNameData
     getItem(RootDatas, Names);
     // 获取中心点
     if (info.handOri != null && info.vect != null)
     {
         // 添加三个点
         currentData.ElementLocation2.Add(info.center);
         currentData.ElementLocation2.Add(info.handOri);
         currentData.ElementLocation2.Add(info.vect);
     }

     if (InstanceNameData != null)
     {
         foreach (ObjectData similar in InstanceNameData.Children)
         {
             //break;
             // 添加相似对象 仅将能通过平移操作得到的模型 添加到列表中,旋转操作不添加;
             if (similar.ElementArea == currentData.ElementArea && similar.ElementVolume == currentData.ElementVolume && similar.ElementArea != null && similar.ElementVolume != null &&
                 similar.handOri != null && currentData.handOri != null &&
                 similar.handOri.IsAlmostEqualTo(currentData.handOri))
             {
                 if (similar.SimilarObjectID != null)
                 {
                     currentData.SimilarObjectID = similar.SimilarObjectID;
                 }
                 else
                 {
                     currentData.SimilarObjectID = similar.ElementId;
                 }
                 similar.Children.Add(currentData);
                 isHasSimilar = true;
                 break;
             }
         }
         if (currentData.SimilarObjectID == null)
         {
             InstanceNameData.Children.Add(currentData);
         }
     }

     //新节点
     glTFNode newNode = new glTFNode()
     {
         name = $"{name}[{id}]"
     };
     Debug.WriteLine("Finishing...");
     currentDatas.AddOrUpdateCurrent(id, currentData);
     if (currentData.ElementLocation2.Count == 0 || currentData.ElementLocation2[0] == null || currentElem is Mullion)
     {
         isHasSimilar = false;
     }
     if (isHasSimilar)
     {
         List<XYZ> SimilarPoints = currentDatas.
            GetElement(currentData.SimilarObjectID).ElementLocation2;
         List<XYZ> CurrentPoints = currentData.ElementLocation2;
         SetNewNodeGeoInfo(SimilarPoints, CurrentPoints, ref newNode);
     }
     Nodes.AddOrUpdateCurrent(id, newNode);
     //将此节点的索引添加到根节点子数组中
     rootNode.children.Add(Nodes.CurrentIndex);
     if (isHasSimilar == true)
     {
         Output.AddMsg($"OnElementBegin isHasSimilar==true");
         return RenderNodeAction.Skip;
     }
     //几何元素
     currentGeometry = new IndexedDictionary<GeometryData>();


     Output.AddMsg($"OnElementBegin END");
     return RenderNodeAction.Proceed;
 }
 /// <summary>
 /// 根据原有模型 生成新模型;(不考虑旋转操作)
 /// </summary>
 /// <param name="SimilarPoints"></param>
 /// <param name="CurrentPoints"></param>
 /// <param name="newNode"></param>
 private void SetNewNodeGeoInfo(List<XYZ> SimilarPoints, List<XYZ> CurrentPoints, ref glTFNode newNode)
 {
     XYZ p1 = SimilarPoints[0];
     double x1 = p1.X;
     double y1 = p1.Y;
     double z1 = p1.Z;
     //当前构件的点
     XYZ elementPoint1 = CurrentPoints[0];
     double px1 = elementPoint1.X;
     double py1 = elementPoint1.Y;
     double pz1 = elementPoint1.Z;
     //平移第一个点
     double MoveX = Math.Round(px1 - x1, 2);
     double MoveY = Math.Round(py1 - y1, 2);
     double MoveZ = Math.Round(pz1 - z1, 2);
     Output.AddMsg($"Prepare Translation, {currentElem.Id.IntegerValue},{currentElem.Name}");
     // 位移
     newNode.translation = new List<double>() { MoveX, MoveZ, -MoveY, };
 }

GetElementGeoInfo:

class ElementGeoInfo
{
    public double[] Center { get; set; }
    public bool IsValid { get => Center != null; }
    public XYZ center { get => Center != null ? new XYZ(Center[0], Center[1], Center[2]) : null; }

    public XYZ handOri { get; set; }
    public string familyName { get; set; }
    public string area { get; set; }
    public string volume { get; set; }
    public XYZ vect { get; set; }
}
/// <summary>
/// 获取元素几何信息
/// </summary>
/// <param name="element"></param>
/// <returns></returns>
private ElementGeoInfo GetElementGeoInfo(Element element)
{

    Category category = element.Category;
    string familyName = element.get_Parameter(BuiltInParameter.ELEM_FAMILY_PARAM)?.AsValueString();
    if (string.IsNullOrWhiteSpace(familyName)) familyName = category?.Name;
    // 
    ElementGeoInfo info = new ElementGeoInfo()
    {
        familyName = familyName,
        area = element.get_Parameter(BuiltInParameter.HOST_AREA_COMPUTED)?.AsValueString(),
        volume = element.get_Parameter(BuiltInParameter.HOST_VOLUME_COMPUTED)?.AsValueString(),
    };
    Options options = new Options();
    GeometryElement geometry = element.get_Geometry(options);
    foreach (GeometryObject obj in geometry)
    {
        if (obj is Solid)
        {
            Solid solid = obj as Solid;
            GetTopPoints(solid, ref info);
        }
        else  //取得族实例几何信息的方法
        {
            GeometryInstance geoInstance = obj as GeometryInstance;
            GeometryElement geoElement = geoInstance.GetInstanceGeometry();
            foreach (GeometryObject obj2 in geoElement)
            {
                Solid solid = obj2 as Solid;
                GetTopPoints(solid, ref info);
                if (info.IsValid)
                {
                    break;
                }

            }
        }
        if (info.IsValid) break;
    }
    if (element.Location is LocationPoint && element is FamilyInstance instance)
    {
        info.handOri = instance.HandOrientation;
        info.vect = info.handOri.CrossProduct(instance.FacingOrientation);
    }
    return info;
}
/// <summary>
/// 找到体的所有顶点
/// </summary>
/// <param name="solid"></param>
/// <returns></returns>
private void GetTopPoints(Solid solid, ref ElementGeoInfo info)
{
    if (solid == null) return;
    //ElementGeoInfo info = new ElementGeoInfo() { };
    FaceArray faceArray = solid.Faces;
    foreach (Face face in faceArray)
    {
        PlanarFace pf = face as PlanarFace;
        if (pf != null && Math.Round(pf.FaceNormal.Z, 2) < 0)
        {
            EdgeArrayArray edgeArrays = face.EdgeLoops;
            // 根据线的密度来确定中心点;
            foreach (EdgeArray edges in edgeArrays)
            {
                List<double[]> points = new List<double[]>();
                // 使用哈希表去除重复顶点
                HashSet<string> str = new HashSet<string>();
                foreach (Edge edge in edges)
                {
                    foreach (XYZ point in edge.Tessellate())
                    {
                        if (str.Add(point.Point()))
                        {
                            points.Add(point.Point1());
                        }
                    }
                    info.Center = GetCenter1(points);
                }
            }
            if (info.IsValid) break;
        }
    }
}
private double[] GetCenter1(List<double[]> PLPoint)
{
    double SumX = 0;
    double SumY = 0;
    double SumZ = 0;
    for (int i = 0; i < PLPoint.Count; i++)
    {
        //string[] Points = PLPoint[i].Split(new char[] { ',' });
        double[] Points = PLPoint[i];
        if (Points == null || Points.Length < 3) return null;
        //  
        SumX += Points[0];
        SumY += Points[1];
        SumZ += Points[2];
    }
    double Xc = SumX / (PLPoint.Count);
    double Yc = SumY / (PLPoint.Count);
    double Zc = SumZ / (PLPoint.Count);
    return new double[] { Xc, Yc, Zc };
}

工具类:

static class Util
{
    public static string Point(this XYZ p) => $"{p.X:F2},{p.Y:F2},{p.Z:F2}";
    public static double[] Point1(this XYZ p) => new double[] { p.X, p.Y, p.Z };
}