Cocos Creator Shader入门实战(四):预处理宏定义和Chunk

引擎: 3.8.5

您好,我是鹤九日!

回顾


学习Shader,前期是让人烦躁无味的,后期可能就是各种的逻辑让人抓耳挠腮。

一成不变的内容:遵循引擎设定的规则,理解引擎要求的规范。

这里,简单回顾下前面所说的大概内容:

一、Shader的实现,需要Material 材质和Effect Asset资源的配合

二、Effect Asset 资源主要通过CCEffect配置渲染参数和属性, 通过CCProgram编写着色器片段代码

三、CCEffect的配置使用的是YAML,而CCProgram使用的则是GLSL

开篇


还记得,我们刚开始通过编译器创建的 传统无光照着色器(Effect) 的资源吗?

属性结构如下:

我当时说过:即使有着官方的文档加持,也是一脸的茫然,不知道这些代表着什么,以及如何使用。

今天的主题便是对EffectAsset资源的说明,看着很繁琐,其实没有那么的难。

简单的理解: 引擎对EffectAsset资源的预编译,其实就是借助了主要两点:

一、预处理宏定义

二、着色器片段Chunk

默认资源


正式开始之前,先做一个小小的延伸,也是自己偶然中的学习发现。

以我们创建的着色器资源,内容是从哪里来呢?

注:从严格意义来说,它不属于Shader的范畴,而是编译器的范畴。

标注的部分便是引擎的配置,甚至来说 .../default_file_content/scene 下的这个资源是否会很眼熟呢。

这些了解下就可以,万一有机会使用呢!

预处理宏定义


预处理宏定义,其实就是#define声明的的一些开关字段,使用的均为大写。

在任意EffectAsset的资源中,我们看到的Precompile Combinations属性展示开关,就是它。

在EffectAsset资源的CCProgram片段中,顶点着色器和片段着色器使用的一些宏,也是宏定义。

c 复制代码
//  顶点着色器
CCProgram sprite-vs %{
  precision highp float;
  #include <builtin/uniforms/cc-global>
  #if USE_LOCAL
    #include <builtin/uniforms/cc-local>
  #endif
  #if SAMPLE_FROM_RT
    #include <common/common-define>
  #endif
  // ...
}
// 片段着色器
CCProgram sprite-fs %{
  // ...
  #if USE_TEXTURE
    in vec2 uv0;
    #pragma builtin(local)
    layout(set = 2, binding = 12) uniform sampler2D cc_spriteTexture;
  #endif
}

甚至,后面要讲到的material材质的属性里面,也包含着宏定义开关。

这里注意:宏定义的显示与否,最终还是受着EffectAsset下的CCEffect属性参数和CCProgram着色器代码控制的。

理由很简单:Effect资源负责编写渲染参数和着色器实现,而Material材质负责对Effect的数据包装和可视化。

优点

引擎提供预处理宏定义的支持,它的几个特性主要有:

一、更好的管理代码内容,根据不宏生成不同组合的代码

二、更好的可视化及控制,方便调试

三、避免代码冗余,执行高效

这里注意:一般情况下,使用到的宏定义都会显示在属性面板上,但以CC_开头的不会显示。

内置宏定义

官方内置了很多的宏定义,这里简单罗列下常见的:

定义 说明
USE_INSTANCING 是否启用几何体实例化
USE_VERTEX_COLOR 是否叠加顶点颜色和 Alpha 值
USE_TEXTURE 是否使用主纹理(mainTexture)
USE_ALPHA_TEST 是否进行半透明测试(AlphaTest)
SAMPLE_FROM_RT 是否是从 RenderTexture 中采样

注:更多的内容可参考官方文档:

无光照宏

卡通渲染宏定义

PBR宏定义

这里注意,我们使用到的预处理定义宏,不仅包含着布尔类型,也会包含着常量、函数等。

在后面说到的Chunk中,它有一个通用的文件叫做:common-define.chunk

路径: ../internal/chunks/common/common-define

里面就包含了很多,常用的宏常量、函数等等。

注:如果仔细观察,引擎对EffectAsset资源预编译后

属性检查器中GLSL 300ES Output的输出就有它的影子。

用法

一般来说,使用#define常用于常量、布尔类型开关。比如:

c 复制代码
#define USE_INSTANCING 0
#define USE_TWOSIDE 0
#define USE_ALBEDO_MAP 0

较为特殊的用法,便是引擎支持的设定指定的范围宏定义,主要有:

c 复制代码
// 通过range([min, max])设定连续数字的宏定义
#pragma define-meta FACTOR range([-5, 5])
// 通过options([...]) 设定指定数字的宏定义
#pragma define-meta FACTOR options([-3, -2, 5])

这里注意:

一、#pragma 是通用的预处理命令,可指定编译选项、定义元数据等

二、#define 用于修饰静态的,比如替换文本,定义常量。

三、#pragma define-meta 是Cocos Creator提供的特有指令,可用于定义动态参数。

这样的好处便是:我们不需要重新编译,并且能够在属性检查器中动态调整。

以EffectAsset的着色器片段为例,主要代码如下

c 复制代码
CCProgram sprite-fs %{
  // ...
  #pragma define-mate FILTER_TYPE range([0, 4]);

  vec4 frag () {
    vec4 o = vec4(1, 1, 1, 1);
    o *= CCSampleWithAlphaSeparated(cc_spriteTexture, v_uv0);

    #if FILTER_TYPE == 1
      // gray
      float gray = (o.r + o.g + o.b)/3.0;
      o = vec4(gray, gray, gray, o.a);
    #elif FILTER_TYPE == 2 
       // ...
    #elif FILTER_TYPE == 3
      // ...
    #elif FILTER_TYPE == 4
      // ...
    #endif
    
    return o;
  }
}%

在Effect的属性检查器中,便可以看到它的属性:

而在对应的材质属性检查器中看到的属性,通过调整便可预览效果。

Chunk


Chunk听着好像是一个很高大上的名词儿,其实简单的理解就是#include

c 复制代码
CCProgram sprite-vs %{
  precision highp float;
  #include <builtin/uniforms/cc-global>
  #if USE_LOCAL
    #include <builtin/uniforms/cc-local>
  #endif
}

在日常开发中,我们会对一些通用的常量、方法等,进行分文件存放,使用的时候引用即可。

这样的方式,可以最大程度的复用代码逻辑。

引擎同样也做了这样的事情,所有的封装文件都在:internal/chunks 中。

这些跨文件代码的chunk封装,即跨文件代码引用机制,对我们编写Shader是很有帮助的。

上面的例子中提到的一个common-define.chunk 便是一个很好的例子。

官方内置的Chunk有很多,这里不在一一列举,可参考官方文档:

内置全局Uniform

公共函数库

注:Chunk的使用,仅针对于GL SL 300ES

使用

除了内置的Chunk外,官方支持开发者自定义创建Chunk片段,如下图所示:

在我们学习参考的示例中,还记得创建无光照Effect资源时候的这段配置吗?

yaml 复制代码
CCEffect %{
  techniques:
  - name: opaque
    passes:
    - vert: legacy/main-functions/general-vs:vert # builtin header
      frag: unlit-fs:frag
}%

如果学习使用,我们可以参考者将顶点着色器的逻辑,作为通用的chunk来使用。

比如,创建sprite-vs.chunk,只需要做到将CCProgram下的着色器代码拷贝过来,放进去就可以了。

c 复制代码
precision highp float;
#include <builtin/uniforms/cc-global>
#if USE_LOCAL 
  #include <builtin/uniforms/cc-local>
#endif

in vec3 a_position; // 顶点的位置坐标(XYZ)
in vec2 a_texCoord; // 顶点的纹理坐标(UV),用于映射纹理
in vec4 a_color;    // 顶点的颜色值(RGBA)

out vec4 v_color;   // 用于向片元着色器传递顶点颜色,片元着色器中会插值处理
out vec2 v_uv0;     // 用于向片元着色器传递纹理坐标,用于纹理采样

vec4 vert() {
  vec4 pos = vec4(a_position, 1);

  #if USE_LOCAL
    // 从局部坐标系转换到世界坐标系
    pos = cc_matWorld * pos;
  #endif

  #if USE_PIXEL_ALIGNMENT
    // cc_matView 视图矩阵, 将世界坐标系转换到视图坐标系
    pos = cc_matView * pos;       
    pos.xyz = floor(pos.xyz);
    // cc_matProj 投影矩阵,将视图坐标转换为裁剪坐标
    pos = cc_matProj * pos;       
  #else 
    // cc_matViewProj 视图投影矩阵,将世界坐标转换为裁剪坐标
    pos = cc_matViewProj * pos;   
  #endif 

  v_uv0 = a_texCoord;
  v_color = a_color;

  return pos;
}

然后,在CCEffect的片段中,修改下顶点着色器的路径即可:

yaml 复制代码
CCEffect %{
  techniques:
  - passes:
    - vert: ../res/effect/rgb/sprite-vs:vert
      frag: sprite-fs:frag
}%

延伸:GLSL 300ES

了解Chunk后,EffectAsset资源的着色器中关于output的预览代码就容易理解了。

由于Chunk基于GLSL 300 ES,那便以它为例,文件是builtin-sprite.effect的顶点着色器为例:

注:内容过多,只显示部分

c 复制代码
// 着色器中调用的 #include <builtin/uniforms/cc-global>
// 引擎便将 ../chunks/builtin/unfiroms/cc-global的内容引用过来
layout(std140) uniform CCGlobal {
 highp   vec4 cc_time;
 mediump vec4 cc_screenSize;
 mediump vec4 cc_nativeSize;
 mediump vec4 cc_probeInfo;
 mediump vec4 cc_debug_view_mode;
};
// ...

// 着色器调用的#include <builtin/uniforms/cc-local>
// 引擎便将 ../chunks/builtin/unfiroms/cc-local的内容引用过来
#if USE_LOCALlayout(std140) uniform CCLocal {
   highp mat4 cc_matWorld;
   highp mat4 cc_matWorldIT;
   highp vec4 cc_lightingMapUVParam;
   highp vec4 cc_localShadowBias;
   highp vec4 cc_reflectionProbeData1;
   highp vec4 cc_reflectionProbeData2;
   highp vec4 cc_reflectionProbeBlendData1;
   highp vec4 cc_reflectionProbeBlendData2;
 };
#endif
// 引擎引用下的 ../common/common-define.chunk的通用宏定义相关
#if SAMPLE_FROM_RT
    #define QUATER_PI         0.78539816340
    #define HALF_PI           1.57079632679
    #define PI                3.14159265359
    // ...'
#endif 

// 着色器的片段实现相关
in vec3 a_position;
in vec2 a_texCoord;
in vec4 a_color;
out vec4 color;
out vec2 uv0;
vec4 vert () {
 // ...
}

// gl_Postion是GLSL语言中顶点着色器的最终输出
// main()接口的使用,是引擎在编译时自动添加的
// 这也是我们自定义着色器入口时不能使用main的原因
void main() { gl_Position = vert(); }

结尾

今天的文章到这里就算是结束了,如果理解有误,编写不当,希望您能指正一二。

我是鹤九日,祝您生活快乐!

相关推荐
方方洛4 小时前
翻译:MCP schema(Model Context Protocol specification)
前端·typescript·mcp
Shawn_LX5 小时前
Vue3 + Vite + Yarn + Fabricjs构建的开源演示系统
javascript·typescript·vue·html5·fabric
丁总学Java7 小时前
解锁 vue-property-decorator 的秘密:Vue 2 到 Vue 3 的 TypeScript 之旅!✨
前端·vue.js·typescript
觉醒法师1 天前
HarmonyOS NEXT - 电商App实例三( 网络请求axios)
前端·华为·typescript·axios·harmonyos·ark-ts
ᥬ 小月亮2 天前
TypeScript基础
前端·javascript·typescript
红尘散仙2 天前
二、WebGPU 基础入门——基础知识
rust·typescript·gpu
残轩2 天前
JavaScript/TypeScript异步任务并发实用指南
前端·javascript·typescript
红尘散仙2 天前
一、WebGPU 基础入门——环境搭建
rust·typescript·gpu
厚礼蟹man2 天前
一键打通api、TS、mock
前端·typescript·前端工程化