目前进入DX11进阶篇,如果不出意外,应该会更新SSR(ScreenSpaceReflection), ComputeShader的各种应用(如计算FFT,海洋渲染),引入PBR材质系统,TileBasedDefferedShading(分块延迟渲染管线)以及深入探讨TBDS下的优化,基于GPU的ParticleSystem,基于GPU的骨骼动画系统,多线程渲染(MultiThreadRendering)。最终应该会将上面各种实现实现一个demo,讲解整个渲染管线,说说GBuffer,Lighting,Shadow,后处理一堆东西是怎么组织的,当然都是自己的探索,没参考有多少其他先进图形引擎,因为 我主张的一种观点,先从头到尾做出自己的图形引擎,自己组织渲染管线,后面再去参考先进的引擎进行改进,这样会有对比,才知道差距(好吧,现在我感觉只是算法的拼凑,完全没有架构的思想),才能深刻体会图形渲染管线的精粹所在。
先看看程序整个的结构:
屏幕空间算法的家族有很多,如SSAO,SSShadow(屏幕空间阴影),SSR之类的,甚至是延迟渲染本身,其实本质上都是屏幕空间的算法,它们都是利用屏幕空间的信息来求出相应的结果(AO,Shadow,Reflect等等)。
说到SSR,其实原理就是反射的原理(有点废话),只不过是建立在屏幕空间上的反射,最终提取到的反射颜色信息都是来源于屏幕空间的颜色信息。
先看看反射是怎么来的:
看上面图,A点发出光线经过C点的反射进入人的眼睛,所以我们从C点看到了物体上的A像素。这里B点是人眼逆着光线看到A点像素的镜像,当然SSR算法不用上B点。
这里我们决定在相机空间进行上图的算法,上面图的眼睛就是相机的位置(在相机空间是原点),C点为反射面的像素位置,我们求出CA向量,也就是反射光线的方向。这里以C为原点,CA为方向的光线,与物体求交,得到像素A点在相机空间的位置,然后转换为屏幕空间(采样纹理空间),对屏幕RT进行采样,从而得到反射的像素值。
这里抛出两个问题:
(1)在相机空间中,我们的CA光线在相机空间C点开始往CA方向每次前进的长度为多少?
(2)我们如何判断CA光线与物体相交?
(1)在相机空间中,我们的CA光线在相机空间C点开始往CA方向每次前进的长度为多少?
说说第一个问题,我们相机空间每次前进的长度单位为多少?按我查找相关资料,SSR算法刚出来那会,在相机空间光线步进的距离是1.0,但是这里有个问题,因为我们是从相机空间进行光线求交,然后我们转化为屏幕空间进行采样获取相应的像素颜色值,这里问题是什么?问题在于我们在相机空间每次步进1.0,在屏幕空间就是相应步进1.0?答案是否定的,所以我们在相机空间的光线每次步进1.0,在转换为屏幕空间的时候,由于透视纠正的存在,屏幕空间并不是步进1.0,光栅化的时候相机空间坐标点并不成线性变换,那么这样造成了我们在屏幕空间提取所有光线求交的像素值的时候,出现下面图的左边情况:
那么我们怎么办呢?新办法:我们从屏幕空间进行光线步进1个单位,然后进行反透视纠正转为相机空间的步进距离,当然之后还得纠正回去。这里顺便说下,相机空间的坐标,方向量,UV属性,世界空间的坐标,法向量等等属性除以相机空间坐标的Z分量后是成线性关系的,这个过程其实就是光栅化,不懂的回去参考下我的“软光栅器实现”系列博客。软件光栅器六之透视纹理映射。
//初始化反射的颜色
float4 reflectColor = float4(0.0, 0.0, 0.0, 0.0f);
float2 screenSize = texSize(DiffuseTex);
float2 texcoord = outa.Tex;
float3 viewPos = ViewPosTex.Sample(SampleClampPoint, outa.Tex).xyz;
float3 viewNormal= ViewNormalTex.Sample(SampleClampPoint, outa.Tex).xyz;
float t = 1;
int2 origin = texcoord * screenSize;
int2 coord;
//像素在相机空间的位置(光线起点)和法线
float3 v0 = viewPos;
float3 vsNormal = viewNormal;
//相机到像素的方向
float3 eyeToPixel = normalize(v0);
//光线反射的方向
float3 reflRay = normalize(reflect(eyeToPixel, vsNormal));
//反射光线终点
float3 v1 = v0 + reflRay * farPlane;
//屏幕空间的坐标
float4 p0 = mul(float4(v0, 1.0), Proj);
float4 p1 = mul(float4(v1, 1.0), Proj);
//这里参考软光栅器 纹理坐标 世界空间坐标的插值原理(透视纠正)
//w为相机空间的Z值
float k0 = 1.0 / p0.w;
float k1 = 1.0 / p1.w;
p0 *= k0;
p1 *= k1;
v0 *= k0;
v1 *= k1;
p0.xy = NormalizedDeviceCoordToScreenCoord(p0.xy, screenSize.xy);
p1.xy = NormalizedDeviceCoordToScreenCoord(p1.xy, screenSize.xy);
//保证屏幕空间的光线起始点终点至少一个单位长度
float ds = distanceSquared(p1.xy, p0.xy);
p1 += ds < 0.0001 ? 0.01 : 0.0;
float divisions = length(p1.xy - p0.xy);
float3 dV = (v1 - v0) / divisions;
float dK = (k1 - k0) / divisions;
float2 traceDir = (p1 - p0) / divisions;
float maxSteps = min(divisions, MAX_STEPS);
(2)我们如何判断CA光线与物体相交?
我们在相机空间每次步进的时候,转为换屏幕空间(采样纹理空间),对DepthBuffer进行采样,然后就得到深度值,进行空间转换变为相机空间的深度,然后拿相机空间光线当前位置的Z值与这个相机空间的深度进行比较(略微大于等于)就行了。如下图所示:
if ((curDepth > storeDepth) && ((curDepth - storeDepth) <= 0.1))
{
reflectColor = DiffuseTex.SampleLevel(SampleClampPoint, texcoord, 0);
reflectColor.a = 0.4;
break;
}
所有代码如下所示:
Texture2D DiffuseTex:register(t0);
Texture2D FrontDepthTex:register(t1);
Texture2D BackDepthTex:register(t2);
Texture2D ViewPosTex:register(t3);
Texture2D ViewNormalTex:register(t4);
SamplerState SampleWrapLinear:register(s0);
SamplerState SampleClampPoint:register(s1);
#define MAX_STEPS 500
cbuffer CBMatrix:register(b0)
{
matrix World;
matrix View;
matrix Proj;
matrix WorldInvTranspose;
float3 cameraPos;
float pad1;
float4 dirLightColor;
float3 dirLightDir;
float pad2;
float3 ambientLight;
float pad3;
};
cbuffer CBSSR:register(b1)
{
float farPlane;
float nearPlane;
float2 perspectiveValues;
};
struct VertexIn
{
float3 Pos:POSITION;
float2 Tex:TEXCOORD;
};
struct VertexOut
{
float4 Pos:SV_POSITION;
float2 Tex:TEXCOORD0;
};
float DepthBufferConvertToViewDepth(float depth)
{
float viewDepth = perspectiveValues.x / (depth + perspectiveValues.y);
return viewDepth;
};
float2 texSize(Texture2D tex)
{
uint texWidth, texHeight;
tex.GetDimensions(texWidth, texHeight);
return float2(texWidth, texHeight);
}
//转换为屏幕空间坐标
float2 NormalizedDeviceCoordToScreenCoord(float2 ndc, float2 screenSize)
{
float2 screenCoord;
screenCoord.x = screenSize.x * (0.5 * ndc.x + 0.5);
screenCoord.y = screenSize.y * (-0.5 * ndc.y + 0.5);
return screenCoord;
}
float distanceSquared(float2 a, float2 b)
{
a -= b;
return dot(a, a);
}
VertexOut VS(VertexIn ina)
{
VertexOut outa;
outa.Pos = float4(ina.Pos.xy,1.0f,1.0f);
outa.Tex = ina.Tex;
return outa;
}
float4 PS(VertexOut outa) : SV_Target
{
//初始化反射的颜色
float4 reflectColor = float4(0.0, 0.0, 0.0, 0.0f);
float2 screenSize = texSize(DiffuseTex);
float2 texcoord = outa.Tex;
float3 viewPos = ViewPosTex.Sample(SampleClampPoint, outa.Tex).xyz;
float3 viewNormal= ViewNormalTex.Sample(SampleClampPoint, outa.Tex).xyz;
float t = 1;
int2 origin = texcoord * screenSize;
int2 coord;
//像素在相机空间的位置(光线起点)和法线
float3 v0 = viewPos;
float3 vsNormal = viewNormal;
//相机到像素的方向
float3 eyeToPixel = normalize(v0);
//光线反射的方向
float3 reflRay = normalize(reflect(eyeToPixel, vsNormal));
//反射光线终点
float3 v1 = v0 + reflRay * farPlane;
//屏幕空间的坐标
float4 p0 = mul(float4(v0, 1.0), Proj);
float4 p1 = mul(float4(v1, 1.0), Proj);
//这里参考软光栅器 纹理坐标 世界空间坐标的插值原理(透视纠正)
//w为相机空间的Z值
float k0 = 1.0 / p0.w;
float k1 = 1.0 / p1.w;
p0 *= k0;
p1 *= k1;
v0 *= k0;
v1 *= k1;
p0.xy = NormalizedDeviceCoordToScreenCoord(p0.xy, screenSize.xy);
p1.xy = NormalizedDeviceCoordToScreenCoord(p1.xy, screenSize.xy);
//保证屏幕空间的光线起始点终点至少一个单位长度
float ds = distanceSquared(p1.xy, p0.xy);
p1 += ds < 0.0001 ? 0.01 : 0.0;
float divisions = length(p1.xy - p0.xy);
float3 dV = (v1 - v0) / divisions;
float dK = (k1 - k0) / divisions;
float2 traceDir = (p1 - p0) / divisions;
float maxSteps = min(divisions, MAX_STEPS);
while (t < maxSteps)
{
coord = origin + traceDir * t;
if (coord.x > screenSize.x || coord.y > screenSize.y || coord.x < 0 || coord.y < 0)
{
break;
}
float curDepth = (v0 + dV * t).z;
float k = k0 + dK * t;
curDepth /= k;
texcoord = float2(coord) / screenSize;
float storeFrontDepth = FrontDepthTex.SampleLevel(SampleClampPoint, texcoord, 0).r;
storeFrontDepth = DepthBufferConvertToViewDepth(storeFrontDepth);
float storeBackDepth = BackDepthTex.SampleLevel(SampleClampPoint, texcoord, 0).r;
storeBackDepth = DepthBufferConvertToViewDepth(storeBackDepth);
if ((curDepth >= storeFrontDepth) && ((curDepth - storeFrontDepth) <= 0.1))
{
reflectColor = DiffuseTex.SampleLevel(SampleClampPoint, texcoord, 0);
reflectColor.a = 0.4;
break;
}
t++;
}
return reflectColor;
}
运行截图:
好吧,SSR效果很差,原因在哪?因为我们没考虑物体的厚度,我们是通过
(curDepth >= storeFrontDepth) && ((curDepth - storeFrontDepth) <= 0.1)
来判定深度差不多来光线求交的,但是我们这里只考虑薄的物体,而厚的物体下,DepthBuffer存储的深度为最前面像素的深度,因此我们在光线与厚的物体求交会出现问题。
看上面图,这种情况下我们是拿B点相机空间的深度减去A点相机空间的深度,也就是AB的距离,毫无疑问是大于0.1的,也就是我们刚开始只考虑到薄的物体,没考虑到厚的物体,因此我们造成了反射的像素丢失。那么我们怎么办?很简单,我们得同时知道整个场景前面的DepthBuffer和背面的DepthBuffer. 也就是CullBackFace设置下的DepthBuffer和CullFrontFace设置下的DepthBuffer。如下所示:
CullBackFace设置下的DepthBuffer(前面的深度):(g,b通道为0,R通道大,因此红色,并且越红代表 越深)
CullFrontFace设置下的DepthBuffer(背后的深度):
则物体在厚度为:
storeBackDepth - storeFrontDepth
最终SSR Shader 如下所示:
Texture2D DiffuseTex:register(t0);
Texture2D FrontDepthTex:register(t1);
Texture2D BackDepthTex:register(t2);
Texture2D ViewPosTex:register(t3);
Texture2D ViewNormalTex:register(t4);
SamplerState SampleWrapLinear:register(s0);
SamplerState SampleClampPoint:register(s1);
#define MAX_STEPS 500
cbuffer CBMatrix:register(b0)
{
matrix World;
matrix View;
matrix Proj;
matrix WorldInvTranspose;
float3 cameraPos;
float pad1;
float4 dirLightColor;
float3 dirLightDir;
float pad2;
float3 ambientLight;
float pad3;
};
cbuffer CBSSR:register(b1)
{
float farPlane;
float nearPlane;
float2 perspectiveValues;
};
struct VertexIn
{
float3 Pos:POSITION;
float2 Tex:TEXCOORD;
};
struct VertexOut
{
float4 Pos:SV_POSITION;
float2 Tex:TEXCOORD0;
};
float DepthBufferConvertToViewDepth(float depth)
{
float viewDepth = perspectiveValues.x / (depth + perspectiveValues.y);
return viewDepth;
};
float2 texSize(Texture2D tex)
{
uint texWidth, texHeight;
tex.GetDimensions(texWidth, texHeight);
return float2(texWidth, texHeight);
}
//转换为屏幕空间坐标
float2 NormalizedDeviceCoordToScreenCoord(float2 ndc, float2 screenSize)
{
float2 screenCoord;
screenCoord.x = screenSize.x * (0.5 * ndc.x + 0.5);
screenCoord.y = screenSize.y * (-0.5 * ndc.y + 0.5);
return screenCoord;
}
float distanceSquared(float2 a, float2 b)
{
a -= b;
return dot(a, a);
}
VertexOut VS(VertexIn ina)
{
VertexOut outa;
outa.Pos = float4(ina.Pos.xy,1.0f,1.0f);
outa.Tex = ina.Tex;
return outa;
}
float4 PS(VertexOut outa) : SV_Target
{
//初始化反射的颜色
float4 reflectColor = float4(0.0, 0.0, 0.0, 0.0f);
float2 screenSize = texSize(DiffuseTex);
float2 texcoord = outa.Tex;
float3 viewPos = ViewPosTex.Sample(SampleClampPoint, outa.Tex).xyz;
float3 viewNormal= ViewNormalTex.Sample(SampleClampPoint, outa.Tex).xyz;
float t = 1;
int2 origin = texcoord * screenSize;
int2 coord;
//像素在相机空间的位置(光线起点)和法线
float3 v0 = viewPos;
float3 vsNormal = viewNormal;
//相机到像素的方向
float3 eyeToPixel = normalize(v0);
//光线反射的方向
float3 reflRay = normalize(reflect(eyeToPixel, vsNormal));
//反射光线终点
float3 v1 = v0 + reflRay * farPlane;
//屏幕空间的坐标
float4 p0 = mul(float4(v0, 1.0), Proj);
float4 p1 = mul(float4(v1, 1.0), Proj);
//这里参考软光栅器 纹理坐标 世界空间坐标的插值原理(透视纠正)
//w为相机空间的Z值
float k0 = 1.0 / p0.w;
float k1 = 1.0 / p1.w;
p0 *= k0;
p1 *= k1;
v0 *= k0;
v1 *= k1;
p0.xy = NormalizedDeviceCoordToScreenCoord(p0.xy, screenSize.xy);
p1.xy = NormalizedDeviceCoordToScreenCoord(p1.xy, screenSize.xy);
//保证屏幕空间的光线起始点终点至少一个单位长度
float ds = distanceSquared(p1.xy, p0.xy);
p1 += ds < 0.0001 ? 0.01 : 0.0;
float divisions = length(p1.xy - p0.xy);
float3 dV = (v1 - v0) / divisions;
float dK = (k1 - k0) / divisions;
float2 traceDir = (p1 - p0) / divisions;
float maxSteps = min(divisions, MAX_STEPS);
while (t < maxSteps)
{
coord = origin + traceDir * t;
if (coord.x > screenSize.x || coord.y > screenSize.y || coord.x < 0 || coord.y < 0)
{
break;
}
float curDepth = (v0 + dV * t).z;
float k = k0 + dK * t;
curDepth /= k;
texcoord = float2(coord) / screenSize;
float storeFrontDepth = FrontDepthTex.SampleLevel(SampleClampPoint, texcoord, 0).r;
storeFrontDepth = DepthBufferConvertToViewDepth(storeFrontDepth);
float storeBackDepth = BackDepthTex.SampleLevel(SampleClampPoint, texcoord, 0).r;
storeBackDepth = DepthBufferConvertToViewDepth(storeBackDepth);
if ((curDepth > storeFrontDepth) && ((curDepth - storeFrontDepth) <= (storeBackDepth - storeFrontDepth)))
{
reflectColor = DiffuseTex.SampleLevel(SampleClampPoint, texcoord, 0);
reflectColor.a = 0.4;
break;
}
t++;
}
return reflectColor;
}
运行效果:
(1)因为是提取屏幕空间的像素值,因此不能反射屏幕外的物体。算是很致命的缺点了。(题外话:其实SS(屏幕空间)系列家族的算法都有类似的致命缺点,所以DXR RealTime RayTrace才是未来)。因此竖直的反射面,球面这些反射屏幕外物体多的表面不适合用SSR,像图中的较为水平的面(水面,江河,地面)采用SSR比较适合。如下图,部分反射信息丢失了。
(2)消耗高,由于可能求一个反射像素就步进几百步,消耗过大,这可以通过 BinarySearch和jitter来优化。以后有空讲解下原理。
(3)不支持粗糙表面的反射,显示中的表面不是理想完全光滑的表面,经常是经过反射变得有些扭曲模糊,后面有空再讲解了。
https://github.com/2047241149/SDEngine
[1]http://jcgt.org/published/0003/04/04/