在3D图形计算中,deferred shading是一个基于屏幕空间的着色技术。之所以被称为deferred shading,是因为我们将场景的光照计算与渲染"deferred"到第二个pass。在第一个pass中,我们会将所有需要的数据(例如diffuse,normals,materials)渲染到一系列render targets中,这些render targets通常被称为geometry buffer(G-Buffer)。然后,在第二个pass中,从这些render targets中取出数据来执行光照计算与渲染。

Deferred shading技术的主要优点是它分离了场景中的光源与几何体。在我们熟悉的forward rendering技术中,为了渲染一盏光源的效果,我们不得不将在这盏光源范围内的几何体数据提交给GPU来进行计算与渲染。这个过程包含了许多states和shaders的切换,并且会产生大量的draw calls。而在deferred shading技术中,我们可以将许多光源甚至所有光源的计算与渲染在一个draw call中完成。所以当场景中的光源数量急剧增加时,deferred shading的优势将有巨大的体现。

Deferred shading技术的缺点在于它需要消耗更多的内存,带宽以及着色器指令。

下面通过一个例子来说明deferred shading的工作流程,在这个例子中,我在场景中发射了1024个粒子,每个粒子都被当作是一个点光源,最终效果如图:

首先来看下粒子的生成过程,我没有使用CPU来计算和生成粒子,而全部都是在GPU上完成的。我创建了四张32*32大小的纹理,纹理中的每个像素都代表了一个粒子,格式都是DXGI_FORMAT_R32G32B32A32_FLOAT。其中两张用于存储粒子的位置和生命周期,另外两张用于存储粒子的方向。这里各创建两张的原因是需要做ping-ponging,因为同一张纹理是不能同时读取和写入的(在渲染时,我们将其中两张作为输入,另外两张作为输出。在下一帧,将它们互换)。

存储粒子位置和生命周期的纹理如图(随意截取了一帧的数据),其中x, y, z存储粒子的位置,而w存储粒子的生命周期:

存储粒子方向的纹理如图(随意截取了一帧的数据),其中x, y, z存储粒子的方向,w没有使用:

每一帧,我都会根据上一帧粒子的位置和方向来计算出这一帧粒子的位置和方向:

PSOut main( PSIn pIn )
{
PSOut psOut; int3 texCoord = int3( int2(pIn.position.xy), ); // 采样上一帧粒子的位置和方向
float4 position = texPositions.Load( texCoord );
float4 direction = texDirections.Load( texCoord ); // Tick
position.w += frameTime;
if ( position.w >= 4.0f ) // 粒子的生命周期是4秒
{
// 重置粒子
direction.xyz = sprinkleDir;
position.xyz = float3( 0.0f, -235.0f, 0.0f );
position.w -= 4.0f;
}
else
{
// 计算重力
direction.y -= (500.0f * frameTime); // 更新位置
position.xyz += (direction.xyz * frameTime); // 伪物理,不让粒子超出场景的边界
direction.xyz = ((position.xyz > sceneBound) ? -0.8f * abs(direction.xyz) : direction.xyz);
direction.xyz = ((position.xyz < -sceneBound) ? 0.8f * abs(direction.xyz) : direction.xyz);
position.xyz = clamp( position.xyz, -sceneBound, sceneBound );
} //
psOut.position = position;
psOut.direction = direction; //
return psOut;
}

接着,在渲染粒子时,直接从计算完毕的纹理中采样粒子的位置数据,利用geometry shader生成朝向摄像机的面片,就完成了粒子的渲染:

[Geometry shader]

Texture1D texColors;
SamplerState samColors; float4x4 viewProjMatrix;
float3 dirX, dirY; [maxvertexcount()]
void main( point GSIn gIn[], inout TriangleStream<PSIn> triStream )
{
PSIn gsOut; //
gsOut.color = texColors.SampleLevel(samColors, gIn[].position.w * 0.25f, ).rgb; // 输出朝向摄像机的面片
gsOut.position = mul( viewProjMatrix, float4(gIn[].position.xyz - dirX - dirY, 1.0f) );
gsOut.texCoord = float2( 0.0f, 1.0f );
triStream.Append( gsOut ); gsOut.position = mul( viewProjMatrix, float4(gIn[].position.xyz - dirX + dirY, 1.0f) );
gsOut.texCoord = float2( 0.0f, 0.0f );
triStream.Append( gsOut ); gsOut.position = mul( viewProjMatrix, float4(gIn[].position.xyz + dirX - dirY, 1.0f) );
gsOut.texCoord = float2( 1.0f, 1.0f );
triStream.Append( gsOut ); gsOut.position = mul( viewProjMatrix, float4(gIn[].position.xyz + dirX + dirY, 1.0f) );
gsOut.texCoord = float2( 1.0f, 0.0f );
triStream.Append( gsOut );
}

在计算完粒子的位置数据后,接着我们就渲染场景,在这个pass中,我们不计算光照,而仅仅是将场景的diffuse以及normal渲染到两张render targets中:

[Fragment shader]

struct PSOut
{
float3 base : SV_TARGET0;
float3 normal : SV_TARGET1;
}; Texture2D texDiffuse;
Texture2D texBumpMap;
SamplerState samTrilinearAnisoWrap; PSOut main( PSIn pIn )
{
float3 base = texDiffuse.Sample(samTrilinearAnisoWrap, pIn.texCoord).rgb; float3 bump = texBumpMap.Sample(samTrilinearAnisoWrap, pIn.texCoord).xyz;
bump = 2.0f * bump - 1.0f; // Convert from [0, 1] to [-1, 1] // 计算法线
float3 normal = pIn.tangent * bump.x + pIn.binormal * bump.y + pIn.normal * bump.z;
normal = normalize( normal ); //
PSOut psOut;
psOut.base = base;
psOut.normal = normal * 0.5f + 0.5f; // Convert from [-1, 1] to [0, 1] return psOut;
}

这样,我们就有了用于在第二个pass中计算光照的G-Buffer数据:

至此,我们可以开始我们的deferred shading了。具体流程是这样的:每个粒子我们都把它作为一个点光源,一种简单的方法是针对每个粒子,我们都绘制一个全屏四边形,触发屏幕上每个像素的着色器,然后去计算光照,但是这种方法的效率太低了。所以我采用的方法是计算每个点光源的覆盖范围,然后生成一个表示覆盖范围的四边形,来触发在范围内的像素的着色器。计算覆盖范围的方法为,将粒子的位置转换到视图空间,然后根据设置的点光源的范围来生成四边形的四个顶点,再将这四个顶点转换到clip-space:

// 投影矩阵分量:
// projMatrix[0][0]
// projMatrix[1][1]
// projMatrix[2][2]
// projMatrix[3][2]
float4 projMatrixComponoents; bool getScreenBox(
const float3 pos,
const float radius,
out float xProjLeft, out float xProjRight,
out float yProjTop, out float yProjBottom,
out float zProjNear, out float zProjFar )
{
// 初始化
xProjLeft = -;
xProjRight = ;
yProjTop = ;
yProjBottom = -;
zProjNear = ;
zProjFar = ; //
float4 viewLightPosition = mul( viewMatrix, float4(pos, 1.0f) ); // 计算z
float zViewFar = viewLightPosition.z + radius;
if ( zViewFar <= 0.0f )
{
// 光源完全位于摄像机之后
return false;
} //
float zViewNear = max( viewLightPosition.z - radius, 0.00001f ); //
zProjNear = projMatrixComponoents.z + projMatrixComponoents.w / zViewNear;
zProjFar = projMatrixComponoents.z + projMatrixComponoents.w / zViewFar; //
// 计算clip-space的四边形
//
float xViewLeft = viewLightPosition.x - radius;
xProjLeft = projMatrixComponoents.x * xViewLeft / viewLightPosition.z;
if ( xProjLeft >= 1.0f )
{
// 光源完全位于投影范围的右边
return false;
} float xViewRight = viewLightPosition.x + radius;
xProjRight = projMatrixComponoents.x * xViewRight / viewLightPosition.z;
if ( xProjRight <= -1.0f )
{
// 光源完全位于投影范围的左边
return false;
} float yViewTop = viewLightPosition.y + radius;
yProjTop = projMatrixComponoents.y * yViewTop / viewLightPosition.z;
if ( yProjTop <= -1.0f )
{
// 光源完全位于投影范围的底部
return false;
} float yViewBottom = viewLightPosition.y - radius;
yProjBottom = projMatrixComponoents.y * yViewBottom / viewLightPosition.z;
if ( yProjBottom >= 1.0f )
{
// 光源完全位于投影范围的顶部
return false;
} // Clamp all
xProjLeft = clamp( xProjLeft, -1.0f, xProjLeft );
xProjRight = clamp( xProjRight, xProjRight, 1.0f );
yProjTop = clamp( yProjTop, yProjTop, 1.0f );
yProjBottom = clamp( yProjBottom, -1.0f, yProjBottom ); return true;
}

接着,在光源范围内的像素的着色器被触发,我们就可以开始计算它们的光照了,从G-Buffer中采样diffuse和normal数据,进行常规的点光源光照计算即可:

[Fragment shader]

Texture2D texBaseGBuffer;
Texture2D texNormalGBuffer;
Texture2D texDepth;
SamplerState samPointClamp; float4x4 invViewProjMatrix; float3 main( PSIn pIn ) : SV_TARGET0
{
float depth = texDepth.Sample(samPointClamp, pIn.texCoord).x; // 只计算在深度范围内的像素的光照
[branch]
if ( (depth >= pIn.zBounds.x) && (depth <= pIn.zBounds.y) )
{
float3 base = texBaseGBuffer.Sample(samPointClamp, pIn.texCoord).rgb;
float3 normal = texNormalGBuffer.Sample(samPointClamp, pIn.texCoord).xyz * 2.0f - 1.0f; // Clip-space position,x和y的偏移量已经计算在invViewProjMatrix中,所以这里就不需要转换了
float4 clipPosition = float4( pIn.texCoord, depth, 1.0f ); // World-space position
float4 worldPosition = mul( invViewProjMatrix, clipPosition );
worldPosition.xyz /= worldPosition.w; // Lighting
float3 lightDir = (pIn.lightPosition - worldPosition.xyz) / LIGHT_SIZE; //
float atten = saturate( 1.0f - dot(lightDir, lightDir) ); //
lightDir = normalize( lightDir );
float diffuse = saturate( dot(lightDir, normal) ); return (0.7f * atten * pIn.color * (diffuse * base));
} return ;
}

至此,我们的deferred shading就完成了 o(∩_∩)o

D3D Deferred Shading的更多相关文章

  1. 引擎设计跟踪(九.14.3.1) deferred shading: Depthstencil as GBuffer depth

    问题汇总 1.Light support for Editor编辑器加入了灯光工具, 可以添加和修改灯光. 问题1. light object的用户互交.point light可以把对应的volume ...

  2. 引擎设计跟踪(九.14.3) deferred shading 准备

    目前做的一些准备工作 1.depth prepass for forward shading. 做depth prepass的原因是为了完善渲染流程, 虽然架构上支持多个pass, 但实际上从来没有测 ...

  3. Deferred Shading延迟渲染

    Deferred Shading 传统的渲染过程通常为:1)绘制Mesh:2)指定材质:3)处理光照效果:4)输出.传统的过程Mesh越多,光照处理越费时,多光源时就更慢了. 延迟渲染的步骤:1)Pa ...

  4. 引擎设计跟踪(九.14.3.2) Deferred shading的后续实现和优化

    最近完成了deferred shading和spot light的支持, 并作了一部分优化. 之前forward shading也只支持方向光, 现在也支持了点光源和探照光. 对于forward sh ...

  5. Deferred Shading 延迟着色(翻译)

    原文地址:https://en.wikipedia.org/wiki/Deferred_shading 在3D计算机图形学领域,deferred shading 是一种屏幕空间着色技术.它被称为Def ...

  6. opengl deferred shading

    原文地址:http://www.verydemo.com/demo_c284_i6147.html 一.Deferred shading技术简介 Deferred shading是这样一种技术:将光照 ...

  7. Deferred Shading,延迟渲染(提高渲染效率,减少多余光照计算)【转】

    Deferred Shading,看过<Gems2> 的应该都了解了.最近很火的星际2就是使用了Deferred Shading. 原帖位置:   http://blog.csdn.net ...

  8. Deferred shading rendering path翻译

    Overview 概述 When using deferred shading, there is no limit on the number of lights that can affect a ...

  9. Unity的Deferred Shading

    什么是Deferred Shading Unity自身除了支持前向渲染之外,还支持延迟渲染.Unity的rendering path可以通过Edit/Project Settings中的Graphic ...

随机推荐

  1. JVM调优总结10-调优方法

    JVM调优工具 Jconsole,jProfile,VisualVM Jconsole : jdk自带,功能简单,但是可以在系统有一定负荷的情况下使用.对垃圾回收算法有很详细的跟踪.详细说明参考这里 ...

  2. Factory Girl使用

    1.使用Rspec,详见http://www.cnblogs.com/fanxiaopeng/p/3563772.html 2.在gemfile中添加 #Gemfile group :developm ...

  3. [转载]--Ubuntu下修改DNS重启也能用的方法

    安装好Ubuntu之后设置了静态IP地址,再重启后就无法解析域名.想重新设置一下DNS,打开/etc/resolv.conf cat /etc/resolv.conf# Dynamic resolv. ...

  4. springMVC+jpa配置之简单案例

    搭建springMVC+jpa的亲身经历,看着网上的博客,自己摸索着搭建框架结果错误一大堆.现在把流程走一遍,方便以后查看. 其中我遇到这样的一个问题:直接启动tomcat运行保存实体能通过,但是通过 ...

  5. Spring IOC 方式结合TESTGN测试用例,测试简单java的命令模式

    java命令模式: 可以命令("请求")封装成一个对象,一个命令对象通过在特定的接收着上绑定一组动作来封装一个请求.命令对象直接把执行动作和接收者包进对象中,只对外暴露出执行方法的 ...

  6. Javascript中“==”与“===”的区别

    在Javascript中有"=="和"==="两种比较运行符,那么他们有什么区别呢? 一.对于string,number等基础类型,==和===是有区别的 1) ...

  7. 数据结构中的堆棧在C#中的实现

    一.大致学习 堆棧是一种面向表的数据结构,堆棧中的数据只能在标的某一短进行添加和删除操作,是一种典型的(LIFO)数据结构. 现实生活中的理解:自助餐厅的盘子堆,人们总是从顶部取走盘子,当洗碗工把洗好 ...

  8. hdu 4417 Super Mario/树套树

    原题链接:http://acm.hdu.edu.cn/showproblem.php?pid=4417 题意很简单,给定一个序列求一个区间 [L, R,]中小于等于H的元素的个数. 好像函数式线段树可 ...

  9. aliyun install php apache mysql nginx

    yum install httpd -y yum install mysql mysql-server -y yum install php-mysql php-pgsql php-pecl-mong ...

  10. UITableView 表视图编辑

    UITableViewController(表视图控制器)继承自UIViewController,自带一个tableView self.view不是UIView而是UITableView dataso ...