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

结尾

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

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

相关推荐
熊猫钓鱼>_>6 小时前
TypeScript前端架构与开发技巧深度解析:从工程化到性能优化的完整实践
前端·javascript·typescript
敲敲敲敲暴你脑袋6 小时前
Canvas绘制自定义流动路径
vue.js·typescript·canvas
m0dw9 小时前
vue懒加载
前端·javascript·vue.js·typescript
流影ng1 天前
【HarmonyOS】并发线程间的通信
typescript·harmonyos
duansamve2 天前
TS在Vue3中的使用实例集合
typescript·vue3
应用市场2 天前
无人机编队飞行原理与Python仿真实现完整指南
python·无人机·cocos2d
FanetheDivine2 天前
ts中如何描述一个复杂函数的类型
前端·typescript
云卓SKYDROID2 天前
无人机中继器模式技术对比
人工智能·游戏引擎·php·无人机·cocos2d·高科技·云卓科技
struggle20253 天前
AxonHub 开源程序是一个现代 AI 网关系统,提供统一的 OpenAI、Anthropic 和 AI SDK 兼容 API
css·人工智能·typescript·go·shell·powershell
执剑、天涯3 天前
通过一个typescript的小游戏,使用单元测试实战(二)
javascript·typescript·单元测试