显示三维方面的软件或者游戏 电脑显示器测试软件会出现小块状闪屏,怎么解决呢?硬件应该都没问题的。

pancy12138的博客
https://blog.csdn.net/
https://static-blog.csdn.net/images/logo.gif
https://blog.csdn.net/pancy12138
https://blog.csdn.net/
https://blog.csdn.net/pancy12138/article/details/
https://blog.csdn.net/pancy12138/article/details/
pancy12138
(本系列文章由pancy12138编写,转载请注明出处:http://blog.csdn/pancy12138)
上上一次(没错,就是往前数两次)的教程为大家讲解了3D游戏是如何通过光照模拟的算法来对物体进行着色的,那么大家如果动手实现了那个亮晶晶的3D球的话,一定会对这种新奇的游戏设计技术灰常的感兴趣吧,当然接下来我们就得讨论上一次的着色方法的不足之处了。大家肯定会发现,通过这种“设置材质-&设置光照-&直接计算”的渲染方法虽然看上去比较直观,效果也显而易见。但是我们发现我们也只是在实现这种“纯色”的物体的时候好用,但是实际上,我们的生活中大部分物体并不是这种“纯红色”或者“纯蓝色”的,甚至像彩色书本或者广告纸,他们上面因为人为的添加了染料或者其他东西,整个物体的表面可以说聚集了各种各样的完全不同的漫反射材质。而且除了漫反射材质,有些合成物体由于组成的物质本身就差异很大(比如衣服和拉链),甚至导致同一个物体表面上各个部分的镜面反射材质都相差甚远。那么如何才能描述这种奇特的物体呢,对于我们来说,如果两个部分的材质和纹理不同,那么要么在顶点的格式里面记录下其材质,要么分成不同的drawcall来进行渲染。前者会浪费大量的位宽,后者会浪费大量的drawcall,这都是不可取的方法。因为材质的种类非常的多,大家现在了解的可能只有漫反射和镜面反射,事实上,真正的渲染过程还会有类似于法线啊,深度啊,反射系数啊等等用于描述材质的东西,如果要做到彻底的根据材质分解物体的话,那工作量之大是难以想象的,因此,人们提出了“纹理映射”的方法,来将复杂的材质存储在图片上,然后把图片整个的贴到三维空间里面的三角面上以代替复杂的材质系统。
事实上,纹理映射属于当今光栅图形学的几大“超级大杀器”之一,一大堆的快速优化和渲染算法都是以纹理映射作为基础的,因此,这一部分的知识非常的重要,应该算是图形设计者必须要熟悉的算法设计方式。接下来我们来探究一下,纹理映射是如何工作以及实现的。
首先,我们要想做纹理映射,至少得有一张图片,也就是决定究竟应该往几何体上贴上什么样子的东西。然后,我们还必须要指定这张图片的映射意义,也就是说这张图片究竟是作为什么材质被导入的,比如漫反射,镜面反射,直接反射等等。这里指定其类型是不需要进行任何编程的,这只是逻辑上的指定而已。有了这张被制定好用处的图片,我们就可以想办法把它贴到物体上面了。现在我们来分析一个比较简单的问题,图片上的颜色信息记录在像素点上,而几何体是一个一个的三角形。如何才能把这张图片,贴在每一个三角形上面呢?这里我们所使用的方法,就是所谓的UVN式的纹理映射方法。大家如果之前学习过建模知识的话,在使用3DSmax或者maya等建模软件的时候就会了解到“展UV”这种纹理制作的操作。这里的UV就是我们在程序里面做纹理映射的方法。也就是每一个三角形的顶点现在不仅仅要存储其位置和法线信息,还要多存储一个uv坐标用来标识其在纹理图片上的映射位置,这个坐标是二维的,理解起来也很简单。如下图:
只要我们定义了一个三角形的三个顶点在图片中对应的位置,我们就可以把图片中对应的区域映射到3D空间的三角面上面。这个时候就有人说了,这种算法听上去还是不错的,但是好似有很多漏洞。比如说首先,你这里只有三个点的坐标,点的内部怎么办,他们要想填充也需要很多的坐标才行。然后就是三角形是矢量的图形,它是可以无限放大缩小的,那你这张图片是光栅图形,根本没法放大缩小,怎么可能进行完美的映射算法呢?首先我们先说点的内部的问题。如果对这个问题有疑惑的童鞋一定是对光栅化管线了解的还不够多,实际上当我们在投影变换结束后对三角形进行光栅化插值的时候,所有的顶点结构体变量都会根据“硬件线性插值算法”获得其内部点的数据,也就是说,我们只要保证顶点的数据是“可插值的”就可以了。那么至于什么信息是可插值的神马是不可插值的,我们前面其实都已经说过了,凡是矢量信息均可以插值,比如坐标,法向量等等。而凡是标量信息均不可插值,比如距离,颜色等等。因此,我们的UV坐标是可插值的,所以可以借助光栅化的过程为内部的三角形进行UV坐标的填充工作。接下来是另一个问题,也就是图片是不可以放大缩小的,而三角形是可以放大缩小的,如何才能处理并解决他们的映射问题呢?这里我们注意,我们的UV坐标并不是根据图片分辨率来决定的,比如说一张的纹理和一张的纹理,他们的UV范围都是[0,1]。也就是说我们并不管图片的原始分辨率是多少,统一认为他们的横纵坐标均属于[0,1]范围内。接下来,我们要对其采样的时候,根据其UV坐标比如[0.221,0.134],去图片上寻找对应数据所对应的像素颜色。当然,很明显一个任意的小数不会都对应图片上的像素点。这也就是我们前面提到的问题,那么接下来我们要做的就是根据这个坐标来映射像素,映射的方法有很多,比如最简单的就是最邻近映射,也就是找离它最近的一个像素点进行映射,当然这是最水的粗糙办法,其他的还有双线性插值映射以及各向异性差值映射,这些都是通过插值算法来解决不能一一对应到像素点的问题。这样我们就可以解决一些图片在放大缩小过程中的映射更改情况。还有一个问题就是在放大缩小的过程中由于同一个部分有可能因为映射不断地发生变化而产生动态的锯齿抖动现象。我们需要为同一张纹理做mipmap得到很多张不同分辨率的问题,这样在映射的过程中不至于因为放大缩小导致剧烈的抖动现象。
上面说了这么多,接下来我们就要进行正式的纹理映射的实现讲解了,首先我们来看的就是纹理的资源创建过程,也就是如何才能创建一份纹理资源并将其保存在GPU的显存上面备用和访问的问题。其实,纹理资源(texture)和缓冲区资源(buffer)同属于一种东西,也就是显存上的资源,这种资源在directx里的保存方式与buffer资源同属于一个基本的类,这个类叫做id3d11resource。也就是说两个人的数据保存方式是相同的,但是他们一个作为纹理资源,一个作为缓冲区资源,在保存格式上还是有些不同的。因此,在继承了Id3d11resource的基础上,纹理资源产生了一个子类叫做Id3d11tex2D(或者1D,2Darray,3D等),而缓冲区资源产生了一个子类叫做Id3d11buffer。这两个子类在显存上存储数据的方式是相同的,但是在访问格式上有很多的不同。而因此,当我们在创建tex2D的时候,也应该使用一个其特有的格式结构体来进行创建,也就是D3D11_TEXTURE2D_DESC:
&span style="font-size:18"&typedef struct D3D11_TEXTURE2D_DESC
UINT ArrayS
DXGI_FORMAT F
DXGI_SAMPLE_DESC SampleD
D3D11_USAGE U
UINT BindF
UINT CPUAccessF
UINT MiscF
D3D11_TEXTURE2D_DESC;&/span&这种格式对应了创建buffer的时候所用的D3D11_BUFFER_DESC,那么接下来我们继续分析这种格式的内部成员的含义。前两个是纹理的大小,非常简单。第三个是mipmap的数量,也就是我们前面所说的解决抖动所使用的算法需要创建几层不同分辨率的纹理。第四个参数是纹理数组的数量,这个参数用于实现GPU上的纹理数组功能,这个问题属于比较特殊的问题,也就是如何在一个drawcall里面使用大量纹理的问题,单纯的纹理映射数组并不能支持变量下标访问,也就是说你在GPU上定义了texture2D
rec[100],在使用的时候只能用rec[0]或rec[1]而不能说用rec[i]这种写法。因此我们需要在这里指定数组的大小以实现可以使用下标访问的纹理数组,不过这种做法属于比较高级的内容,这一次的博客大家可以先忽略。第五个参数是指纹理的格式,也就是RGBA16,RGBA32等标识一个像素到底占多大的空间以及用什么格式保存。第六个是指采样格式,也就是MSAA抗锯齿的信息如4X抗锯齿或者8X抗锯齿。第七个是使用方式,一般来说是default也就是默认方式,也有的时候我们会更改成其他的格式将纹理数据采样回cpu。第八个是绑定格式,这个格式决定了纹理是否能够被当做渲染目标,是否可以充当GPU常量等等。接下来的一个变量就是指定其是否可以被CPU访问,也就是说是否可以把显存资源拷贝回cpu或者被CPU修改,一般来说在播放视频的时候可能会用到。最后一个是用于标记cubemap等多张纹理资源的格式,一般单独的纹理都是直接设为零的。下面给大家展示创建一个纹理资源的过程:
&span style="font-size:18"&HRESULT
D3D11_TEXTURE2D_DESC texD
texDesc.Width = map_
texDesc.Height = map_
texDesc.MipLevels = 1;
texDesc.ArraySize = 1;
texDesc.Format = DXGI_FORMAT_R16G16B16A16_FLOAT;
texDesc.SampleDesc.Count = 1;
texDesc.SampleDesc.Quality = 0;
texDesc.Usage = D3D11_USAGE_DEFAULT;
texDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET;
texDesc.CPUAccessFlags = 0;
texDesc.MiscFlags = 0;
ID3D11Texture2D* Tex = 0;
hr = device_pancy-&CreateTexture2D(&texDesc, 0, &Tex);
if (FAILED(hr))
MessageBox(0, L"create texture error", L"tip", MB_OK);
}&/span&上面就是一个标准的纹理创建方式,那么当我们创建了一个纹理,如何才能在GPU上,或者说在shader里面去访问它呢?这个问题就简单多了,当我们拥有了一个纹理资源之后,我们只要再根据这个资源创建一个shader上的资源访问器Id3d11shaderresourceview就好了。当然啦,很多人会问为什么不能在GPU上直接访问纹理资源呢,要这个shaderresourceview干什么?那么这里我们解释一下,因为纹理的本质实际上和buffer一样,是一堆存储在显存上的线性存储的数据,我们在CPU上可以很方便的对内存做各种各样的操作,但是在GPU上就很难了,所以HLSL以及directx为我们在封装了最基本的数据IO操作,并为我们暴露了一些常用的数据IO接口,比如要想把纹理图片作为GPU常量缓冲区使用就可以依靠shaderresourceview来读取数据,如果希望作为渲染目标来访问就可以使用rendertargetview(比如渲染到纹理),当然还有很多其余的用处,比如depthstencielview以及unorderedaccessview等,这些东西叫做数据访问接口。这里举一个最简单的例子吧。如果我们假设texture资源是一块malloc出来的void*的数据texdata,那么各种各样的view就是各种不同类型的指针,比如int*
shaderresourceview = (int*)texdata,或者char* rendertargetview = (char*)texdata。数据还是那一块数据,但是由于用来访问数据的指针不同,导致我们能够完成的工作也是各有不同。当然在CPU上面我们操作内存比较容易(或者说用C/C++操作起来比较容易),但是在GPU上没有那么容易,所以必须要创建一大堆的接口来封装这些访问格式的问题。当然,这只是为了让大家理解的更为清晰所做的工作,真正使用的时候其实还是很简单的,下面就是创建一个shaderresourceview的方法:
&span style="font-size:18"&ID3D11ShaderResourceView *ambient_tex0;
ID3D11Texture2D* ambientTex0 = 0;
hr = device_pancy-&CreateTexture2D(&texDesc, 0, &ambientTex0);
if (FAILED(hr))
MessageBox(0, L"create ambient map texture1 error", L"tip", MB_OK);
hr = device_pancy-&CreateShaderResourceView(ambientTex0, 0, &ambient_tex0);
if (FAILED(hr))
MessageBox(0, L"create ambient map texture1 error", L"tip", MB_OK);
}&/span&可以看到如果没有特殊的要求(比如有些纹理访问视图需要修改UNORM格式为指定格式)创建流程非常简单,只要先创建一个texture2D,再根据这个纹理资源就可以创建访问视图了,这里注意,创建完访问视图如果以后不需要texture2D资源的信息的话,可以直接释放tex2D,而访问视图里面仍然还保存着纹理的资源信息,这里其实就相当于把指针的信息删除,如果其内部的存储资源没有被删除的话,保留另一个同样指向这块内存的指针也仍然可以访问这一片内存。而directx是基于COM的设计方式的,会自动记录一个资源有多少个指针指向它,当完全没有指针指向它的时候才会彻底释放显存资源。
我们在大部分时间希望能够从文件里面导入一些纹理资源,也就是directx里面的dds纹理资源。这个导入方法要比上述的纹理创建方式更加的简单,只需要一句话就可以搞定了。但是这个纹理导入函数不属于dx11核心库,我们需要导入directx xtk库来完成外部文件的导入,库的地址及使用方法在前面的基础课程第一课就已经提到了,大家可以自己去把这个库配置一下,配制完毕之后可以使用下面所说的方法来进行文件纹理导入:
hr_need = CreateDDSTextureFromFile(device_pancy, texture_name, 0, &matlist_need[i].tex_diffuse_resource, 0, 0);
hr_need = CreateDDSTextureFromFileEx(device_pancy, contex_pancy, texture_name,
0, D3D11_USAGE_DEFAULT, D3D11_BIND_SHADER_RESOURCE, 0, 0.0f,
false, NULL, &matlist_need[i].tex_diffuse_resource);上面的函数是最基本的创建函数,这种创建方法将得到一个不做自动mipmap的纹理资源,而下面的方法将得到一个经过mipmap的纹理资源。大家可以根据纹理的格式来选取创建的方法,这种方法创建起来非常简单,里面的参数和上面我们介绍的也没多大差别,大家看一看估计就能看懂了。
接下来我们分析如何才能将其应用到shader里面,这一步的话其实是工作于pixelshader的,因为前面我们也说了,要借助光栅化插值来得到填充后的UV坐标,那么在shader里面,我们只需要将插值完成之后的shader坐标拿出来,然后根据这个坐标以及纹理采样格式,就可以从相应的纹理图片中取出像素的颜色出来:
&span style="font-size:18"&SamplerState samTex_liner
Filter = MIN_MAG_MIP_LINEAR;
AddressU = WRAP;
AddressV = WRAP;
texture_light_
//漫反射光照贴图
struct Vertex_IN_bone//含法线贴图顶点
float3 pos
: POSITION;
//顶点位置
float3 normal
: TEXCOORD;
//顶点纹理坐标
struct VertexOut
float4 position
: SV_POSITION;
//变换后的顶点坐标
float2 tex
: TEXCOORD;
//纹理坐标
float4 PS(VertexOut pin) :SV_TARGET
float4 tex_color = texture_diffuse.Sample(samTex_liner, pin.tex);
return tex_
}&/span&其中samplerstate就是我们前面所述的采样方式,这里我们使用的是线性采样方式,其他的还有最近点采样以及各向异性采样。下面的UV格式主要是定义了纹理的拓展格式,也就是对于超过[0,1]边界的坐标如何处理的问题,常见的比如截断,重复,拉伸等等。这里我们写了一个非常简单的shader,就是单纯的吧纹理颜色显示出来,效果如下:
上图我们直接把saber的图片作为立方体最终的颜色输出到了屏幕上,整个过程还是很简单的。当然,目前我们只需要快速的了解这种映射算法就可以了,后面我们会提到更多高级一些的应用,这个算法的重要性到时候会体现的淋漓尽致。那么今天的教程就算是结束了,seeyou
作者:pancy12138 发表于
https://blog.csdn.net/pancy12138/article/details/
https://blog.csdn.net/pancy12138/article/details/
https://blog.csdn.net/pancy12138/article/details/
pancy12138
(本系列文章由pancy12138编写,转载请注明出处:http://blog.csdn.net/pancy12138)
上一次的教程大家了解了最简单的着色方法,那么这一节我们来讲解一些与着色无关的渲染管线流程。虽然在上一次的教程中我们成功的展示了如何给一个球体的表面进行光照着色。但是并不是说我们以往所有的知识就足以完美的展示出一个3D场景了。之所以我们能够看到那个球体而没有产生任何不适的感觉,主要是因为我们不知不觉的已经用到了一些缓冲区的知识。当然,这在基础篇一开始的时候就有涉及了,只是当初我们并没有过多的去关注这些知识。那么今天我们就要来仔细的分析缓冲区这一在图形管线中极为重要的算法。
首先,神马是缓冲区,这个名词大家应该不会陌生,所谓缓冲区就是一个和屏幕大小相当的数组。这在我们入门篇讲解双缓冲抗闪屏的时候就已经讲到了。当初我们在讲解交换链的时候第一次提及到了后台(颜色)缓冲区。那么事实上,为了保证绘制过程的完美,整个绘制过程中我们需要很多缓冲区来实现各种各样的算法。那么今天我们要讲的缓冲区包括:深度缓冲区,模板缓冲区,颜色缓冲区。然后,还有一个配合颜色缓冲区的alpha混合。注意,这里所有的缓冲区都只是一个和屏幕一样大的数组,所以他们都是工作于光栅化之后的,并且他们只会对每个屏幕像素保留一个值。使用的时候也是如此。大家不要习惯性的认为这些缓冲区能够存贮整个3D场景的所有信息。
首先是深度缓冲区,这个缓冲区的作用非常的明显,就是为了判断物体之间的“遮挡”关系。比如说,当你把一个苹果放在一个桌子前面的时候,苹果就会把桌子的一部分挡住。但是我们之前讲的3D-&2D的投影操作以及矢量-&光栅的光栅化操作都不能得到这个效果,这就会在之后得到一些错误的图像,比如如果先画苹果后画桌子,就会发现桌子把苹果给覆盖了,这很明显和我们当初的想法是不符的。于是,无论是directx还是opegl都默认提供了一个深度缓冲区,这个缓冲区将会在光栅化完毕之后对每一个点进行一次“深度测试”,看看这个点究竟是不是离摄像机最近的点,如果是,那么久盖住之前的点,如果不是,那么就不允许它绘制。用最简单的程序语言来描述就是:
float depth_buffer[wind_width][wind_height];
void clear_buffer
for(int i = 0; i & wind_ ++i)
for(int j = 0; j & wind_ ++j)
depth_buffer[i][j] = 1.0f;
void depth_check(float depth,int width,int height)
if(depth & depth_buffer[width][height])
draw_point(width,height);
depth_buffer[width][height] =
}也就是跟大家平常写的找一个数组里的最大值差不多。每一帧开始的时候把缓冲区的每个像素都清空成最远(注意这里使用投影之后的点的z坐标作为距离摄像机的距离,所以最远是1.0,最近是0.0)。然后,每次绘制物体的时候就调用检验函数来进行深度测试。当然深度测试函数肯定不能像上面我写的那样,在图形库里面基本上都是调用的GPU加速的算法,并且属于内置算法之一,运行速度是非常快的。这里也许有些人会有一些疑问,为什么物体距离摄像机的深度可以用它的z坐标来表示?如果换了观察视角怎么办。事实上这个问题我们在之前的教程中已经讲解过了。无论是directx还是opengl都是不允许你更改观察视角的,也就是说那个视角其实是固定的。如果我们需要观察视角变更的话,只要把物体挪动就好了。也就是说你想看一个物体的背面,并不需要走到他后面去,只需要把他转一下把背面露给你就好了。深度缓冲区的用法事实上是很简单的。directx里面对于深度缓冲区是使用ID3D11depthstencilview来进行管理和使用的,而由于深度缓冲区的本质是一个二维图片数组。而二维图片数组本身是属于“纹理”范畴的。所以,他的基本资源是一个texture2D资源,而管理器资源是ID3D11depthstencilview。讲到这里大家估计会比较晕。博主你到底在说啥呢?这里我稍微提及一下directx的资源管理方案:
首先,我们要知道,所有的大型资源(几何体,纹理图片,缓冲区等等)都必须存储在显存当中。但是我们控制绘制调用是用的CPU来调用的。因此,为了避免CPU和GPU的数据交换,directx为每一种不同的资源提供了不同的访问方法,比如说shaderresourceview(SRV)用于访问纹理,unorderdaccessview(UAV)用于访问GPU可读写资源,depthstencilview(DSV)用于访问深度模板缓冲区资源,这些资源访问类我们可以理解成定义在cpu的指针,指向GPU的指针。当然这是一种更高级的访问方式。而这些指针只拥有访问资源的权限,并不能创建资源。如果要创建资源的时候,我们对于不同的资源会有不同的类来进行,比如说Id3d11buffer,或者texture2D这些。这些类都是继承了id3d11resource类,其功能就是创建以及修改显存中存储的各种大型资源。由于显存和内存之间进行数据交换非常的缓慢,所以我们一般只有在程序初始化的时候用这些类来进行显存的创建工作,一旦显存创建完毕,我们就可以只使用前面的那些GPU访问类来对显存资源进行调用。这样就可以让程序变的非常的流畅。而openGL也是以类似的思想来进行资源管理的(opengl1.0除外),只不过opengl里面并没有所谓的访问指针,创建指针等分得很细的东西,我们在cpu上用一个int变量就可以代表显存上相应的资源,由于所有的操作都封装在OpenGL的状态机里面,所有我们既可以用这个int进行显存的创建,也可以用它进行显存的访问。这种方法的话有好处也有坏处,好处是使用起来简单,坏处是资源交换以及绑定就不简单以及不太好封装起来。不过这都不是重点,大家程序写多了自然就都习惯了,所以对于这种资源管理的知识,大家要多练习,这样才能掌握的比较好。
D3D11_TEXTURE2D_DESC dsD
dsDesc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;
dsDesc.Width = wind_
dsDesc.Height = wind_
dsDesc.BindFlags = D3D11_BIND_DEPTH_STENCIL;
dsDesc.MipLevels = 1;
dsDesc.CPUAccessFlags = 0;
dsDesc.MiscFlags = 0;
dsDesc.Usage = D3D11_USAGE_DEFAULT;
dsDesc.SampleDesc.Count = 4;
dsDesc.SampleDesc.Quality = 0;
ID3D11Texture2D* depthStencilB
device_pancy-&CreateTexture2D(&dsDesc, 0, &depthStencilBuffer);
device_pancy-&CreateDepthStencilView(depthStencilBuffer, 0, &depthStencilView);
depthStencilBuffer-&Release();上述就是一个深度缓冲区的创建过程,这里要注意的是深度模板缓冲区的msaa抗锯齿倍数一定要和渲染目标,也就是后台缓冲区的渲染倍数相同。不能出现比如后台缓冲区4倍抗锯齿,深度缓冲区8倍或者不开的这种情况。
这里我们发现,深度缓冲区并不是一个单独的缓冲区,他还附带了一个模板缓冲区,一般一个32位的缓冲区会被分为两部分,其中24位作为深度缓冲区,8位作为模板缓冲区。那么神马又是模板缓冲区呢,这个缓冲区又有什么作用呢?接下来我们就要来讲解这一部分的知识。
由于深度缓冲区在directx与opengl中属于功能不可更改的缓冲区(只能工作于深度比较)。但是其比对的思想又非常的好用。所以我们有时候会希望达到类似的像素测试效果,但是不希望用深度作为标准,比如说我们希望x坐标小于1的像素点不要显示在屏幕上。那么这个时候我们就需要一个自定义的缓冲区,这也就是模板缓冲区的作用,我们在使用模板缓冲区的时候,先要给缓冲区设定一个比较函数。然后再给模板缓冲区写入一个用于比较的区域,然后再进行正式的渲染。这个时候就可以进行模板测试,凡是不能符合测试的像素点都不允许通过。这样干说理论可能大家比较晕。我举一个最简单的例子,比如说镜面效果,就是通过镜子能够看到物体的镜像这一效果:
上图就展示了模板缓冲区的使用方法,首先我们先把镜子渲染一遍,借助这一过程把镜子所在的像素点全部标记成可访问的模式(白色区域),得到一张下面所示的模板缓冲区的数据。然后第二遍正式渲染的时候我们就可以开启模板测试,只要最终渲染的像素坐标对应的模板颜色不是白色的(也就是不可访问),那就不能显示,最终就可以使得我们渲染的模型的影子只出现在镜面所在的位置。
当然,这只是最简单的一个模板缓冲区的使用方式,模板缓冲区的应用还是比较多的,最经典的比如说shadow volume算法,这个会在之后讲解全局阴影的时候来进行讲解。现在可以大概的了解模板缓冲区的作用就好了,因为这一缓冲区在大部分传统算法中还是很少用到的,等到真正用到的时候再仔细学习也不算迟。
最后我们来讲一个比较常用的东西,后台(颜色)缓冲区以及alpha混合。颜色缓冲区以及其对应的颜色测试算法属于最后一个测试算法。这个测试算法主要是用来屏蔽一些颜色输出以及进行最终的颜色处理工作。我们在一开始讲解后台缓冲区的时候只是提到他是为了防止闪屏而存在的。不过经过多年的优化,他也在进行图像累计的过程中同时做了很多的颜色处理工作。首先是颜色屏蔽。如果我们的程序需要最终渲染的颜色的某个通道(RGBA)被屏蔽掉的话就可以开启这个颜色屏蔽,这样就可以达到一些比较特殊的效果,比如说我的博客译文里面的毛发渲染,就是在pass1屏蔽了RGB通道来记录最终的alpha效果。除了直接屏蔽通道以外,我们还可以根据自己的想法来进行颜色剔除工作,这个也很简单,就是在pixel
shader里面,使用clip()函数来裁剪掉我们不想要的像素点。
然后是alpha混合,我们知道很多时候我们用于渲染的图片并不是全部都是我们想要的,比如说一个角色的原画,周围的一些黑乎乎的东西我们就不希望他出现在渲染结果里面:
比如说上面这张图片,我们希望把角色显示出来,但是不要显示周围的黑乎乎的背景。在ps中我们一般会选择抠图来解决。换算到shader里面就是我们说的clip(color.r & 0.001f && color.g &=0.001f && color.b &=0.001f)这种类似的方式。但是这种硬生生的抠图算法会使得图像的边缘非常的不平整,也就是不够平滑。这是其一。其二就是对于一些“半透明”的物体,比如说水晶啊,水域啊这些的,我们希望透过它看到一些东西的时候就不能简单地靠抠图来进行解决。这个时候就需要alpha混合算法来进行。所谓的alpha混合就是指在进行颜色合成的时候,我们新渲染的物体的颜色会和之前渲染的物体的颜色进行合成。合成的公式有很多,最简单的就是a.rgb*a.a
+ b.rgb*(1-a.a)这样得到一个新的颜色效果,这种做法即可以平滑的处理“抠图”的效果,也可以很好的处理“透过a看到b”这一现象。alpha混合在很多算法里面都有涉及,其应用范围可以说是非常的广。不过目前来说我们基础篇的程序尚且没有用到这一功能。所以大家先了解这一概念就可以了。之后再在讲解相关的渲染算法的时候我们会经常提到这一概念。
ok,那么这一次的教程到这里就算是结束了。这一次的教程我们并没有讲解任何程序相关的东西,只是给大家普及了缓冲区以及alpha混合的概念。这些东西暂时还是用处比较小的,但是他们是确实存在于渲染管线当中,并且在以后的算法设计过程中会体现出非常巨大的效果。所以大家可以先了解了解这些知识。这样之后在了解更为复杂的算法的时候就会更加的得心应手。
作者:pancy12138 发表于
https://blog.csdn.net/pancy12138/article/details/
https://blog.csdn.net/pancy12138/article/details/
https://blog.csdn.net/pancy12138/article/details/
pancy12138
Advanced Real-Time Rendering in 3D Graphics and Games Course – SIGGRAPH 2007
探寻次时代渲染 - CryEngine2
作者:Martin Mittring
Crytek 游戏公司
翻译:潘曦
(译文里的(pancy:XXX)为译者注)
(本系列文章由pancy12138编写,转载请注明出处:http://blog.csdn.net/pancy12138)
图1:来自于获奖游戏”孤岛危机”的一张截图,它的发布为渲染界开辟了下一个时代。
图2:来自于即将到来的最新的孤岛危机的截图。
在这篇文章中,我们并不具体探讨某一种特定的算法。而是尝试去寻找德国公司Crytek使用的一些能在一起发挥出更好的效果的渲染算法。(pancy:这篇论文所提到的大部分算法都是以往就提出的,Crytek所做的工作是从众多的图形算法中找到最适合使用的算法,将他们经过一些改变,让他们能够更易实现,互相融合并应用于真正的图形工业当中。这对于现代实时渲染技术的发展意义很大)
我们认为这些信息对于所有希望能够实现与Crytek公司相似的渲染效果的人员来说都是有用的,因为往往我们在算法实现的时候所遭遇到的瓶颈正是在某一渲染效果的基础上实现一个渲染效果的时候(pancy:也就是我们在同时用多种渲染算法渲染场景的时候,出现的问题和bug也最多),我们同时简明的介绍了一些你可能会考虑使用的可以用于替代那些方法的途径。我们并没有把图形方面所做的所有工作完整的进行讲解,因为在这篇文章中我们挑选了读者们所感兴趣并且我们能够拿得出手的领域的拓展技术进行讲解。Crytek的开发选取了图形学社区近年来最具先进意义的理论研究,并且结合了一些新的思想以使得这些先进的理论能够有效的映射到当今的图形硬件当中。
Crytek studios所开发的一个技术出色的第一人称射击游戏”孤岛危机”在其一经发布便广受好评,“孤岛危机”的出现提高了所有同类型游戏的制作门槛,当我们公司成功开发了“孤岛危机I”之后,一个很自然的想法是使用这目前的的引擎来经过很少的改变制作一个它的续作。因此续作会几乎使用在第一作中用到的相同的引擎。当然,这是一个最简单且获利最高的决定,但是我们相信这种想法必然会限制我们最初的目标-为游戏赋予更好的技术和艺术。因此我们决定去设计一个全新的次时代渲染引擎并且设法提高新引擎的设计和架构,除了新添加的一些特性,新的游戏“孤岛危机II”将会继承一代的游戏风格,不过新的游戏获得各个方面的极大范围的效果提升。游戏的一切将会变得更为庞大和优秀。而我们所设计的新的游戏引擎
CryEngine2则会让我们的梦想成真。(pancy:这篇文章的讲解重点,众所周知的著名次时代引擎CryEngine,introduction写的简直豪气冲天)
在我们阅读了设计文档以及开过了一个紧张的审议会议之后,我们所有的设计师,程序员,以及艺术家达讨论出了一组新引擎需要达成的如下目标:
游戏需要包含三种不同的环境:
图3:丛林中的天堂
拥有大量的模型对象,三维高度图,海洋,广阔的视野,来自于一个方向光源的环境光。
图4:外星人的室内场景。
拥有许多点光源,整体偏暗,区域划分,几何体相互遮挡,体积雾效果
图5:冰雪环境
冰材料层,地面的次表面散射
完成所有的三个场景是一项极具挑战性的工作,主要的优化难点在于他们拥有完全不同的特征。
1.避免触发恐怖谷效应的电影级渲染(pancy:恐怖谷效应,当虚拟人物的真实程度将要达到真实人类水平而未达到时会出现一个“恐怖谷”此时人类会对这种虚拟人物感到最大的恐惧和厌烦,当真实程度真正达到真实人类水平的时候才会让人类对其的好感回升):渲染质量越接近视频质量,则观众就需要对游戏投入越少的忍耐。
2.动态光线与阴影:提前算好的光照数据是许多算法提高渲染质量和性能的一个关键因素,而由于这些算法往往具有静态依赖的特性,所以我们不使用这些算法而是使用动态的光影算法。
3.支持多个GPU与CPU协同工作:多线程加多个图形卡协作开发是一项很复杂的任务,因为我们大多时候必须要为了更好地协同而牺牲其他的配置。
4.场景大小需要21km*21km的游戏范围:我们希望做到这一点,但是最终的生产,流水线,以及世界的持久性收益并不值得我们做这些努力,最终我们实现了多个4km*4km的游戏范围。
5.显卡指令从shader model2.0升级至shader model4.0(Directx10):虽然看上去使用shader model2.0作为开发指令是一个最方便的选择,但是这种做法会使得我们在directx10下使用最新的图形卡和图形驱动进行开发受到阻碍。
6.高动态曝光(HDR):我们在“孤岛危机I”中使用HDR获得了非常好的效果。为了更真实的视觉效果,我们希望设计的游戏不受低动态的亮度差所限制。
7.动态环境(可能会被break):这是一项很酷炫的技术,不过非常难以实现。
8.在开发渲染引擎的时候同时开发游戏:这将迫使我们总是在一些正在使用的状态中进行编码,这对于一个小型的工程是比较简单的,但是当工程变得巨大的时候这项工作将变得极具挑战性。
我们的概念艺术家创作了许多概念图来确定了游戏最初的雏形。但是为了让大家感受到最终的渲染效果,我们通过外包公司 Blur3 studio制作了一份宣传我们设计理念的视频。这有助于让我们达到我们希望达到的外观和感受。
图6:Blur3制造的用于为Crysis公司的引擎设计的概念视频的一帧截图
在这篇文章的余下部分,我们会先讨论CryEngine2所使用的新的着色框架,这个领域对于我们的大范围场景来说是一个极大的挑战。之后,我们会介绍我们在实现直接照明与非直接照明效果时所使用的解决方法(包括一些设计思路),我们使用专门的算法将不同的光照从总问题中隔离出来讨论,并且用更为先进的方法来解决它。在做完这些处理后,我们先从阴影的角度来处理直接光照(因为阴影在不同复杂度的shader中都可以很轻易的完成)。间接光照我们可以用环境光遮蔽技术来进行模拟,通过简单的令光线变暗来代表环境光遮蔽的贡献。最后,我们涵盖了各种算法去解决各个层次的细节问题。当然,本篇文章只会包含我们引擎在渲染方面所做的工作,但是这依然会给予读者读我们整个系统的一个不错的”品尝”,并且运行我们在这些选定的领域做足够深入的挖掘。
着色器与着色技术:
1.cryengine1的历史回顾
在“孤岛危机I”中,我们支持的最低等级的显卡设备是NVIDIA GeForce 2系列。这也意味着我们不仅可以使用顶点着色器和像素着色器,还能共同支持固定管线T&L函数与寄存器管线(通过提前运行pixel shader解决方案来进行纹理混合)。为了达到这个目标以及支持更复杂的材质,我们的Directx和Opengl的shader脚本有了更复杂的规则。在“孤岛危机I”之后,我们希望提高前作的质量并且重构之前的系统。首先我们移除了所有固定管线的支持,并让语法更贴近于[Microsoft07]中所描述的FX语法格式。在项目的晚期,我们为项目加入了一个基于接über-shader的一些新的渲染路径。这基本上是由一个使用CG或者HLSL语言书写的包含了很多#ifdef的一个pixel
shader和一个vertexshader组成的渲染路径,这使得开发变得简单而快捷,因为我们完全的避免了手动优化的步骤。早期的shader编译器还不能够总是创建出达到人类手工优化的汇编指令,但是在shadermodel2.0的显卡上已经修缮了很多。über-shader渲染路径的内置变化非常的多,所以将他们一次全部编译时不可能的,我们在开发过程中发现在编译的时候有明显的延迟(当我们不得不编译shader的时候),但是我们希望能够在建立游戏的时候拥有一个已经预编译好的shader存储器。我们会在一开始暂停我们的游戏在Nvida或者Ati硬件设备上的运行直到我们的缓存器读完所有的信息。更多细节可以参考我们的第一款引擎的[Wenzel05]章节。
2.cryengine2
我们决定减少需求的数量以使得新的引擎更加简洁,因此我们移除了opengl的支持以及固定管线的支持,这使得我们可以将shder的格式更接近于FX格式,shder的开发和学习也变得更加方便和简洁(pancy:FX格式是微软为directx实现的一种shder管理格式,cryengine要想在opengl以及固定管线中也是用这种格式是比较困难的,移除这两个支持极大的统一了shader格式)。我们面临的另一个待解决的问题是shder的组合太过庞大,我们重建了整个shader系统,围棋添加了一个shader需求列表缓冲器。这一列表的数据通过网络从公司的所有机器上获得,并且我们在夜间对缓存中的shader进行编译工作。然而编译时间仍然显得过长了,因此我们决定通过以下操作来减少shader的组合个数。
1.动态分支。
2.减少组合并且接受更少的功能。
3.减少组合并减少一定的性能。
4.多通道分离。
我们通过使用分布式的shader编译器的多次迭代,在一个小时内完成了所有shader的编译。
3.格式法线贴图
由ATI[ARI04]提出的3DcTM纹理格式允许将法线贴图的每个像素点通过一个高质量且只有一点点额外shader(重建z缓冲区)的代价压缩至一个字节,未压缩的法线贴图一般使用4个字节来存储一个像素(法线的xyz存储在RGB分量中,另一个字节的分量会被填充并浪费),在我们的新引擎中,我们决定在纹理的加载过程中不做压缩操作,而是在我们的纹理处理工具中进行处理,在纹理处理工具中为其创建mip levels和压缩。这种方法使得我们通过简单的预处理得到了快捷的加载速度。对于那些不支持3Dctm的硬件,我们会在加载的时候将这种格式转化为DXT5格式,这种格式与之前的格式非常的相似因此转换是很方便的。当视频质量规格较低的时候纹理转换带来的质量上的损失是可以接受的。而且即便是较为古老的NVIDA显卡的驱动也是有3DcTM纹理模拟的,所以我们不需要在这个方面做过多的关心(这种模拟是没有转换损失的,相应的代价是每个像素需要2个字节的存储空间)。
4.逐像素景深
使用提前的深度缓冲测试可以减少像素着色的代价,因为大部分像素都可以在深度测试的时候被剔除掉,这样就这些像素就不需要在pixel shader里面被执行。由于我们希望使用更复杂的pixel shader进行像素着色。我们的渲染管线从一开始就使用提前的深度缓冲区测试。如果这样做的话,我们就需要添加更多的draw call(pancy:我们通常使用的z-test是在 pixel/fragment
shader 之后的,如果希望在此之前进行测试则需要先把场景写入到深度缓冲区一遍,这样就至少多了一次draw call)。而在很多效果器(pancy: effect是FX格式所定义的单位,包含一整套渲染路径,此处翻译为效果器)当中,我们还希望能够使用到场景的深度信息。这样我们就需要将深度缓冲区的输出绑定到纹理资源上以备使用。一开始我们使用R16G16的纹理格式,原因是这种格式能够被所有的硬件所支持并且16位的大小也足以实现我们想要的质量。最初我们是使用双通道的纹理资源但是最终我们优化了这个方法。在ATI的显卡上,我们使用R16纹理格式以节省一些内存和带宽的消耗。我们意识到在一些硬件设备上R16G16格式要比R32格式的速度要慢,所以当显卡不支持R16格式的时候,我们采用R32格式去替代它。当然,直接访问深度缓冲区是一种更好的选择,因为这样我们不需要额外的内存,提前的深度测试会变得更加快捷(在一些硬件上不写入颜色到纹理可以提升两倍的速度)。因此在最后我们根据机器的支持程度来使用R16,R32甚至是默认的深度缓冲区。深度数据被用于在延迟渲染中的一些渲染技巧当中(pancy:
CryEngine在这篇论文里面所使用到的很多算法都是需要深度重建的,比shadowmap和ssao,这些算法的特点就是通过屏幕空间的深度还原原始的点坐标的三维信息来大大减少算法在像素着色器上的复杂度)。通过一个MAD操作以及一个三分量插值机,我们完全有希望根据深度信息重建这个点的原始三维信息。当然,如果需要达到浮点型的精度,还是最好根据摄像机的位置或者临近点的位置进行三维重建,如果我们在pixel shader里面使用了24位或者16位的浮点数据的话,那么这种三维重建的方式就变得很重要了。通过偏移所有的模型对象以及光源,我们很容易让观察者接近坐标原点(0,0,0)。如果不做这个操作的话,贴图和动画效果会出现很多跳动和闪烁。我们将景深效果应用于逐像素的场景效果其当中,最终效果和全局雾效,体积雾,以及软深度缓存粒子的效果很相像。
阴影模板的创建也借助场景的深度来减少draw call个数,对于水面的模拟,我们也可以使用深度数据去柔化水面效果,并沿着海岸逐渐增强。一些后处理效果例如运动模糊,景深模拟以及边缘模糊也同样会用到各个像素的深度信息。这些算法的细节记录在[Wenzel07]。
5.世界空间着色
在“孤岛危机I”当中,我们将视点和光源转换到了切线空间(根据物体的表面方向),由于所有传入pixel shader的数据都是定义于切线空间的,所以很自然的着色算法也是在切线空间进行的。而这也使得我们在程序中能够运行的光照数量受制于shder内部插值器数量的限制。为了解决这一问题,我们决定将着色模型从且切线空间着色转换成世界空间着色(事实上,我们在运行世界空间着色的时候做了一些偏移以减少浮点类型顶点的精度丢失)。这种着色方法在实现cube
map反射算法的时候同样需要用到。因此着色代码会变得更加统一和高质量,并且世界空间也不会像切线空间一样容易扭曲。一些影响因素例如光源的位置可以在pixel shader中设置为常量,这样就不必在所有的模型对象中都进行更新。当然如果我们只是用一个光源或者单一的shader进行着色的话,这种做法在额外的像素计算上付出的代价就会显得比较高。(pancy:这里提到的方法相当于我们现在所说的延迟渲染框架,延迟渲染的好处是剔除了所有我们眼睛看不到的顶点使得多个光源的性能大大提升,如果场景只有一个光源的话那就完全丧失了其优势。)
阴影与环境光遮蔽
1.CryEngine1的阴影模拟
在我们的第一作“孤岛危机I”中,我们使用了shadow map技术根据一个全局太阳光为每个模型结构构建了一个阳光下的阴影。在当时我们就已经遇到了一个典型的shadow map贴图混淆的质量问题,但是在当时这项技术已经是一个最好的选择了。考虑到性能方面的问题,我们将植被的阴影做了预先的计算,但是由于内存的限制我们只能使用极其模糊的贴图来模拟植被。在高端配置的硬件设备上,我们甚至为植被也加上了实时的shadow map算法,但是将这些阴影与与计算阴影相互结合的时候仍然有很多缺陷。对于点光源,我们采取了模板阴影算法来计算阴影,而这对于点光源来说是简洁而先进的阴影算法。CPU上的蒙皮操作(pancy:蒙皮操作是指骨骼动画里根据父子关系存储的骨骼坐标系对人体或动物的表面顶点进行变换的过程。如今所有的蒙皮操作都是在GPU上进行的,如果在CPU上进行的话不仅会拖慢程序的帧率,还会大量消耗CPU)允许我们将阴影轮廓从CPU里抽出,然后在GPU里面渲染模板阴影体。很明显,这种做法要求我们拥有更精致的模型对象,并且需要额外的CPU开销来做蒙皮工作以及将蒙皮结果上传至GPU,同时,我们还得准备更多的内存来存储轮廓数据。这使得我们几乎无法预测最终的性能特征(pancy:CPU过度使用将使得性能预测不可用,因为此时有可能CPU成为渲染瓶颈以至于不能根据GPU运算能力和问题规模预测和优化算法复杂度)。并且,这种做法还会使得我们无法使用alpha混合以及阴影投射技术。这也就意味着这项技术甚至不能用于渲染对于热带岛屿非常重要的棕榈树(图7)。
图7:“孤岛危机”截屏:注意与计算的阴影与实时计算的阴影柔和的混合在了一起
在开发的过程中,我们希望对所有的室内阴影使用模板阴影体技术,然而,这种技术产生的多光源下的硬阴影的外观和性能都不能令人满意,这使得我们希望能寻找其它的技术来解决室内阴影问题。一个解决方案是将阴影存储在光照贴图当中,光照贴图能够在无论多少个光源的照射下保证阴影的柔和性。然而不幸的是光照贴图存储的是着色之后的光照结果,这是一个单纯的RGB纹理,因此我们无法在其上面实现法线贴图。我们解决了这项问题并且为我们的解决方案起名叫做Dot3Lightmaps[Mittring04],在我们的解决方案中,我们在光照贴图中存储了一个平均后的切线空间中的光线方向,一个平均后的光照颜色以及一个在纯粹的环境光和纯粹的方向光之间插值之后的混合值。这个操作使得我们能够很快地计算出各个静态光源对于软阴影效果的贡献。然而这种阴影贴图难以和实时阴影混合,在“孤岛危机”发布之后,我们试验了一种简单的修改,并为之命名为遮蔽映射(Occlusion
maps),这种做法的主要概念是使用一个0-1的值作为阴影掩码来代表几何体对于纹理元素的遮蔽几率。我们在光照贴图当中存储了来自于多个光源的阴影掩码并且通常所用的四通道纹理允许一个像素点存储四个光源的掩码信息。通过这种方式,我们实现了一种高质量的软阴影并且成功的计算出了每一个漫反射光以及镜面反射光对其的贡献值,同时,光照的颜色和强度还是可以灵活改变的。我们一直保持所有光源的信息是分开的,这样才能保证他们和其余类型的阴影完美的融合。
2.CryEngine2的计划
我们认为是时候统一一下整个阴影系统了。由于上面提到的一些问题,我们决定放弃模板阴影算法(pancy:shadow voluem就这么没了deth)。而shadow map总是可以提供高质量的软阴影并且可以方便的调控性能和质量,因此我们选择这项技术来实现我们的阴影。然而这项技术只能用于直接光照下的阴影模拟,而如果没有考虑到间接光照的话,我们的游戏就很难达到电影级别的渲染效果。因此,我们计划使用这个方法来解决直接光照阴影,而使用另一个方法来解决间接光照阴影。
3.直接光照
对于直接光照,我们决定只使用shadowmap技术(将几何体对象在视线方向的的深度存储在一个2D纹理当中)而取消所有的模板阴影的使用。
&1&:动态遮蔽贴图:
为了能够有效的处理静态光源的位置,我们希望做一些新的探讨。通过对室内几何体进行一些独特的展开,shadowmap的查询结果可以被存储在遮蔽贴图中被动态的进行更新。动态遮蔽贴图的思想史很好的,并且确实起到了一定的作用。但是由于阴影总是会产生混淆错误,所以现在我们不仅仅是阴影,连几何体的展开也出现了混淆。以往用于改善这个问题的纹理拉伸技术有些过于古老了,并且它也不能避免掉所有的纹理拼接缝隙。由于我们依然需要为动态的物体计算shadowmap,所以我们决定最大程度的加大通用的shadowmap并且降低遮蔽贴图的大小(pancy:shadowmap算法虽然非常简洁,但是问题也很多,常见的包括阴影锯齿(也就是这里提到的混淆效果),阴影瑕疵等等,这是由于shadowmap在一定程度上属于光栅类算(shadowmap被存储于纹理并且在使用的时候被反投影来寻找深度),因此其效果取决于视截体的大小以及阴影贴图的分辨率)
&2&:shadow map与屏幕空间随机纹理查询:
平面shadow mapping因为混淆现象而变得非常糟糕并且会产生非常严重的锯齿边缘(见第一张印象图)PCF滤波算法(百分比滤波)可以减轻这个问题,但是它需要进行更多的采样,考虑到当初的硬件运算水平,这种滤波算法仅仅能够在NVIDA6代或者7代的图形加速卡上运转,并且仿真速度被大大的拖慢了。而在更先进的ATI的显卡上,我们可以使用Fetch4函数(在[Isidoro06]中定义)。为了代替使用更多的采样实现PCF算法,我们还设计了一种随机像素查找的方法以减少采样数量来达到相似的阴影效果。当然,这会带来一点图像的噪声,噪声(或者躁点)在任何影视图像上都会有,所以我们可以根据采样数量来灵活的控制和调整运算性能和质量。我们的灵感来源于光线追踪的软阴影算法。并且这项算法已经应用于GPU上的shadowmap创建。(具体的提升和优化shadowmap质量的细节请阅读[Uralsky05]与[Isidoro06])
盘状分布的随机偏移可以被应用于2D纹理的查询。当我们使用一个较大的偏移量的时候,平坦的表面的阴影渲染质量将会由于其表面被随机纹理确定的方向而提高,一个3D的图形例如说球体将会消耗更多的渲染资源,但他应归属于偏移的柔化问题。
如果想要得到没有太多噪声的可接受结果,我们就需要更多的对阴影图进行采样。因此,采样次数与随机算法就可以根据需求的质量和效率不同来进行选择。我们尝试使用了两种方法产生圆盘状随机点,一个是对于各粒子进行随机的进行旋转[Isidoro06],另一个是使用一个简单的像素着色器。
图8:不同质量的shadowmap样例:从左至右依次是,无PCF,PCF,8次采样,8次采样加blur,PCF加8次采样,PCF加8次采样加blur。
第一种技术需要一个静态的2D随机点以及一个存储着随机二维旋转矩阵的纹理。幸运的是二位旋转矩阵比较小(2*2)可以被存储在4通道的纹理中,由于矩阵是正交的,所以还能够进行进一步的压缩,但这不是必须的。负数的话可以用常用的“放缩偏移”方法进行转化(乘2减一)(pancy:x= x*2-1可以将[0,1]转换到[-1,1],因为通常的纹理只能存储0-255,因此shader中得到的颜色也是0-1)或者使用浮点纹理来存储(浮点纹理格式,不用0-255存储颜色而使用float存储颜色,最新的显卡都加入了这种扩展以方便的实现shadowmap,ssao,hdr等算法)。我们尝试了很多种不同的采样表,并且在图8中你可以看到应用这些采样来模拟软阴影的效果是不错的。对于一个圆形的随机盘状数据也许你希望将其完全填充,但是我们并没有填充其里面部分的数据因为他们对于采样来说很少被用到。这种算法效果的瑕疵已经很难被察觉了,但是为了更正确的结果,我们仍希望在算法上做一些改进。
一个更简单的获取采样点的方法是将一个或者两个2D纹理上的随机正采样点进行简单的变换。第一个点可以放在中间(mx,my)然后其他的四个点将根据随机向量(x,y)放在中心点的周围:
(mx + x,my+y)
(mx - y ,my+x)
(mx - x ,my -y)
(mx + y,my -x)
这种做法使得我们可以构造出更多的采样点,当然我们发现这种做法只是能够提升那些低端显卡的材质渲染的速度(因为只有在低端显卡上我们才需要考虑限制采样个数)。
这两种方法都允许我们调整采样点的大小来实现软阴影。为了得到正确的结果,这个点的大小取决于投影距离以及光照半径,当然这通常都是很容易近似计算的,最初我们采用64*64的纹理来1:1的映射屏幕像素(图9)。
图9:一个随机采样点纹理样例。
这个随机纹理是经过精心制作以体现其随机性:在很高维度上都不可辨认出其特征细节。创建一个随机纹理图是最直接的方法,我们还可以手动生成一张特征易于辨认的纹理然后我们就能够通过一个简易的算法来寻找一些可以用于交换的相邻点,一个好的交换方式可以提高纹理的随机性频率(这个计算结果来自于对差异值的总结收集(pancy:大概是用类似于我们计算方差的方法来判断纹理的随机程度如何))。当然,直接创建一张高随机频率的随机纹理是一个更好的选择。我们描述这个方法是因为这项技术更适合我们的目的。
影视级的效果并不意味着我们要实现静态的效果,所以我们有可能将噪点进行运动并且希望这么做能够在采样率比较低的时候借助运动来隐藏这些噪点。不幸的是我们最终得到的结果就像是一个新的工艺品一般,帧率变得很低而且不稳定。那些没有动画的静态场景的噪点还能够差强人意,而随着摄像机的运动,一些随机纹理的静态特征所造成的噪声开始出现在屏幕上。
&3&:shadow map与光照空间随机纹理查询:
幸运的是我们找到了一个很好的办法来解决这个问题,我们在世界空间的太阳光照方向上创建了一个mip-map映射的噪声纹理以代替之前的屏幕空间噪声纹理,在中远距离上的渲染结果与之前是相似的,但是由于使用的双线性插值所以近处的阴影边缘变得扭曲而不再有噪声点了。这个视觉效果看上去比以前强了很多,特别是植物和植被,那些很难描述具体阴影形状的模型。
&4&:阴影掩码纹理:
我们将阴影的查找从我们的渲染管线中分离了出来,这样就可以避免受到shader mode2.0的最大指令数的限制,减少shder的组合个数以及允许多个阴影进行混合。我们通过查询平铺空间纹理获得了一个8位的阴影贴图,并称之为“阴影掩码”由于一张普通的纹理有四个通道32位作为渲染目标,所以一张4通道的纹理可以将四个灯光的阴影掩码存储在一个像素点里。
图10:一个使用随机查找的阴影样例。左上:没有抖动,1次采样。右上:屏幕空间噪声,8次采样。左下:世界空间噪声,8次采样。右下:世界空间噪声,调整后的8次采样
图11:一个给定场景的阴影掩码纹理样例:左图:通过一个太阳光(视作一个阴影投射)以及两个阴影投射光源。右图:来自于三个光源的阴影模板纹理的RGB显示
图12:一个给定场景的阴影掩码纹理的样例,RGB通道代表三个独立光源的阴影掩码。
在渲染管线中,我们将这个纹理进行绑定并且立即根据这个纹理渲染出所有的光照和环境光效果。当然,我们除了RGB通道外,还可以将帧缓冲区的alpha通道一并使用,但是这样我们就需要更多的pass(pancy:每个渲染算法的一趟渲染称为一个pass,一般高级的特效都是经过好几个pass共同渲染的结果)以及draw call(pancy:每次调用图形api的draw操作称为一次draw
call,在当年drawcall的数量会极大的影响程序的效率,因此一般都会严格控制到几百个左右,不过最新的硬件和api已经在这个问题上做了很大的修缮,Directx12甚至宣称十万drawcall都不会影响到渲染效率)。对于不透明的物体以及仅仅需要alpha测试的物体(pancy:与alpha混合不同,alpha测试是直接扣除alpha通道不符合要求的像素点,一般来说如果一个点只需要alpha测试意味着这个点不属于需要进行半透明混合的点,多半是全透明或者用于某些算法的预计算)阴影模板可以得到非常好的效果,但是对于那些需要进行alpha混合的几何体来说这种算法起不到任何作用(pancy:当今流行的大部分屏幕空间的算法呢最大的问题就是很难处理半透明物体,像延迟渲染等都有这个问题,一旦物体需要进行alpha混合则这些算法就很难起到作用)。所有的不透明物体都可以被记录在深度缓冲区当中,但是需要进行alpha混合的点是不能够记录在深度缓存区当中的。因此,我们需要在着色器中为半透明的物体建立一张阴影法线贴图索引。
4.直接光照下的阴影贴图
在“孤岛危机I”当中,我们仅仅使用了很少的阴影投射对象,并且每个被投射的对象都有一个独立的shadowmap。当阴影投射的对象变得更多的时候,我们认为应当将这些阴影数据统一到一个shadowmap当中。在光照方向使用一个简单的平行投影就可以能够轻易的得到一个shadow
Map,但是这样会使得靠近观察者的阴影贴图分辨率非常的低,出现块状的阴影。如果我们更改参数,例如生成一个透视投影矩阵将靠近观察者的阴影贴图分辨率提升。但这也会出现一些问题。我们还尝试了梯形shadow map(TSM)([MT04])以及透视shadowmap(PSM)([SD02])。
我们在使用级联shadowmap(CSM)的时候取得了较大的成功,这种算法通过对视野所在区域进行多次投影得到了许多张不同范围的相同分辨率的shadowmap。每一个投影的影像像素范围都包含于他前面的那个(pancy:更大的)投影所在的世界范围的像素区域内。这项技术得到了较好的结果,但是同时也浪费了一些纹理存储空间,主要是由于这些投影结果只是粗略的包括到了观察者前面的一些区域。为了找到合适的可划分的视截体(随着阴影区域大小的必须距离的减少而减少),每一个shadowmap都需要包含一个划分,划分越远则视截体就可以包含越大的世界空间,当一个shadowmap视截体恰好完全的包含一个划分的时候,比他更小的shadowmap就应该被舍弃掉。如果使用以往的技术,我们已经知道当摄像机进行旋转和平移的时候会出现阴影混淆。而在使用PSM和TSM的时候我们也没能解决这个问题,但是我们使用CSM加上我们的一些改进则解决了这个问题。我们只是更改了每个shadowmap像素的投影方式就得到了一个非常清晰的阴影结果。
5.延迟阴影模板的建立
初始化阴影模板的创建需要渲染所有的可见几何体,这将会使用到更多的draw call。我们使用延迟技术将阴影模板的额建立从几何体渲染中分离了出来。我们先根据我们用提前z缓冲渲染的深度纹理进行一次全屏渲染pass,这次渲染使用一个简单的像素着色器(根据深度数据获取shadow map查询结果)。并且此次渲染不需要各个点在世界空间的坐标。正如前面所说的,我们使用了多张shadow map,这使得我们用于创建阴影掩码的着色器需要定义所有的像素点,以确定每个像素的阴影映射到了正确的纹理。为每一个纹理添加索引可以使用directx10的纹理数组功能或者在一个大的纹理资源中进行偏移寻址。通过使用模板缓冲区,我们能够单独处理各个划分并且简化了像素着色器。修改后的技术运行速度变得更快了,因为像素着色器不需要再做复杂的操作。并且算法还屏蔽了那些不需要计算阴影的离视点较远的场景。
6.展开点光源的shadow map
通常点光源的shadow map都需要一张cube map贴图来进行深度索引。但是硬件PCF优化并不能应用于cube map类型的纹理上面,因为这种纹理很难控制和管理其所占的存储空间。我们将cube map通过在模板缓存上的6次采样展开成6张阴影纹理贴图,这与我们在CSM中所做的操作很相似。这样我们就将点光源类型的阴影问题转换成了聚光灯类型的阴影问题。这使得代码更加的统一和易于维护并且减少了shader的组合。
7.方差shadow map算法(VSM)
对于地形,我们最初希望先提前算好纹理的起始和结束角度,并且还试图实时的更新一张遮蔽映射纹理的增量。然而地形上的一些几何体总是出现一些问题。对于那些大的物体,在不同的地形中需要适应不同的阴影。我们尝试使用我们的法线阴影贴图技术,这项技术统一了阴影外观,但是却导致了阴影的柔和度不足。我们还尝试使用更大范围的简单随机纹理进行索引,但是这使得噪声变得更加严重。在这里我们尝试使用方差shadowmap技术[DL06],而这项技术的最终效果非常的不错。当多个阴影贴图互相遮盖的时候方差阴影贴图的缺点就很明显了。但是对于地形模型来说这种情况是很少发生的。
图13:使用VSM渲染场景的一个样例。上图:未使用方差阴影贴图(注意有硬法线阴影)。下图:使用了方差阴影贴图(注意两种阴影的结合)。
1.3D Transport Sampler
在第一部分我们已经计划设计一个叫做“3D transport sampler”的工具,这个工具可以将全局的光照数据分配到多个机器上(考虑到性能原因)。
光子映射([jensen01])是一种被大家所认可的计算全局光照的不错的方法。我们决定采用这种方法模拟全局光照,因为它可以简便的统一到系统中,并且快速的得到较好的效果。
图14:一个光源的实时环境光。
光子映射最先被应用于创建一个简单的光照映射。这项技术在我们过去的光线映射中实现的很简单,并且仅仅将相邻的三角形连接起来并只使用了一个简单的平面方程。这导致我们填充到多重纹理中的结果出现了很多2D的小块。同时当我们将算法用于更为精细的模型的时候纹理的使用效率开始显得很低,并且在纹理的边界拓展的时候出现了很多细小的间断(pancy:wrap,纹理拓展,一般来说纹理采样UV的范围是[0,1],如果模型的UV超过了这个值就需要进行纹理拓展,具体的拓展方式根据应用而定,常见的有重复寻址和最外边寻址等)。因此我们更改了纹理的拓展方式,使用模型的纹理拓展UV作为基础并且只在我们需要的地方做拓展定义。这样的话负责美工的艺术家就可以对精细的模型的技术细节做更多且更合适的操控和修缮。我们本想将结果存储在Dot3lightmaps(前面已经做过介绍)但是我们希望尝试用更好的方式来提升渲染质量。这个方法的主要思路是存储四个方向的光对表面的贡献。类似的技术曾经被用于“半条命II”(pancy:CS吧??)但是他们只存储了三个方向的光。显然更多的数据将会带来更好的渲染效果。这些数据运行我们进行高质量的逐像素渲染,并且如果不追究细节的话可以和实时阴影较好的结合。但是,由于巨大的存储量和缓慢的运算速度,我们最终还是放弃了这种方式。其实我们最初的想法只是存储每个像素的光照贴图系数以及其余的每个顶点的信息。将这些信息结合存储顶点连接关系的图形数据结构,我们好似有可能实现动态间接光照的。低频的光照分量可以存储在各个顶点结构中,高频的分量例如锋利的角落等可以存储在屏幕空间像素当(pancy:顶点结构数量与几何体有关,屏幕空间像素数量与光栅化有关,看开头还以为cryengine想挑战人类极限加入光子映射......结果竟然是放弃了...弃了.....了)。由于开发时间是很珍贵的,所以我们最终还是放弃了这个想法。
2.实时环境光映射(RAM)
作为替代,我们选择了一个更简单的解决方案,只需要为每个像素存储一个环境光遮挡值标量。
环境光遮蔽值([ZIK98,Landis02])可以根据各个方向的发射光线来进行计算。这些光线有些是光子映射中没有使用到的。我们在着色器中使用这些必要的信息进行重建:存储了遮蔽值的像素,与表面方向相关的光源位置,光照颜色及表面法向量。我们最终得到了一个粗略的近似环境光,但是人眼对环境光并不是很敏感,所以最终效果还是不错的。为了能支持法线贴图技术,我们需要对光照方向进行平均化,因为我们无法获得能较好的和法线贴图表面配合的光照方向。即便使用这种方法,法线贴图依然能够被察觉到,而着色效果也很大的依赖于光照角度。因此我们让环境光的亮度,颜色以及衰减系数都是可以调控的以便设计人员设计出更好的最终外观。当考虑到入口效果的时候,这项技术可以进行扩展以使得太阳光可以混合多个其他光线。对于更大的室外环境,为每个表面计算一个RAM是不可行的,因此我们决定使用其他的技术。
3.屏幕空间环境光遮蔽(SSAO)
我们的一个很有创意的程序员想出了一个不错的方法。这种方法使用我们之前已经计算出来的z缓冲数据来计算出一种环境光遮蔽。(pancy:我就想知道那个有创意的程序员叫啥名字......一个不小心成了SSAO之父啊!一个不小心推动了实时渲染的改革.....)这个方法非常的诱人,因为所有的不透明对象都可以在没有特殊处理的情况下得到环境光遮蔽,并且不需要额外的内存开销。同时,很多渲染部分的的复杂处理也都可以顺手移除了。虽然我们现有的解决方案已经工作的不错了,但是我们还是希望能够计算出所有种类的动态环境光。这种方法基于屏幕像素与采样像素进行简单的深度比较在物体的周围得到一个由遮蔽因子决定的轮廓。而这种环境光遮蔽的效果仅仅决定于这个点对周围光线的的接收程度。经过多次的迭代和优化,我们得到了一种意想不到的效果,我们将其称为“屏幕空间环境光遮蔽”(SSAO)。我们通过一次对全屏幕像素执行的pass来计算环境光遮蔽效果。我们还尝试将这种效果同时应用于环境光,漫反射和镜面反射,但是最终发现他只是对环境光模拟效果最好。最大的原因可能是这种算法使得渲染的真实化受到了影响,这也是我们未来的优化目标。
为了减少采样次数我们将采样点跟改为像素附近点的采样,初始的样本分布于一个以源点为中心的球体表面。然后通过一个随机的3D平面将这些采样点反射到随机的采样位置。
n:来自于纹理的归一化的随机像素向量。
来自于球面的随机采样点
Float3 reflect(float3 i,float3 n){return i-2*dot(i,n)*n}
反射是很容易计算的,并且将随机平面法向量存储在纹理中是非常足够的。
图15:使用屏幕空间环境光遮蔽只计算环境光得到的效果(注意在远距离处,遮蔽使得颜色非常的暗)
图16:场景A使用特殊材质使ssao可视化的一个样例(左:使用ssao,右:未使用ssao)
图17:场景B使用特殊材质使ssao可视化的一个样例(左:使用ssao,右:未使用ssao)
图18:场景A使用特殊材质使ssao可视化的一个样例(左:使用ssao,右:未使用ssao)
多细节层次(LOD)
当渲染的复杂度不容易控制的时候,层次细节技术(LOD)是非常重要的,很多游戏通常使用雾化效果对视野做了很大的限制,甚至有时候依靠层次设计来强行遮蔽掉远处的信息。这也就是为什么大多数游戏都是以室内环境为主导的,但是在“孤岛危机I”中,我们希望能够展示非常远处的风景以及许多细节,同时不限制玩家的视角和运动位置。因此,我们保持了“孤岛危机”中的视角范围,但是我们为模型增加了不同层次的高质量细节描述。质量的提升意味着我们需要使用更为复杂的着色器,更为高质量的纹理。全新的纹理类型(例如地表纹理)以及更多的网格点。因为我们还希望使用z缓冲前置技术以及实时阴影效果,所以我们需要对每个模型对象付出更多的代价。而过多的drawcall使得CPU负担增加,不过这在Directx10上有所改善。因此在“孤岛危机”中,我们可以很轻易的在美工创作的不同细节程度的模型中进行切换。当然,我们还为植被添加了一些欺骗的技术以改善他们在游戏中的运行效率(pancy:估计是billboard或者cross
billboard之类的公告板技术 )但是这不属于本章的讨论范围。
在“孤岛危机”中,我们打算使用一个基于移动顶点的平滑的LOD过度。但是这种技术经常对于模型创建带来很多限制。而如果没有这种限制的话模型往往能够快速而高效的建立出来。特别是那些需要alpha测试或者alpha混合的植被模型。这使得有时候我们的模型来不及建立LOD转换而不得不在显示demo的时候直接取消低LOD等级的部分。用于演示demo的机器拥有非常高的硬件配置因此低等级的LOD并不能增强运算速度。而低等级的LOD所引用的范围通常在屏幕上只占很少的一部分,所以总的像素消耗是很少的。使用大量的LOD还有可能适得其反,因为这项技术并不容易让不同层级的对象完美融合,并且我们还得消耗更多的draw
2.融合技术
我们的一个程序员最终想到了一个基于在z缓冲前置事前进行模型融合的方式来柔化LOD的转换。而在我们之后的额渲染过程中只需要打开z缓冲前置。当然这并不是一个完全正确的方法因为物体的表面有可能会拥有相同的深度值,这样使用add类型的alpha混合(pancy:add型混合 finalpixel = sourcepixel + destpixel)使得这些点获得了两倍的亮度。然而由于每个物体第一次渲染的目标帧缓冲区不允许混合,所以这种情况只有在之后的pass才有可能发生。不过我们将所有的光照合并到了一个pass当中,因此这种情况发生的概率就非常小了。融合后的纹理属于屏幕空间的一个投影,然后我们随机从纹理中采集一个值与每个物体的变换值进行融合,并在之后使用简单的alpha测试来排除不符合要求的点。我们在最新的图形硬件卡上采用了较为新颖的alpha2coverage(A2C)技术(pancy:alphatocoverage技术,通常alpha测试因为简单地根据alpha排除不透明点使得边界锯齿严重,为了达到类似于alpha混合的效果,DIrectx10使用了一种叫A2C的方法来消除一些alpha测试留下的锯齿)以及全屏抗锯齿技术(FSAA)在亚像素级别上进行优化。即便不使用FSAA技术,如果我们使用到我们的边缘模糊后处理效果,融合的瑕疵也不宜被察觉到。最初我们只是根据模型与视点的距离来进行状态的过度。当时处于过渡期的模型的渲染速度和质量都变得很差,因此我们决定在过渡期隐藏这些模型。这也就是为什么我们为模型过度添加了一小段代码以让其在定义的一段时间内完成。我们不仅仅对变换的3D对象使用融合技术,还将这项技术用于隐藏远处的模型以及隐藏过渡期间的模型已达到欺骗视觉的效果。
3.水面的LOD技术
海面或者大范围的水面通常拥有特殊的属性,因此可以使用特殊的渲染算法进行渲染。最初我们在“孤岛危机”中实现的水面是一个随着玩家运动的简单网格,由像素着色器,反射效果以及透明度决定了水面的视觉效果。然而,我们希望拥有真实的3D波浪,当然不是基于物理模拟,而是一种廉价的解决方案。我们尝试通过使用基于FFT(pancy:快速傅里叶变换)的水波仿真([Jensen01a],
[Tessendorf04]).要想获得3D的波浪就必须要进行必要的顶点操作,而我们以往使用的网格模型显然是不能够做这样的操作的。
4.立方水域
前面提到的FFT仅仅适能够输出海面的较小的一部分,因此我们必须将表面渲染很多次才能获得想要的范围。不同的细节层次所使用的索引缓冲区不同,但是他们共享一个由FFT生成的顶点缓冲区(pancy:顶点缓冲区决定组成几何体的多边形的顶点位置,索引缓冲区决定了多边形的组织方式。)我们虽然共享了所有LOD的顶点缓冲区以节省内存,但是为了更好地渲染质量(pancy:也就是LOD中被认为不重要的几何体需要降低渲染质量)我们需要更好的向下采样方式。为了减少随距离而产生的混淆现象以及限制低质量多边形的外观,对于近处的点,我们淡化了其对远处顶点的扰动。并且大大限制了其对周围近处顶点的扰动(pancy:大概是很大程度上简化了液体之间的受力方式)。这种方法确实起到了作用但是很多美工建议我们还是再找一些更好的方法。
5.屏幕空间曲面细分
我们决定使用一些暴力的方法,这种方法很简单,但是很好的解决了问题。我们使用一个提前计算好的屏幕空间曲面细分四边形并且将所有的水面顶点投影到这张四边形上。这项操作必须严格保证z缓冲区的正确性,甚至我们还要裁剪掉所有地平面上面的顶点所产生的像素。为了能够使用FFT波浪模拟数据来修改顶点的位置,我们需要建立一张顶点纹理索引,因为这项技术并不能在硬件上实现(pancy:硬件曲面细分始于directx11,当时虽然有使用曲面细分模拟水面的算法但是只能通过一些别的手段实现这项功能)
图2:屏幕空间曲面细分产生的波浪
线框中可以注意到的垂直线条是由于我们为了更好的日升顶点缓冲区性能所做的顶点分层。这个结果看上去让我们觉得最终成功的希望很大,但是屏幕边界上的顶点总是在远离边界的位置,这是一个很严重的问题。当然,为缓冲区扩容,以包括哪些超出边界的顶点可以解决这个问题。不过衰减顶点的扰动的方法更适合,因为不仅对屏幕边界影响并不是很明显,并且还能最小化的增加额外成本。
图3:左:不做衰减顶点扰动的屏幕空间曲面细分(注意左边界的部分没有被包括到水面)。右:衰减顶点扰动的屏幕空间曲面细分。
为了更好的提高性能,我们减少了顶点的细分程度,而即便我们使用比之前少很多的顶点,工程的质量仍然是可接受的。取景角度的倾斜将使得渲染效果大大降低,这不是我们期望的结果。主要原因是边缘衰减主要依赖于水面方向的摄像机,而不是真实的物理模拟。因此我们不得不减少波动的幅度以解决这个问题。
6.摄像机对齐
剩余的一些历史遗留的锯齿混淆问题以及物理运动学的问题困扰着我们的shader程序员。然后他花了一些时间找到了一个如下的解决方案:这种新的方法更之前一样使用了一个静态的网格。但是这个网格的投影从之前的单一角度转换成了简单的上下投影。整个网格由摄像机来进行移动,我们通过调整摄像机的偏移量以使得摄像机面对方向所获的顶点数量最大化。要渲染地平线以上的顶点,我们就需要对网格的边缘进行显著的扩展。而这些顶点的曲面细分不再很关键因为对于这种远距离定点,其受扰动的系数几乎可以视为0。
图204:摄像机对齐后的水面网格的线框:左:上下投影的摄像机对齐空间,右:从观察者角度观看的相机空间。
这种做法得到的效果要优于屏幕空间的做法,特别是在相机做微小的运动的时候更加的明显。除了随距离的衰减,水波现在是与观察者位置无关的。因此现在使用CPU模拟FFT物理运动是可行的。
图21:左:摄像机对齐的结果。右:用于比较的屏幕空间曲面细分
通过这次曲折的探究,我们不仅找到了我们的下一代引擎的发展方向,还从中学到了很多的东西。为了找到,验证以及比较不同的解决方案,学习的过程是必不可少的。在过去的说法中,这项工作可以被归类于研究工作。我们认为我们选取他人的研究成果最大的根据是质量,生产时间,性能以及可扩展性。“孤岛危机II”,我们的新一代的游戏,是一个巨大的工程,为了掌控这项工程,时间是非常重要的。一个解决方案的性能与硬件息息相关(如CPU,GPU,内存),因此对于不同的平台我们都可能要重新考虑我们所使用的算法。目前的引擎对于拥有多个核心的CPU以及较为快速的支持Directx9和Directx10的显卡来说优化是不错的。在早期使用z测试前置得到的深度信息也是很有用的,很多功能现在都依赖于这个数据。如今,更为规范的延迟渲染像素数据也包含了更多的像素信息,比如漫反射颜色,法线,以及一些特殊的材质。对于渲染外星人的室内场景,延迟渲染可能是最好的解决方案,但是其他的环境则不能从中受益,因为只有一个光源的情况下延迟渲染并不能很好的发挥其优势。
此次演示文稿是很多程序员,美工和设计师们共同努力的结果。在这里我们要特别感谢Vladimir Kajalin, Andrey Khonich,Tiago Sousa, Carsten Wenzel 以及Nick Kasyan. 我们已经成为了NVIDA的合作伙伴,我们获得了他们的现场帮助而包括而不仅限于G80 Directx9以及Directx10方面的问题。因此特别感谢NVIDA的工程师Miguel Sainz, Yury Uralsky
以及 Philip Gerasimov。同时为业界的领袖企业Microsoft,AMD,Intel,NVIDA以及其余的支持过我们工作的公司致以感谢。同时为帮助我完成这篇论文的Natalya Tatarchuk 以及Tim Parlett致以感谢。
[ATI04] ATI 2004, Radeon X800 3DcTM Whitepaper http://ati.de/products/radeonx800/3DcWhitePaper.pdf
[DL06] DONNELLY W. AND LAURITZEN A. 2006. Variance shadow maps. In Proceedings of the 2006 ACM SIGGRAPH Symposium on Interactive 3D graphics and games, pp. 161-165. Redwood City, CA
[ISIDORO06] ISIDORO J. 2006. Shadow Mapping: GPU-based Tips and Techniques. GDC presentation. http://ati.amd.com/developer/gdc/2006/Isidoro-ShadowMapping.pdf
[JENSEN01]
JENSEN, H. W. 2001. Realistic image synthesis using photon mapping, A. K. Peters, Ltd., Natick, MA.
[JENSEN01a] JENSEN, L. 2001, Deep-Water Animation and Rendering, Gamasutra article http://www.gamasutra.com/gdce/2001/jensen/jensen_pfv.htm
[LANDIS02] LANDIS, H., 2002. RenderMan in Production, ACM SIGGRAPH 2002 Course 16.
[MICROSOFT07] MICROSOFT DIRECTX SDK. April 2007.
http://www.microsoft.com/downloads/details.aspx?FamilyID=86cf7fa2-e953-475cabde-f016e4f7b61a&DisplayLang=en
[MT04] MARTIN, T. AND TAN, T.-S. 2004. Anti-aliasing and continuity with trapezoidal shadow maps. In proceedings of Eurographics Symposium on Rendering 2004, pp. 153–160, 2004.
[MCTAGGART04] MCTAGGART, G. 2004. Half-Life 2 Shading, GDC Direct3D Tutorial
http://www2.ati.com/developer/gdc/D3DTutorial10_Half-Life2_Shading.pdf
[MITTRING04] MITTRING, M. 2004. Method and Computer Program Product for Lighting a Computer Graphics Image and a Computer. US Patent
A1, August 12, 2004.
[SD02] STAMMINGER, M. AND DRETTAKIS, G. 2002. Perspective shadow maps. In SIGGRAPH 2002 Conference Proceedings, volume 21, 3, pages 557–562, July 2002
[TESSENDORF04] TESSENDORF, J. 2004. Simulating Ocean Surfaces. Part of ACM SIGGRAPH 2004 Course 32, The Elements of Nature: Interactive and Realistic Techniques, Los Angeles, CA
[URALSKY05] URALSKY, Y. 2005. Efficient Soft-Edged Shadows Using Pixel Shader Branching. In GPU Gems 2, M. Pharr, Ed., Addison-Wesley, pp. 269 – 282.
[WENZEL05] WENZEL C. 2005. Far Cry and DirectX. GDC presentation, San Francisco, CA
http://ati.amd.com/developer/gdc/D3DTutorial08_FarCryAndDX9.pdf
Advanced Real-Time Rendering in 3D Graphics and Games Course – SIGGRAPH 2007
[WENZEL06] WENZEL, C. 2006. Real-time Atmospheric Effects in Games. Course 26: Advanced Real-Time Rendering in 3D Graphics and Games. Siggraph, Boston, MA. August 2006
http://ati.amd.com/developer/techreports/2006/SIGGRAPH2006/Course_26_SIGGR APH_2006.pdf
[WENZEL07] WENZEL C. 2007. Real-time Atmospheric Effects in Games Revisited. Conference Session. GDC 2007. March 5-9, 2007, San Francisco, CA. http://ati.amd.com/developer/gdc/2007/D3DTutorial_Crytek.pdf
[ZIK98] ZHUKOV, S., IONES, A., AND KRONIN, G. 1998. An ambient light illumination model. In Rendering Techniques ’98 (Proceedings of the Eurographics Workshop on Rendering), pp. 45–55.
作者:pancy12138 发表于
https://blog.csdn.net/pancy12138/article/details/
阅读:2929
https://blog.csdn.net/pancy12138/article/details/
https://blog.csdn.net/pancy12138/article/details/
pancy12138
(本系列文章由pancy12138编写,转载请注明出处:http://blog.csdn.net/pancy12138)
上一次的教程为大家补充了一些程序方面的知识,这些知识在现在以及以后的很多程序中都会经常的用到。当然,这些程序技巧也只是很少的一部分,如果以后学到更深层次的知识的时候我会继续对程序方面进行一些补充。这篇文章将继续对图形学的讲解部分,这一次我们讲解的重点是光照与着色,也就是如何为3D空间中的物体进行上色的过程。
“但是就在那一刻,张是非的眼中忽然一阵恍惚,他这才明白,原来色彩是假的,阳光才是真的。”
——《我当鸟人的那几年》
我们在观察物体的时候,一般都会注意到物体的大概的颜色,因此我们在一开始在小学学画画的时候因为水平比较低,大都会一片一片的涂色,比如我们知道苹果是红色的,就一股脑的涂上一堆红色。或者天鹅是白色的,就一股脑的涂上一堆白色。这样虽然能够大概的画出一些东西,但是得到的图片一般都和真正的物体差的很远。如果大家之后再多练习的话就会了解一些简单的渐变色的技巧,不仅仅是用纯色来上色了,比如说苹果,会给暗的地方涂上暗红色,亮的地方涂上亮红色。当然这样画出的画会比之前好一点,得到一些插画或者漫画的风格。但是这依然和真正的物体差之甚远。这也就是为什么大家很容易看出来哪些是二次元的图片,哪些不是的原因。那么,如果我们拿一张照片来分析的话,会发现上面的物体基本上每一点的颜色都是不同的,想要画到这种境界显然是非常难的,那么既然大部分物体表面材料可以说是一样的,为什么会造成每一点的颜色都不相同呢?如果要回答这个问题,我们就得去了解颜色出现的本质,事实上如段首所说。所谓的颜色,其实是光照到物体之上然后反射到我们眼睛里面所导致的,由于物体上每一点所在的位置,以及其对应的表面法向量不同,最终反射到我们眼睛里的光线强度也就自然会有差别。这就是颜色的本质。在图形学里,我们不可能模仿画家绘画的技巧来进行上色,因为这是强模式识别和经验的产物,目前的计算机尚且不能模拟,并且这种绘制方法得到的图像的真实程度是有限的,即便是最优秀的画家也无法保证快速而完美的在再现他所看到的物体。因此我们只能一点点的对自然界的光照进行分析,借助分析所得到的一系列公式来对自然界中的物体进行上色操作。
那么,光照是一个怎样的过程呢,首先我们看看整个光照过程所用到的物理原型,也就是:光源,接受照射的物体,以及观察者。这三个东西可以直接决定最终眼睛得到的画面的各个点的颜色。而光线照射原理也很简单,大家高中物理肯定也学过:
上图展示了对于一个点而言,光照的反射与折射原理。这个简单的原理告诉了我们光照的传播过程,但是事实上,这只是一种最为简单的光照原理,并且即便是这个简单的原理,我们现在还有很多东西实现不了。要注意,这只是对于一个点,反射与折射也只有一次的情况得到的公式。真正要想算出一个物体的所有的正确颜色,我们需要对于每一个光源,每一个物体上的点,递归的计算好多次的反射与折射,如果大家实际上手编程的话,就知道这种复杂度根本就是一个天文数字,而这还不是真正的渲染方程,因为这里我们假设光源只是一个点,假设物体都是由一个硬的,不透明的表面组成的等等。事实上真的要深入的模拟光照情况的话,是一个非常复杂的过程。这也就是为什么如今的特效和CG都很烧经费。因为模拟这些光照需要进行超大量的运算,大量到需要借助一大批高性能计算机一起计算都要很长时间的地步。这种一心要模拟最优秀的渲染效果的领域一般称为离线渲染领域,这里所用到的算法和资源基本上都不是游戏能用的了的,因为这些算法(比如光线追踪,辐射度,SSS等)虽然渲染效果非常酷炫,但是不能在民用机上很快的得到结果。因此这些算法并不是我们要讲的重点.......
那么,游戏渲染所需要的光照模拟方法是神马样子的呢,这里我们所需要的光照算法称作实时渲染领域的算法。也就是必须能够在民用机器上,在1/30秒内得到渲染答案的算法。首先由于模型以及像素的基数过于庞大,目前所有的实时渲染算法必须要求是O(n)的算法,这也就是说多次折射,反射等效果都是不能进行的。或者说不能直接进行计算的,只能用别的近似算法进行模拟。我们先讲解最简单而且能够直接运算的算法,只有直接光照效果。也就是假设光源射出的光线不进行反射,也不进行折射,只对其第一次碰到的物体表面上的点产生效果。这样整个着色的算法就非常简单了,我们根据光源以及顶点的位置来进行一次光照计算,这一次计算我们可以得到两种反射到眼睛的效果,一个是漫反射效果,一个是镜面反射效果:
上图展示了两种不同的反射光的计算方法,其一是漫反射光,也就是入射光被平面均匀的散射到各个方向,并且所有方向的反射强度只取决于入射光与法线的夹角。这就很容易的得到了反射到我们眼睛里的漫反射光的颜色。然后是第二种,也就是镜面反射,这个复杂一点,入射光虽然也被散射到各个方向,但是强度却不是取决于入射光与法线夹角的,这个强度取决于反射光与视线的夹角,并且衰减速度极快,呈指数级别的衰减,大家平常在太阳下看到的车辆或者反光镜等物体上耀眼的亮斑就是这种反射造成的,而除非特别光亮的物体,其余物体的镜面反射都很不明显,颜色多半是取决于漫反射。下面是漫反射与镜面反射的公式:
漫反射公式:Idiffuse = L*M*Cos(theta)/dot(X(1,r,r^2));
其中L是光强,M是物体的漫反射系数,theta就是入射光与法线的夹角,X是衰减系数,这里我们可以看到其点乘了一个距离向量,也就是X的三个分量分别是二次系数,一次系数以及常数系数。这种写法是我经常用的写法,可能和别的书籍不太一样,不过衰减公式是不会变的。只是看怎么快速的实现它。
镜面反射公式:Ispecular
L*M*(reflect(LightDir,Normal) * normalize(view))^w/dot(X(1,r,r^2));
这里reflect(LightDir,Normal)指的是光线沿法线反射后的反射光线方向,view是实现的方向,w是镜面反射的指数系数,代表镜面反射的特有衰减。
上述只是指出了当我们知道光源位置的时候,直接光照的计算方法。并没有提及光源是怎么传播光线的。下面我们来讲解一些常见的光源形式。一般来说实时渲染中用到最多的光源是点光源,方向光,聚光灯。这三种,前两者很好理解,点光源就是有一个点像四周发射灯光的光源,方向光就是沿着一个方向不做衰减的光源,而聚光灯比较复杂,主要就是类似于舞台灯光一样的圆锥状发射的光源,不仅仅沿距离方向衰减,沿着圆锥半径方向也会衰减。定义每一种光源所用到的数据结构是有一些不同的,下面是三种光源的定义:
struct pancy_light_dir//方向光结构
//光照强度
//光照方向
//光照范围
struct pancy_light_point//点光源结构
//光照强度
//光照位置及衰减
//光照范围
struct pancy_light_spot//聚光灯结构
//光照强度
//光照位置,方向及衰减
//聚光灯属性
};上述就是三种光源的一些属性,前两者大家很容易理解我就不多说了,而聚光灯其实也就比点光源多个theta值和spot值用于判断圆锥照明区域的半径方向衰减系数。这里我要提示大家一下,虽然三种光源的数据结构有所不同,但是我并不建议大家在写程序的时候把三种光源分别定义结构,因为这在引擎开发的后期及其不利于管理,所以这里我建议大家把光源的数据结构做一下统一,然后用一个参数来标记光源的类型:struct pancy_light_basic
//光照强度
//光照位置,方向及衰减
//聚光灯属性
//光照类型
};ok,现在我想大家已经对光源,光的直接传播有了很多的认识了,那么接下来我们就要开始根据这些知识进行最简单的光照着色了。注意上面我们只给出了漫反射光以及镜面反射光的计算方法,这里每个光源发出的光线中还有一种叫做“环境光”的分量,这个分量的来源大家可以想

我要回帖

更多关于 电脑显示手机屏幕软件 的文章

 

随机推荐