最近需要在项目的软件中增加一个功能,根据连续测斜数据展示三维的井眼轨迹图,由于购买的厂商的图件效果不理想,所以研究自己写代码实现类似的功能。
井眼轨迹的计算内容繁杂涉及了大量的钻井工程专业专业知识。由于笔者是信息技术专业出身,所以只能依照自己的行业经验研究井眼轨迹的三维技术实现。其中涉及到钻井工程细节部分可能不准确或错误,本文内容仅供技术实现方面参考。
支持3D绘图方面的技术框架很多,本文介绍使用SharpGL这个开源项目来实现三维井眼轨迹图。
SharpGL 可以让你在 Windows Forms 或者 WPF 应用中轻松的使用 OpenGL 开发图形应用。从核心内容来说,SharpGL是一个OpenGL API的封装。一般来说,OpenGL API可以直接用于C/C++应用的开发,但是使用起来比较复杂, SharpGL直接提供了OpenGL全部的功能和扩展。SharpGL将所有的函数和一组丰富的对象,以及高级功能的对象集合放到一个包装器中,你可以使用SharpGL执行opengl绘图。不过SharpGL也包括一些不属于OpenGL的内容,针对WinForms和WPF的用户控件用户控件提供了OpenGL渲染界面和用于处理类似于shader和纹理等复杂问题能力。甚至提供了一个高级类SceneGraph可以更好的使用面向对象的思想创建各类场景。
SharpGL背后的原理是OpenGL in .NET, SharpGL并不是一个需要重新学习的新框架,它只不过是封装的OpenGL。
为什么不直接使用OpenGL,而是使用SharpGL呢?
首先是我喜欢做.Net开发,可以使用托管代码轻松调用C API,Dlllmport可以方便的调用这些API。但是必须要为所有的函数创建签名。如果发生错误,整个错误将是非常庞大的,并且很难分析错误。
另外一个使用SharpGL的原因是SharpGL可以作为标准平台调用来调用多数OpenGL函数,而不用创建外部方法的签名。OpenGL扩展函数在运行时被夹在-这就没有一个固定的进入点进入DLL,这样就增加了工作量。
在OpenGL中获得一个RD是比较困难的,底层的Win32代码有大量的函数获得像素格式,这些工作是大量重复和痛苦的,好在这一切SharpGL帮我做了。
最后一个选择使用SharpGL的原因是,在OpenGL中大量的很痛苦的重复的工作如加载信息等,但是这些工作在.NET中是非常容易处理的。
OpenGL内容很多,而且非常成熟,相应的SharpGL内容也很多, 我不会在本文中写出太多细节,我们只是用了其中很简单(小)的一部分内容,在写代码过程中发现网上SharpGL的中文资料很有限。谷歌的原因英文资料找起来也很费劲,SharpGL缺少一个官方技术社区,值得一说的是官方提供了比较详细的示例代码,说实话其中很多代码都是参考官方示例写的。
SharpGL中的主要对象介绍:
SharpGL - 包含主OpenGL对象- 这个对象包装所有的OpenGL函数,枚举和扩展。
SharpGL.SceneGraph 包含OpenGL对象和场景元素光。材质。纹理。NURBs。着色器和其他对象的所有包装。
SharpGL.WinForms - 包含应用程序的Windows 窗体控件。
SharpGL.WPF - 包含用于你的应用程序的WPF控件。
SharpGL.Serialization - 包含用于从 3D Studio Max文件。谨慎obj文件和trueSpace文件加载几何图形和数据的类。
实际中井连续测斜数据如下:
测量井深(斜深):指井口至测点的井眼长度。
井斜角:井身轴线上某点的切线与垂线之间的夹角。
方位角:井身轴线上某点的切线的投影与正北方向的夹角。
依靠这三个参数可以绘制井眼轨迹,具体做法是通过这三个参数计算垂深、东西位移、南北位移。分别映射到SharpGL三维模型中世界坐标的Y轴坐标、X轴坐标、Z轴坐标。井眼轨迹参数计算本文不作介绍,感兴趣的朋友可以去查找钻井工程计算相关知识,我们这里只介绍软件实现方面内容。
4.1 三维绘图中坐标系简单介绍
二维绘图:笛卡尔坐标有一个X轴和一个Y轴组成,X轴为水平方向,Y轴为垂直方向,X和Y相互垂直
三维绘图:笛卡尔坐标多了一个Z轴,Z轴同时垂直于X和Y轴。Z轴的实际意义代表着三维物体的深度
为了描述3D世界,首先要设计一些三维模型出来。
设计三维模型的时候用的坐标系就是Model Coordinate System。
Model Space只负责描述一个模型。在Model Space设计模型的时候,要注意使模型的包围盒的中心位于原点(0, 0,0)。
包围盒就是能够把模型包围的最小的长方体。为什么要围绕原点?因为这样才能在下文所述的WorldSpace里"正常地"旋转、缩放和平移模型。
世界坐标系 它是一个特殊的坐标系,它建立了描述其他坐标系所需要的参考系。也就是说,可以用世界坐标系去描述其他所有坐标系或者物体的位置。所以有很多人定义世界坐标系是“我们所关心的最大坐标系”,通过这个坐标系可以去描述和刻画所有想刻画的实体。
世界坐标系又称全局坐标系或者宇宙坐标系。
5种比较重要的坐标系系统
局部空间(Local Space,或者称为物体空间(Object Space))
世界空间(World Space)
观察空间(View Space,或者称为视觉空间(Eye Space))
裁剪空间(Clip Space)
屏幕空间(Screen Space)
将顶点从一个坐标系转换到另一个坐标系需要用到几个变换矩阵,其中几个比较重要的是模型(Model)、观察(View)、投影(Projection)三个矩阵。
物体顶点的起始坐标按序经过上述5个坐标系系统最终转换为屏幕坐标。
坐标系及坐标转换的内容感兴趣的朋友请自行baidu。
项目开始
启动VS,建立一个Windows桌面程序,引入如下Dlls:
在这里我们使用SharpGL.WinForms命名空间中的OpenGLControl 控件。
把控件拖拽到当前窗体中,这里主要用到下面三个方法:
void openGLControl1_OpenGLInitialized(object sender, System.EventArgs e)
用于执行任何OpenGL初始化。
void openGLControl1_Resize(object sender, System.EventArgs e)
Resize方法:控件大小变化是逻辑处理。
privatevoid openGLControl1_OpenGLDraw(object sender, RenderEventArgs e)
绘制方法:用于做OpenGL渲染。
绘制后背景面/左侧背景面
如图所示:灰色的两个面就是左背景面和后背景面
首先需要在openGLControl1_OpenGLDraw方法中获取SharpGL绘制对象
SharpGL.OpenGL gl = this.openGLControl1.OpenGL;
然后开始绘制背景面。
为了测试我们使用两种方式分别绘制后背景面和左侧背景面。
后背景面使用一个图片来渲染,而左侧背景面之间用颜色来绘制,这两种方式显示的效果是相同的。
后背景面:
Texture texture = new Texture();
texture.Create(gl, "20190919154917.png");
加载一个图片当作材质。
绘制后背景面:
gl.Begin(OpenGL.GL_QUADS);
gl.TexCoord(1.0f, 0.0f); /
gl.Vertex(-2.0f, 0.0f, -2.0f); // 右下
gl.TexCoord(1.0f, 1.0f);
gl.Vertex(-2.0f, 4.0f, -2.0f); // 右上
gl.TexCoord(0.0f,1.0f);
gl.Vertex(2.0f, 4.0f, -2.0f);// 左上
gl.TexCoord(0.0f, 0.0f);
gl.Vertex(2.0f, 0.0f, -2.0f);// 左下
然后绘制右侧背景面:
设置灰色背景色
gl.Color(0.8f,0.8f,0.8f);
接着绘制四个顶点
gl.Vertex(-2.0f,0.0f, -2.0f);
gl.Vertex(-2.0f, 0.0f, 2.0f);
gl.Vertex(-2.0f, 4.0f, 2.0f);
gl.Vertex(-2.0f, 4.0f, -2.0f);
gl.End();
gl.Flush();
绘制底部网格
设置OpenGL.GL_LINES类型,绘制网格线。
在X,Z平面上绘制网格
for (float i = -2; i <= 2; i+= 0.5f)
{
//设置类型为绘制线
gl.Begin(OpenGL.GL_LINES);
//X轴方向
gl.Vertex(-2f, 0f, i);
gl.Vertex(2f, 0f, i);
//Z轴方向
gl.Vertex(i, 0f, -2f);
gl.Vertex(i, 0f, 2f);
gl.End();
}
}
绘制东西轴线/南北轴线/深度轴线
使用gl.Begin(OpenGL.GL_LINE_STRIP);来绘制坐标轴线
设置线宽,使用比网格粗一点的线。
gl.LineWidth(2f);
//Y轴方向
gl.Vertex(0.0f, 0.0f, 0.0f);
gl.Vertex(0.0f, 4.0f, 0.0f);
//X轴方向
gl.Vertex(-2.0f, 4.0f, 0.0f);
gl.Vertex(0.0f, 4.0f, 0.0f);
//Z轴方向
gl.Vertex(0.0f, 0.0f, -2.0f);
gl.Vertex(0.0f, 0.0f, 2.0f);
绘制井口横纵投影线
井口横纵投影线有点特殊,用的是虚线。
使用代码:
//设置颜色
gl.Color(0.8f, 0.8f, 0.8f);
//线宽
gl.LineWidth(1f);
//允许绘制虚线
gl.Enable(OpenGL.GL_LINE_STIPPLE);
//设置虚线的种类,具体类型可搜索OpenGL文档
gl.LineStipple(1, 0x3F07);
其它的代码与前面一样:
gl.Vertex(-2.0f, 4.0f, 0.0f);
gl.Vertex(0.0f, 4.0f, 0.0f);
绘制底部东西/南北刻度线/深度刻度线
接下来,我们在底部面上绘制刻度线。
我们把X轴当作东西轴,Z轴当作南部轴。
我们需要在背景面的底部线上和相邻的底部面的一个边上绘制刻度数,比如0米100米200米300米等。
说白了其实就是在不同的屏幕位置绘制文字。
在SharpGL中有两种类型的绘制文字,立体文字和平面文字
立体文字是在世界坐标系上绘制文字,方法是:
gl.DrawText3D,
平面文字是在二维屏幕上绘制文字,对应的方法是:
gl.DrawText
这里我们更适合使用平面文字,我们需要把三维的世界坐标转换成只有x,y的二维屏幕坐标。只有这样当旋转三维图形时候,二维文字一直会显示在正面。
SharpGL中提供了OpenGLSceneGraphExtensions.Project,可以处理此类问题。
绘制刻度的代码:
for (float i = 0; i<= 4;i=i+0.5f )
{
gl.Begin(OpenGL.GL_LINE_STRIP);
gl.Vertex(-0.04f, i, 0f);
gl.Vertex(0.04f, i, 0f);
gl.End();
//获取三维点的二维坐标
var sv =OpenGLSceneGraphExtensions.Project(gl, new Vertex(0.05f, i, 0.0f));
gl.DrawText((int)(sv.X), (int)(sv.Y),1, 1, 1, "宋体",10, (i * 1000).ToString());
}
注意:我们这里只是示例,实际使用过程中可能需要根据一口井连续测斜数据,找到最大的东西位移和南北位移,然后结合井深来确定坐标刻度的大小。比如:东西位移和南北位移加起来不够100米,那最大刻度值就设置成100米。又例如:井深10000米,南北和东西位移都比较小,还需要调整深度和底部面的比例尺范围,让图形显得更正常。而不是去显示一条特别长,没有什么弯度的轨迹线。
绘制深度轴刻度方式与上面的类似。
绘制井眼轨迹线/投影线
井眼轨迹线分真正的井眼轨迹线(黄色),还有在背景面,左侧面,和底部面的投影线。
这里涉及到比例尺换算的问题,我们需要把井的实际井深换算到三维图里的世界坐标位置。具体做法如下根据测斜点测量井深和方位角算出该测点的的实际井垂深,根据垂深算出该测斜点的Y坐标值(比如:井深1000米对应三维高度4)。
根据测点方位角,计算出东西位移和南北位移值,然后换算出X坐标和Y坐标。这些内容与软件技术没什么关系,就不细说了。
设置颜色
gl.Color(1.0f, 1.0f, 0f);
gl.LineWidth(1f);
//绘制连续的曲线
gl.Begin(OpenGL.GL_LINE_STRIP);
//获取井眼轨迹的三维坐标值
var vertexList = LoadWellVertexList()
foreach(var vertex in vertexList)
{
//绘制测点
gl.Vertex(vertex);
}
绘制投影线就更简单了,把测点对应的投影面的坐标设置为0即可。
绘制水平投影图,把所有测点的Z坐标设置为0进行绘制。
绘制井底点水平线
查找到最底部的测点,然后绘制一条到Y轴的直线即可。
缩放/旋转
缩放和旋转就更简单了
声明一个缩放值变量,用鼠标滚轴进行控制,对图进行缩放。
gl.Scale(ScaleValue, ScaleValue, ScaleValue);
旋转使用方法:
gl.Rotate(rtri,0.0f, 1.0f, 0.0f);
感兴趣可以去查手册。
可以在井口处使用三维工具软件设计好的模型,加载进来,效果好看很多。
井眼轨迹也可以使用材质渲染出一个实际的井的外观,现在只画一条线很丑。
增加光照和阴影效果