引言:

本文作者 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 时场景的不同。特别注意对比电话亭后面和墙角部分,你会发现(环境)光被遮蔽了许多:

虽然这个效果不是非常明显,但是启用 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)相关推荐

  1. 两个空间点直接距离投影公式_HBAO(屏幕空间的环境光遮蔽)

    别的不多扯,直接进入正题,HBAO全程Image-space horizon-based ambient occlusion 对于屏幕上的像素点P,HBAO算法通过以下几个步骤来计算它的环境光遮蔽 1 ...

  2. cocos creator 保持屏幕常亮 屏幕不休眠

    此教程唯小白教程 只为了实现功能 安卓原生 很简单 一学就会 具体原理什么的多参考 android屏幕唤醒帖子等 参考 http://www.cnblogs.com/king_dy/archive/2 ...

  3. 屏幕空间环境光遮蔽(SSAO)算法的实现

    SSAO SSAO介绍 之前写SSAO的时候最后一直没达到想要的效果,最近闲下来又重新写了下,才发现自己之前真的蠢- -!(位置和法线忘转到视口坐标).这里就好好整理一下这个算法,以免下次想拿起来又不 ...

  4. Learn OpenGL 笔记6.10 SSAO(Screen Space Ambient Occlusion屏幕空间环境光遮蔽)

    我们在基本照明一章中简要介绍了该主题:ambient lighting环境光. Ambient lighting环境光是一个固定的光常数,我们添加到场景的整体照明中以模拟光的scattering散射. ...

  5. 图形 4.2 SSAO算法 屏幕空间环境光遮蔽

    链接: SSAO算法 屏幕空间环境光遮蔽思维导图. SSAO算法 屏幕空间环境光遮蔽 SSAO介绍 什么是AO SSAO原理详解 SSAO介绍 SSAO原理 计算近似AO SSAO算法实现 比较与分析 ...

  6. Cocos Creator学习目录

    目录 安装和启动 文件结构 编辑器基础 基本概念 (场景树 节点 坐标 组件 ) Cocos Creator 脚本简介 Cocos Creator调试 节点 cc.Node 组件开发cc.Compon ...

  7. Cocos Creator教程 ——(二)UI系统介绍(上)

    目录 前言 一.Sprite(精灵) 二.Label(文字) 三.Weight(对齐挂件) 四.Layout(自动布局) 前言 这里会通过一个demo项目去介绍现在的UI系统有生产什么游戏的能力,以及 ...

  8. 022 - cocos creator 3D

    #cocos creator 3D warning 报错:"project:///assets/main.js,将https中的export注视掉重试一遍 知识点 scrollview组件添 ...

  9. cocos creator 学习随笔 day03 节点和组件属性

    目录 节点本身属性 控件属性 场景 空节点 3D对象 2D对象 UI组件 光线 特效 摄像机 地形 节点本身属性 第一栏为节点名,可修改,前面得勾,是表示是否使用该节点,去掉代表隐藏该节点,但是该节点 ...

最新文章

  1. 用Leangoo Scrum看板工具做多团队大规模敏捷
  2. invalid signature 错误原因验签失败_Nginx 失败重试机制
  3. idea安装Maven Helper
  4. Web框架——Flask系列之请求上下文与应用上下文请求钩子Flask-Script扩展命令行(十七)
  5. 服务器具有挂起的重新启动_ESP8266与网络服务器实时通讯
  6. ClickOnce Cannot download the application解决方法
  7. 重量计算python月球_千年前的古诗,苏轼的不知月亮上是何年何月|现在我用Python来计算出来了...
  8. 使用MySQL UDFs来调用gearman分布式任务分发系统
  9. 5-1 可维护性的度量和构造原则
  10. Kubernetes 搭建 ES 集群(存储使用 local pv)
  11. 一、简单工厂模式 : 面向对象 特性 封装
  12. 如何在Mac OS上从Photoshop 2020作为插件访问Topaz DeNoise AI?
  13. 使用google搜索自己在csdn写的博客
  14. html设置背景颜色以及背景图片
  15. QQ能上网页打不开解决办法
  16. python sklearn库 rnn_scikit-learn 逻辑回归类库使用小结
  17. SEO优化 TDK的写法思路
  18. 有没有好奇过路由器宽带拨号的mtu值为什么是1492呢?了解MTU与IP分片
  19. c语言编程齿轮模数选择,如何画齿轮,一看就懂
  20. DHCP的原理与配置

热门文章

  1. mysql和FTP结合,vsftp基于mysql和ssl的配置
  2. python-shutil学习
  3. 【WPF】右键菜单ContextMenu可点击区域太小的问题
  4. 1.数据库和表的创建
  5. Tensorflow中2D卷积API使用
  6. 【OpenCV开发】使用OpenCV的OpenCL(ocl)模块
  7. oracle创建数据库的三种方法
  8. SharePoint 数据库管理-PowerShell
  9. 推荐优秀的SQL脚本调试工具Embarcadero DBArtisan 可以调试SQL Server 2000/2005 SQL调试工具汇总...
  10. 有关phpmailer的详细介绍及使用方法