前面在浏览器环境中基于 WebGL 的 GLSL 代码编写顶点(Vertex)和片元(Fragment)的 Shader 程序,在游戏引擎中,为了适配工业化制作流,提升着色器片段的易用性,往往会有一些额外的处理,本章将介绍在 cocos creator 中编写 Shader 程序,可以参考官方文档 https://docs.cocos.com/creator/3.8/manual/zh/shader/
在游戏引擎中,为了适配工业化制作流,提升着色器片段的易用性,以及更灵活地复用 GLSL 代码,封装的 Shader 中除了包含 GLSL 代码,还会包含参数配置信息和渲染状态开关等,方便开发者在引擎中可视化配置 Shader 参数以及从外部程序传入参数动态渲染。因此引擎的 Shader 为了区分普通 Shader,一般会有个别名,如 cocos 和 Unity3D 中称为 Effect,也有些引擎叫 FX。
Cocos Effect 的基本结构如下:
CCEffect %{
techniques:
- passes:
- vert: vs:vert # 指向 CCProgram unlit-vs 的 vert 入口函数
frag: fs:frag
}%
CCProgram vs %{
//...
vec4 vert () {
//
}
}%
CCProgram fs %{
//...
vec4 frag () {
//
}
}%
Cocos Effect 是 cocos creator 中一种基于 YAML 和 GLSL 的单源码嵌入式领域特定语言,包含 YAML 和 GLSL 两部分:
一个 YAML 可包含一套或多套 Technique 渲染方案,但每个 effect 同时只会激活一套 Technique,每套 Technique 方案包含一个或多个 Pass 融合,每个 Pass 就是一次 GPU 绘制,一般包括一对顶点着色器和片元着色器,有一些复杂效果需要使用 Paas 多次渲染同一个物体才能实现,大部分情况下只需要一个 Pass 即可,YAML 语法参考,而 Pass 中的各类参数可以参考 Pass参数
由 CCProgram 包裹的基于 GLSL 300es 格式的着色器 (shader) 片段,提供给 Pass 引用
在实际的复杂 Effect 开发中,一般会直接复制全局的 builtin-standard
进行修改,而非新建,这样能大大提高正确率。
关于 Effect 的绘制执行,需要注意,在前面章节中会编写一堆 WebGL 的绘制命令,而在 cocos 中创建 Canvas 节点时默认会创建一个 Camera 节点来自动调用绘制指令,其中 Camera 组件的 ClearFlags / ClearColor / Rect 属性分别对于 WebGL 中的 gl.viewport / gl.clear / gl.clearColor,其中 ClearFlags 的 SOLID_COLOR 模式会每帧清除屏幕内容,ClearColor 会在清屏后填充指定颜色,Rect 定义了屏幕空间视口,xy 值限制 -1~1,wh 值限制 0~1。
Cocos Effect 所编写的 Shader 须结合材质 material 使用,material 可以与光交互,可以为渲染器提供贴图纹理、光照算法等数据集。material 材质资源可以挂载到模型对象上使用,可以有多个 technique,但在运行时有且仅能使用一个。
接下来在 cocos 中使用 Cocos Effect 渲染一个无光照纯色球体。首先在 cocos 中创建 MaterialCtrl.effect
如下:
CCEffect %{
techniques:
- name: opaque # technique名称
passes:
- vert: unlit-vs:vert # 顶点着色器
frag: unlit-fs:frag # 片段着色器
properties: &props # 面板参数
a_color: { value: [1.0, 1.0, 1.0, 1.0], editor: { type: color } }
colorScale: { value: [1.0, 1.0, 1.0], target: colorScaleAndCutoff.xyz }
migrations: &migs
properties:
}%
CCProgram unlit-vs %{
// 定义默认精度
precision highp float;
#include <cc-global>
in vec3 a_position;
vec4 vert () {
// 获取顶点位置信息 本地坐标系
vec4 pos = vec4(a_position, 1);
// 本地空间 -> 世界空间 -> 视图空间 -> 投影(裁剪坐标)
pos = cc_matViewProj * pos;
return pos;
}
}%
CCProgram unlit-fs %{
precision highp float;
#include <output>
// Cocos Shader要求,常量需要定义在uniform-block常量块(uniform-block)中
uniform Constant {
vec4 a_color;
vec4 colorScaleAndCutoff; // 颜色调节参数,rgb为调节因子
};
vec4 frag () {
vec4 o = a_color;
o.rgb *= colorScaleAndCutoff.rgb;
return o;
}
}%
接着创建球体节点 Sphere
,再创建材质 MaterialCtrl.mtl
并绑定 MaterialCtrl.effect
,即可在编辑器中看到渲染结果。
properties: &props
用于定义Inspector面板上可操作的参数,可以定义向量、颜色、贴图、整数,支持在面板中调整参数并实时预览:
若需要在运行过程中修改参数,可以在节点上绑定脚本 MaterialCtrl.ts
如下:
// 应用于 Sphere
import { _decorator, Component, MeshRenderer, Color, Vec4 } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('MaterialCtrl')
export class MaterialCtrl extends Component {
start() {
const target = this.node.getComponent(MeshRenderer);
const color = new Vec4(0.6, 0.6, 1, 1.0); // 不使用Color,因为Color的颜色分量 [0,255]
target.sharedMaterial.setProperty('a_color', color);
const colorScaleAndCutoff = new Vec4(1.0, 1.0, 1.0, 1.0);
target.sharedMaterial.setProperty('colorScaleAndCutoff', colorScaleAndCutoff);
}
}
在游戏开发中经常会需要在已有模型中融合一些特效贴图的效果,如下图所示,可以通过增加一个贴图参数,如 detailTexture
,再配合一些简单的运算轻松实现,可以参考 PS 中的图层的混合参数(Blending)。
CCEffect %{
techniques:
- name: opaque
passes:
- vert: legacy/main-functions/general-vs:vert # builtin header
frag: unlit-fs:frag
properties: &props
mainTexture: { value: white }
detailTexture: { value: black }
method: { value: 0 }
mainColor: { value: [1, 1, 1, 1], editor: { type: color } }
- name: transparent
passes:
- vert: legacy/main-functions/general-vs:vert # builtin header
frag: unlit-fs:frag
blendState:
targets:
- blend: true
blendSrc: src_alpha
blendDst: one_minus_src_alpha
blendSrcAlpha: src_alpha
blendDstAlpha: one_minus_src_alpha
properties: *props
}%
CCProgram unlit-fs %{
precision highp float;
#include <legacy/output>
#include <legacy/fog-fs>
in vec2 v_uv;
in vec3 v_position;
uniform sampler2D mainTexture;
uniform sampler2D detailTexture;
uniform Constant {
vec4 mainColor;
int method;
};
vec4 frag () {
vec4 col = mainColor * texture(mainTexture, v_uv);
vec4 detailColor = texture(detailTexture,v_uv);
if(method == 0){
//直接相加
col.rgb += detailColor.rgb;
}
else if(method == 1){
//直接相乘
col.rgb *= detailColor.rgb;
}
else if(method == 2){
//插值混合
col.rgb = mix(col.rgb,detailColor.rgb,0.618);
}
else if(method == 3){
//加权求和
col.rgb = col.rgb * 0.2 + detailColor.rgb * 1.2;
}
else if(method == 4){
//灰度混合
float gray = detailColor.r * 0.299 + detailColor.g * 0.587 + detailColor.b * 0.114;
col.rgb = mix(col.rgb, detailColor.rgb * 1.5, gray);
}
else if(method == 5){
//相减
col.rgb = col.rgb - detailColor.rgb;
}
CC_APPLY_FOG(col, v_position);
return CCFragOutput(col);
}
}%
使用上面的知识,已经可以尝试实现游戏特效中最常见的溶解特效了!
在游戏开发中,会经常使用噪声图实现一些特殊的效果,从程序的角度来看,黑白噪声图可以向程序中引入一些随机变量。借助一张噪声贴图采样得到的随机变量和一个溶解阈值参数即可控制溶解效果。
CCEffect %{
techniques:
- name: test # technique名字
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: # Pass面板上的配置参数
u_dissolveMap: { value: white, editor: { tooltip: '噪声贴图' } }
dissolveThreshold: { value: 0.5, editor: { range: [0, 1, 0.01], slide: true, tooltip: '溶解阈值' } }
}%
CCProgram sprite-vs %{
precision highp float;
#include <cc-global>
in vec3 a_position; // GLSL es300 中使用 in / out 关键字代替 attribute / varing,但旧关键字仍然兼容
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;
uniform Dissolve{ // Cocos Shader 规定,所有非 sampler 类型的 uniform 都应以 UBO(Uniform Buffer Object/Uniform Block)形式声明
float dissolveThreshold; // 溶解阈值[0, 1]
};
#if USE_TEXTURE
in vec2 uv0;
uniform sampler2D u_dissolveMap; // 噪声图纹理
#pragma builtin(local)
layout(set = 2, binding = 10) uniform sampler2D cc_spriteTexture; // 新版内存分配
#endif
vec4 frag () {
vec4 o = vec4(1, 1, 1, 1);
float value = 1.0;
#if USE_TEXTURE
vec4 dissolveMap = texture(u_dissolveMap, uv0);
value *= dissolveMap.r; // 取贴图 r 分量
#endif
if (value < dissolveThreshold) {
discard; // 小于阈值的片段丢弃,形成溶解
}
#if USE_TEXTURE
o *= texture(cc_spriteTexture, uv0); // 与原纹理混合
#endif
o *= color;
if (value < dissolveThreshold + 0.05) {
o = vec4(0.9, 0.6, 0.3, o.a); // 溶解的边缘设置边缘过度色
}
return o;
}
}%
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。