内置 Surface Shader 导读

Cocos Creator 3.7.2 版本开始, builtin-standard.effect 使用 Surface Shader 架构实现。

本文以 builtin-standard.effect 作为典型案例,讲解 Surface Shader 细节。

你可以属性 Surface Shader 结构定义、语法细节以及渲染流程。

下面的内容,建议配合 internal/effects/builtin-standard.effect 一起阅读。

基本结构

Surface Shader 代码通常由几个部分组成:

  • 信息描述(CCEffect):描述此 Shader 的技术、渲染过程组成部分,以及每个渲染过程使用的 Shader、渲染状态、属性等。
  • 共享常量(Shared UBOs):把 vs 和 fs 都需要用到的 uniforms 定义在一起,方便管理。
  • 宏映射(Macro Remapping):处理一些宏定义,以及映射一些内部宏,使其可以显示到材质面板上。
  • 函数(Surface Functions):用于声明表面材质信息相关的 Surface 函数。
  • 组装器(Shader Assembly):用于组装每个顶点着色器(Vertex Shader)和片元着色器(Fragment Shader)的代码模块。

可前往 Surface Shader 基本结构 了解更多详情。

CCEffect

一个物体被渲染到屏幕上,需要以下信息:

  • 模型数据(顶点、UV、法线等)
  • 光照信息
  • 世界空间(旋转、平移、缩放)信息
  • 绘制过程(材质)
  • 渲染状态(材质)
  • 纹理(材质)
  • Uniform(材质)
  • Shader 代码(材质)

其中模型数据、光照信息、世界空间信息与材质无关,而 纹理、Uniform、渲染状态、Shader 代码、绘制过程 则属于材质信息。

CCEffect 则描述了以上材质相关信息,并且与引擎渲染管线共同完成一个模型的渲染流程。

渲染技术(technique)

内置的 Surface Shader 实现了 opaquetransparent 两种渲染技术,前者用于渲染非透明物体,后者用于渲染半透明物体。

渲染过程(passes)

内置的 Surface Shader 的每一个技术,只有一个 pass,且均为 PBR。

抛开其余细节,我们可以看到整个 Surface Shader 的信息描述如下:

  1. CCEffect %{
  2. techniques:
  3. - name: opaque
  4. passes:
  5. - vert: standard-vs
  6. frag: standard-fs
  7. properties: &props
  8. ...
  9. - name: transparent
  10. passes:
  11. - vert: standard-vs
  12. frag: standard-fs
  13. ...
  14. properties: *props
  15. }%

Shader 入口(vert 与 frag)

opaquetransparent 在渲染效果上完全一致,差异仅仅是渲染状态。

可以看到,它们使用同样的 vert 和 frag 入口。

  1. - vert: standard-vs
  2. frag: standard-fs

属性(properties)

由于 opaquetransparent 在渲染效果上完全一致,Shader 代码也是用的同一样,所以涉及到的属性也就完全一样。

所有渲染过程中用到的属性集中放在了 properties 字段里。关于属性的语法,可以查看 Pass 可选配置参数

配置复用

在属性配置中,可以看到, opaque 的属性定义为 properties: &props,而 transparent 的属性定义为 properties: *props

这是一个属性命名和复用机制。

properties: &props 的含义是为当前 properties 起名为 props

properties: *props 的含义是使用名为 props 的属性块填充当前 properties

上面的配置结果为:transparent 直接使用 opaqueproperties 字段。

用于特定阶段(phase)

默认情况下, Surface Shader 会参与到场景渲染阶段。 但也有一些特殊的场合,比如 阴影,反射探针烘焙 等。

针对这类需求,我们可以添加特定的渲染过程(pass),并标记参与的阶段(phase)来实现目的。

当 Cocos 引擎的渲染器在执行渲染时,会获取到材质中相应 phase 的 pass,用于渲染。 如果没有,表示此物体不参与这个阶段。

如 Surface Shader 中的:

  • forward-add:用于附加光照阶段,当物体受主光源以外的光源影响时,会调用这个。
  • shadow-caster:用于阴影图渲染阶段
  • reflect-map:用于环境反射探针烘焙阶段
  1. - &forward-add
  2. vert: standard-vs
  3. frag: standard-fs
  4. phase: forward-add
  5. ...
  6. - &shadow-caster
  7. vert: shadow-caster-vs
  8. frag: shadow-caster-fs
  9. phase: shadow-caster
  10. ...
  11. - &reflect-map
  12. vert: standard-vs
  13. frag: reflect-map-fs
  14. phase: reflect-map
  15. ...

如上面代码所示,phase 用于标记此 pass 的参与阶段。 而 &forward-add&shadow-caster&reflect-map 则是给这个 pass 起了一个名字,方便后面的技术对它进行复用。

比如,transparent 就直接复用了 opaqueforward-addshadow-caster pass。

  1. - name: transparent
  2. passes:
  3. - vert: standard-vs
  4. frag: standard-fs
  5. ...
  6. properties: *props
  7. - *forward-add
  8. - *shadow-caster

渲染状态

如开头所说,为了完成一个模型的渲染,除了定义好渲染过程,所需要的属性以外,还需要配合渲染状态。

渲染状态主要涉及 模板测试、深度测试、光栅器状态、透明混合等。

同样的渲染流程、属性、Shader 代码,配合不同的渲染状态,就可以实现不同的效果。

  1. //深度、模板测试
  2. depthStencilState:
  3. depthFunc: equal
  4. depthTest: true
  5. depthWrite: false
  6. //透明混合状态
  7. blendState:
  8. targets:
  9. - blend: true
  10. blendSrc: one
  11. blendDst: one
  12. blendSrcAlpha: zero
  13. blendDstAlpha: one
  14. //光栅器状态
  15. rasterizerState:
  16. cullMode: front

渲染状态会有一套默认值,在有需要的时候进行修改即可。

比如 opaquetransparent 就只有渲染状态的区别。

内嵌宏

  1. embeddedMacros: { CC_FORCE_FORWARD_SHADING: true }

有时候,我们想为某个pass单独开启或者关闭一些宏,可以使用 embeddedMacros 字段进行。

代码引用机制

Surface Shader 提供了两种代码块引用机制:头文件和CCProgram。详情请查看 include 机制

共享常量

许多常量是 vs 和 fs 都会用到的,或者多个 technique 和 pass 都需要用到的,定义在一起方便管理。

共享常量本质上还是属于 Shader 代码片段,使用 GLSL 来编写。

  1. CCProgram shared-ubos %{
  2. uniform Constants {
  3. vec4 tilingOffset;
  4. vec4 albedo;
  5. vec4 albedoScaleAndCutoff;
  6. vec4 pbrParams;
  7. vec4 emissive;
  8. vec4 emissiveScaleParam;
  9. vec4 anisotropyParam;
  10. };
  11. }%

在后面的组装环节,只需要 #include <shared-ubos> 即可。

宏映射

关于宏映射详细信息,请参考 宏定义与重映射

在 内置的 Surface Shader 中,使用 CCProgram macro-remapping 片段来组织所的宏映射工作,方便管理。

可以看到,在内置的 Surface Shader 中,使用 #pragma define-meta 将许多内置的宏重定向到了面板上。

  1. CCProgram macro-remapping %{
  2. // ui displayed macros
  3. #pragma define-meta HAS_SECOND_UV
  4. #pragma define-meta USE_TWOSIDE
  5. #pragma define-meta IS_ANISOTROPY
  6. #pragma define-meta USE_VERTEX_COLOR
  7. #define CC_SURFACES_USE_SECOND_UV HAS_SECOND_UV
  8. #define CC_SURFACES_USE_TWO_SIDED USE_TWOSIDE
  9. #define CC_SURFACES_LIGHTING_ANISOTROPIC IS_ANISOTROPY
  10. #define CC_SURFACES_USE_VERTEX_COLOR USE_VERTEX_COLOR
  11. // depend on UI macros
  12. #if IS_ANISOTROPY || USE_NORMAL_MAP
  13. #define CC_SURFACES_USE_TANGENT_SPACE 1
  14. #endif
  15. // functionality for each effect
  16. #define CC_SURFACES_LIGHTING_ANISOTROPIC_ENVCONVOLUTION_COUNT 31
  17. }%

Surface 函数段

在 Surface Shader 中,定义了两个 CCProgram 用于处理具体的Shader计算。

  • CCProgram surface-vertex:用于处理 vs 相关计算
  • CCProgram surface-fragment:用于处理 fs 相关计算

CCProgram surface-vertex

内置的 vs 流程基本上能满足 Surface Shader 的 vs 需求,导致 surface-vertex 非常简单,只做了少量的特殊处理。

我们以第二套 UV 的处理函数为例。

它先定义了 CC_SURFACES_VERTEX_MODIFY_UV 宏,然后实现了 SurfacesVertexModifyUV 方法。

  1. #define CC_SURFACES_VERTEX_MODIFY_UV
  2. void SurfacesVertexModifyUV(inout SurfacesStandardVertexIntermediate In)
  3. {
  4. In.texCoord = In.texCoord * tilingOffset.xy + tilingOffset.zw;
  5. #if CC_SURFACES_USE_SECOND_UV
  6. In.texCoord1 = In.texCoord1 * tilingOffset.xy + tilingOffset.zw;
  7. #endif
  8. }

这就是 Surface Shader 的核心机制,可以通过宏定义改写内部函数,在不修改内部源码的情况下实现特定的渲染需求。

具体的机制请参考使用宏定义实现函数替换Surface Shader 内置可替换函数列表

CCProgram surface-fragment

surface-fragment 主要实现了 PBR 计算时需要的表面材质信息填充。

宏开关

  1. #if USE_ALBEDO_MAP
  2. uniform sampler2D albedoMap;
  3. #pragma define-meta ALBEDO_UV options([v_uv, v_uv1])
  4. #endif

可以看到,在内置的 Surface Shader 中,所有的贴图,都被宏定义包裹起来,这样的好处就是可以根据需求关闭对应的宏,以提升性能。

材质面板可选择的宏

#pragma define-meta + 名称 + options([item0,item1,....]) 可以定义一个供用户选择的宏。

以下面代码为例:

  1. #pragma define-meta ALBEDO_UV options([v_uv, v_uv1])

材质面板上, ALBEDO_UV 会出现下拉选择框,Shader 编译时,会以用户选择值为准。

比如,用户如果选择了 v_uv1,这条语句编译出来的最终结果为:

  1. #define ALBEDO_UV v_uv1
  1. #if USE_ALPHA_TEST
  2. #pragma define-meta ALPHA_TEST_CHANNEL options([a, r])
  3. #endif

ALPHA_TEST_CHANNEL 也是如此,默认使用 a 通道,但也可以选择 r 通道。

PBR 通道

  1. #pragma define OCCLUSION_CHANNEL r
  2. #pragma define ROUGHNESS_CHANNEL g
  3. #pragma define METALLIC_CHANNEL b
  4. #pragma define SPECULAR_INTENSITY_CHANNEL a

Surface Shader 使用一张图作为 PBR 贴图,根据定义就可以知道,PBR 贴图各通道的含义:

  • r通道:环境遮蔽
  • r通道:粗糙度
  • b通道:金属度
  • a通道:高光强度

具体实现

与 surface-vertex 一样, surface-fragment 中也通过函数替换方式,实现 PBR 参数填充。

想要了解更多具体的机制请参考以下文章:

Shader 组装

上面提到的几个 CCProgram:

  • shared-ubos
  • macro-remapping
  • surface-vertex
  • surface-fragment

只是一些实现 Surface Shader 必要的组成部分,想要实现一个完整的 Surface Shader,还需要将这些部分,配合Surface Shader 内置的其它模块进行组装。

具体的组装机制请查看:Surface Shader 组装

最后组装出来的 CCProgram,才是 CCEffect 部分引用的内容。

  • CCProgram standard-vs

  • CCProgram shadow-caster-vs

  • CCProgram standard-fs

  • CCProgram shadow-caster-fs

  • CCProgram reflect-map-fs