这里面的Revit转GLTF项目(RevitExportGltf)可以将Revit模型转成gltf模型,确实挺好用的,就是有很多小毛病,现在我把这些都优化了一下)
优化方式:
OnMaterial(MaterialNode node)方法调用的是最频繁的,为了避免反复使用RevitAPI读取材质文件,将材质基本信息做成全局缓存,如果是重复的,直接读取而不是再调RevitAPI;OnElementBegin(ElementId elementId),- 去掉string和double转换过程,全部都使用double类型来进行存储;
- 不将需要旋转操作的模型视为相似模型,这些模型全部重新创建一次而不是使用matrix转换,这会让gltf体积小很多。
获取材质信息
- 判断材质是否有效,无效则创建默认材质
- 使用RevitAPI获取材质信息(重点是材质文件路径)并缓存
- 生成gltf材质数据文件,对于存在材质文件的,将材质文件复制到生成目录下,并生成
sampler和texture数据结构;不存在的根据节点盐酸直接设置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有出入。
具体有两种解决思路:
- 放弃旋转矩阵,只要涉及到旋转的,统统排除掉相似模型之外,gltf中只有translation属性;
- 找到形状不一致或坐标系不一致的,仅将这些排除掉相似模型之外,gltf中有translation属性和matrix属性; 测试结果如下:第一种方法速度和体积均优于第二种;
优化后的代码如下:
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 };
}