在机械测量过程中,测量的数据需要进行软件处理。通常测量一个零件之后,需要重建零件的3D模型,便于观察测量结果是否与所测工件一致。 重建的3D模型需要以填充图和线框图两种方式切换显示,其中填充图的材质需要根据不同深度进行着色,线框图需要消隐(不能透视)。以圆柱为例,如下图: 由于WPF对DirectX进行了封装,并构建出一套简单的3D绘图框架,因此我们可以快速的创建所需要的3D模型,便于像我这样的对三维计算机图形学不太了解的人进行开发。 关于WPF 3D绘图的基础,大家可以参考《Practical WPF Charts and Graphics》一书,里面也讲到一些3D显示的计算机图形学和数学基础。 下面我们开始3D模型的构建。 在WPF中,我们可以按如下步骤进行操作: 1、创建一个Viewport3D对象来host 三维模型; 2、继承ModelVisual3D或者ModelUIElement3D来定义3D对象; 3、指定3D场景中的光源,在WPF中光源也是一种ModelVisual3D; 4、创建一个相机来进行3D图形到2D显示器的映射。 根据工程中的经验,我们考虑传感器采集的数据是圆柱体工件的五个截面(实际可能更多或者更少),每个截面包含8192(或者更多)个点,并且是极坐标格式数据。 这里我们使用ModelVisual3D来定义三维物体。其中重要的是三角剖分和材质贴图的部分。由于每一层数据点个数都是一致的,因此我们可以将圆柱体分成三个部分,上下底面和侧面。参考《Practical WPF Charts and Graphics》,我们可以把圆柱的侧面分成多个四边形,每个四边形分成两个三角形,并将上下底面分成多个三角形(类似扇形)。 剖分方式大致如下图: 由于数据点坐标是极坐标,每一层有不同的高度(即三维坐标系中的Y),因此需要将极坐标转化为三维坐标系中的坐标,即Point3D结构,坐标系如下图: 需要注意的是,我们绘制的3D模型是三个曲面构成,每个曲面没有封闭,这样看上去会在首尾相接处出现缺口,因此需要多加一些三角形将首尾进行缝合。未缝合的模型如下: 在三维模型创建结束之后,需要贴上材质,由于工程中需要体现出不同深度(即不同半径 )的差别,需要使用渐变色进行呈现,类似Matlab中的效果 参考我转载的一篇文章(http://blog.csdn.net/congduan/article/details/21092341),就可以得出基本思路:在计算模型的时候,根据不同的R值,生成一张RGB color的映射表,并将其作为材质应用在3D模型上。 参考Codeproject上http://www.codeproject.com/KB/WPF/WPFChart3D.aspx这篇文章的源码,其中的TextureMapping.cs文件便是进行材质颜色映射的类,可以根据需要将多种颜色改写成2种颜色等等,做出适合自己的定制。当然,这个类使用了C#指针(unsafe关键字),需要在项目属性生成一项中开启不安全代码支持。以下是我修改例子,做出的两种颜色的渐变图: 以下便是参考该文例子写出的本文圆柱的代码:
for (int i = 0; i < drawPointCount; i++){ for (int j = 0; j < PointCollectionList.Count; j++){//从极坐标构造笛卡尔坐标系数据 points[i, j] = MathHelper.GetPosition(baseRadius +ZoomContourValue * (PointCollectionList[j][i * interval].Y - fittedCircles[j].Radius) / (maxR - minR),PointCollectionList[j][i * interval].X,(j - halfPointCollectionCount) * h,(Vector3D)Center); //根据表面半径差值,生成RGB颜色纹理坐标double u = (PointCollectionList[j][i * interval].Y - minR) / (maxR - minR);Color color = TextureMapping.PseudoColor(u);Point mapPt = m_mapping.GetMappingPosition(color);texcoords[i, j] = new Point(mapPt.X, mapPt.Y);}}注意其中半径都是归一化之后才生成颜色值的。 最后将TextureMapping生成的材质贴到模型上。
//填充表面材质surfaceModel = new GeometryModel3D(surfaceMeshBuilder.ToMesh(), m_mapping.m_material);====================================================================== 对于线框图(WireFrame),WPF支持的不好,因为WPF中没有3D线段的类,好在微软的人意识到了这一点,开发出开源的3D Tools for WPF(http://3dtools.codeplex.com/)作为补充,其中就有ScreenSpaceLines3D类用于3D线段绘制。这样前面的填充图也可以加上轮廓了。 但是,问题又来了。绘制出的线框图是下面这样的: 由于没有消隐,很多线段交叉重叠在一起,影响判断。《Practical WPF Charts and Graphics》一文中的例子是这样处理的,在前面填充图的基础上加上线框,但是填充的材质改成纯白色,并将线框线条加粗。效果还是不错的,如下: 但是我这样试过之后,发现一些问题,偶尔线条会被填充白色的三角形遮挡住,为了解决这个问题,我找到OpenGL的C#开源封装库,比如OpenTK和SharpGL,其中SharpGL中做如下处理之后会得到较好的效果:
//突出边框,设置多边形偏移gl.Enable(OpenGL.GL_POLYGON_OFFSET_FILL);gl.PolygonOffset(1.0f, 1.0f);//开启反走样gl.Enable(OpenGL.GL_BLEND);gl.Enable(OpenGL.GL_LINE_SMOOTH);gl.Enable(OpenGL.GL_POLYGON_SMOOTH);gl.Hint(OpenGL.GL_POINT_SMOOTH_HINT, OpenGL.GL_NICEST); // Make round points, not square pointsgl.Hint(OpenGL.GL_LINE_SMOOTH_HINT, OpenGL.GL_NICEST); // Antialias the linesgl.BlendFunc(OpenGL.GL_SRC_ALPHA, OpenGL.GL_ONE_MINUS_SRC_ALPHA);效果: 可以SharpGL未能很好地利用GPU,将数据全部拷贝到内存中,导致内存暴涨,甚至有时候内存泄露。因此最后放弃了这个库。而OpenTK并没有WPF的控件,只能使用WindowsFormsHost来host WinForm的控件,不是很方便。 后来使用Helix 3D Toolkit,它将WPF和SharpDX进行了再次封装,并提供了一些比较好的3D算法。并使用其中的MeshBuilder更方便地创建三角形和3D线段,最终出现本文开始的效果。 最后,重点提一下线框图中的线段分布的算法。 显示线框图的时候,会绘制一部分竖线,开始考虑的是均匀间隔绘制,但是竖线绘制多少,是一个需要仔细思考的问题。线段多了,损耗性能,线段少了,轮廓的还原度不会搞,尤其是一些凹槽和凸出的部分会因为过于稀疏的线段无法显示出来,如下图的部分: 思考了很久,想到一种比较满意的算法:计算每一层所有数据点的陡变程度,提取前100个(可以根据不同疏密需要的情况进行选取)陡变最大的点绘制竖线。 由于数据点是离散的,可以使用离散曲率刻画点的陡变程度,离散曲率的计算公式如下: 参考论文《一种度量图像像素陡变程度的方法》可以知道,这种连续曲线曲率直接离散化的结果并不是很好,并且该论文提出了一种更好的算法,具体算法大家可以去阅读论文原文。公式如下: 有了这样的评判标准,实现之后变成功将需要的轮廓比较完整地显示了出来。由于数据点很多,计算公式也比较复杂,最好是放入后台线程异步处理,避免UI线程阻塞。 最后,注意大量数据点会造成三角形过多(这里会达到20W个左右),导致构建性能问题突出,最好在保证3D模型还原度高的情况下,进行稀疏处理,减少三角形数量。