内容概要
2007年上半年写的一系列基于D3D9 API的Shader开发教程,包括从基本的光照计算,到延迟渲染等。
【欢迎转载,请注明作者:房燕良,原文出处:游戏程序员的自我修养】
D3D9 Shader
Shader是一段运行在GPU上的小程序,是运行在GPU上的Pipeline上的特定的可编程单元的小程序。
随着Unity3D的流行,很多同学会把Unity ShaderLab和Shader这两个概念搞混。 ShaderLab是Unity引擎中的Shader系统的名称,它完成的是组织Shader、渲染状态、渲染Pass,以及参数绑定等功能,有点像D3D SDK提供的D3D Effect。
如果想系统的学习Shader,就需要从图形API的Pipeline开始。当然,现在理想的是去学习D3D 12,Valukan,Metal这样的新一代API,至少也是D3D 11这样的成熟API,或者OpenGL ES,如果你专供移动端图形的话。
下面这套代码,是我在2007年的时候,基于D3D9 API写的一组教程代码,因为最近有同学问起,所以找出来重新发布。
代码已经上传到GitHub: https://github.com/neil3d/myshaders
D3D9 Shader教程文档
程序的基本思路
class SimpleD3DApp处理了win32窗口创建和D3D初始化的工作,然后响应菜单,生成相应的ShaderSimpler派生类实例。 一个个ShaderSimpler的派生类为一个shader试验小程序。
Effect的基本处理步骤
-
编辑一个纯文本文件,推荐后缀为fx. shader\SimpleDraw.fx是一个使用vertex shader和pixel shader的最简单的Effect文件;
-
使用D3DXCreateEffectFromFile函数加载生成一个ID3DXEffect对象;另外也可以使用fxc命令行工具把上述文本文件编译成二进制文件。
-
在渲染前调用ID3DXEffect::SetMatrix()设置effect中的参数;
-
使用ID3DXEffect::Begin(),BeginPass(),EndPass(),End()来控制渲染流程。
实现基本光照
这个Demo实现一个简单omni灯光的计算
- 对于每个顶点进行灯光计算;
- 光照公式使用”距离衰减*(N.L)”;
参考资料
Phong shading
这个小程序通过把顶点法线逐象素插值,再计算每个象素的diffuse,specular光照结果来简单的实现phong模型。因为specular是view depended,所以Effect中增加了eyePos参数。
- 通过观察box的渲染可以看出与basic lighting的明显不同。
- 这段小程序没有考虑性能的问题,为了提升速度,可以ps中的L,V,R放到vs中去计算。
- 镜头操作:WS前后,AD左右,ZX上下移动,鼠标拖动为转动。
卡通渲染
卡通渲染主要包括两个效果:
- 把普通光照的平滑亮度变化变成几个强度级别。这是通过计算出光照强调,然后查找一个贴图来实现的,此贴图存储0~1的4个固定段。
- 描出轮廓线。法线与视线垂直的点为轮廓,所以使用两者的点积结果来查找一个贴图,然后乘以光照颜色来实现描边,此贴图0附近为全黑,其余为全摆。
参考资料
环境映射
Cube Map环境映射特别适合与曲面,实现也很简单--构造一个视点到顶点的向量,然后计算出相对与normal的反射向量,用此向量索引cube map贴图即可。
顺便比较了一下shpere环境映射的效果。Sphere环境映射的实现也很简单,把顶点法向量变换到view space,然后使用贴图坐标(n.x/2+0.5,n.y/2+0.5)来索引sphere map贴图即可。
参考资料
- DX9SDK simple, HLSL workshop, Goal 3
Bump Mapping(Normal mapping)
从贴图生成normal map
normal map可以从一个颜色贴图对应的高度图生成。为了计算i,j点的normal,首先可以根据高度差计算出s,t方向的切向量:
S(i,j) = <1, 0, aH(i+1,j)-aH(i-1,j)>
T(i,j) = <0, 1, aH(i,j+1)-aH(i,j-1)>
其中a是一个缩放系数。normal可以由这两个切向量得出
*N(i,j) = normalize(cross(S,T)) = cross(S,T)/length(cross(S,T)) = <-Sz, -Tz, 1> / sqrt(Sz^2 + Tz^2 +1)
然后可以把这个向量使用RGB编码存储到贴图文件中。原理是这样,本例使用nvidia提供的工具生成normal map。
计算出Tangent space
这个计算稍微有些复杂。记得老早的时候nvidia有一个文档写的很清楚,不过我怎么也搜不出来了。哪位要是知道,请给我个链接。在Eric Lengyel的书里面找到了另外一种解法,讲得也很透彻。
本例子中简单的调用了D3DXComputeTangent,也可以使用nvidia sdk中提供的nv_meshmender来计算。
光照计算
使用Bump map的光照计算与前面的PhongShading例子相比没有什么新鲜的。前面的例子中每个象素的normal只是简单的从顶点normal插值而来,而使用bump map后,每个顶点的normal可以从贴图中查找;另外因为这个normal是存在与tangent space中,所以需要把light dir和view dir都转换到tangent space再进行计算。
参考资料
- Eric Lengyel, 3D游戏与计算机图形学中的数学方法
- Normal Map贴图使用nVidia Photoshop plugin生成
- Wolfgang Engel, Implementing Lighting Models With HLSL
Parallax Mapping
Parallax map通过简单的计算可以大幅提高per-pixel光照的效果。Parallax map的想法基于一个简单的事实,那就是当我们用一个多边形去表现一个凹凸不平的表面的时候,事实上随着视线的变化,看到的实际的texel并不是顶点uv坐标差值出来的那个结果,而是有一定的偏移。而这个偏移可以通过是个高度图来计算。
在本例中,经过简化,最后单个象素uv偏移量的公式为:
offset = <viewDir.x, viewDir.y>*(height*SCALE+BIAS)
其中的height就是高度图在这个uv的高度值,然后根据实际需要进行缩放和偏移。SCALE和BIAS可以根据所要渲染的物体的单位来估计,例如要渲染一个1平方米的砖墙,那么我们估计每个砖的起伏在几厘米。在本例中使用这个offset将uv进行偏移之后,再去采样bump map和color map。
- 程序运行时按字母键“P”可以打开、关闭parallax map,用来比较效果。:)
参考资料
- Terry Welsh, Parallax Mapping, ShaderX3 p89
实现类似魔兽世界的“全屏泛光”
现在很多游戏,像WOW、激战都有这种“全屏泛光”的画面效果,很多人以为是使用了HDR,其实就是一个post process效果。 PostProcess是一个很简单又有用的概念,顾名思义,就是把渲染之后的场景,在图像空间进行一些处理。它可以做出各种各样的效果。
全屏泛光的实现步骤如下
- 分配三个texture,F,A,B,其中F的大小与back buffer一致,A和B为back buffer面积的四分之一;
- 将场景渲染到F;
- 将F渲染到A - 执行down sample和high-pass filter;
- 将A渲染到B - 执行H blur;
- 将B渲染到A - 执行V blur,先横向再纵向计算,把每个象素需要的计算由n*n变为n+n,其中n为blur区域的大小;
- 将F和A混合,渲染到back buffer,然后present。至于混合的公式,就可以自己发挥了。:)
参考资料
- nVidia FXComposer
初探Deferred shading
对于一个基于Shader的渲染系统如何来计算多个动态光源的光照呢?这是一个困扰我很久的问题。基本上有这样三种解决方案:
- 在单个pass中计算物体的所有光照。这种方案的主要缺点就是shader管理非常复杂。常见的思路是基于一套shader模版,自动生成n个光照shader,n往往能达到上百个;另外一个主要的缺点就是很多fragment经过复杂的光照计算之后又被z测试剔除,造成计算的浪费(可以先进行一个z only pass来优化);
- 为每个灯光进行一个pass。这种方案也会有前者over draw的问题,另外还会造成draw call数量增加;
- deferred shading,这种方案结构简单,但是对显卡要求较高,需要multi-render targets, float point render target;还有就是对于透明面,目前还没有很好的解决方法。
下面就介绍一下Deferred Shading的思路。它的想法非常的简单、直接,简单的说就是我们可以把光照计算需要的一些参数先通过一个pass渲染到一些render target中(称为Attributes buffer或G-Buffer),然后可以为每个灯光进行一个pass来计算光照。
源码中演示了deferred shading的基本概念,实现了4个点光源的phong 光照渲染。为了能在Geforce 5600上跑,没有使用multi-render targets,而是用了多个pass来生成G-Buffer,这样也可以更清楚的演示deferred shading的概念。 首先程序创建了position,normal,diffuse三个render target,在渲染时使用3个pass,把场景的“世界坐标”,“法线(世界坐标系)”,“颜色”,分别渲染到这三个render target中;为了简单起见,假设所有灯光都覆盖整个屏幕――为每个灯光进行一个pass,渲染一个全屏幕的quad,然后为每个象素计算光照,并使用additive混合,把光照强度累加到屏幕缓冲中。
Position
Normal
Diffuse
Deferred shading with Multiple Render Targets
今天抽空是试验了一下使用MRT进行deferred shading。基本是在上个例子的基础上修改的,主要有一下几个地方:
- 使用SetRenderTarget分别将各个render target设置好;
- 在pixel shader中使用“COLOR[n]”来写入各个render target;
- 有一点需要注意的是,MRT的各个surface的bits必须是一致的,所以这个例子使用了两个G16R16的float - texture来保存position。 OK,就是这样简单。渲染效果与上个例子一样。这个例子程序在geforce 7600GT上测试通过,其他显卡没测过。