前言

在前面的章节中,我们学到了法线贴图和曲面细分。现在我们可以将这两者进行结合以改善效果,因为法线贴图仅仅只是改善了光照的细节,但它并没有从根本上改善几何体的细节。从某种意义上来说,法线贴图只是一个光照的小把戏。接下来我们将会学习如何使用位移贴图来改善网格细节。

在此之前你需要了解如下章节:

章节
25 法线贴图
33 曲面细分阶段(Tessellation)

学习目标:

  1. 了解位移贴图
  2. 熟悉如何用曲面细分来改善网格细节

DirectX11 With Windows SDK完整目录

Github项目源码

欢迎加入QQ群: 727623616 可以一起探讨DX11,以及有什么问题也可以在这里汇报。

位移贴图(Displacement Mapping)

位移贴图的想法是利用一个额外的贴图,称作高度图,它描述了一个表面的凸起和缝隙。换句话说,法线贴图有三个颜色通道来为每个像素产生法线向量(x, y, z),而高度图仅仅由一个颜色通道来为每个像素产生高度值h。从视觉上来看,高度图仅仅是一张灰度图(因为灰度图只有一个颜色通道),每个像素可以解释成一个高度值,它基本上是一个2D标量场的离散表示h = f(x, z)。当我们对网格进行曲面细分时,我们在域着色器对高度图进行采样,然后利用法线的方向来对顶点进行偏移,以此来增加网格的几何体细节。

尽管我们通过镶嵌来对几何体增加三角形,但是它并没有增加其本身的细节。那是因为如果你对三角形进行多次细分,你只是获得了更多的和原来的三角形同属于一个平面的三角形。为了增加细节(如凸起和缝隙),你需要以某种方式来偏移这些经过镶嵌后得到的顶点。高度图是其中一个座位输入的纹理资源,它可以用来对镶嵌后的顶点进行偏移。通常情况下,我们会用到下面的公式,为此我们还需要用到法线贴图采样出来的法向量来确定偏移的方向:

\[\mathbf{p'}=\mathbf{p}+s(h-1)\mathbf{n}
\]

其中标量h∈[0, 1]是从高度图得到的高度值。我们对高度值减1来让区间[0, 1]→[-1, 0]。因为表面的法向量通常是面向网格的外部,这意味着我们以向内偏移的方式来替代向外偏移。一般将几何体弹入会比将几何体拉出更为方便一些。标量s则是用来控制在世界空间的塌陷程度。这样高度值的将从[0, 1]→[-s, 0],即高度值最大的时候将不会有向内的偏移,而高度值最小的时候将会产生最大的向内偏移。通常我们会将高度图存放在法线贴图中的alpha通道。

生成高度图是一项艺术性的工作,纹理艺术家可以绘制它们,或者使用工具来产生(例如:crazybump

位移贴图的着色器代码

位移贴图的代码主要在顶点着色器、外壳着色器和域着色器有所变化。像素着色器则和我们之前使用了法线贴图的版本一样无需改动。

图元类型

为了将位移贴图整合到我们的渲染当中,我们需要曲面细分的支持,这样我们就可以细化我们的几何分辨率,使得他能够与位移贴图更好地匹配。接下来我们将使用图元类型D3D11_PRIMITIVE_TOPOLOGY_3_CONTROL_POINT_PATCHLIST而不是D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST来绘制我们的网格三角形。通过这种方式,三角形的三个顶点将解释成三角形面片的3个控制点,以允许我们来对每个三角形进行镶嵌。

顶点着色器

当我们处理曲面细分的时候,我们必须决定每个三角形的细分程度。这里我们将引入一个简单的距离量来确定细分数目。若三角形离摄像机越近,它的细分程度越大。顶点着色器通过计算每个顶点和摄像机之间的距离来帮助我们计算出曲面细分因子,然后将其传递给外给着色器。

在常量缓冲区中,我们引入了下面这些数据来控制距离的计算。这些值的设置非常依赖于场景(你的场景有多大,以及你想要怎样的细分程度):

cbuffer CBChangesEveryFrame
{
// ...
float g_HeightScale;
float g_MaxTessDistance;
float g_MinTessDistance;
float g_MinTessFactor;
float g_MaxTessFactor;
}
  1. g_MaxTessDistance:从摄像机到该顶点的距离拉近到某个阈值时,将会达到最大的曲面细分因子
  2. g_MinTessDistance:从摄像机到该顶点的距离拉远到某个阈值时,将会达到最小的曲面细分因子
  3. g_MinTessFactor:曲面细分因子的最小值。比如说你想让每个三角形无论距离摄像机多远,都要让它最少被镶嵌成3份
  4. g_MaxTessFactor:曲面细分因子的最大值。比如说你想让这些三角形无论距离摄像机多近,它最多的镶嵌份数不超过6.此外,回想起上一章所提到的建议,镶嵌后的三角形如果少于8个像素将会变得低效。

此外我们应该留意到g_MaxTessDistance < g_MinTessDistance,因为随着顶点距离我们的摄像机越近,镶嵌的份数将会越多。

使用这些变量,我们就可以创建一个关于距离的线性函数来决定如何根据距离来确定镶嵌的份数。

// DisplacementMapObject_VS.hlsl
#include "Basic.hlsli" // 顶点着色器
TessVertexOut VS(VertexPosNormalTangentTex vIn)
{
TessVertexOut vOut; vOut.PosW = mul(float4(vIn.PosL, 1.0f), g_World).xyz;
vOut.NormalW = mul(vIn.NormalL, (float3x3) g_WorldInvTranspose);
vOut.TangentW = mul(vIn.TangentL, g_World);
vOut.Tex = vIn.Tex; float d = distance(vOut.PosW, g_EyePosW); // 标准化曲面细分因子
// TessFactor =
// 0, d >= g_MinTessDistance
// (g_MinTessDistance - d) / (g_MinTessDistance - g_MaxTessDistance), g_MinTessDistance <= d <= g_MaxTessDistance
// 1, d <= g_MaxTessDistance
float tess = saturate((g_MinTessDistance - d) / (g_MinTessDistance - g_MaxTessDistance)); // [0, 1] --> [g_MinTessFactor, g_MaxTessFactor]
vOut.TessFactor = g_MinTessFactor + tess * (g_MaxTessFactor - g_MinTessFactor); return vOut;
}
// DisplacementMapInstance_VS.hlsl
#include "Basic.hlsli" // 顶点着色器
TessVertexOut VS(InstancePosNormalTangentTex vIn)
{
TessVertexOut vOut; vOut.PosW = mul(float4(vIn.PosL, 1.0f), vIn.World).xyz;
vOut.NormalW = mul(vIn.NormalL, (float3x3) vIn.WorldInvTranspose);
vOut.TangentW = mul(vIn.TangentL, vIn.World);
vOut.Tex = vIn.Tex; float d = distance(vOut.PosW, g_EyePosW); // 标准化曲面细分因子
// TessFactor =
// 0, d >= g_MinTessDistance
// (g_MinTessDistance - d) / (g_MinTessDistance - g_MaxTessDistance), g_MinTessDistance <= d <= g_MaxTessDistance
// 1, d <= g_MaxTessDistance
float tess = saturate((g_MinTessDistance - d) / (g_MinTessDistance - g_MaxTessDistance)); // [0, 1] --> [g_MinTessFactor, g_MaxTessFactor]
vOut.TessFactor = g_MinTessFactor + tess * (g_MaxTessFactor - g_MinTessFactor); return vOut;
}

外壳着色器

回想上一章说的,常量外壳着色器对每个面片进行计算,并且它的任务是要输出该面片的曲面细分因子。曲面细分因子将告诉镶嵌器阶段对该面片以怎样的程度来进行镶嵌处理。曲面细分因子计算的大部分工作是由顶点着色器所完成的,但仍有一部分的工作需要交给常量外壳着色器处理。特别地,我们通过对顶点曲面细分因子进行求平均值的方式来得到边缘的曲面细分因子。至于内部的曲面细分因子,我们就随意挑选了第一条边的曲面细分因子。

PatchTess PatchHS(InputPatch<TessVertexOut, 3> patch,
uint patchID : SV_PrimitiveID)
{
PatchTess pt; // 对每条边的曲面细分因子求平均值,并选择其中一条边的作为其内部的
// 曲面细分因子。基于边的属性来进行曲面细分因子的计算非常重要,这
// 样那些与多个三角形共享的边将会拥有相同的曲面细分因子,否则会导
// 致间隙的产生
pt.EdgeTess[0] = 0.5f * (patch[1].TessFactor + patch[2].TessFactor);
pt.EdgeTess[1] = 0.5f * (patch[2].TessFactor + patch[0].TessFactor);
pt.EdgeTess[2] = 0.5f * (patch[0].TessFactor + patch[1].TessFactor);
pt.InsideTess = pt.EdgeTess[0]; return pt;
}

那些与多个三角形所共享的边应当拥有相同的曲面细分因子,否则网格的裂纹可能会出现(见下图)。举个例子说下不计算曲面细分因子的情况,加入我们通过摄像机到三角形中心点的距离来计算内部曲面细分因子。然后我们将内部的曲面细分因子也设置到边缘曲面细分因子上。如果两个邻接三角形拥有不同的内部曲面细分因子,它们的边也将会拥有不同的曲面细分因子,从而导致在进行位移映射后会产生T型连接的裂纹效果。

可以看到,图a展示了两个三角形共享一条边。图b上面的三角形进行了一次边缘细分,下面的三角形则没有细分。图c上面的三角形进行了一次内部细分,经过位移映射后,新产生的顶点被移走了(一般是向内移动),从而在两个三角形之间产生了一条缝隙。

控制点外壳着色器以面片的控制点作为输入,每次调用处理一个控制点并输出。在本章示例项目中,控制点外壳着色器仅仅是将数据进行直传:

// 外壳着色器
[domain("tri")]
[partitioning("fractional_odd")]
[outputtopology("triangle_cw")]
[outputcontrolpoints(3)]
[patchconstantfunc("PatchHS")]
HullOut HS(InputPatch<TessVertexOut, 3> patch,
uint i : SV_OutputControlPointID,
uint patchId : SV_PrimitiveID)
{
HullOut hOut; // 直传
hOut.PosW = patch[i].PosW;
hOut.NormalW = patch[i].NormalW;
hOut.TangentW = patch[i].TangentW;
hOut.Tex = patch[i].Tex; return hOut;
}

域着色器

经过镶嵌器创建出来的每个顶点都会有调用一次域着色器。在这里我们将会对高度图(即法线贴图的Alpha通道部分)进行采样,然后利用法向量对顶点偏移,从而完成整个位移映射的过程。

// DisplacementMap_DS.hlsl
#include "Basic.hlsli" [domain("tri")]
VertexOutNormalMap DS(PatchTess patchTess,
float3 bary : SV_DomainLocation,
const OutputPatch<HullOut, 3> tri)
{
VertexOutNormalMap dOut; // 对面片属性进行插值以生成顶点
dOut.PosW = bary.x * tri[0].PosW + bary.y * tri[1].PosW + bary.z * tri[2].PosW;
dOut.NormalW = bary.x * tri[0].NormalW + bary.y * tri[1].NormalW + bary.z * tri[2].NormalW;
dOut.TangentW = bary.x * tri[0].TangentW + bary.y * tri[1].TangentW + bary.z * tri[2].TangentW;
dOut.Tex = bary.x * tri[0].Tex + bary.y * tri[1].Tex + bary.z * tri[2].Tex; // 对插值后的法向量进行标准化
dOut.NormalW = normalize(dOut.NormalW); //
// 位移映射
// // 基于摄像机到顶点的距离选取mipmap等级;特别地,对每个MipInterval单位选择下一个mipLevel
// 然后将mipLevel限制在[0, 6]
const float MipInterval = 20.0f;
float mipLevel = clamp((distance(dOut.PosW, g_EyePosW) - MipInterval) / MipInterval, 0.0f, 6.0f); // 对高度图采样(存在法线贴图的alpha通道)
float h = g_NormalMap.SampleLevel(g_Sam, dOut.Tex, mipLevel).a; // 沿着法向量进行偏移
dOut.PosW += (g_HeightScale * (h - 1.0f)) * dOut.NormalW; // 生成投影纹理坐标
dOut.ShadowPosH = mul(float4(dOut.PosW, 1.0f), g_ShadowTransform); // 投影到齐次裁减空间
dOut.PosH = mul(float4(dOut.PosW, 1.0f), g_ViewProj); // 从NDC坐标[-1, 1]^2变换到纹理空间坐标[0, 1]^2
// u = 0.5x + 0.5
// v = -0.5y + 0.5
// ((xw, yw, zw, w) + (w, w, 0, 0)) * (0.5, -0.5, 1, 1) = ((0.5x + 0.5)w, (-0.5y + 0.5)w, zw, w)
// = (uw, vw, zw, w)
// => (u, v, z, 1)
dOut.SSAOPosH = (dOut.PosH + float4(dOut.PosH.ww, 0.0f, 0.0f)) * float4(0.5f, -0.5f, 1.0f, 1.0f); return dOut;
}

这里值得注意的是,我们需要在域着色器中自行选择mipmap等级。像素着色器中的方法Texture2D::Sample在域着色器中是不能使用的,所以我们必须使用Texture2D::SampleLevel方法并手工指定mipmap等级。

如果我们只是学了法线贴图的话,到这里基本上就了解的差不多了。但如果学了阴影映射和SSAO的话,那么这里就又多了两个坑要填了。如果我们用了位移映射来绘制,那么在绘制阴影的时候,也一样要走一遍位移映射;对于SSAO来说也更是如此,如果不对SSAO写入深度值的过程加入位移映射,那么在正式绘制场景的时候就会因为像素深度值不一致而被剔除,从而导致了在运行龙书的SSAO Demo时,在开启了位移映射之后,那些拥有法线贴图的物体都没有被画出来的现象:

所以接下来做的事情就是体力活了,把DisplacementMap从顶点着色器到域着色器的实现原理也要搬运到绘制阴影图的过程,以及在SSAO绘制法向量/深度缓冲区顺便写入深度/模板缓冲区的过程中。因为代码上高度相似,这里我就只是列出本章新增的着色器文件列表:

BasicEffect SSAOEffect ShadowEffect
DisplacementMapObject_VS SSAO_NormalDepth_ObjectTess_VS ShadowObjectTess_VS
DisplacementMapInstance_VS SSAO_NormalDepth_InstanceTess_VS ShadowInstanceTess_VS
DisplacementMap_HS SSAO_NormalDepth_HS Shadow_DS
DisplacementMap_DS SSAO_NormalDepth_DS Shadow_HS

C++端代码实现

在本章中,与位移映射直接相关的类有BasicEffectSSAOEffectShadowEffect类,都是在前面的基础上作的修改。然后GameObjectDraw也为此有所修改。GameApp类承担了实现过程,和SSAO的相比绘制框架的变动比较小。这里就不放出修改的部分了,读者可以自行浏览。

网格细节问题

首先要注意的是,我们是对顶点进行位移映射。如果网格的三角形比较大,比如说只有4个顶点的地板,经过曲面细分后能生成的新顶点也比较有限,位移映射的效果就不明显。为此,我们需要增大网格模型的顶点密集程度,意味着我们增大了高度图的采样点数目,以让我们能够逼近真实的地形。如果我们不走曲面细分,那我们就需要提前准备三角形密集的网格数据,这样需要占用比较多的显存或内存。但即便是用了曲面细分,我们要权衡初始网格的顶点密集程度,以及经过曲面细分后的顶点密集程度如何。

在本章的示例中,我们的地面不再使用Geometry::CreatePlain,而是用Geometry::CreateTerrain来创建出更加精细的地面网格。由于一开始写的Geometry::CreateCylinder它的侧面三角形比较大,曲面细分后的顶点数目也不够密集,为此我已经修改了它的实现,让侧面能够支持分层的三角形。

演示

下面的动图展示了基础绘制、法线贴图绘制、位移贴图绘制下的区别,以及曲面细分前后网格的区别。

而下面的动图则展示了不同的HeightScale下位移映射的效果。

DirectX11 With Windows SDK完整目录

Github项目源码

欢迎加入QQ群: 727623616 可以一起探讨DX11,以及有什么问题也可以在这里汇报。

DirectX11 With Windows SDK--34 位移贴图的更多相关文章

  1. 粒子系统与雨的效果 (DirectX11 with Windows SDK)

    前言 最近在学粒子系统,看这之前的<<3D图形编程基础 基于DirectX 11 >>是基于Direct SDK的,而DXSDK微软已经很久没有更新过了并且我学的DX11是用W ...

  2. DirectX11 With Windows SDK--25 法线贴图

    前言 在很早之前的纹理映射中,纹理存放的元素是像素的颜色,通过纹理坐标映射到目标像素以获取其颜色.但是我们的法向量依然只是定义在顶点上,对于三角形面内一点的法向量,也只是通过比较简单的插值法计算出相应 ...

  3. DirectX11 With Windows SDK--33 曲面细分阶段(Tessellation)

    前言 曲面细分是Direct3D 11带来的其中一项重要的新功能.它引入了两个可编程着色器阶段以及一个固定的镶嵌处理过程.简单来说,曲面细分技术可以将几何体细分为更小的三角形,并以某种方式把这些新生成 ...

  4. DirectX11 With Windows SDK--38 级联阴影映射(CSM)

    前言 在31章我们曾经实现过阴影映射,但是受到阴影贴图精度的限制,只能在场景中相当有限的范围内投射阴影.本章我们将以微软提供的例子和博客作为切入点,学习如何解决阴影中出现的Atrifacts: 边缘闪 ...

  5. DirectX11 With Windows SDK--06 使用ImGui

    前言 Dear ImGui是一个开源GUI框架.除了UI部分外,本身还支持简单的键鼠交互.目前项目内置的是V1.87版本,大概半年时间会更新一次版本,并且对源码有小幅度调整. 注意:直接下载源码使用会 ...

  6. DirectX11 With Windows SDK--10 摄像机类

    前言 DirectX11 With Windows SDK完整目录:http://www.cnblogs.com/X-Jun/p/9028764.html 由于考虑后续的项目需要有一个比较好的演示环境 ...

  7. DirectX11 With Windows SDK--27 计算着色器:双调排序

    前言 上一章我们用一个比较简单的例子来尝试使用计算着色器,但是在看这一章内容之前,你还需要了解下面的内容: 章节 26 计算着色器:入门 深入理解与使用缓冲区资源(结构化缓冲区/有类型缓冲区) Vis ...

  8. DirectX11 With Windows SDK--07 添加光照与常用几何模型

    前言 对于3D游戏来说,合理的光照可以让游戏显得更加真实.接下来会介绍光照的各种分量,以及常见的光照模型.除此之外,该项目还用到了多个常量缓冲区,因此还会提及HLSL的常量缓冲区打包规则以及如何设置多 ...

  9. DirectX11 With Windows SDK--08 Direct2D与Direct3D互操作性以及利用DWrite显示文字

    前言 注意:从这一章起到后面的所有项目无一例外都利用了Direct2D与Direct3D互操作性,但系统要求为Win10, Win8.x 或 Win7 SP1且安装了KB2670838补丁以支持Dir ...

随机推荐

  1. Java收徒,高级架构师关门弟子

    最近感悟天命,偶有所得,故而打算收徒若干,以继吾之传承. 有缘者,可破瓶颈,走向架构师之峰,指日可待. 拜师要求: 1.工作经验:1年或以上. 2.入门费用:10000元(RMB). 联系方式(联系时 ...

  2. 贪吃蛇游戏(printf输出C语言版本)

    这一次我们应用printf输出实现一个经典的小游戏—贪吃蛇,主要难点是小蛇数据如何存储.如何实现转弯的效果.吃到食物后如何增加长度. 1 构造小蛇 首先,在画面中显示一条静止的小蛇.二维数组canva ...

  3. [置顶] linux中fork()函数详解(原创!!实例讲解)

    分类: 计算机系统 linux2010-06-01 23:35 60721人阅读 评论(105) 收藏 举报 linux2010存储  一.fork入门知识 一个进程,包括代码.数据和分配给进程的资源 ...

  4. Typescript的interface、class和abstract class

    interface,class,和abstract class这3个概念,既有联系,又有区别,本文尝试着结合官方文档来阐述这三者之间的关系. 1. Declaration Merging Declar ...

  5. 7000 字说清楚 HashMap,面试点都在里面了

    我是风筝,公众号「古时的风筝」,一个兼具深度与广度的程序员鼓励师,一个本打算写诗却写起了代码的田园码农! 文章会收录在 JavaNewBee 中,更有 Java 后端知识图谱,从小白到大牛要走的路都在 ...

  6. django drf 10大请求序列化方法

    ## 整体单改 路由层.模型层.序列化层不需要做修改,只需要处理视图层:views.py ```python"""1) 单整体改,说明前台要提供修改的数据,那么数据就需要 ...

  7. Python三大器之生成器

    Python三大器之生成器 生成器初识 什么是生成器 生成器本身属于迭代器.继承了迭代器的特性,惰性求值,占用内存空间极小. 为什么要有生成器 我们想使用迭代器本身惰性求值的特点创建出一个可以容纳百万 ...

  8. python字典套字典

    定义字典 familyinfo = { "family name":"Python", "family structure":[ {&quo ...

  9. 《UNIX环境高级编程》(APUE) 笔记第十一章 - 线程

    11 - 线程 Github 地址 1. 线程概念 典型的 UNIX进程 可以看成只有一个 控制线程 :一个进程在某一时刻只能做一件事情.有了 多个控制线程 ,就可以把进程设计成在某一时刻能够做不止一 ...

  10. 用WebIDE(coding)编写c语言

    本期教程有对应视频 首先注册登录coding,打开coding的Cloud Studio 创建一个新的工作环境 安装依赖命令build-essential sudo apt-get update su ...