Directx11进阶之ScreenSpaceReflection(SSR)屏幕空间反射(1)

目前进入DX11进阶篇,如果不出意外,应该会更新SSR(ScreenSpaceReflection),  ComputeShader的各种应用(如计算FFT,海洋渲染),引入PBR材质系统,TileDefferedRenderPipeline(分块延迟渲染管线)以及深入探讨TDRP下的优化,基于GPU的ParticleSystem,基于GPU的骨骼动画系统,多线程渲染(MultiThreadRendering)。最终应该会将上面各种实现实现一个demo,讲解整个渲染管线,说说GBuffer,Lighting,Shadow,后处理一堆东西是怎么组织的,当然都是自己的探索,没参考有多少其他先进图形引擎,因为 我主张的一种观点,先从头到尾做出自己的图形引擎,自己组织渲染管线,后面再去参考先进的引擎进行改进,这样会有对比,才知道差距(好吧,现在我感觉只是算法的拼凑,完全没有架构的思想),才能深刻体会图形渲染管线的精粹所在。

先看看程序整个的结构:




ScreenSpaceReflection(屏幕空间反射)的介绍与实现.

屏幕空间算法的家族有很多,如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<float4> DiffuseTex:register(t0);
Texture2D<float4> FrontDepthTex:register(t1);
Texture2D<float4> BackDepthTex:register(t2);
Texture2D<float4> ViewPosTex:register(t3);
Texture2D<float4> 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<float4> DiffuseTex:register(t0);
Texture2D<float4> FrontDepthTex:register(t1);
Texture2D<float4> BackDepthTex:register(t2);
Texture2D<float4> ViewPosTex:register(t3);
Texture2D<float4> 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;
}

运行效果:



ScreenSpaceReflection(屏幕空间反射)的缺点与改进:

(1)因为是提取屏幕空间的像素值,因此不能反射屏幕外的物体。算是很致命的缺点了。(题外话:其实SS(屏幕空间)系列家族的算法都有类似的致命缺点,所以DXR RealTime RayTrace才是未来)。因此竖直的反射面,球面这些反射屏幕外物体多的表面不适合用SSR,像图中的较为水平的面(水面,江河,地面)采用SSR比较适合。如下图,部分反射信息丢失了。


(2)消耗高,由于可能求一个反射像素就步进几百步,消耗过大,这可以通过 BinarySearch和jitter来优化。以后有空讲解下原理。

(3)不支持粗糙表面的反射,显示中的表面不是理想完全光滑的表面,经常是经过反射变得有些扭曲模糊,后面有空再讲解了。


源码链接:

https://github.com/2047241149/SDEngine


参考资料:

[1]http://jcgt.org/published/0003/04/04/


猜你喜欢

转载自blog.csdn.net/qq_29523119/article/details/80463133