光影的魔法!Cocos Creator 实现屏幕空间的环境光遮蔽(SSAO)
引言:
本文作者 alpha 從事游戲前端開發已經5年,畢業后他先是入職了騰訊無線大連研發中心,而后開啟了北漂生涯,在北京的這3年一直都在使用 Cocos Creator,對前端業務,包體、內存優化有很多的實踐經驗。最近 alpha 在學習計算機圖形學相關技術,今天他將同大家分享 Cocos Creator 3.3 實現屏幕空間的環境光遮蔽(SSAO)的技術經驗。
什么是 AO ?
環境光(Ambient Lighting)是場景總體光照中的一個固定光照常量,用來模擬光的散射(Scattering)。在現實中,光線會以任意方向散射,它的強度是會改變的。
其中一種間接光照的模擬叫做環境光遮蔽(Ambient Occlusion),它的原理是通過將褶皺、孔洞和非??拷膲γ孀儼档姆椒ń颇M出間接光照。這些區域很大程度上是被周圍的幾何體遮擋的,所以這些地方看起來會更暗一些。
在2007年,Crytek 公司發布了一款叫做屏幕空間環境光遮蔽(Screen Space Ambient Occlusion,SSAO)的技術,并用在了他們的看家作孤島危機上。這一技術使用了屏幕空間場景的深度而不是真實的幾何體數據來確定遮蔽量。這一做法相對于真正的環境光遮蔽(基于光線追蹤)不但速度快,而且還能獲得較好的效果,使得它成為近似實時環境光遮蔽的標準。
下面這幅圖展示了在使用和不使用 SSAO 時場景的不同。特別注意對比電話亭后面和墻角部分,你會發現(環境)光被遮蔽了許多:
?
雖然這個效果不是非常明顯,但是啟用 AO 確實給我們更真實的感覺,這些小的遮蔽細節能讓整個場景看起來更有立體感。
SSAO 原理
SSAO 背后的原理很簡單:對于屏幕上的每一個片段,會根據周邊深度值計算一個遮蔽因子(Occlusion Factor)。這個遮蔽因子之后會被用來決定片段的環境光分量。遮蔽因子是通過采集片段周圍球型核心(Kernel)的多個深度樣本,并和當前片段深度值對比而得到的。高于片段深度值樣本的個數就是我們想要的遮蔽因子。
?
上圖中在幾何體內灰色的深度樣本都是高于片段深度值的,他們會增加遮蔽因子;幾何體內樣本個數越多,片段獲得的環境光照也就越少。
很明顯,渲染效果的質量和精度與采樣的樣本數量有直接關系。如果樣本數量太低,渲染的精度會急劇減少,會得到一種叫做波紋(Banding)的效果;如果它太高了,會影響性能。通過引入隨機性到采樣核心(Sample Kernel)從而減少樣本的數目。通過隨機旋轉采樣核心,能在有限樣本數量中得到高質量的結果。然而隨機性引入了一個很明顯的噪聲圖案,需要通過模糊降噪來修復這一問題。下面這幅圖片展示了波紋效果還有隨機性造成的效果:
?
可以看到,盡管在低樣本數的情況下得到了很明顯的波紋效果,引入隨機性之后這些波紋效果就完全消失了。最初 Crytek 的實現是用一個深度緩沖做為輸入,但是這種方式存在一些問題(如自遮閉, 光環),由于這個原因,現在通常不會使用球體的采樣核心,而是使用一個沿著表面法向量的半球體采樣核心。
?
通過在法向半球體(Normal Oriented Hemisphere)周圍采樣,將不會考慮到片段背面的幾何體,它消除了環境光遮蔽灰蒙蒙的感覺,從而產生更真實的結果。
SSAO 特點:
獨立于場景復雜性,僅和投影后最終的像素有關,和場景中的頂點數三角數無關。
跟傳統的 AO 處理方法相比,不需要預處理,無需加載時間,也無需系統內存中的內存分配,所以更加適用于動態場景。
對屏幕上的每個像素以相同的一致方式工作。
沒有 CPU 使用 - 它可以在 GPU 上完全執行。
可以輕松集成到任何現代圖形管線中。
在了解了 AO & SSAO 之后,我們來看看要怎么基于 Cocos Creator 3.3.1 實現 SSAO。
Demo 地址:
https://gitee.com/yanjifa/cc-ssao-demo
樣本緩沖
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 也可以算是一種后處理效果,管線資源的屬性設置如下:
自定義管線腳本如下:
// 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 管線:
?
采樣核心
我們需要沿著表面法線方向生成大量的樣本。就像前面介紹的那樣,想要生成形成半球形的樣本。由于對每個表面法線方向生成采樣核心非常困難,也不合實際,所以將在切線空間(Tangent Space)內生成采樣核心,法向量將指向正 z 方向。
?
假設有一個單位半球,生成一個擁有最大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為范圍,取樣核心就變成球型了)。由于采樣核心將會沿著表面法線對齊,所得的樣本矢量將會在半球里。通過權重插值,得到一個大部分樣本靠近原點的核心分布。
?
獲取深度數據
通過 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;
}
深度圖如下:
?
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);
}
}%
下圖展示了環境遮蔽著色器產生的紋理:
?
可見,環境遮蔽產生了非常強烈的深度感。僅僅通過環境遮蔽紋理就已經能清晰地看見模型一定躺在地板上而不是浮在空中。
現在的效果仍然看起來不是很完美,不連續的噪點清晰可見,為了創建一個光滑的環境遮蔽結果,需要模糊環境遮蔽紋理進行降噪。
?
應用 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 開啟的效果:
?
SSAO 關閉的效果:
?
屏幕空間環境遮蔽是一個可高度自定義的效果,它的效果很大程度上依賴于我們根據場景類型調整它的參數。對所有類型的場景并不存在什么完美的參數組合方式。一些場景只在小半徑情況下工作,又有些場景會需要更大的半徑和更大的樣本數量才能看起來更真實。當前這個演示用了64個樣本,屬于比較多的了,你可以調整核心大小和半徑從而獲得合適的效果。
已知問題
編輯器攝像機預覽會渲染不正確。
資源管理器里面點擊自定義管線資源文件,編輯器控制臺會報錯,可能會導致編輯器無響應 (目前建議沒事別碰,碰過重啟編輯器可恢復正常)。
手機瀏覽器 (小米10 Pro) 下使用最大采樣核心 (64) 時,幀數只有個位數,可以確定當前版本基本不能應用到實際項目中,還需優化。
Native 下自定義渲染管線同時還需要自定義 Engine-Native[2] 引擎,所以 Native 暫時還未支持,可參考 PR 3934[3] 添加對 Native 的支持,這里要感謝 大表姐Kristine 提供的信息。
總結
以上是生活随笔為你收集整理的光影的魔法!Cocos Creator 实现屏幕空间的环境光遮蔽(SSAO)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: CEDEC 2021 | 让巨大化角色充
- 下一篇: 动效如何构成连接 篇肆