光影的魔法!Cocos Creator 实现屏幕空间的环境光遮蔽(SSAO)
引言:
本文作者 alpha 從事游戲前端開(kāi)發(fā)已經(jīng)5年,畢業(yè)后他先是入職了騰訊無(wú)線大連研發(fā)中心,而后開(kāi)啟了北漂生涯,在北京的這3年一直都在使用 Cocos Creator,對(duì)前端業(yè)務(wù),包體、內(nèi)存優(yōu)化有很多的實(shí)踐經(jīng)驗(yàn)。最近 alpha 在學(xué)習(xí)計(jì)算機(jī)圖形學(xué)相關(guān)技術(shù),今天他將同大家分享 Cocos Creator 3.3 實(shí)現(xiàn)屏幕空間的環(huán)境光遮蔽(SSAO)的技術(shù)經(jīng)驗(yàn)。
什么是 AO ?
環(huán)境光(Ambient Lighting)是場(chǎng)景總體光照中的一個(gè)固定光照常量,用來(lái)模擬光的散射(Scattering)。在現(xiàn)實(shí)中,光線會(huì)以任意方向散射,它的強(qiáng)度是會(huì)改變的。
其中一種間接光照的模擬叫做環(huán)境光遮蔽(Ambient Occlusion),它的原理是通過(guò)將褶皺、孔洞和非常靠近的墻面變暗的方法近似模擬出間接光照。這些區(qū)域很大程度上是被周?chē)膸缀误w遮擋的,所以這些地方看起來(lái)會(huì)更暗一些。
在2007年,Crytek 公司發(fā)布了一款叫做屏幕空間環(huán)境光遮蔽(Screen Space Ambient Occlusion,SSAO)的技術(shù),并用在了他們的看家作孤島危機(jī)上。這一技術(shù)使用了屏幕空間場(chǎng)景的深度而不是真實(shí)的幾何體數(shù)據(jù)來(lái)確定遮蔽量。這一做法相對(duì)于真正的環(huán)境光遮蔽(基于光線追蹤)不但速度快,而且還能獲得較好的效果,使得它成為近似實(shí)時(shí)環(huán)境光遮蔽的標(biāo)準(zhǔn)。
下面這幅圖展示了在使用和不使用 SSAO 時(shí)場(chǎng)景的不同。特別注意對(duì)比電話亭后面和墻角部分,你會(huì)發(fā)現(xiàn)(環(huán)境)光被遮蔽了許多:
?
雖然這個(gè)效果不是非常明顯,但是啟用 AO 確實(shí)給我們更真實(shí)的感覺(jué),這些小的遮蔽細(xì)節(jié)能讓整個(gè)場(chǎng)景看起來(lái)更有立體感。
SSAO 原理
SSAO 背后的原理很簡(jiǎn)單:對(duì)于屏幕上的每一個(gè)片段,會(huì)根據(jù)周邊深度值計(jì)算一個(gè)遮蔽因子(Occlusion Factor)。這個(gè)遮蔽因子之后會(huì)被用來(lái)決定片段的環(huán)境光分量。遮蔽因子是通過(guò)采集片段周?chē)蛐秃诵?Kernel)的多個(gè)深度樣本,并和當(dāng)前片段深度值對(duì)比而得到的。高于片段深度值樣本的個(gè)數(shù)就是我們想要的遮蔽因子。
?
上圖中在幾何體內(nèi)灰色的深度樣本都是高于片段深度值的,他們會(huì)增加遮蔽因子;幾何體內(nèi)樣本個(gè)數(shù)越多,片段獲得的環(huán)境光照也就越少。
很明顯,渲染效果的質(zhì)量和精度與采樣的樣本數(shù)量有直接關(guān)系。如果樣本數(shù)量太低,渲染的精度會(huì)急劇減少,會(huì)得到一種叫做波紋(Banding)的效果;如果它太高了,會(huì)影響性能。通過(guò)引入隨機(jī)性到采樣核心(Sample Kernel)從而減少樣本的數(shù)目。通過(guò)隨機(jī)旋轉(zhuǎn)采樣核心,能在有限樣本數(shù)量中得到高質(zhì)量的結(jié)果。然而隨機(jī)性引入了一個(gè)很明顯的噪聲圖案,需要通過(guò)模糊降噪來(lái)修復(fù)這一問(wèn)題。下面這幅圖片展示了波紋效果還有隨機(jī)性造成的效果:
?
可以看到,盡管在低樣本數(shù)的情況下得到了很明顯的波紋效果,引入隨機(jī)性之后這些波紋效果就完全消失了。最初 Crytek 的實(shí)現(xiàn)是用一個(gè)深度緩沖做為輸入,但是這種方式存在一些問(wèn)題(如自遮閉, 光環(huán)),由于這個(gè)原因,現(xiàn)在通常不會(huì)使用球體的采樣核心,而是使用一個(gè)沿著表面法向量的半球體采樣核心。
?
通過(guò)在法向半球體(Normal Oriented Hemisphere)周?chē)蓸?#xff0c;將不會(huì)考慮到片段背面的幾何體,它消除了環(huán)境光遮蔽灰蒙蒙的感覺(jué),從而產(chǎn)生更真實(shí)的結(jié)果。
SSAO 特點(diǎn):
獨(dú)立于場(chǎng)景復(fù)雜性,僅和投影后最終的像素有關(guān),和場(chǎng)景中的頂點(diǎn)數(shù)三角數(shù)無(wú)關(guān)。
跟傳統(tǒng)的 AO 處理方法相比,不需要預(yù)處理,無(wú)需加載時(shí)間,也無(wú)需系統(tǒng)內(nèi)存中的內(nèi)存分配,所以更加適用于動(dòng)態(tài)場(chǎng)景。
對(duì)屏幕上的每個(gè)像素以相同的一致方式工作。
沒(méi)有 CPU 使用 - 它可以在 GPU 上完全執(zhí)行。
可以輕松集成到任何現(xiàn)代圖形管線中。
在了解了 AO & SSAO 之后,我們來(lái)看看要怎么基于 Cocos Creator 3.3.1 實(shí)現(xiàn) SSAO。
Demo 地址:
https://gitee.com/yanjifa/cc-ssao-demo
樣本緩沖
SSAO 需要幾何體的信息來(lái)確定一個(gè)片段的遮蔽因子,對(duì)于每個(gè)片段(像素),需要如下數(shù)據(jù):
逐片段位置向量
逐片段法線向量
逐片段反射顏色
采樣核心
用來(lái)旋轉(zhuǎn)采樣核心的隨機(jī)旋轉(zhuǎn)向量
通過(guò)使用一個(gè)逐片段觀察空間位置,可以將一個(gè)采樣半球核心對(duì)準(zhǔn)片段的觀察空間表面法線。對(duì)于每一個(gè)核心樣本會(huì)采樣線性深度紋理來(lái)比較結(jié)果。采樣核心會(huì)根據(jù)旋轉(zhuǎn)矢量稍微偏轉(zhuǎn)一點(diǎn);所獲得的遮蔽因子將會(huì)之后用來(lái)限制最終的環(huán)境光照分量。
?
通過(guò)以上發(fā)現(xiàn) SSAO 所需的數(shù)據(jù)不正是延遲管線的 G-buffer,關(guān)于 G-buffer 是什么可通過(guò)文章「延遲著色法」[1]做一個(gè)簡(jiǎn)單的了解。閱讀引擎代碼 editor/assets/chunks/standard-surface-entry-entry.chunk 和 cocos/core/pipeline/define.ts :
// editor/assets/chunks/standard-surface-entry-entry.chunk 33 行附近
#elif CC_PIPELINE_TYPE == CC_PIPELINE_TYPE_DEFERRED
layout(location = 0) out vec4 fragColor0;
layout(location = 1) out vec4 fragColor1;
layout(location = 2) out vec4 fragColor2;
layout(location = 3) out vec4 fragColor3;
void main () {
StandardSurface s; surf(s);
fragColor0 = s.albedo;? ?? ?? ?? ?? ?? ?? ?? ? // 漫反射顏色 -> 反照率紋理
fragColor1 = vec4(s.position, s.roughness);? ? // 位置 -> 世界空間位置
fragColor2 = vec4(s.normal, s.metallic);? ?? ? // 法線 -> 世界空間法線
fragColor3 = vec4(s.emissive, s.occlusion);? ? // 和本文無(wú)關(guān), 不做介紹
}
#endif
// cocos/core/pipeline/define.ts??117 行 附近
export enum PipelineGlobalBindings {
UBO_GLOBAL,
UBO_CAMERA,
UBO_SHADOW,
SAMPLER_SHADOWMAP,
SAMPLER_ENVIRONMENT,
SAMPLER_SPOT_LIGHTING_MAP,
SAMPLER_GBUFFER_ALBEDOMAP,? ?// 6
SAMPLER_GBUFFER_POSITIONMAP, // 7
SAMPLER_GBUFFER_NORMALMAP,? ?// 8
SAMPLER_GBUFFER_EMISSIVEMAP,
SAMPLER_LIGHTING_RESULTMAP,
COUNT,
}
// cocos/core/pipeline/define.ts??283 行 附近
const UNIFORM_GBUFFER_ALBEDOMAP_NAME = 'cc_gbuffer_albedoMap';
export const UNIFORM_GBUFFER_ALBEDOMAP_BINDING = PipelineGlobalBindings.SAMPLER_GBUFFER_ALBEDOMAP; // 6
// ...
const UNIFORM_GBUFFER_POSITIONMAP_NAME = 'cc_gbuffer_positionMap';
export const UNIFORM_GBUFFER_POSITIONMAP_BINDING = PipelineGlobalBindings.SAMPLER_GBUFFER_POSITIONMAP; // 7
// ...
const UNIFORM_GBUFFER_NORMALMAP_NAME = 'cc_gbuffer_normalMap';
export const UNIFORM_GBUFFER_NORMALMAP_BINDING = PipelineGlobalBindings.SAMPLER_GBUFFER_NORMALMAP; // 8
// ...
通過(guò)以上代碼可以分析出引擎 G-buffer 的數(shù)據(jù)布局,和具體 G-buffer 數(shù)據(jù)內(nèi)容,深度值后面將會(huì)使用 G-buffer 計(jì)算得出。
自定義渲染管線
通過(guò)擴(kuò)展延遲渲染管線的方式,在內(nèi)置渲染管線的 LightFlow 上增加 一個(gè) SsaoStage 用來(lái)生成 AO 紋理。首先創(chuàng)建一個(gè)渲染管線資源,資源管理器右鍵->創(chuàng)建->Render Pipeine->Render Pipeline Asset,命名為 ssao-deferrd-pipeline,創(chuàng)建 ssao-material | ssao-effect 著色器用來(lái)計(jì)算 AO 紋理,完整文件如下:
.
├── ssao-constant.chunk? ?? ?? ?? ?// UBO 描述
├── ssao-deferred-pipeline.rpp? ???// 管線資源文件
├── ssao-effect.effect? ?? ?? ?? ? // ssao shader
├── ssao-lighting.effect? ?? ?? ???// 光照 shader, 直接拷貝內(nèi)置 internal/effects/pipeline/defferrd-lighting
├── ssao-lighting.mtl
├── ssao-material.mtl
├── ssao-render-pipeline.ts? ?? ???// 定制管線腳本
├── ssao-stage.ts? ?? ?? ?? ?? ?? ?// stage 腳本
└── uboDefine.ts? ?? ?? ?? ?? ?? ? // Uniform Buffer Object 定義腳本
對(duì)應(yīng)管線配置如下,在 LightingFlow 下 Stages 最前面加入 SsaoStage,二手手游交易并指定對(duì)應(yīng)的材質(zhì),可以看到,引擎現(xiàn)在其實(shí)已經(jīng)支持后處理(PostProcess)了,只要指定材質(zhì)就可以了,可能當(dāng)前版本還不完善,所以引擎組還沒(méi)公開(kāi),其實(shí) SSAO 也可以算是一種后處理效果,管線資源的屬性設(shè)置如下:
自定義管線腳本如下:
// uboDefine.ts
import { gfx, pipeline } from "cc";
const { DescriptorSetLayoutBinding, UniformSamplerTexture, DescriptorType, ShaderStageFlagBit, Type } = gfx;
const { SetIndex, PipelineGlobalBindings, globalDescriptorSetLayout } = pipeline;
let GlobalBindingStart = PipelineGlobalBindings.COUNT; // 11
let GlobalBindingIndex = 0;
/**
* 定義 SSAO Frame Buffer, 布局描述
*/
const UNIFORM_SSAOMAP_NAME = 'cc_ssaoMap';
export const UNIFORM_SSAOMAP_BINDING = GlobalBindingStart + GlobalBindingIndex++; // 11
const UNIFORM_SSAOMAP_DESCRIPTOR = new DescriptorSetLayoutBinding(UNIFORM_SSAOMAP_BINDING, DescriptorType.SAMPLER_TEXTURE, 1, ShaderStageFlagBit.FRAGMENT);
const UNIFORM_SSAOMAP_LAYOUT = new UniformSamplerTexture(SetIndex.GLOBAL, UNIFORM_SSAOMAP_BINDING, UNIFORM_SSAOMAP_NAME, Type.SAMPLER2D, 1);
globalDescriptorSetLayout.layouts[UNIFORM_SSAOMAP_NAME] = UNIFORM_SSAOMAP_LAYOUT;
globalDescriptorSetLayout.bindings[UNIFORM_SSAOMAP_BINDING] = UNIFORM_SSAOMAP_DESCRIPTOR;
/**
* 采樣核心、相機(jī)遠(yuǎn)近裁剪面 near & far 等 UniformBlock 布局描述
*/
export class UBOSsao {
public static readonly SAMPLES_SIZE = 64; // 最大采樣核心
public static readonly CAMERA_NEAR_FAR_LINEAR_INFO_OFFSET = 0;
public static readonly SSAO_SAMPLES_OFFSET = UBOSsao.CAMERA_NEAR_FAR_LINEAR_INFO_OFFSET + 4;
public static readonly COUNT = (UBOSsao.SAMPLES_SIZE + 1) * 4;
public static readonly SIZE = UBOSsao.COUNT * 4;
public static readonly NAME = 'CCSsao';
public static readonly BINDING = GlobalBindingStart + GlobalBindingIndex++; // 12
public static readonly DESCRIPTOR = new gfx.DescriptorSetLayoutBinding(UBOSsao.BINDING, gfx.DescriptorType.UNIFORM_BUFFER, 1, gfx.ShaderStageFlagBit.ALL);
public static readonly LAYOUT = new gfx.UniformBlock(SetIndex.GLOBAL, UBOSsao.BINDING, UBOSsao.NAME, [
new gfx.Uniform('cc_cameraNFLSInfo', gfx.Type.FLOAT4, 1), // vec4
new gfx.Uniform('ssao_samples', gfx.Type.FLOAT4, UBOSsao.SAMPLES_SIZE), // vec4[64]
], 1);
}
globalDescriptorSetLayout.layouts[UBOSsao.NAME] = UBOSsao.LAYOUT;
globalDescriptorSetLayout.bindings[UBOSsao.BINDING] = UBOSsao.DESCRIPTOR;
/**
*??ssao-render-pipeline.ts
*??擴(kuò)展延遲渲染管線
*/
import { _decorator, DeferredPipeline, gfx, renderer } from "cc";
import { UNIFORM_SSAOMAP_BINDING } from "./uboDefine";
const { ccclass } = _decorator;
const _samplerInfo = [
gfx.Filter.POINT,
gfx.Filter.POINT,
gfx.Filter.NONE,
gfx.Address.CLAMP,
gfx.Address.CLAMP,
gfx.Address.CLAMP,
];
const samplerHash = renderer.genSamplerHash(_samplerInfo);
export class SsaoRenderData {
frameBuffer?: gfx.Framebuffer | null;
renderTargets?: gfx.Texture[] | null;
depthTex?: gfx.Texture | null;
}
@ccclass("SsaoRenderPipeline")
export class SsaoRenderPipeline extends DeferredPipeline {
private _width = 0;
private _height = 0;
private _ssaoRenderData: SsaoRenderData | null = null!;
private _ssaoRenderPass: gfx.RenderPass | null = null;
public activate(): boolean {
const result = super.activate();
this._width = this.device.width;
this._height = this.device.height;
this._generateSsaoRenderData();
return result;
}
public resize(width: number, height: number) {
if (this._width === width && this._height === height) {
return;
}
super.resize(width, height);
this._width = width;
this._height = height;
this._destroyRenderData();
this._generateSsaoRenderData();
}
public getSsaoRenderData(camera: renderer.scene.Camera): SsaoRenderData {
if (!this._ssaoRenderData) {
this._generateSsaoRenderData();
}
return this._ssaoRenderData!;
}
/**
* 核心代碼, 創(chuàng)建一個(gè) FrameBuffer 存儲(chǔ) SSAO 紋理
*/
private _generateSsaoRenderData() {
if (!this._ssaoRenderPass) {
const colorAttachment = new gfx.ColorAttachment();
colorAttachment.format = gfx.Format.RGBA8;
colorAttachment.loadOp = gfx.LoadOp.CLEAR;
colorAttachment.storeOp = gfx.StoreOp.STORE;
colorAttachment.endAccesses = [gfx.AccessType.COLOR_ATTACHMENT_WRITE];
const depthStencilAttachment = new gfx.DepthStencilAttachment();
depthStencilAttachment.format = this.device.depthStencilFormat;
depthStencilAttachment.depthLoadOp = gfx.LoadOp.CLEAR;
depthStencilAttachment.depthStoreOp = gfx.StoreOp.STORE;
depthStencilAttachment.stencilLoadOp = gfx.LoadOp.CLEAR;
depthStencilAttachment.stencilStoreOp = gfx.StoreOp.STORE;
const renderPassInfo = new gfx.RenderPassInfo([colorAttachment], depthStencilAttachment);
this._ssaoRenderPass = this.device.createRenderPass(renderPassInfo);
}
this._ssaoRenderData = new SsaoRenderData();
this._ssaoRenderData.renderTargets = [];
// 因?yàn)?SSAO 紋理最終是一張灰度圖, 所以使用 Format.R8 單通道紋理, 減少內(nèi)存占用, 使用時(shí)只需要讀取 R 通道即可
this._ssaoRenderData.renderTargets.push(this.device.createTexture(new gfx.TextureInfo(
gfx.TextureType.TEX2D,
gfx.TextureUsageBit.COLOR_ATTACHMENT | gfx.TextureUsageBit.SAMPLED,
gfx.Format.R8,
this._width,
this._height,
)));
this._ssaoRenderData.depthTex = this.device.createTexture(new gfx.TextureInfo(
gfx.TextureType.TEX2D,
gfx.TextureUsageBit.DEPTH_STENCIL_ATTACHMENT,
this.device.depthStencilFormat,
this._width,
this._height,
));
this._ssaoRenderData.frameBuffer = this.device.createFramebuffer(new gfx.FramebufferInfo(
this._ssaoRenderPass!,
this._ssaoRenderData.renderTargets,
this._ssaoRenderData.depthTex,
));
this.descriptorSet.bindTexture(UNIFORM_SSAOMAP_BINDING, this._ssaoRenderData.frameBuffer.colorTextures[0]!);
const sampler = renderer.samplerLib.getSampler(this.device, samplerHash);
this.descriptorSet.bindSampler(UNIFORM_SSAOMAP_BINDING, sampler);
}
public destroy(): boolean {
this._destroyRenderData();
return super.destroy();
}
private _destroyRenderData() {
if (!this._ssaoRenderData) {
return;
}
if (this._ssaoRenderData.depthTex) {
this._ssaoRenderData.depthTex.destroy();
}
if (this._ssaoRenderData.renderTargets) {
this._ssaoRenderData.renderTargets.forEach((o) => {
o.destroy();
})
}
if (this._ssaoRenderData.frameBuffer) {
this._ssaoRenderData.frameBuffer.destroy();
}
this._ssaoRenderData = null;
}
}
通過(guò)項(xiàng)目設(shè)置修改渲染管線為自定義的 SSAO 管線:
?
采樣核心
我們需要沿著表面法線方向生成大量的樣本。就像前面介紹的那樣,想要生成形成半球形的樣本。由于對(duì)每個(gè)表面法線方向生成采樣核心非常困難,也不合實(shí)際,所以將在切線空間(Tangent Space)內(nèi)生成采樣核心,法向量將指向正 z 方向。
?
假設(shè)有一個(gè)單位半球,生成一個(gè)擁有最大64樣本值的采樣核心:
// ssao-stage.ts
activate(pipeline: DeferredPipeline, flow: RenderFlow) {
super.activate(pipeline, flow);
const device = pipeline.device;
this._sampleBuffer = device.createBuffer(new gfx.BufferInfo(
gfx.BufferUsageBit.UNIFORM | gfx.BufferUsageBit.TRANSFER_DST,
gfx.MemoryUsageBit.HOST | gfx.MemoryUsageBit.DEVICE,
UBOSsao.SIZE,
UBOSsao.SIZE,
));
this._sampleBufferData = new Float32Array(UBOSsao.COUNT);
const sampleOffset = UBOSsao.SSAO_SAMPLES_OFFSET / 4;
// 64 樣本值采樣核心, 這里寫(xiě)的不太詳細(xì), 可結(jié)合 LearnOpenGL CN 的教程, 加深理解
for (let i = 0; i < UBOSsao.SAMPLES_SIZE; i++) {
let sample = new Vec3(
Math.random() * 2.0 - 1.0,
Math.random() * 2.0 - 1.0,
Math.random() + 0.01, // 這里和原教程有點(diǎn)區(qū)別, Z 稍微增加一個(gè)很小的值, 可改善平面波紋(Banding)的效果, 可能會(huì)對(duì)精度造成影響
);
sample = sample.normalize();
let scale = i / UBOSsao.SAMPLES_SIZE;
// 通過(guò)插值, 將核心樣本靠近原點(diǎn)分布
scale = lerp(0.1, 1.0, scale * scale);
sample.multiplyScalar(scale);
const index = 4 * (i + sampleOffset);
this._sampleBufferData[index + 0] = sample.x;
this._sampleBufferData[index + 1] = sample.y;
this._sampleBufferData[index + 2] = sample.z;
}
this._pipeline.descriptorSet.bindBuffer(UBOSsao.BINDING, this._sampleBuffer);
}
我們?cè)谇芯€空間中以-1.0到1.0為范圍變換 x 和 y 方向,并以 0.0 和 1.0 為范圍變換樣本的 z 方向 (如果以-1.0到1.0為范圍,取樣核心就變成球型了)。由于采樣核心將會(huì)沿著表面法線對(duì)齊,所得的樣本矢量將會(huì)在半球里。通過(guò)權(quán)重插值,得到一個(gè)大部分樣本靠近原點(diǎn)的核心分布。
?
獲取深度數(shù)據(jù)
通過(guò) G-buffer 中的 PostionMap 獲取線性深度值:
float getDepth(vec3 worldPos) {
// 轉(zhuǎn)到觀察空間
vec3 viewPos = (cc_matView * vec4(worldPos.xyz, 1.0)).xyz;
// cc_cameraNFLSInfo.y -> 相機(jī) Far, 通過(guò) ssao-stage.ts 腳本更新
float depth = -viewPos.z / cc_cameraNFLSInfo.y;
return depth;
}
深度圖如下:
?
SSAO 著色器
/**
* ssao-effect.effect
*/
CCProgram ssao-fs %{
precision highp float;
#include <cc-global>
#include <cc-shadow-map-base>
#include <ssao-constant>
// 最大 64
#define SSAO_SAMPLES_SIZE 64
in vec2 v_uv;
#pragma builtin(global)
layout (set = 0, binding = 7) uniform sampler2D cc_gbuffer_positionMap;
#pragma builtin(global)
layout (set = 0, binding = 8) uniform sampler2D cc_gbuffer_normalMap;
layout(location = 0) out vec4 fragColor;
// 隨機(jī)數(shù) 0.0 - 1.0
float rand(vec2 uv, float dx, float dy)
{
uv += vec2(dx, dy);
return fract(sin(dot(uv,??vec2(12.9898, 78.233))) * 43758.5453);
}
// 隨機(jī)旋轉(zhuǎn)采樣核心向量
vec3 getRandomVec(vec2 uv){
return vec3(
rand(uv, 0.0, 1.0) * 2.0 - 1.0,
rand(uv, 1.0, 0.0) * 2.0 - 1.0,
0.0
);
}
// 獲取線性深度
float getDepth(vec3 worldPos) {
vec3 viewPos = (cc_matView * vec4(worldPos.xyz, 1.0)).xyz;
float depth = -viewPos.z / cc_cameraNFLSInfo.y;
return depth;
}
// 深度圖
// void main () {
//? ?vec3 worldPos = texture(cc_gbuffer_positionMap, v_uv).xyz;
//? ?fragColor = vec4(getDepth(worldPos));
// }
void main () {
vec3 worldPos = texture(cc_gbuffer_positionMap, v_uv).xyz;
vec3 normal = texture(cc_gbuffer_normalMap, v_uv).xyz;
vec3 randomVec = getRandomVec(v_uv);
float fragDepth = -getDepth(worldPos);
// 創(chuàng)建一個(gè)TBN矩陣,將向量從切線空間變換到觀察空間
vec3 tangent = normalize(randomVec - normal * dot(randomVec, normal));
vec3 bitangent = cross(normal, tangent);
mat3 TBN = mat3(tangent, bitangent, normal);
// 取樣半徑
float radius = 1.0;
float occlusion = 0.0;
for(int i = 0; i < SSAO_SAMPLES_SIZE; ++i)
{
vec3 ssaoSample = TBN * ssao_samples.xyz;
ssaoSample = worldPos + ssaoSample * radius;
float aoDepth = -getDepth(ssaoSample);
vec4 offset = vec4(ssaoSample, 1.0);
offset? ?? ?= (cc_matProj * cc_matView) * offset;? ?// 轉(zhuǎn)換到裁剪空間
offset.xyz /= offset.w;? ?? ?? ?? ?? ?? ?? ?? ?? ???// 透視除法
offset.xyz??= offset.xyz * 0.5 + 0.5;? ?? ?? ?? ?? ?// 從 NDC (標(biāo)準(zhǔn)化設(shè)備坐標(biāo), -1.0 - 1.0) 變換到 0.0 - 1.0
vec3 samplePos = texture(cc_gbuffer_positionMap, offset.xy).xyz;
float sampleDepth = -getDepth(samplePos);
// 范圍檢查
float rangeCheck = smoothstep(0.0, 1.0, radius / abs(fragDepth - sampleDepth));
// 檢查樣本的當(dāng)前深度值是否大于存儲(chǔ)的深度值,如果是,添加到最終的貢獻(xiàn)因子上
occlusion += (sampleDepth >= aoDepth ? 1.0 : 0.0) * rangeCheck;
}
// 將遮蔽貢獻(xiàn)根據(jù)核心的大小標(biāo)準(zhǔn)化,并輸出結(jié)果
occlusion = 1.0 - (occlusion / float(SSAO_SAMPLES_SIZE));
fragColor = vec4(occlusion, 1.0, 1.0, 1.0);
}
}%
下圖展示了環(huán)境遮蔽著色器產(chǎn)生的紋理:
?
可見(jiàn),環(huán)境遮蔽產(chǎn)生了非常強(qiáng)烈的深度感。僅僅通過(guò)環(huán)境遮蔽紋理就已經(jīng)能清晰地看見(jiàn)模型一定躺在地板上而不是浮在空中。
現(xiàn)在的效果仍然看起來(lái)不是很完美,不連續(xù)的噪點(diǎn)清晰可見(jiàn),為了創(chuàng)建一個(gè)光滑的環(huán)境遮蔽結(jié)果,需要模糊環(huán)境遮蔽紋理進(jìn)行降噪。
?
應(yīng)用 SSAO 紋理
最后將 SSAO 紋理進(jìn)行模糊降噪,并逐片段將環(huán)境遮蔽因子乘到環(huán)境光照分量上,拷貝內(nèi)置光照著色器(internal/effects/pipeline/deferred-lighting.effect)命名為 ssao-lighting.effect。
/**
* 本文改動(dòng)部分添加了中文注釋
*/
CCProgram lighting-fs %{
precision highp float;
#include <cc-global>
#include <shading-standard-base>
#include <shading-standard-additive>
#include <output-standard>
#include <cc-fog-base>
in vec2 v_uv;
#pragma builtin(global)
layout (set = 0, binding = 6) uniform sampler2D cc_gbuffer_albedoMap;
#pragma builtin(global)
layout (set = 0, binding = 7) uniform sampler2D cc_gbuffer_positionMap;
#pragma builtin(global)
layout (set = 0, binding = 8) uniform sampler2D cc_gbuffer_normalMap;
#pragma builtin(global)
layout (set = 0, binding = 9) uniform sampler2D cc_gbuffer_emissiveMap;
#pragma builtin(global)
layout (set = 0, binding = 11) uniform sampler2D cc_ssaoMap;
layout(location = 0) out vec4 fragColor;
vec4 gaussianBlur(sampler2D Tex, vec2 UV, float Intensity)
{
// 省略, 詳見(jiàn) demo 工程
return texture(Tex, UV);
}
// 屏幕展示 SSAO 紋理
// void main() {
//? ?// 降噪
//? ?vec4 color = gaussianBlur(cc_ssaoMap, v_uv, 3.0);
//? ?// 不降噪
//? ?vec4 color = texture(cc_ssaoMap, v_uv);
//? ?fragColor = vec4(vec3(color.r), 1.0);
// }
void main () {
StandardSurface s;
vec4 albedoMap = texture(cc_gbuffer_albedoMap,v_uv);
vec4 positionMap = texture(cc_gbuffer_positionMap,v_uv);
vec4 normalMap = texture(cc_gbuffer_normalMap,v_uv);
vec4 emissiveMap = texture(cc_gbuffer_emissiveMap,v_uv);
// ssao 環(huán)境遮蔽因子, 單通道紋理, 所以只取 R 通道
vec4 ssaoMap = vec4(vec3(gaussianBlur(cc_ssaoMap, v_uv, 3.0).r), 1.0);
s.albedo = albedoMap * ssaoMap; // 乘到輻照率貼圖上, 應(yīng)用遮蔽紋理
s.position = positionMap.xyz;
s.roughness = positionMap.w;
s.normal = normalMap.xyz;
s.metallic = normalMap.w;
s.emissive = emissiveMap.xyz;
s.occlusion = emissiveMap.w;
// fixme: default value is 0, and give black result
float fogFactor;
CC_TRANSFER_FOG_BASE(vec4(s.position, 1), fogFactor);
vec4 shadowPos;
CC_TRANSFER_SHADOW_BASE(vec4(s.position, 1), shadowPos);
vec4 color = CCStandardShadingBase(s, shadowPos) +
CCStandardShadingAdditive(s, shadowPos);
CC_APPLY_FOG_BASE(color, fogFactor);
fragColor = CCFragOutput(color);
}
}%
最后來(lái)看下最終的渲染結(jié)果對(duì)比,首先是 SSAO 開(kāi)啟的效果:
?
SSAO 關(guān)閉的效果:
?
屏幕空間環(huán)境遮蔽是一個(gè)可高度自定義的效果,它的效果很大程度上依賴(lài)于我們根據(jù)場(chǎng)景類(lèi)型調(diào)整它的參數(shù)。對(duì)所有類(lèi)型的場(chǎng)景并不存在什么完美的參數(shù)組合方式。一些場(chǎng)景只在小半徑情況下工作,又有些場(chǎng)景會(huì)需要更大的半徑和更大的樣本數(shù)量才能看起來(lái)更真實(shí)。當(dāng)前這個(gè)演示用了64個(gè)樣本,屬于比較多的了,你可以調(diào)整核心大小和半徑從而獲得合適的效果。
已知問(wèn)題
編輯器攝像機(jī)預(yù)覽會(huì)渲染不正確。
資源管理器里面點(diǎn)擊自定義管線資源文件,編輯器控制臺(tái)會(huì)報(bào)錯(cuò),可能會(huì)導(dǎo)致編輯器無(wú)響應(yīng) (目前建議沒(méi)事別碰,碰過(guò)重啟編輯器可恢復(fù)正常)。
手機(jī)瀏覽器 (小米10 Pro) 下使用最大采樣核心 (64) 時(shí),幀數(shù)只有個(gè)位數(shù),可以確定當(dāng)前版本基本不能應(yīng)用到實(shí)際項(xiàng)目中,還需優(yōu)化。
Native 下自定義渲染管線同時(shí)還需要自定義 Engine-Native[2] 引擎,所以 Native 暫時(shí)還未支持,可參考 PR 3934[3] 添加對(duì) Native 的支持,這里要感謝 大表姐Kristine 提供的信息。
總結(jié)
以上是生活随笔為你收集整理的光影的魔法!Cocos Creator 实现屏幕空间的环境光遮蔽(SSAO)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: CEDEC 2021 | 让巨大化角色充
- 下一篇: 动效如何构成连接 篇肆