光影的魔法!Cocos Creator 實現螢幕空間的環境光遮蔽(SSAO)

alpha發表於2021-10-12
引言:

本文作者 alpha 從事遊戲前端開發已經5年,畢業後他先是入職了騰訊無線大連研發中心,而後開啟了北漂生涯,在北京的這3年一直都在使用 Cocos Creator,對前端業務,包體、記憶體優化有很多的實踐經驗。最近 alpha 在學習計算機圖形學相關技術,今天他將同大家分享 Cocos Creator 3.3 實現螢幕空間的環境光遮蔽(SSAO)的技術經驗。

什麼是 AO ?

環境光(Ambient Lighting)是場景總體光照中的一個固定光照常量,用來模擬光的散射(Scattering)。在現實中,光線會以任意方向散射,它的強度是會改變的。

其中一種間接光照的模擬叫做環境光遮蔽(Ambient Occlusion),它的原理是通過將褶皺、孔洞和非常靠近的牆面變暗的方法近似模擬出間接光照。這些區域很大程度上是被周圍的幾何體遮擋的,所以這些地方看起來會更暗一些。

在2007年,Crytek 公司釋出了一款叫做螢幕空間環境光遮蔽(Screen Space Ambient Occlusion,SSAO)的技術,並用在了他們的看家作孤島危機上。這一技術使用了螢幕空間場景的深度而不是真實的幾何體資料來確定遮蔽量。這一做法相對於真正的環境光遮蔽(基於光線追蹤)不但速度快,而且還能獲得較好的效果,使得它成為近似實時環境光遮蔽的標準。

下面這幅圖展示了在使用和不使用 SSAO 時場景的不同。特別注意對比電話亭後面和牆角部分,你會發現(環境)光被遮蔽了許多:

光影的魔法!Cocos Creator 實現螢幕空間的環境光遮蔽(SSAO)

雖然這個效果不是非常明顯,但是啟用 AO 確實給我們更真實的感覺,這些小的遮蔽細節能讓整個場景看起來更有立體感。

SSAO 原理

SSAO 背後的原理很簡單:對於螢幕上的每一個片段,會根據周邊深度值計算一個遮蔽因子(Occlusion Factor)。這個遮蔽因子之後會被用來決定片段的環境光分量。遮蔽因子是通過採集片段周圍球型核心(Kernel)的多個深度樣本,並和當前片段深度值對比而得到的。高於片段深度值樣本的個數就是我們想要的遮蔽因子。

光影的魔法!Cocos Creator 實現螢幕空間的環境光遮蔽(SSAO)

上圖中在幾何體內灰色的深度樣本都是高於片段深度值的,他們會增加遮蔽因子;幾何體內樣本個數越多,片段獲得的環境光照也就越少。

很明顯,渲染效果的質量和精度與取樣的樣本數量有直接關係。如果樣本數量太低,渲染的精度會急劇減少,會得到一種叫做波紋(Banding)的效果;如果它太高了,會影響效能。通過引入隨機性到取樣核心(Sample Kernel)從而減少樣本的數目。通過隨機旋轉取樣核心,能在有限樣本數量中得到高質量的結果。然而隨機性引入了一個很明顯的噪聲圖案,需要通過模糊降噪來修復這一問題。下面這幅圖片展示了波紋效果還有隨機性造成的效果:

光影的魔法!Cocos Creator 實現螢幕空間的環境光遮蔽(SSAO)

可以看到,儘管在低樣本數的情況下得到了很明顯的波紋效果,引入隨機性之後這些波紋效果就完全消失了。最初 Crytek 的實現是用一個深度緩衝做為輸入,但是這種方式存在一些問題(如自遮閉, 光環),由於這個原因,現在通常不會使用球體的取樣核心,而是使用一個沿著表面法向量的半球體取樣核心。

光影的魔法!Cocos Creator 實現螢幕空間的環境光遮蔽(SSAO)

通過在法向半球體(Normal Oriented Hemisphere)周圍取樣,將不會考慮到片段背面的幾何體,它消除了環境光遮蔽灰濛濛的感覺,從而產生更真實的結果。

SSAO 特點:

獨立於場景複雜性,僅和投影后最終的畫素有關,和場景中的頂點數三角數無關。

跟傳統的 AO 處理方法相比,不需要預處理,無需載入時間,也無需系統記憶體中的記憶體分配,所以更加適用於動態場景。

對螢幕上的每個畫素以相同的一致方式工作。

沒有 CPU 使用 - 它可以在 GPU 上完全執行。

可以輕鬆整合到任何現代圖形管線中。

在瞭解了 AO & SSAO 之後,我們來看看要怎麼基於 Cocos Creator 3.3.1 實現 SSAO。

Demo 地址:

https://gitee.com/yanjifa/cc-ssao-demo

樣本緩衝

SSAO 需要幾何體的資訊來確定一個片段的遮蔽因子,對於每個片段(畫素),需要如下資料:

逐片段位置向量

逐片段法線向量

逐片段反射顏色

取樣核心

用來旋轉取樣核心的隨機旋轉向量

通過使用一個逐片段觀察空間位置,可以將一個取樣半球核心對準片段的觀察空間表面法線。對於每一個核心樣本會取樣線性深度紋理來比較結果。取樣核心會根據旋轉向量稍微偏轉一點;所獲得的遮蔽因子將會之後用來限制最終的環境光照分量。

光影的魔法!Cocos Creator 實現螢幕空間的環境光遮蔽(SSAO)

通過以上發現 SSAO 所需的資料不正是延遲管線的 G-buffer,關於 G-buffer 是什麼可通過文章「延遲著色法」[1]做一個簡單的瞭解。閱讀引擎程式碼 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);    // 和本文無關, 不做介紹

}

#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

// ...

通過以上程式碼可以分析出引擎 G-buffer 的資料佈局,和具體 G-buffer 資料內容,深度值後面將會使用 G-buffer 計算得出。


自定義渲染管線

通過擴充套件延遲渲染管線的方式,在內建渲染管線的 LightFlow 上增加 一個 SsaoStage 用來生成 AO 紋理。首先建立一個渲染管線資源,資源管理器右鍵->建立->Render Pipeine->Render Pipeline Asset,命名為 ssao-deferrd-pipeline,建立 ssao-material | ssao-effect 著色器用來計算 AO 紋理,完整檔案如下:

.

├── ssao-constant.chunk            // UBO 描述

├── ssao-deferred-pipeline.rpp     // 管線資原始檔

├── ssao-effect.effect             // ssao shader

├── ssao-lighting.effect           // 光照 shader, 直接拷貝內建 internal/effects/pipeline/defferrd-lighting

├── ssao-lighting.mtl

├── ssao-material.mtl

├── ssao-render-pipeline.ts        // 定製管線指令碼

├── ssao-stage.ts                  // stage 指令碼

└── uboDefine.ts                   // Uniform Buffer Object 定義指令碼

對應管線配置如下,在 LightingFlow 下 Stages 最前面加入 SsaoStage,並指定對應的材質,可以看到,引擎現在其實已經支援後處理(PostProcess)了,只要指定材質就可以了,可能當前版本還不完善,所以引擎組還沒公開,其實 SSAO 也可以算是一種後處理效果,管線資源的屬性設定如下:

光影的魔法!Cocos Creator 實現螢幕空間的環境光遮蔽(SSAO)

自定義管線指令碼如下:

// 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;

/**

* 取樣核心、相機遠近裁剪面 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

*  擴充套件延遲渲染管線

*/

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!;

}

/**

* 核心程式碼, 建立一個 FrameBuffer 儲存 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 = [];

// 因為 SSAO 紋理最終是一張灰度圖, 所以使用 Format.R8 單通道紋理, 減少記憶體佔用, 使用時只需要讀取 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;

}

}

通過專案設定修改渲染管線為自定義的 SSAO 管線:

光影的魔法!Cocos Creator 實現螢幕空間的環境光遮蔽(SSAO)

取樣核心

我們需要沿著表面法線方向生成大量的樣本。就像前面介紹的那樣,想要生成形成半球形的樣本。由於對每個表面法線方向生成取樣核心非常困難,也不合實際,所以將在切線空間(Tangent Space)內生成取樣核心,法向量將指向正 z 方向。

光影的魔法!Cocos Creator 實現螢幕空間的環境光遮蔽(SSAO)
假設有一個單位半球,生成一個擁有最大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 樣本值取樣核心, 這裡寫的不太詳細, 可結合 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, // 這裡和原教程有點區別, Z 稍微增加一個很小的值, 可改善平面波紋(Banding)的效果, 可能會對精度造成影響

);

sample = sample.normalize();

let scale = i / UBOSsao.SAMPLES_SIZE;

// 通過插值, 將核心樣本靠近原點分佈

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);

}

我們在切線空間中以-1.0到1.0為範圍變換 x 和 y 方向,並以 0.0 和 1.0 為範圍變換樣本的 z 方向 (如果以-1.0到1.0為範圍,取樣核心就變成球型了)。由於取樣核心將會沿著表面法線對齊,所得的樣本向量將會在半球裡。通過權重插值,得到一個大部分樣本靠近原點的核心分佈。

光影的魔法!Cocos Creator 實現螢幕空間的環境光遮蔽(SSAO)

獲取深度資料

通過 G-buffer 中的 PostionMap 獲取線性深度值:

float getDepth(vec3 worldPos) {

// 轉到觀察空間

vec3 viewPos = (cc_matView * vec4(worldPos.xyz, 1.0)).xyz;

// cc_cameraNFLSInfo.y -> 相機 Far, 通過 ssao-stage.ts 指令碼更新

float depth = -viewPos.z / cc_cameraNFLSInfo.y;

return depth;

}

深度圖如下:

光影的魔法!Cocos Creator 實現螢幕空間的環境光遮蔽(SSAO)

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;

// 隨機數 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);

}

// 隨機旋轉取樣核心向量

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);

// 建立一個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;   // 轉換到裁剪空間

offset.xyz /= offset.w;                             // 透視除法

offset.xyz  = offset.xyz * 0.5 + 0.5;               // 從 NDC (標準化裝置座標, -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));

// 檢查樣本的當前深度值是否大於儲存的深度值,如果是,新增到最終的貢獻因子上

occlusion += (sampleDepth >= aoDepth ? 1.0 : 0.0) * rangeCheck;

}

// 將遮蔽貢獻根據核心的大小標準化,並輸出結果

occlusion = 1.0 - (occlusion / float(SSAO_SAMPLES_SIZE));

fragColor = vec4(occlusion, 1.0, 1.0, 1.0);

}

}%

下圖展示了環境遮蔽著色器產生的紋理:

光影的魔法!Cocos Creator 實現螢幕空間的環境光遮蔽(SSAO)

可見,環境遮蔽產生了非常強烈的深度感。僅僅通過環境遮蔽紋理就已經能清晰地看見模型一定躺在地板上而不是浮在空中。

現在的效果仍然看起來不是很完美,不連續的噪點清晰可見,為了建立一個光滑的環境遮蔽結果,需要模糊環境遮蔽紋理進行降噪。

光影的魔法!Cocos Creator 實現螢幕空間的環境光遮蔽(SSAO)

應用 SSAO 紋理

最後將 SSAO 紋理進行模糊降噪,並逐片段將環境遮蔽因子乘到環境光照分量上,拷貝內建光照著色器(internal/effects/pipeline/deferred-lighting.effect)命名為 ssao-lighting.effect。

/**

* 本文改動部分新增了中文註釋

*/

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)

{

// 省略, 詳見 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 環境遮蔽因子, 單通道紋理, 所以只取 R 通道

vec4 ssaoMap = vec4(vec3(gaussianBlur(cc_ssaoMap, v_uv, 3.0).r), 1.0);

s.albedo = albedoMap * ssaoMap; // 乘到輻照率貼圖上, 應用遮蔽紋理

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);

}

}%

最後來看下最終的渲染結果對比,首先是 SSAO 開啟的效果:

光影的魔法!Cocos Creator 實現螢幕空間的環境光遮蔽(SSAO)

SSAO 關閉的效果:

光影的魔法!Cocos Creator 實現螢幕空間的環境光遮蔽(SSAO)

螢幕空間環境遮蔽是一個可高度自定義的效果,它的效果很大程度上依賴於我們根據場景型別調整它的引數。對所有型別的場景並不存在什麼完美的引數組合方式。一些場景只在小半徑情況下工作,又有些場景會需要更大的半徑和更大的樣本數量才能看起來更真實。當前這個演示用了64個樣本,屬於比較多的了,你可以調整核心大小和半徑從而獲得合適的效果。

已知問題

編輯器攝像機預覽會渲染不正確。

資源管理器裡面點選自定義管線資原始檔,編輯器控制檯會報錯,可能會導致編輯器無響應 (目前建議沒事別碰,碰過重啟編輯器可恢復正常)。

手機瀏覽器 (小米10 Pro) 下使用最大采樣核心 (64) 時,幀數只有個位數,可以確定當前版本基本不能應用到實際專案中,還需優化。

Native 下自定義渲染管線同時還需要自定義 Engine-Native[2] 引擎,所以 Native 暫時還未支援,可參考 PR 3934[3] 新增對 Native 的支援,這裡要感謝 大表姐Kristine 提供的資訊。

相關教程

LearnOpenGL-CN->目錄->高階光照->SSAO:

https://learnopengl-cn.github.io/05%20Advanced%20Lighting/09%20SSAO/

環境遮罩之 SSAO 原理:

https://zhuanlan.zhihu.com/p/46633896

GAMES202-高質量實時渲染(視訊00:46:25開始):

https://www.bilibili.com/video/BV1YK4y1T7yY?p=8

參考連結

延遲著色法[1]:

https://learnopengl-cn.github.io/05%20Advanced%20Lighting/08%20Deferred%20Shading/

Engine-Native[2]:

https://github.com/cocos-creator/engine-native/tree/develop/cocos/renderer/pipeline

PR 3934[3]:

https://github.com/cocos-creator/engine-native/pull/3934


來源:COCOS
原文:https://mp.weixin.qq.com/s/0aNtXI7s41meJhI5M4JItQ


相關文章