引擎: 3.8.5
您好,我是鹤九日!
回顾
学习Shader,前期是让人烦躁无味的,后期可能就是各种的逻辑让人抓耳挠腮。
一成不变的内容:遵循引擎设定的规则,理解引擎要求的规范。
这里,简单回顾下前面所说的大概内容:
一、Shader的实现,需要Material 材质和Effect Asset资源的配合
二、Effect Asset 资源主要通过CCEffect
配置渲染参数和属性, 通过CCProgram
编写着色器片段代码
三、CCEffect
的配置使用的是YAML,而CCProgram
使用的则是GLSL
开篇
还记得,我们刚开始通过编译器创建的 传统无光照着色器(Effect) 的资源吗?

属性结构如下:

我当时说过:即使有着官方的文档加持,也是一脸的茫然,不知道这些代表着什么,以及如何使用。
今天的主题便是对EffectAsset资源的说明,看着很繁琐,其实没有那么的难。
简单的理解: 引擎对EffectAsset资源的预编译,其实就是借助了主要两点:
一、预处理宏定义
默认资源
正式开始之前,先做一个小小的延伸,也是自己偶然中的学习发现。
以我们创建的着色器资源,内容是从哪里来呢?
注:从严格意义来说,它不属于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 中采样 |
注:更多的内容可参考官方文档:
这里注意,我们使用到的预处理定义宏,不仅包含着布尔类型,也会包含着常量、函数等。
在后面说到的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有很多,这里不在一一列举,可参考官方文档:
注: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(); }
结尾
今天的文章到这里就算是结束了,如果理解有误,编写不当,希望您能指正一二。
我是鹤九日,祝您生活快乐!