这一节教程是关于如何用DirectX11实现SSAO(ScreenSpaceAmcientOccusion,屏幕空间环境遮挡),程序的代码结构如下:
一,SSAO(屏幕空间环境遮挡)是什么?
SSAO,全称ScreenSpaceAmbientOcculusion,中文翻译为屏幕空间环境遮挡,为一种加强空间立体感的一种实时渲染算法.
下面看看应用了SSAO和没应用SSAO对比的场景渲染。从某个赛车游戏选取了SSAO和没选取SSAO效果的对比图:
(1)SSAO的效果对比
开启SSAO:
未开启SSAO:
你可以从上面可以看出开了SSAO的空间感比没开SSAO的空间感强烈很多,或者说真实感好很多,好吧,下面我放出图勾画出大体哪些地方不一样的
在SSAO效果下红圈的部分显得更加阴暗,这些地方往往是缝隙,往往是沟壑,往往是角落,下面正式开始SSAO算法的话题.
(2)AO(环境遮挡)是什么?
AO是什么,这得从我们AmbientLight(环境光)说起了,AmbientLight用来模拟3D场景中的IndirectxLight(间接光),计算Ambient的公式为
AmcientColor=La*Ma
一般在平时的渲染中,我们认定每个像素接受的La是固定值,都是一样的,但是实际上这是不对的,为什么呢?来看看下面这幅图
(a)场景中,存在一个像素P,然后各个方向的环境光(AmbientLight)入射P点,此时P点正半球的环境光全部到达P点
(b)场景中,存在一个像素P,然后由于P点像素周围存在其它物体的阻挡,因此部分环境光无法进行入射,也就是部分环境光能到达P点,而部分环境光不能到达P点,那么这就推翻了上面":AmbientColor=La*Ma中的La对任意物体来说是一个固定不变的常量"的理论,也就是说根据一个像素是否受到其它物体的遮挡以及遮挡程度的大小,各个像素接受的环境光大小是不一样的.AO(环境遮挡)的概念从此而来.
这里假设像素接受环境光的遮挡值为Occlusion,则接受环境光的接受值值为AmbientAcess=1.0-Occlusion,计算每个像素接受的环境光公式变为下面的:
AmcientColor=La*AmbientAcess*Ma
(3)实时渲染中的AO算法---SSAO
SSAO即屏幕空间环境遮挡算法,为什么称为屏幕空间环境遮挡呢,因为用到了屏幕空间的像素的各种属性吧,先更具体知道原因,往下看.
这里SSAO算法的渲染分为四次RenderPass:
(1)第一步,渲染物体,并将位于相机空间(ViewSpace)的像素的法向量和Z值渲染到一张RT上,其中法向量存储到RT的RGB通道,Z值存储到RT的A通道,这张RT我们称为“NormalZMap”,
(2)第二步,生成一张存储着随机向量的RT,我们称为RandomVecMap,并且利用NormalZMap, 对这两张纹理进行相应的采样,计算最终输出到屏幕空间的每个像素的AmbientAcess值,并且AmbientAcess存储于一张灰度图RT的RGB通道,我们称为"SSAORT"
(3)第三步,将SSAORT进行水平模糊(HorizontalBlur)和垂直模糊(VerticalBlur),消除SSAORT的噪点,最终得到SSAOBlurRT,这也是一张灰度图。
(4)第四次,将SSAOBlurRT作为一张纹理进行采样,获取的纹理像素颜色值也就是最终输出到屏幕的对应像素接受环境光的接受值AmbientAcess,并利用AmbientAcess计算每个像素的着色。
二,SSAO的具体实现.
这次我们渲染的目标模型为"Skull",骷颅头,如下所示:
第一步,RenderNormalDepthPass
NormalDepthPassShader.fx
//VertexShader
cbuffer CBMatrix:register(b0)
{
matrix World;
matrix View;
matrix Proj;
matrix WorldInvTranspose;
matrix ViewInvTranspose;
};
struct VertexIn
{
float3 Pos:POSITION;
float2 Tex:TEXCOORD0; //多重纹理可以用其它数字
float3 Normal:NORMAL;
};
struct VertexOut
{
float4 Pos:SV_POSITION;
float2 Tex:TEXCOORD0;
float3 PosV:Position; //相机空间的位置
float3 NormalV:NORMAL; //相机空间的法向量
};
VertexOut VS(VertexIn ina)
{
VertexOut outa;
outa.Pos = mul(float4(ina.Pos,1.0f), World);
outa.Pos = mul(outa.Pos, View);
outa.Pos = mul(outa.Pos, Proj);
//求出相机空间的位置
outa.PosV = mul(float4(ina.Pos, 1.0f), World).xyz;
outa.PosV = mul(float4(outa.PosV, 1.0f), View).xyz;
//求出相机空间的法向量
outa.NormalV = mul(ina.Normal, (float3x3)WorldInvTranspose); //此事世界逆转置矩阵的第四行本来就没啥用
outa.NormalV = mul(outa.NormalV, (float3x3)ViewInvTranspose);
outa.Tex= ina.Tex;
return outa;
}
float4 PS(VertexOut outa) : SV_Target
{
//规格化相机空间的法向量
outa.NormalV = normalize(outa.NormalV);
return float4(outa.NormalV, outa.PosV.z);
}
得到NormalDepthRT,如下所示:
这一步注意的事情:
请注意NormalDepthRT刚开始不是清除为黑色,而是清除(0,0,-1,SCREEN_FAR), 也就是将NormalZRT上每个初始的像素的都视为在视截体远截面上的像素,在ViewSpace上远截面的像素的法向量为(0,0,-1),而Z值为SCREEN_FAR,即远截面距离.
第二步,RenderSSAOMapPass
SSAOMapShader.fx
Texture2D NormalDepthMap:register(t0); //纹理资源
Texture2D RandomVecMap:register(t1); //随机向量纹理资源
SamplerState SampRandVec:register(s0); //采样方式
SamplerState SampNormalDepth:register(s1); //采样方式
//VertexShader
cbuffer CBMatrix:register(b0)
{
matrix World;
matrix View;
matrix Proj;
matrix WorldInvTranspose;
};
cbuffer CBFrustum:register(b1)
{
float4 FrustumCorner[4];
};
cbuffer CBPerFrame:register(b2)
{
matrix ProjMatrix;//用于投影纹理技术
float4 OffsetVectors[26];
//在相机空间给予的坐标
float OcclusionRadius;
float OcclusionFadeStart;
float OcclusionFadeEnd ;
float SurfaceEpsilon;
};
struct VertexIn
{
float3 PosL:POSITION;
float2 Tex:TEXCOORD0; //多重纹理可以用其它数字
float3 ToFarPlaneIndex:NORMAL;
};
struct VertexOut
{
float4 PosH:SV_POSITION;
float2 Tex : TEXCOORD0;
float3 ToFarPlane : TEXCOORD1;
};
VertexOut VS(VertexIn ina)
{
VertexOut outa;
//由于这次顶点数据的特殊性,顶点坐标直接当作是位于NDC空间,则刚好覆盖整个视口
outa.PosH = float4(ina.PosL, 1.0f);
//求出每个相机空间中原点到远裁剪面角落的向量,便于插值得到原点到每个远裁剪面每个像素的向量
outa.ToFarPlane = FrustumCorner[ina.ToFarPlaneIndex.x].xyz;
//纹理坐标
outa.Tex = ina.Tex;
return outa;
}
//求遮挡值的函数
float OcculusionFunction(float distZ)
{
float occlusion = 0.0f;
//过于近的认为两点在同一平面,无法产生遮挡
if (distZ > SurfaceEpsilon)
{
float fadeLength = OcclusionFadeEnd - OcclusionFadeStart;
//线性减少遮挡值从1到0,随着distZ增加
//从OcclusionFadeStart到OcclusionFadeEnd
occlusion= saturate((OcclusionFadeEnd - distZ) / fadeLength);
}
return occlusion;
}
float4 PS(VertexOut outa) : SV_Target
{
//P-我们计算P点的AmbienOcculusion值
//n在P点的法向量
//q--来自P点的随机向量
//r--一个潜在的可能遮挡P的点
//获取像素在相机空间的法向量和Z坐标
//ScreenQuad的纹理坐标已经是处于纹理坐标空间了(感觉是废话)
float4 normalDepth= NormalDepthMap.SampleLevel(SampNormalDepth, outa.Tex, 0.0f);
//像素位于相机空间的法向量和Z坐标
float3 n = normalDepth.xyz;
float pz = normalDepth.w;
//重构(获取)像素在相机空间的位置,这利用了相似三角形原理
//由于相应的原点到远裁剪面的线段经过P点,所以有P=t*outa.ToFarPlane,所以有P.z=t*outa.ToFarPlane.z
//所以有P.z=t*outa.ToFarPlane.z,有t=P.z/outa.ToFarPlane.z ,有P=(P.z/outa.ToFarPlane.z)*outa.ToFarPlane
float3 p = (pz / outa.ToFarPlane.z)*outa.ToFarPlane;
//获取随机向量贴图对应的值,并将值从[0,1]变到[-1,1]的范围,注意平铺随机向量
float3 randVec= 2.0f*RandomVecMap.SampleLevel(SampRandVec, 4.0f*outa.Tex, 0.0f).rgb-1.0f;
//遮挡值总和
float occulusionSum = 0.0f;
//设置采样数量为
int sampleCount = 128;
//对P点的半球范围以n向量朝向的邻接点采样,一般分为8/16/32,但这里14个位移向量,也就是14个采样点
for (int i = 0; i < sampleCount; ++i)
{
//位移向量是固定的和一致分散的,因为我们的位移在相同的方向是一样的,如果我们用一个位移向量来反射,则 我们得到一个随机同一的位移分散向量
//用随机向量来入射,固定立方体向量作为法向量,来求出反射向量作为最终的位移向量
float3 offset = reflect(OffsetVectors[i%26].xyz, randVec);
//如果位移向量位于面之后,则Flip翻转位移向量,使得q点永远在P的靠近视点的那一面,sign()求出值的符号
float flip = sign(dot(offset, n));
//求出P点在遮挡半径的一个点q
float3 q = p + flip*OcclusionRadius*offset;
//投射q并生成投影纹理坐标,由于q本身位于相机空间,变到投影纹理坐标,注意最后得进行透视除法
float4 ProjQ = mul(float4(q, 1.0f), ProjMatrix);
ProjQ /= ProjQ.w;
ProjQ.x = ProjQ.x / 2.0f + 0.5f;
ProjQ.y = ProjQ.y /(-2.0f) + 0.5f;
//求出相机空间从原点到q点这条光线下对应NormalDepthMap的最小深度,注意这个最小深度并不等于q在相机空间的深度
float rz= NormalDepthMap.SampleLevel(SampNormalDepth, ProjQ.xy, 0.0f).a;
//再次利用相似三角形原理
//r=t*q有r.z=t*q.z有t=r.z/q.z有r=(r.z/q.z)*q
float3 r = (rz / q.z)*q;
//dot(n,normalize(r-p))计算点r位于p点前面遮挡值,角度越小遮挡值越大,并且r与p在Z距离上距离越近,遮挡越大,当然近到一个临界值,遮挡值就为0了,防止自阴影现象
float distZ = p.z - r.z;
float dp = max(dot(normalize(n), normalize(r - p)),0);
float occulusion = dp*OcculusionFunction(distZ);
occulusionSum += occulusion;
}
//求出最终的遮挡值
occulusionSum /= (float)sampleCount;
//求出AmbientLight的接受值
float access = 1.0f - occulusionSum;
//用幂函数使得SSAOMap的效果更加尖锐化,凸显效果
//注意SSAOMap为一张灰度图
return saturate(pow(access, 4.0f));
}
说说这一步的具体原理,来看看一张图,如下所示
(1)如何求像素P点的位置的?看下面做出辅助线的图.
(1)求出相机空间原点O穿过P点到底B点的向量,由于三角形OPzP和三角形OCB相似,所以可以利用这个原理求出P点像素位于相机空间的位置,这里怎么知道B点位于相机空间的位置呢?其实很简单,在此次渲染Pass中,我们把一个长方形渲染到RT上。长方形四个顶点刚好对应于相机空间的原点到视截体远截面四个顶点的,利用这个原理进行插值就能获取长方形的每个像素或者说屏幕上每个像素对应于远截面的上的每个像素的位置。
cbuffer CBFrustum:register(b1)
{
float4 FrustumCorner[4];
};
//求出每个相机空间中原点到远裁剪面角落的向量,便于插值得到原点到每个远裁剪面每个像素的向量
outa.ToFarPlane = FrustumCorner[ina.ToFarPlaneIndex.x].xyz;
//ScreenQuad的纹理坐标已经是处于纹理坐标空间了(感觉是废话)
float4 normalDepth= NormalDepthMap.SampleLevel(SampNormalDepth, outa.Tex, 0.0f);
//像素位于相机空间的法向量和Z坐标
float3 n = normalDepth.xyz;
float pz = normalDepth.w;
//重构(获取)像素在相机空间的位置,这利用了相似三角形原理
//由于相应的原点到远裁剪面的线段经过P点,所以有P=t*outa.ToFarPlane,所以有P.z=t*outa.ToFarPlane.z
//所以有P.z=t*outa.ToFarPlane.z,有t=P.z/outa.ToFarPlane.z ,有P=(P.z/outa.ToFarPlane.z)*outa.ToFarPlane
float3 p = (pz / outa.ToFarPlane.z)*outa.ToFarPlane;
(2)Shader里有26个偏移向量,在PixelShader对RandomVecMap进行采样得到的随机向量,以偏移向量为法线进行反射得到P点偏移向量,P点偏移向量位于P点正半球。这26个偏移向量是原点到单位立方体的各个面中心(6个),各个顶点(8个),各条边中点(12个)的向量,满足了各个方向的随机性。
SSAOClass.h
#pragma once
#ifndef _SSAO_CLASS_H
#define _SSAO_CLASS_H
#include<Windows.h>
#include<xnamath.h>
#include<stdio.h>
#include<time.h>
class SSAOClass
{
private:
//视截体在相机空间的四个向量
XMFLOAT4 mFrustumFarCorner[4];
//22用于获取采样的反射向量
XMFLOAT4 mOffsets[26];
//渲染目标区域的宽度
float mRTWidth, mRTHeight;
public:
SSAOClass();
SSAOClass(const SSAOClass&other);
~SSAOClass();
void Initialize(float RTWidth, float RTHeight);
void BuildFrustumFarCorner(float FovY, float farZ);
void BuildOffsetVectors();
XMFLOAT4* GetFrustumFarCorne() { return mFrustumFarCorner; }
XMFLOAT4* GetOffsets() { return mOffsets; }
};
#endif // !_SSAO_CLASS_H
SSAOClass.CPP
#include"SSAOClass.h"
SSAOClass::SSAOClass()
{
}
SSAOClass::SSAOClass(const SSAOClass&other)
{
}
SSAOClass::~SSAOClass()
{
}
void SSAOClass::Initialize(float RTWidth, float RTHeight)
{
mRTWidth = RTWidth;
mRTHeight = RTHeight;
}
void SSAOClass::BuildFrustumFarCorner(float FovY, float farZ)
{
//利用相似三角形的原理
float aspect = (float)mRTWidth / (float)mRTHeight; //宽高比
float halfHeight = farZ*tanf(0.5f*FovY);
float halfWidth = aspect*halfHeight;
mFrustumFarCorner[0] = XMFLOAT4(-halfWidth, -halfHeight, farZ, 0.0f);
mFrustumFarCorner[1] = XMFLOAT4(-halfWidth, +halfHeight, farZ, 0.0f);
mFrustumFarCorner[2] = XMFLOAT4(+halfWidth, +halfHeight, farZ, 0.0f);
mFrustumFarCorner[3] = XMFLOAT4(+halfWidth, -halfHeight, farZ, 0.0f);
}
void SSAOClass::BuildOffsetVectors()
{
//建立十四个Cube采样向量,八个角向量,6个面中心向量
//八个角向量
mOffsets[0] = XMFLOAT4(+1.0f, +1.0f, +1.0f,0.0f);
mOffsets[1] = XMFLOAT4(-1.0f, -1.0f, -1.0f, 0.0f);
mOffsets[2] = XMFLOAT4(-1.0f, +1.0f, +1.0f, 0.0f);
mOffsets[3] = XMFLOAT4(+1.0f, -1.0f, -1.0f, 0.0f);
mOffsets[4] = XMFLOAT4(+1.0f, +1.0f, -1.0f, 0.0f);
mOffsets[5] = XMFLOAT4(-1.0f, -1.0f, +1.0f, 0.0f);
mOffsets[6] = XMFLOAT4(-1.0f, +1.0f, -1.0f, 0.0f);
mOffsets[7] = XMFLOAT4(+1.0f, -1.0f, +1.0f, 0.0f);
//六个面中心向量
mOffsets[8] = XMFLOAT4(-1.0f, 0.0f, 0.0f, 0.0f);
mOffsets[9] = XMFLOAT4(+1.0f, 0.0f, 0.0f, 0.0f);
mOffsets[10] = XMFLOAT4(0.0f, -1.0f, 0.0f, 0.0f);
mOffsets[11] = XMFLOAT4(0.0f, +1.0f, 0.0f, 0.0f);
mOffsets[12] = XMFLOAT4(0.0f, 0.0f, -1.0f, 0.0f);
mOffsets[13] = XMFLOAT4(0.0f, 0.0f, +1.0f, 0.0f);
//12个边向量
//最上面的四个点
mOffsets[14] = XMFLOAT4(-1.0f, 1.0f, 0.0f, 0.0f);
mOffsets[15] = XMFLOAT4(1.0f, 1.0f, 0.0f, 0.0f);
mOffsets[16] = XMFLOAT4(0.0f, 1.0f, -1.0f, 0.0f);
mOffsets[17] = XMFLOAT4(0.0f, 1.0f, 1.0f, 0.0f);
//中间四个点
mOffsets[18] = XMFLOAT4(1.0f, 0.0f, 1.0f, 0.0f);
mOffsets[19] = XMFLOAT4(-1.0f, 0.0f, 1.0f, 0.0f);
mOffsets[20] = XMFLOAT4(-1.0f, 0.0f, -1.0f, 0.0f);
mOffsets[21] = XMFLOAT4(1.0f, 0.0f, -1.0f, 0.0f);
//下面四个点
mOffsets[22] = XMFLOAT4(-1.0f, -1.0f, 0.0f, 0.0f);
mOffsets[23] = XMFLOAT4(1.0f, -1.0f, 0.0f, 0.0f);
mOffsets[24] = XMFLOAT4(0.0f, -1.0f, -1.0f, 0.0f);
mOffsets[25] = XMFLOAT4(0.0f, -1.0f, 1.0f, 0.0f);
//初始化时间种子;
srand((unsigned)time(NULL));
for (int i = 0; i < 26; ++i)
{
//创建随机长度[0.25,1.0]
float s = (float)((float)rand() / (float)RAND_MAX)*0.75f+0.25f;
//缩放采样向量
XMVECTOR v =s*XMVector4Normalize(XMLoadFloat4(&mOffsets[i]));
XMStoreFloat4(&mOffsets[i], v);
}
}
由P点坐标和P点位移向量得到q点的坐标,由q点坐标算出潜在的遮挡点r点的位置,之后在利用(位于相机空间,并且都是最终输出到屏幕的像素点)P点的位置和P点的法向量,r点的位置来计算遮挡值,计算公式如下:
最终渲染得到一张灰度图,像素值代表了接受环境光的程序AmbientAcess,越白色代表接受越多环境光,反之越少环境光被接受。
第三步,RenderSSAOMapBlurPass(高斯模糊处理,在这里是降噪处理)
原理看技术博客:D3D11教程二十六之Blur(模糊处理)
这一步进行横向和纵向模糊,让纹理噪点大大减少
HorizontalBlur(水平模糊)
SSAOHorizontalBlurShader.fx
Texture2D SSAOMap:register(t0); //纹理资源
Texture2D NormalDepthMap:register(t1); //纹理资源
SamplerState SampleType:register(s0); //采样方式
//VertexShader
cbuffer CBMatrix:register(b0)
{
matrix World;
matrix View;
matrix Proj;
matrix WorldInvTranspose;
};
cbuffer CBTexel:register(b1)
{
float mTexelWidth;
float3 pad;
};
struct VertexIn
{
float3 Pos:POSITION;
float2 Tex:TEXCOORD0; //多重纹理可以用其它数字
float3 Normal:NORMAL; //这并非法向量,虽然没用到法向量
};
struct VertexOut
{
float4 Pos:SV_POSITION;
float2 Tex:TEXCOORD0;
};
VertexOut VS(VertexIn ina)
{
VertexOut outa;
//直接从局部空间变到NDC空间
outa.Pos = float4(ina.Pos, 1.0f);
outa.Tex= ina.Tex;
return outa;
}
float4 PS(VertexOut outa) : SV_Target
{
//权重数组
float weight[11] =
{
0.05f, 0.05f, 0.1f, 0.1f, 0.1f, 0.2f, 0.1f, 0.1f, 0.1f, 0.05f, 0.05f
};
float2 texOffset = float2(mTexelWidth,0.0f);
//中心纹理像素的颜色
float4 color= weight[5]* SSAOMap.SampleLevel(SampleType, outa.Tex, 0.0);
float4 centerNormalDepth = NormalDepthMap.SampleLevel(SampleType, outa.Tex, 0.0);;
float totalWeight = weight[5];
for (float i = -5.0f; i <= 5.0f; ++i)
{
if (i == 0)
continue;
float2 tex = outa.Tex + i*texOffset;
//如果中心纹理像素与邻接像素的的(Normal或者Depth)值相差过大,则丢弃邻接纹理像素不进行计算
float4 neighborNormalDepth= NormalDepthMap.SampleLevel(SampleType, tex, 0.0);;
if (dot(centerNormalDepth.xyz, neighborNormalDepth.xyz) >= 0.8f&&abs(centerNormalDepth.w - neighborNormalDepth.w) <= 0.2f)
{
float w = weight[i + 5];
//增加纹理颜色和权重
color+= w*SSAOMap.SampleLevel(SampleType, tex, 0.0);
totalWeight+=w;
}
}
color /= totalWeight;
return color;
}
VerticalBlur(垂直模糊)
SSAOVerticalBlurShader.fx
Texture2D SSAOMap:register(t0); //纹理资源
Texture2D NormalDepthMap:register(t1); //纹理资源
SamplerState SampleType:register(s0); //采样方式
//VertexShader
cbuffer CBMatrix:register(b0)
{
matrix World;
matrix View;
matrix Proj;
matrix WorldInvTranspose;
};
cbuffer CBTexel:register(b1)
{
float mTexelHeight;
float3 pad;
};
struct VertexIn
{
float3 Pos:POSITION;
float2 Tex:TEXCOORD0; //多重纹理可以用其它数字
float3 Normal:NORMAL; //这并非法向量,虽然没用到法向量
};
struct VertexOut
{
float4 Pos:SV_POSITION;
float2 Tex:TEXCOORD0;
};
VertexOut VS(VertexIn ina)
{
VertexOut outa;
//直接从局部空间变到NDC空间
outa.Pos = float4(ina.Pos, 1.0f);
outa.Tex= ina.Tex;
return outa;
}
float4 PS(VertexOut outa) : SV_Target
{
//权重数组
float weight[11] =
{
0.05f, 0.05f, 0.1f, 0.1f, 0.1f, 0.2f, 0.1f, 0.1f, 0.1f, 0.05f, 0.05f
};
float2 texOffset = float2(0.0f,mTexelHeight);
//中心纹理像素的颜色
float4 color = weight[5] * SSAOMap.SampleLevel(SampleType, outa.Tex, 0.0);
float4 centerNormalDepth = NormalDepthMap.SampleLevel(SampleType, outa.Tex, 0.0);;
float totalWeight = weight[5];
for (int i = -5.0f; i <= 5.0f; ++i)
{
if (i == 0)
continue;
float2 tex = outa.Tex + i*texOffset;
//如果中心纹理像素与邻接像素的的(Normal或者Depth)值相差过大,则丢弃邻接纹理像素不进行计算
float4 neighborNormalDepth = NormalDepthMap.SampleLevel(SampleType, tex, 0.0);
if (dot(centerNormalDepth.xyz, neighborNormalDepth.xyz) >= 0.8f&&abs(centerNormalDepth.w - neighborNormalDepth.w) <= 0.2f)
{
float w = weight[i + 5];
//增加纹理颜色和权重
color += w*SSAOMap.SampleLevel(SampleType, tex, 0.0);
totalWeight+= w;
}
}
color /= totalWeight;
return color;
}
渲染得到一张很少噪点的灰度图:
第四步,RenderSkull
这一步利用对第三步得到的SSAOBlurRT采样得到像素值作为AmbientAcess,然后利用公式AmbientColor=AmbientAcess*AmbientLigfht*AmbientMaterial获取每个像素接受的环境光。
Texture2D ShaderTexture:register(t0); //纹理资源
SamplerState SampleType:register(s0); //采样方式
//VertexShader
cbuffer CBMatrix:register(b0)
{
matrix World;
matrix View;
matrix Proj;
matrix WorldInvTranspose;
};
//这里DiffuseLight=SpecularLight,即大小颜色和方向都一样
cbuffer CBLight:register(b1)
{
float4 AmbientLight;
float4 DiffuseLight;
float3 LightDirection;
float pad1;
}
cbuffer CBMaterial:register(b2)
{
float3 AmbientMaterial;
float pad2;
float3 DiffuseMaterial;
float pad3;
float3 SpecularMaterial;
float SpecularPower; //镜面指数
};
cbuffer CBCamera:register(b3)
{
float3 CameraPos;
float pad4;
};
struct VertexIn
{
float3 Pos:POSITION;
float2 Tex:TEXCOORD0; //多重纹理可以用其它数字
float3 Normal:NORMAL;
};
struct VertexOut
{
float4 Pos:SV_POSITION;
float2 Tex:TEXCOORD0;
float3 W_Normal:NORMAL; //世界空间的法线
float4 H_Pos:POSITION; //顶点在世界空间的位置
float3 W_Pos:NORMAL1; //顶点在投影空间的店
};
VertexOut VS(VertexIn ina)
{
VertexOut outa;
outa.Pos = mul(float4(ina.Pos,1.0f), World);
//顶点位于世界空间的位置
outa.W_Pos = outa.Pos.xyz;
outa.Pos = mul(outa.Pos, View);
outa.Pos = mul(outa.Pos, Proj);
//投影坐标
outa.H_Pos = outa.Pos;
//将法线向量变换到世界空间
outa.W_Normal = mul(ina.Normal, (float3x3)WorldInvTranspose); //此事世界逆转置矩阵的第四行本来就没啥用
outa.W_Normal = normalize(outa.W_Normal);
outa.Tex= ina.Tex;
return outa;
}
float4 PS(VertexOut outa) : SV_Target
{
float DiffuseFactor;
float SpecularFactor;
float4 Ambient;
float4 Diffuse;
float4 Specular;
float4 color;
//求出投影纹理坐标
float2 Tex = float2(outa.H_Pos.x / (2.0f*outa.H_Pos.w) + 0.5f, outa.H_Pos.y / (-2.0f*outa.H_Pos.w) + 0.5f);
float4 AmbientMapColor= ShaderTexture.Sample(SampleType, Tex);
//初始化Ambient,Diffuse,Specular
Ambient = float4(0.0f, 0.0f, 0.0f, 1.0f);
Diffuse = float4(0.0f, 0.0f, 0.0f, 1.0f);
Specular = float4(0.0f, 0.0f, 0.0f, 1.0f);
/*第一,求出环境光颜色*/
Ambient = AmbientLight*float4(AmbientMaterial, 1.0f);
/*第二,求出漫反射颜色*/
//规格化灯光方向
float3 NormalLightDir = normalize(LightDirection);
//求出漫反射因子
float3 InvLightDir = -NormalLightDir;
DiffuseFactor = saturate(dot(InvLightDir,outa.W_Normal));
//求出漫反射颜色
if (DiffuseFactor>0)
{
Diffuse = DiffuseFactor*DiffuseLight*float4(DiffuseMaterial, 1.0f);
}
/*第三,求出漫镜面光颜色*/
float3 ReflectLightDir = reflect(NormalLightDir, outa.W_Normal);
//规格化反射光方向
ReflectLightDir= normalize(ReflectLightDir);
//求出反射点到相机之间的向量
float3 PixelToCameraPosVector = CameraPos - outa.W_Pos;
//规格化PixelToCameraPosVector
PixelToCameraPosVector= normalize(PixelToCameraPosVector);
//求出镜面因子
SpecularFactor = pow(saturate(dot(PixelToCameraPosVector, ReflectLightDir)), SpecularPower);
//求出镜面光颜色
if (SpecularFactor > 0)
{
Specular = SpecularFactor*DiffuseLight*float4(SpecularMaterial, 1.0f);
}
//三种光加一起,注意调节AmbientColor值实现环境遮挡的效果
color = saturate(Diffuse + Ambient*AmbientMapColor +Specular);
return color;
}
这里特别注意因为我们是SSAO,也就是屏幕空间的环境遮挡,强调的是屏幕空间,也就是我们利用的最终输出到屏幕空间的像素(输出到屏幕的最后一层像素,DefferedRender也是这样的,SSAO在延迟渲染下是非常合适的,毕竟都是屏幕空间),我们对那张代表了AmbientAcess采样的纹理坐标是顶点从齐次裁剪空间变换到投影纹理空间的纹理坐标,原理见:D3D11教程三十之ProjectiveTexturing(投影纹理).
实际上,上面好多细节我没说,因为所有都说出来,得扯半天,不过SSAO技术的实现确实很大程序考验了我们对3D渲染管线,对RT,对投影纹理技术等等的掌握程序。
最终渲染得到的:
集成了SSAO的3D渲染引擎源码
https://github.com/2047241149/SDEngine
(DX11版本的SSAO)源代码链接:
http://download.csdn.net/download/qq_29523119/10047432
(OpenGL版本的SSAO)源代码链接:
http://download.csdn.net/download/qq_29523119/10031792