这篇笔记发布距离上篇笔记应该是隔了很久… 主要是临近毕业,在找工作,加之 《计算机图形学原理及实践》这本书在 第七章及之后就开始加速起飞了… 本人水平有限理解起来比较困难,因为感觉理解的不是很通透就也没有立即做笔记,再加之 我习惯一章的内容都写入在一篇笔记中,但是 印象笔记一篇的内容越多,往里面也内容就越卡顿体验非常差,还有印象笔记的markdown 怎么搬到其它平台的问题…
总之,最近工作有了着落,学校也不放我去实习工作,那么就有了时间来回顾内容,把之前不理解的再理解理解,把一章的内容分成几篇写,最后再粘贴到一篇内,这样也解决了印象笔记卡顿的问题。
那么就来更新第六章的学习笔记。
第六章 固定功能的 3D 图像平台 和 层次建模简介
引言
WPF 3D 部分的设计
WPF 是一个 保留模式(RM)平台——应用程序使用 XAML 或 WPF.NET API 指定并维护一个层次 场景图 并将其存储在该平台上。
对光与物体交互的物理过程的近似
-
物体表面每个点所接收的光中,既有来自光源的 直接入射光 也有经由场景中其它物体表面反射而来的 间接入射光。
- 直接模拟场景物体之间递归交互反射过程、完全基于物理的算法需要大量的计算。 实时计算机图形生成方法中,目前占主导地位的是近似技术,从大体上基于物理的模拟到完全不基于物理的模拟但足以"骗过"眼睛的一些技巧,不一而足。
-
渲染管线
- 在图形学发展过程中,一系列优秀算法组成的特定序列,称为 固定功能3D图形管线
- 而固定管线现在逐步被淘汰,可编程管线 是现在的主流技术
WPF 3D 概述
- 在 WPF应用程序中要加入 3D 场景,需要创建一个 Viewport3D 实例,其功能与 Canvas 类似。
- 与 2D抽象应用坐标系对应的是 场景坐标系,其 x轴、y轴、z轴满足右手系规则。可选用一个物理单位(毫米、英寸等)作为计量单位,也可不为坐标值附加任何意义。
- 相机定位于建模空间中,其位置采用场景坐标表示,具体设置由几个参数组成,这些参数共同定义了 视域体
- 与 WPF2D 相同,平台将使绘制过程自动与模型空间保持同步。
- 例如,当场景中景物或相机配置有所变动时,将导致视窗中绘制结果自动更新。因此通过实时编辑场景即可实现动画,例如:
- 添加或删除物体
- 改变物体的几何形状
- 对物体、相机或光源进行变换
- 改变物体的材质属性
- 改变相机或光源的特性
- 例如,当场景中景物或相机配置有所变动时,将导致视窗中绘制结果自动更新。因此通过实时编辑场景即可实现动画,例如:
网格和光照属性
本节我们将使用XAML建立一个四面的金字塔。
场景设计
- 设地面 与 xz平面共面,则 y 轴垂直向上
- 金字塔高 75 米,基座取 100 平方米
- 令基座中心位于原点(0, 0, 0),四个角点位于 (±50, 0, ±50),塔顶位于 (0, 75, 0)
准备一个视窗
- 视窗(展示3D场景的2D平面)必须位于 2D 的 WPF 结构(如一个 window 或者 canvas),这里我们使用 Page 作为视窗的 2D 显示区。
<Page
x:Class="WpfApp2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Page1">
<Page.Resources>
材质 和 网格 在这里声明
</Page.Resources>
<Viewport3D Name="view" Width="640" Height="480">
<Viewport3D.Camera>
摄像机在这里声明
</Viewport3D.Camera>
<ModelVisual3D>
<ModelVisual3D.Content>
<Model3DGroup>
灯光 和 物体 将在这里声明
</Model3DGroup>
</ModelVisual3D.Content>
</ModelVisual3D>
</Viewport3D>
</Page>
注意,这里视窗时 Page 而不是 Window,因此 CS代码要修改一下,让类继承自 Page 而不是 Window
namespace WpfApp2
{
/// <summary>
/// MainWindow.xaml 的交互逻辑
/// </summary>
public partial class MainWindow : Page
{
public MainWindow()
{
InitializeComponent();
}
}
}
- 设置相机
<Viewport3D.Camera>
<PerspectiveCamera
Position="0, 0, 300"
LookDirection="0, 0, -1"
UpDirection="0, 1, 0"
NearPlaneDistance="0.02" FarPlaneDistance="1000"
FieldOfView="45"/>
</Viewport3D.Camera>
-
LookDirection 是描述 相机投影方向的向量,采用场景坐标定义。可以将 LookDirection 想象成相机镜头对准场景中央物体时镜头主体的中心线。
-
UpDirection 是指围绕镜头的观察方向对相机进行旋转,使得对观察者而言,镜头中视图呈现朝上的方向。上面的代码,向上方向向量为[0, 1, 0],该方向摄像机成像平面与地面垂直。
-
NearPlaneDistance 为近裁剪平面距离,其防止出现所拍摄景物过于靠近相机的情形。
-
FarPlaneDistance 为远裁剪平面距离,其可忽略遥远的景物,从而减少计算量
-
环境光
<Model3DGroup>
<AmbientLight Color="White"/>
</Model3DGroup>
- 我们采用 无方向的环境光, 即不论场景中景物表面的位置和朝向,泛光始终是恒定的光照
放置第一个三角形
- 三角形网格是目前 WPF 支持的唯一 3D几何 基元类型。这也是我们选择金字塔作为第一个建模的原因。
- 创建3D物体的第一步是需要一个网格:MeshGeometry3D
- 这需要提供一个 3D顶点表(Positions) 和 一个 三角形表(TriangleIndices)
- 三角形表由 顶点表中顶点的下标组成。
- 我们还需要指定三角面的正面。三角形的正面可以通过 三角形表来隐式定义。我们看向三角形的正面,以逆时针的旋转方向 将顶点下标放入 三角形表中,这样就定义了三角形正面。显然,如果我们从背面观察,那么这个顶点顺序就是顺时针旋转的了。
<Page.Resources>
<MeshGeometry3D x:Key="RSPCmeshPyramid"
Positions="0,75,0 -50,0,50 50,0,50"
TriangleIndices="0 1 2"/>
</Page.Resources>
- 但网格只是声明在 Resources 中,并不存在于场景中,我们还需要在场景中使用网格来实例化物体
- 所以下一步是在场景中创建 GeometryModel3D 类型的元素。该元素需要:
- 几何属性,是对之前已创建的几何资源的引用
- 材质属性,通常也是对已有资源的引用。材质描述了表面对光的反射属性,WPF的材质模型提供了多种类型材质的近似属性值
- 所以下一步是在场景中创建 GeometryModel3D 类型的元素。该元素需要:
- 那么我们还缺少材质,因此在 Resources 中声明一个材质。为了简化问题,我们给每一面赋予纯黄色材质。
<Page.Resources>
<MeshGeometry3D x:Key="RSPCmeshPyramid"
Positions="0,75,0 -50,0,50 50,0,50"
TriangleIndices="0 1 2"/>
<DiffuseMaterial x:Key="RSPCmaterialFront" Brush="Yellow"/>
</Page.Resources>
- 之后,在场景中创建物体:
<Model3DGroup>
<AmbientLight Color="White"/>
<GeometryModel3D
Geometry="{StaticResource RSPCmeshPyramid}"
Material="{StaticResource RSPCmaterialFront}"/>
</Model3DGroup>
- 此时,如果我们让相机拍摄 三角面的背面,会发现什么也没有
<PerspectiveCamera
Position="0, 0, -300"
LookDirection="0, 0, 1"
UpDirection="0, 1, 0"
NearPlaneDistance="0.02" FarPlaneDistance="1000"
FieldOfView="45"/>
- 这是因为,默认情况下,WPF 不绘制任何表面的背面。对于常见的 封闭物体而言,这种设置有益于简化计算。而对于我们现在这种情况,我们可以给 GeometryModel3D 添加一个 背面材质(BackMaterial) 来关闭绘制优化。
- 那么我们就还需要一个 材质 来作为 背面材质。我们在 Resources 中再声明一个材质,设为红色,以和正面进行区别。
<DiffuseMaterial x:Key="RSPCmaterialBack" Brush="Red"/>
- 然后在将其设置给三角面的 BackMaterial
<GeometryModel3D
Geometry="{StaticResource RSPCmeshPyramid}"
Material="{StaticResource RSPCmaterialFront}"
BackMaterial="{StaticResource RSPCmaterialBack}"/>
- 接下来创建第二个面
<MeshGeometry3D x:Key="RSPCmeshPyramid"
Positions="0,75,0 -50,0,50 50,0,50
0,75,0 50,0,50 50,0,-50"
TriangleIndices="0 1 2 3 4 5"/>
- 调整摄像机位置,得到:
<PerspectiveCamera
Position="300, 150, 300"
LookDirection="-1, -0.5, -1"
UpDirection="0, 1, 0"
NearPlaneDistance="0.02" FarPlaneDistance="1000"
FieldOfView="45"/>
生成更真实的光照
-
在真实世界中,光源是场景的一部分,照射到表面 P 点的光 是有方向的。从 P 点射向摄像机的光能也不是一个常数,而是由若干变量决定:
- 相机位置
- 表面在P点的朝向(法向)
- 光源入射方向
- 表面材质的反射特性
-
点光源
- 点光源是一个 几何光源,它在场景中位置固定并向四面八方发射出同等强度的光。
- 由于其发射方向 l 是一个无穷集合,点光源的加入可能引起场景很大的变换
-
方向光源
- 如果把点光源拿到无穷远的地方(例如,太阳),那么光源的方向就可以认为是单一的直线方向
- WPF 将这种光源 与 几何光源区分开,称其为 方向光源
-
我们将 泛光(环境光) 设小一点,然后加上一个 方向光
<Model3DGroup>
<AmbientLight Color="Gray"/>
<DirectionalLight Color="White" Direction="1, -1, -1"/>
- 对于完全漫反射表面,其反射光朝所有观察方向具有相同的亮度,而与观察者的方位无关。
- 反射光强度只取决于 光源入射物体表面的方向。
- 光照方向 l 和 表面法向 n 之间的夹角 θ,θ 越大,光线越倾斜,反射的光能量越小。
- 根据 “朗伯”余弦定律 计算漫反射光的强度:
-
I = I d i r c o s θ I = I_{dir}cosθ I=Idircosθ
-
由于表面反射强度与观察方向无关,该方程 不能用了模拟具有 光泽的材质。例如 金属、玻璃等
-
固定功能绘制中的 “光照” 与 “着色”
- 光照明方程 是 描述物体表面材料反射光的可计算的表达式。
- 从理论角度看,绘制程序在处理某一给定的可见表面时,先根据该表面的材质 “加载” 光照明方程,然后对表面上的点 “执行” 相应的计算。
- 理论上,我们可以在表面图像每个像素所覆盖的表面区域内选择一个代表点进行一次运算。离线渲染使用的就是这种技术。
- 但对于当前商业硬件上运行的实时绘制系统来说这种策略开销太大。包括 固定功能管线 和 可编程渲染管线 所采取的方法只是在表面上选取若干关键点进行计算,然后采用地耗费的着色规则来确定表面上位于关键点之间的各点的值。
- 以最简单的 均匀着色(常色着色)的着色技术为例,每个三角形选取一个顶点作为关键点,采用光照明方程计算该顶点的亮度值,然后用该亮度填充整个三角形。
- 从理论角度看,绘制程序在处理某一给定的可见表面时,先根据该表面的材质 “加载” 光照明方程,然后对表面上的点 “执行” 相应的计算。
曲面表示和绘制
- 现实世界中,大多数物体的表面为曲面或者具有弧形的边缘。
- 我们可以使用一个多面棱锥来逼近一个圆锥体,但是通过增加面数(网格分辨率) 来解决曲面问题,不仅代价高昂(从存储/处理成本来说)且效果并不理想。仍会存在明显的边痕迹。因此我们需要寻找更好的方法。
基于插值的着色处理(Gouraud 着色)
- Gouraud 着色,是选取顶点进行光照计算,然后顶点间的像素亮度通过对顶点计算结果进行插值得到。
- 由于光照计算需要 法线方向,那么顶点处的法线方向该怎样得到呢?
- 对于 2D,我们可以通过对 顶点邻近的两条线段的法线取平均进行近似
- 对于 3D,我们可以通过对 顶点邻近的面的法线取平均进行近似
将表面设置为多面体表面和光滑表面
- WPF 默认就使用的是 Gouraud 插值方法着色。那么为什么我们上面绘制出的金字塔的两个面的颜色分别为常值,而没有插值的感觉呢?
- 在上面,我们定义网格时,为每一个三角面 都声明了 三个顶点。也就是说三角面之间,它们的顶点是相互独立的,那么法线的计算也就无需取平均。如果三角面的三个顶点计算结果都一样,那么插值就没有意义了。
- 我们可以减少一些顶点,即 共用一些顶点:
<MeshGeometry3D x:Key="RSPCmeshPyramid"
Positions="0,75,0 -50,0,50 50,0,50 50,0,-50"
TriangleIndices="0 1 2 0 2 3"/>
- 因此,在 WPF 中,对于光滑曲面,我们需要共享顶点,以参与 Gouraud 插值计算。而像 折痕、不同平面的连接处等 不连续性的表面,则就需要一个顶点只被一个面引用。
- 即,这两种方式往往是混合在一起使用的
WPF 中的表面纹理
- 采用 纹理映射,复杂的材质 和 复杂的场景 可以得到大致的模拟而无需提高网格的复杂度。
- WPF 中在 3D 表面上贴 “纹理” 相当于将一张可伸缩的接触式印纸覆盖到物体表面上。
- 理论上,对物体表面上的每一点 P,都需要指定纹理上接触 P 点的那个点。
- 在实践中,我们只需要指定每个 顶点 在纹理上的映射点,然后通过 插值 即可确定网格内各点的纹理值。
- 指定纹理图像上的点需要一个坐标系统,对于纹理我们采用 uv坐标系。我们不采用整数像素坐标,而是使用 浮点纹理坐标系,u 和 v 轴的取整范围为 0~1,坐标原点位于纹理 左上角。
- 在 XAML 中定义纹理:
<DiffuseMaterial x:Key="RSPCtextureSand">
<DiffuseMaterial.Brush>
<ImageBrush ImageSource="Resources\sand.gif"/>
</DiffuseMaterial.Brush>
</DiffuseMaterial>
- 有了纹理,我们还需要把它贴在网格上。我们再声明一个沙漠的网格,即一个平面。
在声明网格时,还需要把网格 和 纹理坐标进行指定
<MeshGeometry3D x:Key="RSPCmeshSand"
Positions="-9999, 0, -9999
9999, 0, -9999
9999, 0, 9999
-9999, 0, 9999"
TriangleIndices="3 1 0 3 2 1"
TextureCoordinates="0,0 1,0 1,1 0,1"/>
- 之后,实例化沙漠平面
<GeometryModel3D
Geometry="{StaticResource RSPCmeshSand}"
Material="{StaticResource RSPCtextureSand}"/>
</Model3DGroup>
- 但由于沙漠平面很大,而沙漠纹理只是一张 64×64 的贴图,因此 纹理会被拉伸的很大,从而丢失了纹理的细节。
- 我们可以进行 基于分片拼接的纹理映射
基于分片拼接的纹理映射
-
如果纹理是被用了模拟外观一致且不存在明显不连续点的 材质(如 沙砾、沥青、砖 等),为了覆盖整个目标表面,我们可以根据需要对纹理图像进行复制。
- 此时纹理通常是材质的一个小样本图像,该样本经过特别裁剪以确保相邻图片之间能够 无缝连接
-
WPF 将纹理 设置为 分片拼接:
<DiffuseMaterial x:Key="RSPCtextureSand">
<DiffuseMaterial.Brush>
<ImageBrush ImageSource="Resources\sand.gif"
TileMode="Tile"
Viewport="0,0 0.01,0.01"/>
</DiffuseMaterial.Brush>
</DiffuseMaterial>
基于拉伸的纹理映射
- 如果纹理是用来替代一个高度复杂的模型(例如,高空中的云),所采用的纹理图像往往具有相当大的幅画(有足够高的 分辨率)
WPF 反射模型
颜色设置
- 尽管通过 RGB 三原色来指定颜色是图形 API 和 绘图/着色应用程序中的常用方法,但它是计算机图形学所有近似方法中一种最粗糙的近似
光源几何
- 几何光源 增加了场景光源的真实感,它位于场景某一位置 并且 所发出的光是 衰减的。也就是说 到达特定表面上某点 P 的光能 依赖于光源与该点的距离。WPF 提供了两种光源:
- 点光源
- 聚光灯
反射率
-
过去数十年间,固定功能管线均采用经典的 Phong反射光照模型。
- Phong 模型将反射光分解为三个不同的分量:
- 环境光(泛光)
- 漫反射光
- 镜面反射光
- 这三个分量的光强值相加得到的就是最终的绘制效果
- Phong 模型将反射光分解为三个不同的分量:
-
光照模型中,材质的输入参数:
符号 | 描述 | 格式 |
---|---|---|
C d C_d Cd | 表面 “漫反射层” 的固有颜色 | ( C d , R , C d , G , C d , B C_{d,R}, C_{d,G}, C_{d,B} Cd,R,Cd,G,Cd,B) |
C s C_s Cs | 表面 “镜面反射层” 的固有颜色 | ( C s , R , C s , G , C s , B ) (C_{s,R}, C_{s,G}, C_{s,B}) (Cs,R,Cs,G,Cs,B) |
k a k_a ka | 表面漫反射层对泛光的反射系数 | ( k a , R , k a , G , k a , B ) (k_{a,R}, k_{a,G}, k_{a,B}) (ka,R,ka,G,ka,B) |
k d k_d kd | 表面漫反射层对方向光和几何光源光的反射系数 | ( k d , R , k d , G , k d , B ) (k_{d,R}, k_{d,G}, k_{d,B}) (kd,R,kd,G,kd,B) |
k s k_s ks | 表面镜面反射层对方向光和几何光源光的反射系数 | ( k s , R , k s , G , k s , B ) (k_{s,R}, k_{s,G}, k_{s,B}) (ks,R,ks,G,ks,B) |
-
这里的 反射系数 与 物理上的反射率 是密切相关的,我们以后会再讨论它。
-
光照模型中,光源的输入参数
符号 | 描述 | 格式 |
---|---|---|
I a I_a Ia | 场景泛光的颜色/光强 | ( I a , R , I a , G , I a , B ) (I_{a,R}, I_{a,G}, I_{a,B}) (Ia,R,Ia,G,Ia,B) |
I d i r I_{dir} Idir | 方向光源的颜色/光强 | ( I d i r , R , I d i r , G , I d i r , B ) (I_{dir,R}, I_{dir,G}, I_{dir, B}) (Idir,R,Idir,G,Idir,B) |
I g e o m I_{geom} Igeom | 几何光源的颜色/光强 | ( I g e o m , R , I g e o m , R , I g e o m , B ) (I_{geom,R}, I_{geom,R}, I_{geom,B}) (Igeom,R,Igeom,R,Igeom,B) |
F a t t F_{att} Fatt | 几何光源的衰减因子 | ( F a t t , R , F ( a t t , G ) , F a t t , B ) (F_{att,R}, F_(att,G), F_{att,B}) (Fatt,R,F(att,G),Fatt,B) |
-
如果场景中包含多个光源,或者如果表面反射中含有多个分量并具有高的反射系数,其计算结果可能超过入射光强,显然这是无意义的,简单的光照模型会对超出的部分直接丢弃,但这种处理可能会有负面影响,我们后面会再讨论这个。
-
泛光反射(环境光)
- 泛光对于整个场景保持不变,因此即使十分简单,以 红色分量为例:
- I a , R I_{a,R} Ia,R k a , R k_{a,R} ka,R C a , R C_{a,R} Ca,R
- 泛光对于整个场景保持不变,因此即使十分简单,以 红色分量为例:
-
漫反射
- 漫反射使用 朗伯余弦定律计算,以 红色分量 考虑 场景中的方向光源为例:
- ∑ d i r e c t i o n a l l i g h t I d i r , R k d , R C d , R ( c o s θ ) \sum_{directional light}{I_{dir,R} k_{d,R} C_{d,R} (cosθ)} ∑directionallightIdir,Rkd,RCd,R(cosθ)
- 漫反射使用 朗伯余弦定律计算,以 红色分量 考虑 场景中的方向光源为例:
-
镜面反射
-
以场景中所有方向光源为例:
- ∑ d i r e c t i o n a l l i g h t I d i r k s C s ( c o s δ ) s \sum_{directional light} {I_{dir} k_s C_s (cosδ)^s} ∑directionallightIdirksCs(cosδ)s
-
大多数材质产生的镜面反射均为 表面漫反射颜色 和 光源颜色的某种混合,但两者的混合比例不是一个定值。
- 例如,黄铜茶壶上的高光是入射光颜色掺入了黄铜颜色后的"染色版"。而对于塑料材质,其镜面高光颜色主要决定于光源的颜色,而不是塑料本身的颜色。
-
上面镜面反射计算中也包含了一个 余弦的衰减因子,但它于 朗伯余弦定律中的不同。
- 镜面反射光与视点有关,其反射光强取决于 δ 值,在 Phong 模型的原始公式中,δ 为 反射方向向量 r 和 从表面给定点到相机的向量 e 之间的夹角。cosδ 的作用在于:当视点位于方向向量 r 上时 镜面反射效果最强,而当其偏离 r 时则不断减弱。
-
虽然 cosδ 可以确保反射光的强度下降,但我们还需要控制其下降的快慢,因此我们提供了一个控制镜面反射性能的变量 s,称为材质的 镜面指数(或 高光指数)
- 经验表明,调整 镜面指数的对数 可以让艺术家更好地调整表面的镜面反射性能。这样 数值从 0 到 3 s 就是从 1 变化为 1000
-
-
自发光光源
- 自发光允许表面自身"反射"外部实际不存在的光。
- 自发光与几何无关,且不会衰减
- 计算其发射光强时只需指定一种颜色,将其添加到表面的其它三个反射分量中即可