持续进阶!暴改一个带纹理贴图的 Shader
上一章,我们了解了基础绘图组件的渲染流程。本章将继续深入,选取一个带纹理贴图的 Shader 进行讲解和改造。带有纹理之后,我们能呈现的画面将更加丰富。
Sprite Shader
我们首先要尝试了解的 Shader 是 2D 精灵使用的 shader--builtin-sprite,它在基础绘图 shader 的基础上增加了纹理的处理。
创建一个独立的 Effect 文件 foo.effect,拷贝 builtin-sprite 内所有的内容,让我们尝试着了解一下这部分。不用全懂,毕竟有很多地方涉及到更深入的知识,同时我也会标注“可忽略”的内容。
// Copyright (c) 2017-2020 Xiamen Yaji Software Co., Ltd.
CCEffect %{
techniques:
- passes:
- vert: sprite-vs:vert # 关联顶点着色器和片元着色器
frag: sprite-fs:frag
depthStencilState:
depthTest: false
depthWrite: false
blendState:
targets:
- blend: true
blendSrc: src_alpha
blendDst: one_minus_src_alpha
blendDstAlpha: one_minus_src_alpha
rasterizerState:
cullMode: none
properties:
alphaThreshold: { value: 0.5 } # 定义属性透明度阈值,在启用透明测试的时候,透明度低于阈值内值的片段会被丢弃
}%
// 顶点着色器
CCProgram sprite-vs %{
// 声明所有顶点着色器浮点数的精度
precision highp float;
// 类 C++ include 机制,可以查看下方说明。
#include
// 宏定义。控制代码执行的开关。如果在编辑器或者代码里没有启用 USE_LOCAL,则 #include代码不会被执行。可以查看下方说明。
#if USE_LOCAL
#include
#endif
// RenderTexture 图像翻转问题,可忽略
#if SAMPLE_FROM_RT
#include
#endif
// 顶点属性数据,位置 + 纹理坐标 + 顶点颜色
in vec3 a_position;
in vec2 a_texCoord;
in vec4 a_color;
out vec4 color;
out vec2 uv0;
vec4 vert () {
vec4 pos = vec4(a_position, 1);
#if USE_LOCAL
// 如果是不常更新的顶点数据,每帧只需要更新矩阵即可,这类数据提供局部坐标,在 GPU 上进行变换计算,减少 CPU 计算压力。可暂时忽略。
pos = cc_matWorld * pos;
#endif
// 是否启用像素对齐,可忽略,这部分主要是为了将观察坐标取整,避免像素抖动
#if USE_PIXEL_ALIGNMENT
pos = cc_matView * pos;
pos.xyz = floor(pos.xyz);
pos = cc_matProj * pos;
#else
// 因为 UI 具有对齐特性,顶点数据可能会因为对齐而导致经常变动,常常每帧都需要重新计算顶点数据,因此,Sprite 默认的顶点数据采用的是世界坐标数据。
pos = cc_matViewProj * pos;
#endif
uv0 = a_texCoord;
// 图像翻转
#if SAMPLE_FROM_RT
CC_HANDLE_RT_SAMPLE_FLIP(uv0);
#endif
color = a_color;
return pos;
}
}%
// 片元着色器
CCProgram sprite-fs %{
precision highp float;
// 这两个 include 部分有关于 UI 透明像素处理以及图像压缩处理,也可忽略
#include
#include
in vec4 color;
#if USE_TEXTURE
in vec2 uv0;
// 这部分参考下方扩展处的内容
#pragma builtin(local)
layout(set = 2, binding = 10) uniform sampler2D cc_spriteTexture;
#endif
vec4 frag () {
vec4 o = vec4(1, 1, 1, 1);
#if USE_TEXTURE
// 压缩纹理使用时的处理方式,默认按正常方式采样,具体请查看内置 chunks 文件夹里的 embedded-alpha 文件
o *= CCSampleWithAlphaSeparated(cc_spriteTexture, uv0);
// 图像置灰处理
#if IS_GRAY
float gray = 0.2126 * o.r + 0.7152 * o.g + 0.0722 * o.b;
o.r = o.g = o.b = gray;
#endif
#endif
o *= color;
// 透明度测试。开启透明度测试时,与透明度阈值属性进行对比从而丢弃部分片段。可忽略。
ALPHA_TEST(o);
return o;
}
}%
include 机制
类似 C/C++ 的头文件 include 机制,你可以在任意 shader 代码(CCProgram 块或独立的头文件)中引入其他代码片段:
// 内置头文件引入
#include
// 自定义头文件引入,采用的是:
// 1. 相对于项目目录 assets/chunks 文件夹位置(即使 assets 下没有 chunks 文件夹,也会假设有一个 chunks 文件夹,并相对于这个文件夹查找)
// 2. 相对于当前文件路径查找
// 前者优先。
// 更多有关绝对路径或同文件引用,请查看:https://docs.cocos.com/creator/3.2/manual/zh/material-system/effect-syntax.html#include-机制
#include "../headers/my-shading-algorithm.chunk"
Cocos Creator 所有的内置 chunk 都在“资源管理器” internal 下的 chunks 文件夹里,引用内置的 chunk,直接 "#include
预处理宏定义
在任意 shader 代码(CCProgram 块或独立的头文件)中,都可以通过 大写字母 + _ 的方式定义宏。每一个宏都是一个开关,默认是关闭状态,可以在编辑器或者运行时开启,需要通过 if 语句进行判断。如果不是 GLSL 内置宏,请不要使用 ifdef 或者 if defined 做判断,否则始终为 true。
// USE_TEXTURE 就是一个宏定义,所有的宏定义最终都会被序列化到“属性检查器”面板上,方便随时启/禁用。
// 如果宏定义处于禁用中, #if 到 #endif 内的代码不会执行
#if USE_TEXTURE
in vec2 uv0;
#pragma builtin(local)
layout(set = 2, binding = 10) uniform sampler2D cc_spriteTexture;
#endif
// 也可以通过代码对宏进行启/禁用
const meshRenderer = this.getComponent(MeshRenderer);
const mat = meshRenderer.material;
mat.recompileShaders({ USE_TEXTURE: true });
最终,尝试着剔除我们暂时用不到的部分,并且把 include 里可以提取的部分提取出来。
// Copyright (c) 2017-2020 Xiamen Yaji Software Co., Ltd.
CCEffect %{
techniques:
- passes:
- vert: sprite-vs:vert
frag: sprite-fs:frag
depthStencilState:
depthTest: false
depthWrite: false
blendState:
targets:
- blend: true
blendSrc: src_alpha
blendDst: one_minus_src_alpha
blendDstAlpha: one_minus_src_alpha
rasterizerState:
cullMode: none
}%
CCProgram sprite-vs %{
precision highp float;
#include
in vec3 a_position;
in vec2 a_texCoord;
in vec4 a_color;
out vec4 color;
out vec2 uv0;
vec4 vert () {
vec4 pos = vec4(a_position, 1);
pos = cc_matViewProj * pos;
uv0 = a_texCoord;
color = a_color;
return pos;
}
}%
CCProgram sprite-fs %{
precision highp float;
in vec4 color;
#if USE_TEXTURE
in vec2 uv0;
#pragma builtin(local)
layout(set = 2, binding = 10) uniform sampler2D cc_spriteTexture;
// 此处的 cc_spriteTexture 已经被 Sprite 组件使用,因此保留 layout 部分的声明
// 如果后续有自定义的 uniform,则可以不指定 layout
#endif
vec4 frag () {
vec4 o = vec4(1, 1, 1, 1);
#if USE_TEXTURE
// 直接取出 embedded-alpha 里 CCSampleWithAlphaSeparated 我们需要的那部分内容
o *= texture(cc_spriteTexture, uv0);
#endif
o *= color;
// alpha-test 内的 ALPHA_TEST 是一个和阈值进行判断然后丢弃片段的行为,在这里不需要,所以直接删除
return o;
}
}%
在上一章中我们也提到,Cocos Effect 所编写的 Shader 是无法单独使用的,必须要结合材质使用。材质是一个具备与光交互,供渲染器读取的数据集,可以为渲染器提供贴图纹理、光照算法等,可以直接将材质资源挂载到模型对象身上。材质可以持有多个 technique,但是在运行时有且仅能使用一个。材质包含的可配置参数如下:
effectAsset 或 effectName:effect 资源引用,指定使用哪个 EffectAsset 所描述的流程进行渲染。(必备)
technique:指定使用 EffectAsset 中的第几个 technique,默认为第 0 个。
defines:宏定义列表,指定开启哪些宏定义,默认全部关闭。
states:管线状态重载列表,指定对渲染管线状态(深度模板透明混合等)有哪些重载,默认与 effect 声明一致。
可以通过以下方式对材质初始化:
const mat = new Material();
mat.initialize({
effectName: 'pipeline/skybox',
defines: {
USE_RGBE_CUBEMAP: true
}
});
> 注意:此处不是说着色器一定会产生光影效果,只是说具备这样的能力,如果着色器里根本没有处理这些,是不会有光影效果的。
接着,在“层级管理器”右击创建一个材质资源 foo.mtl,材质的 Effect 关联上我们之前创建的 foo.effect,应用即可。然后,在场景创建 Sprite 节点,将材质拖入节点身上挂载的 Sprite 组件上的 CustomMaterial 属性上。观察 Sprite。此时会发现,Sprite 渲染的内容直接变成了一张白色图片。那是因为 Sprite 组件虽然会帮助上传贴图,但是由于我们自定义的材质没有启用 USE_TEXTURE 宏定义,所以那部分代码也没有被执行。因此,在编辑器上将宏定义启用即可。
这样,我们就成功的把 Sprite 组件内置的材质替换成了我们自定义的材质。在下一个章节里,我们尝试对自定义的材质做一些特殊处理,实现贴图溶解效果。
扩展知识
layout 布局限定符
这部分内容是属于 GLSL 的部分,既然这里有使用到,就简单地提一下。这里我特地做说明的理由是针对需要在原有的内置 shader 基础上做改造,了解什么部分需要保留而存在。在 Cocos Creator 3.0 里,用户新增的 uniform 或顶点属性都无需写这部分内容,而是由渲染底层自行处理。对于初学者来说,了解即可。
// 在之前 WebGL 里的内容,我们有提及在访问输入属性的时候,需要调用 gl.getAttribLocation 获取当前顶点属性的位置,然后再激活属性 gl.enableVertexAttribArray
// 这个位置默认是由着色器分配,当然我们也可以自己指定。
// 可以通过 gl 指令 gl.bindAttribLocation 按照指定的位置绑定,然后直接根据位置值激活属性 gl.enableVertexAttribArray
// 或者也可以通过 layout 布局限定符指定,location 就代表属性要设定位置。在两者同时设定的情况下,layout 优先级更高
layout(location = 2) in vec3 position;
// 这里只是简单列举了顶点属性的内存分配,uniform 那些也是可以指定内存分配
// Cocos Creator 内置两大数据内存结构,一个是 CCLocal 一个是 CCGlobal
// #pragma builtin(local) 代表申请 CCLocal 内存
// layout(set = 2, binding = 10) 代表绑定到 CCLocal 内存指定位置,跟 location 类似,只不过这是 GLSL 更高版本的使用方式,比低版本在指定上更加灵活
#pragma builtin(local)
layout(set = 2, binding = 10) uniform sampler2D cc_spriteTexture;
内容参考:
1. WebGL 基础:
https://webglfundamentals.org/webgl/lessons/zh_cn/webgl-fundamentals.html
2. WebGL API 对照表:
https://www.khronos.org/files/webgl/webgl-reference-card-1_0.pdf
3. OpenGL 中文文档:
https://learnopengl-cn.github.io/01%20Getting%20started/04%20Hello%20Triangle/
4. shader 案例:
https://www.shadertoy.com/browse
Cocos Shader 基础入门系列
···更新中···
该系列教程视频版
已更新至 EP06
欢迎点击【阅读原文】
前往 B 站观看交流 ↓
B 站关注「Cocos 引擎官方」
https://www.bilibili.com/video/BV1Cq4y1d726
往期精彩