使用SharpGL三维建模技术生成3D井眼轨迹图
前面的文章里写过使用sharpGL三维建模生产3D井眼轨迹,这篇文章主要是说一下在WPF中如何进行3d图绘制。
(一)、先介绍一下3D绘图基本概念三维坐标系由于我们要将三维模型显示在二维显示器上,所以我们创建场景时,实际上是要创建三维对象的二维表现形式。前面的文章已经讲过,WPF中二维图形坐标系原点在屏幕左上角,x轴正方向朝右,y轴正方向朝下。但是在三维坐标系中原点位于呈现中心的中间,x轴正方向朝右,y轴正方向朝上,z轴正方向朝外。这点和OpenGL类似,三维坐标系统使用的也是右手坐标系。
二维坐标系统与三维坐标系统
在WPF中使用右手坐标系统
WPF三维坐标系统
相机和投影
当我们创建三维场景时,实际上是要创建三维对象的在显示屏幕上二维表示形式。由于三维场景的外观会因观察者的观察位置不同而异,因此我们必须设置观察位置。可以使用相机来为三维场景指定观察位置。 了解三维场景如何在二维图面上表示的另一种方法就是将场景描述为到观察表面上的投影。“投影”这个词听起来比较抽象,生活中的物品都是三维的,但人的眼睛只能看到正面,不能看到被遮挡的背面。我们眼睛看到三维几何体就像看到被相机拍摄的二维相片。三维空间体转化为二维图的过程就叫投影,例如在现实世界中摄像机拍摄物体,是由物体表面反射的光线经过凸透镜聚到感光元件CCD单元上产生的。投影就是把三维空间投影到二维空间的过程。而不同的投影方式投影尺寸的算法不同。针对于不同的三维场景通常使用不同的投影方式,比如工业设计通常使用正投影(平行投影),而各种游戏场景则通常采用透视投影。
正投影和透视投影在三维图中的效果:
透视投影相机代码示例
// Defines the camera used to view the 3D object. In order to view the 3D object,
// the camera must be positioned and pointed such that the object is within view
// of the camera.
PerspectiveCamera myPCamera = new PerspectiveCamera();
// Specify where in the 3D scene the camera is.
myPCamera.Position = new Point3D(0, 0, 2);
// Specify the direction that the camera is pointing.
myPCamera.LookDirection = new Vector3D(0, 0, -1);
// Define camera's horizontal field of view in degrees.
myPCamera.FieldOfView = 60;
// Asign the camera to the viewport
myViewport3D.Camera = myPCamera;
ProjectionCamera,可以指定不同的投影及其属性以更改观察者查看三维模型的方式。PerspectiveCamera 指定用来对场景进行透视收缩的投影。 换言之,PerspectiveCamera 提供消失点透视。 您可以指定照相机在场景坐标系中的位置、照相机的方向和视野以及用来定义场景中“向上”方向的向量。下图阐释 PerspectiveCamera 的投影。 ProjectionCamera 的 NearPlaneDistance 和 FarPlaneDistance 属性限制照相机的投影范围。由于照相机可以位于场景中的任何位置,因此照相机实际上可能会位于模型内部或者紧靠模型,这使得很难正确区分对象。使用 NearPlaneDistance,可以指定一个距离照相机的最小距离,即,在超过该距离后将不绘制对象。 相反,使用 FarPlaneDistance,可以指定一个距离照相机的距离(即,在超过该距离后将不绘制对象),从而确保因距离太远而无法识别的对象将不包括在场景中。
正投影:
OrthographicCamera 指定三维模型到二维可视化图面上的正投影。与其他照相机一样,它指定位置、观察方向和“向上”方向。但是,与 PerspectiveCamera 不同的是,OrthographicCamera 描述了不包括透视收缩的投影。换言之,OrthographicCamera 描述了一个侧面平行的取景框,而不是侧面汇集在场景中一点的取景框。
Point3D position = new Point3D(1.5, 2, 3);
Vector3D lookDirection = new Vector3D(
-position.X, -position.Y, -position.Z);
Vector3D upDirection = new Vector3D(0, 1, 0);
double width = 4;
OrthographicCamera camera =
new OrthographicCamera(position, lookDirection, upDirection, width);
viewport.Camera = camera;
3D 图形是由3D网格构成的,3D网格也被称为模型,一个3D图形通常是由一些小的基本元素(顶点,边,面,多边形)构成。顶点是3D建模时用到的最小构成元素,顶点定义为两条或是多条边交会的地方,是一个具有x、y、z坐标的空间位置。通过连接多个顶点形成多边形,而面特指一个三角形,由三个顶点和三条边构成。根据网格的几何形状,网格可能会由多个三角形组成,其中的一些三角形共用相同的角(顶点)。三个点才能构成一个平面,而且仅有三个面才能保证面是平的,多一个点不能保证面是平的,少一个点不能构成一个平面,所以不多不少正好是三个。
三维模型是若干3D点(Point3D)的集合,每3个3D点按一定环绕方向组成1个三角形,WPF采用逆时针的环绕方向,符合所谓“右手法则”,即垂直竖起右手的大拇指,弯曲其余4指,其余4指指向正是三角形的环绕方向,大拇指的指向是三角形的正面,反向是其背面,如下图所示,正是这些三角形构成了WPF中的三维造型世界。
在3D模型中,通常面数越多(也就是三角形的数量),模型越精细,反之则越粗糙(根据面数多少也就有了高模与低模之分)
WPF 三维系统目前提供 MeshGeometry3D 类,使用该类,可以指定任何几何形状。 首先通过将三角形顶点的列表指定为它的Positions 属性来创建 MeshGeometry3D。 每个顶点都指定为 Point3D。 根据网格的几何形状,网格可能会由多个三角形组成,其中的一些三角形共用相同的角(顶点)。 若要正确地绘制网格,WPF 需要有关哪些顶点由哪些三角形共用的信息。 可以通过指定具有 TriangleIndices 属性的三角形索引列表来提供此信息。 此列表指定在 Positions 列表中指定的点将按哪种顺序确定三角形。
MeshGeometry3D 常用的有四个属性(Positions、TriangleIndices 、TextureCoordinates、Normals)。其中Positions、TriangleIndices两个最重要,需要手动赋值,而其它两个属性,不写的话,会自动判断来给出缺省值。
这张简单描述了一个三位坐标系,里面有四个坐标点,也就是顶点位置,都已标出,也就组成了集合(Positions),它所标示的是一个正方形。
举个例子:
TriangleIndices="0 1 2 2 3 0"
它所表示的是什么。每个数字什么意思。
先讲一下概念,字面意思是三角形索引的集合。为什么要用到三角形呢,因为在3D图形的世界里,所有物体都可以被描述成为一系列三角形的集合。
比如我们现在画的这个正方形,可以有两个三角形组成。
那么TriangleIndices="0 1 2 2 3 0" 按照图片显示的可以翻译成 “P0 P1 P2,P2 P3 P0”,或者 0 对应 (-1,1,0),1 对应 (-1,-1,0),以此类推。
这里面的每个数字对应着图片里的每个点。可是为什么这样对应呢。
这关系到三角形呈现的是有正反面区分的,可以看出上面每三个点组成的一个三角形都是逆时针顺序的,这是因为WPF采用逆时针的环绕方式来显示正面,
到这里基本就搞清了TriangleIndices 和 Positions 的关系。
TextureCoordinates:纹理坐标用于确定将 Material 映射到构成网格的三角形的顶点的方式。
TextureCoordinates="0,0 0,1 1,1 1,0"
一般材质的的正常坐标按照上图来说顺序依次是 P0,P3,P2,P1。也就是说 0,0 0,1 1,1 1,0 这是一个正常顺序,是按照本来画面显示的。
但如果换成TextureCoordinates="1 0, 0 0, 0 1, 1 1",你会发现显示的画面向左倒了。
这也和你定义的坐标集合有关系。
Normals:法向量是与定义网格的每个三角形的面垂直的向量。 法向量用于确定是否亮显给定三角形面。如果指定了三角形索引,则将考虑相邻面来生成法向量。
光源光源与实际的光一样,三维图形中的光能够使图面可见。 更确切地说,光确定了场景的哪个部分将包括在投影中。 WPF 中的光对象创建了各种光和阴影效果,而且是按照各种实际光的行为建模的。 您必须至少在场景中包括一个光,否则模型将不可见。
WPF中支持不同类型的光源,如下:
AmbientLight (环境光):它所提供的环境光以一致的方式照亮所有的对象,而与对象的位置或方向无关。这个灯光会照亮场景里全部的物体(前提是被光照的物体肯定是可以接受灯光),这种灯没有方向所有无法产生阴影。
DirectionalLight (平行光):像远处的光源那样照亮。 将方向光的 Direction 指定为 Vector3D,但是没有为方向光指定位置。
PointLight(点光源) :像近处的光源那样照亮。 PointLight 具有一个位置并从该位置投射光。 场景中的对象是根据对象相对于光源的位置和距离而被照亮的。PointLightBase 公开 Range 属性,该属性确定一个距离,超过该距离后模型将无法由光源照亮。 PointLight 还公开了多个衰减属性,这些属性确定光源的亮度如何随距离的增加而减小。 您可以为光源的衰减指定恒定、线性或二次内插算法。
SpotLight(聚光灯) :从 PointLight 继承, Spotlight 的照亮方式与 PointLight 类似,但是它既具有位置又具有方向。 它们在 InnerConeAngle 和 OuterConeAngle 属性所设置的锥形区域(以度为单位指定)中投射光。
光源是 Model3D 对象,因此您可以转换光源对象并对光源属性(包括位置、颜色、方向和范围)进行动画处理。
不同光源场景区别如下图:
材质、纹理
为了让一个三维模型看起来像一个三维物体,它必须有一个应用的纹理来覆盖由顶点和三角形定义的表面,这样它才能被摄像机照亮和投射。在2D中,您使用画笔类将颜色、模式、渐变或其他视觉内容应用于屏幕区域。然而,3D对象的外观是照明模型的功能,而不仅仅是应用于它们的颜色或图案。实际对象的图面质量不同,他们反射光的方式也会有所不同,你可以将同样的笔刷应用到3D对象上,就像你可以应用到2D对象上一样,但是你不能直接应用它们。
为了定义模型表面的特征,WPF使用Material abstract类。Material的具体子类决定了模型表面的一些外观特征,每个子类还提供了一个Brush属性,您可以将SolidColorBrush、TileBrush或VisualBrush传递给它。
- DiffuseMaterial 指定将向模型应用画笔,就好像模型是使用漫射光来照亮的一样。 使用 DiffuseMaterial 与直接针对二维模型使用画笔非常相似;模型表面不反射光,就好像是自发光一样。
- SpecularMaterial 指定将向模型应用画笔,就好像模型的表面坚硬或者光亮,能够反射光线一样。 可以通过为 SpecularPower 属性指定一个值来设置系统将为纹理的反射特质(或“发光”)建议的度数。
- 使用 EmissiveMaterial 可以指定将应用纹理,就好像模型所发出的光与画笔的颜色相同。 这不会使模型成为光源;但是,它参与阴影设置的方式将不同于用 DiffuseMaterial 或 SpecularMaterial 设置纹理时的情况。
在3D世界中,模型是骨架,纹理为皮肤,二者缺一不可。
示例代码:构造一个材质对象,这里就用一个简单的画刷作为材质的纹理。然后用这个材质和上面构造的网格构造一个3D模型,然后设置灯光。
DiffuseMaterial dm = new DiffuseMaterial();
dm.Brush = Brushes.Cyan;
GeometryModel3D gm = new GeometryModel3D();
gm.Geometry = meshg;
gm.Material = dm;
DirectionalLight dl = new DirectionalLight ( );
dl.Color = Colors.Blue;
dl.Direction = new Vector3D ( 0, 0, -1 );
顶点在缩放、旋转、平移等变换中的坐标转换矩阵。当您创建模型时,它们在场景中具有固定的位置。为了在场景中移动、旋转这些模型或者更改这些模型的大小而更改用来定义模型本身的顶点是不切实际的。
相反,您可以像在二维模型一样应用转换。 每个模型对象都有一个可用来对模型进行移动、重定向或调整大小的 Transform 属性。 当您应用转换时,实际上是按照由Transform 属性指定的向量或值来偏移模型的所有点。
也就是说变换了定义模型的坐标系(“模型空间”)而模型所在的整个场景的坐标系(“全局空间”)却没有改变,从而实现了3D模型的变换。
(二)、三维井眼轨迹实现首先在窗体中引入Viewport3D容器,然后定义及设置相机属性,设置灯光材质等。
<Viewport3D Grid.Row="0" Grid.Column="0" Name="MainViewport" />
TheCamera = new PerspectiveCamera();
TheCamera.FieldOfView = 60;
MainViewport.Camera = TheCamera;
PositionCamera();
DefineLights(MainModel3Dgroup);
最后定义3d模型,进行展示。
坐标轴、网格线及井眼轨迹绘制
为坐标和网格线,创建材质对象
NormalMaterial = new DiffuseMaterial(Brushes.White);
SelectedMaterial = new DiffuseMaterial(Brushes.Yellow);
var faceMaterial = new DiffuseMaterial(Brushes.DarkGray);
画线段方法:
public static void AddSegment(MeshGeometry3D mesh,
Point3D point1, Point3D point2, Vector3D up, double thickness,)
{
Vector3D v = point2 - point1;
Vector3D n1 = up.Scale(thickness / 2.0);
Vector3D n2 = Vector3D.CrossProduct(v, n1);
n2 = n2.Scale(thickness / 2.0);
Point3D p1pp = point1 n1 n2;
Point3D p1mp = point1 - n1 n2;
Point3D p1pm = point1 n1 - n2;
Point3D p1mm = point1 - n1 - n2;
Point3D p2pp = point2 n1 n2;
Point3D p2mp = point2 - n1 n2;
Point3D p2pm = point2 n1 - n2;
Point3D p2mm = point2 - n1 - n2;
AddTriangle(mesh, p1pp, p1mp, p2mp);
AddTriangle(mesh, p1pp, p2mp, p2pp);
AddTriangle(mesh, p1pp, p2pp, p2pm);
AddTriangle(mesh, p1pp, p2pm, p1pm);
AddTriangle(mesh, p1pm, p2pm, p2mm);
AddTriangle(mesh, p1pm, p2mm, p1mm);
AddTriangle(mesh, p1mm, p2mm, p2mp);
AddTriangle(mesh, p1mm, p2mp, p1mp);
AddTriangle(mesh, p1pp, p1pm, p1mm);
AddTriangle(mesh, p1pp, p1mm, p1mp);
AddTriangle(mesh, p2pp, p2mp, p2mm);
AddTriangle(mesh, p2pp, p2mm, p2pm);
}
调用此方法画出x,y,z坐标轴刻度,然后再画出网格线。
定义添加面的方法,在坐标系 左侧和后侧画出背景框。
public static void AddFace(this MeshGeometry3D mesh,
float x, float y, float z, float dx, float dy, float dz)
{
// Right //outside
AddTriangle(mesh,
new Point3D(x dx, y, z),
new Point3D(x dx, y dy, z),
new Point3D(x dx, y dy, z dz));
AddTriangle(mesh,
new Point3D(x dx, y, z),
new Point3D(x dx, y dy, z dz),
new Point3D(x dx, y, z dz));
// Right//inside
AddTriangle(mesh,
new Point3D(x dx, y dy, z dz),
new Point3D(x dx, y dy, z),
new Point3D(x dx, y, z) );
AddTriangle(mesh,
new Point3D(x dx, y, z dz),
new Point3D(x dx, y dy, z dz),
new Point3D(x dx, y, z));
// Back //outside
AddTriangle(mesh,
new Point3D(x, y, z),
new Point3D(x, y dy, z),
new Point3D(x dx, y dy, z));
AddTriangle(mesh,
new Point3D(x, y, z),
new Point3D(x dx, y dy, z),
new Point3D(x dx, y, z));
// Back //inside
AddTriangle(mesh,
new Point3D(x dx, y dy, z),
new Point3D(x, y dy, z),
new Point3D(x, y, z));
AddTriangle(mesh,
new Point3D(x dx, y, z),
new Point3D(x dx, y dy, z),
new Point3D(x, y, z));
}
这里每个面由两个相邻的三角形组成,因为内外都需要可见,每个面里外总共用了四个三角形。
,