CocosCreator Shader Effect 入门

这是一篇基于CocosCreator2.x版本的科普

一、前言

CocosCreator在2.0版本重写了底层渲染器,并逐步在后续版本中加入了对材质系统的支持。我们现在可以很方便的通过编写组合不同的 Effect 和材质,并关联到对应的渲染组件上,实现丰富的渲染效果表现;

本文基于CocosCreator2.x版本,主要以讲解 CocosCreator Shader Effect 知识为主,相关涉及到的知识点包括:WebGL、GLSL ES、YAML、ShaderToy 等等;由于涉及到的知识点非常多,所以只会介绍编写 Creator Effect 时所必须了解的内容,其它相关的知识只做简单说明不会展开太多,需要了解更详细的相关资料请点击参考部分链接;

因为TypeScript可以很方便的写出WebGL渲染示例,同时也是Creator引擎采用的标准开发脚本,所以本文所有示例代码都是基于WebGL且使用TypeScript编码,并在Creator2.3.3版本的Web环境下调试通过;

在学习Creator Effect 之前,我们先来了解一下Creator渲染基础WebGL部分的相关知识,这样能更好地理解本文后续的内容;

二、WebGL 渲染简介

  • OpenGL、OpenGL ES 和 WebGL

OpenGL 是一套跨平台的底层图形渲染API,几乎所有的游戏引擎都支持它;

OpenGL ES 是OpenGL为嵌入式和移动系统派生的轻量化子集;

WebGL 是基于OpenGL ES的跨平台Web标准API,并通过HTML5 Canvas元素公开给ECMAScript;

OpenGL 从2.0开始支持可编程渲染管线。OpenGL ES 则是使用OpenGL着色器语言(GLSL)基于Shader的API。WebGL 在语义上与基础OpenGL ES API相似,非常接近OpenGL ES规范,并为JavaScript之类的内存管理语言做了进一步的兼容;WebGL 1.0公开了OpenGL ES 2.0的API;WebGL 2.0公开了OpenGL ES 3.0的API;

WebGL的渲染流程简单概括如下图:

  • 一个简单的WebGL示例
/**
 *  使用 WebGL 着色器代码在屏幕中心画一个红点
 */

// 顶点着色器代码

let VS =
    "attribute vec4 a_Position; \n" +    // 外部传入坐标信息

    "void main() { \n" +
    "  gl_Position = a_Position; \n" +   // 赋值给内置变量,控制每一个点
   "  gl_PointSize = 10.0; \n" +        // 赋值给内置变量,控制所有的点
    "} \n"

// 片元着色器代码
let FS =
    "precision mediump float; \n" +      // 指定变量精度
    "uniform vec4 u_FargColor; \n" +     // 外部传入颜色信息
    "void main() { \n" +
    "  gl_FragColor = u_FargColor; \n" + // 赋值给内置变量,控制每个片元的颜色
    "} \n"

// 主函数
function main() {
    // 获取Canvas
    let canvas = document.getElementById("webgl")
    // 获取WebGL上下文
    let gl = getWebGLContext(canvas)
    // 初始化定义的着色器代码
    if (initShaders(gl, VS, FS)) {
        // 绑定着色器开放的变量地址
        let a_Position = gl.getAttribLocation(gl.program, "a_Position")
        let u_FargColor = gl.getUniformLocation(gl.program, "u_FargColor")
        // 指定颜色清屏
        gl.clearColor(0.0, 0.0, 0.3, 1.0)
        gl.clear(gl.COLOR_BUFFER_BIT)
        // 将值传递给着色器变量
        gl.vertexAttrib3f(a_Position, 0.0, 0.0, 0.0)
        gl.uniform4f(u_FargColor, 1.0, 0.0, 0.0, 1.0)
        // 绘制一个点
        gl.drawArrays(gl.POINTS, 0, 1)
    }
}

示例代码分成两部分,通过调用WebGL的API代码部分(main函数部分)和GLSL的着色器代码部分(VS、FS变量定义部分)。API调用部分完成设备、缓冲初始化,参数传递和具体绘制操作;着色器部分则完成渲染过程中具体的表现效果实现;完整代码请查看本文参考部分,限于篇幅原因我们将主要介绍着色器代码;

着色器(Shader)代码分为两部分,都是基于GLSL ES着色器语言编写的代码段;

  1. 顶点着色器:处理渲染过程中顶点的特性,计算顶点的位置和大小;
  2. 片元着色器:处理渲染过程中每个片元所需要呈现的表现效果,比如颜色;(片元可以理解为像素)
  • GLSL ES 常用语法简介

本文只介绍部分在编写Creator Effect 时会涉及到的常用的GLSL ES 语法、变量和函数,更详细的相关资料请点击参考部分链接;

  1. 大小写敏感
Test1     // 两个不同的变量
test1
2. 每条语句都以分号;结束
float ftest = 1.0;    // 定义一个浮点数
3. 每个着色器程序都要有main()入口函数,并且必须是void类型;
void main () {...}
4. 单行注释使用 //,多行注释则是/* 注释 */
// 单行注释
/* 多行注释*/
5. 支持基本数据类型,但不支持String
// 支持 float、int、bool 等基本类型
float fnumber = 1.1;     // 浮点数
int inumber = 1;	     // 整型
bool blogic = true;	     // 布尔型

// 强制类型转换
int fresult = int(fnumber); // 去掉小数部分,例如:3.14转换为3
bool bresult = int(blogic); // true转为1,false转为0

int iresult = float(inumber); // int转换为float,例如:3转换为3.0
bresult = float(blogic); // bool转换为float,true转为1.0,false转为0.0

iresult = bool(inumber); // int转换为bool,0转为false,其余非0数转为true
fresult = bool(fnumber); // int转换为bool,0.0转为false,其余非0.0数转为true
6. 支持矢量类型和矩阵类型;
// ------------------矢量
// 定义构造具有2、3、4个浮点数元素的矢量类型
vec2 v2 = vec2(1.0, 1.0);
vec3 v3 = vec3(1.0, 1.0, 1.0);
vec4 v4 = vec4(1.0, 1.0, 1.0, 1.0);

// 还可以使用:ivec2/ivec3/ivec4,bvec2/bvec3/bvec4 定义int和bool矢量类型
// 按矢量类型定义不同(vec2/vec3/vec4),访问矢量元素分量值

v4.x = 1.0;   // 可以通过 x/y/z/w 方式获取矢量的顶点坐标分量
v4.r = 1.0;   // 可以通过 r/g/b/a 方式获取矢量的颜色分量
v4.s = 1.0;  // 可以通过 s/t/p/q 方式获取矢量的纹理坐标分量
vec2 = v4.xy; // 混合访问矢量的顶点x和y坐标分量
vec3 = v4.rgb; // 混合访问矢量的颜色r,g,b色值分量

// 矢量运算
vec2 v2_1 = vec2(2.0, 3.0);
v2 = v2 + v2_1;	// (v2.x + v2_1.x, v2.y + v2_1.y)
v2 = v2 + 2.0;	// (v2.x + 2.0, v2.y + 2.0)

// ------------------矩阵
// 定义构造具有 2x2、3x3、4x4的浮点数元素的矩阵

mat2 m2 = mat2(1.0, 1.0,
               1.0, 1.0);

mat3 m3 = mat3(1.0, 1.0, 1.0,
               1.0, 1.0, 1.0,
               1.0, 1.0, 1.0);

mat4 m4 = mat4(1.0, 2.0, 3.0, 4.0,
               1.0, 1.0, 1.0, 1.0,
               1.0, 1.0, 1.0, 1.0,
               1.0, 1.0, 1.0, 1.0);

// 矩阵取值

vec4 v4 = m4[0];
float mx_1 = m4[0].x; 
float mx_2 = m4[0][1];	

// 矩阵运算
mat2 m2_1, m2_2;
vec2 v2;
float f;

// 加法
m2_1 + f; // m2_1[0].x + f; m2_1[0].y + f; 
          // m2_1[1].x + f; m2_1[1].y + f;

// 矩阵右乘矢量
vec2 v2_r = m2_1 * v2; // v2_r.x = m2_1[0].x * v2.x + m2_1[1].x * v2.y;
			    // v2_r.y = m2_1[0].y * v2.x + m2_1[1].y * v2.y;

// 矩阵左乘矢量
v2_r = v2 * m2_1; // v2_r.x = v2.x * m2_1[0].x + v2.y * m2_1[0].y;
		       // v2_r.y = v2.x * m2_1[1].x + v2.y * m2_1[1].y;

// 矩阵相乘
mat2 m2_r = m2_1 * m2_2;	// m2_r[0].x = m2_1[0].x * m2_2[0].x + m2_1[1].x * m2_2[0].y;
// m2_r[1].x = m2_1[0].x * m2_2[1].x + m2_1[1].x * m2_2[1].y;
// m2_r[0].y = m2_1[0].y * m2_2[0].x + m2_1[1].y * m2_2[0].y;
// m2_r[1].y = m2_1[0].y * m2_2[1].x + m2_1[1].y * m2_2[1].y;
7. 支持结构体定义
// 定义结构体
struct light {
  vec4 color;
  vec3 position;
}

// 声明light类型变量
light light_1;
8. 变量限定符:限定符赋给变量特殊的含义
  • const 用于声明非可写的编译时常量变量

  • attribute 用于经常更改的信息,只可以在顶点着色器中使用

  • uniform 用于不经常更改的信息,可同时用于顶点着色器和片元着色器

  • varying 用于顶点着色器传递到片元着色器的插值信息

    1. 支持取样器
/* GLSL ES有一种内置类型称作取样器(sampler),我们必须通过此类型变量访问纹理,
* 并且取样器变量只能是uniform变量。取样器有两种基本类型:sampler2D和samplerCube
*/

uniform sampler2D u_Sampler;
sampler1D     访问一个一维纹理
sampler2D     访问一个二维纹理
sampler3D     访问一个三维纹理
samplerCube  访问一个立方体纹理
sampler1DShadow 访问一个带对比的一维深度纹理
sampler2DShadow 访问一个带对比的二维深度纹理
10. 精度定义
//包括 lowp highp mediump 三种精度,例如:
// 在数据定义前指定变量精度
highp vec4 position;

// 在 Shader 代码的开始处指定默认精度
precision highp float;

/* 在顶点 Shader 中,如果没有默认的精度,则 float 和 int 精度都为 highp
* 在片元 Shader 中,float 没有默认的精度,所以必须在片元 Shader中为 float 
* 指定一个默认精度或为每个 float 变量指定精度。
*/
  • GLSL ES 常用内置的变量与函数

    1. 常用内置变量:
gl_Position    // 用于vertex shader, 写顶点位置;被图元收集、裁剪等固定操作功能所使用;
gl_PointSize   // 用于vertex shader, 写光栅化后的点大小,像素个数;
gl_FragColor   // 用于Fragment shader,写fragment color;被后续的固定管线使用;
gl_FragData    // 用于Fragment shader,是个数组,写gl_FragData[n] 为data n;被后续的固定管线使用;
gl_FragCoord   // 用于Fragment shader,只读, Fragment相对于窗口的坐标位置 x,y,z,1/w; 这个是固定管线图元差值后产生的;z 是深度值; 
gl_FrontFacing // 用于判断 fragment是否属于 front-facing primitive;只读;
gl_PointCoord  // 仅用于 point primitive;     
2. 常用内置函数:
// 弧度计算
radians(degree) : 角度变弧度;
degrees(radian) : 弧度变角度;
sin(angle), cos(angle), tan(angle)
asin(x): arc sine, 返回弧度 [-PI/2, PI/2];
acos(x): arc cosine,返回弧度 [0, PI];
atan(y, x): arc tangent, 返回弧度 [-PI, PI];
atan(y/x): arc tangent, 返回弧度 [-PI/2, PI/2];

// 数学计算
pow(x, y): x的y次方;
exp(x): 指数, log(x):
exp2(x): 2的x次方, log2(x):
sqrt(x): x的根号; inversesqrt(x): x根号的倒数
abs(x): 绝对值
sign(x): 符号, 1, 0 或 -1

// 取值
floor(x): 底部取整
ceil(x): 顶部取整
fract(x): 取小数部分
mod(x, y): 取模, x - y*floor(x/y)
min(x, y): 取最小值
max(x, y): 取最大值
clamp(x, min, max):  min(max(x, min), max);
mix(x, y, a): x, y的线性混叠, x(1-a) + y*a;
step(edge, x): 如 x

// 向量计算
length(x): 向量长度
distance(p0, p1): 两点距离, length(p0-p1);
dot(x, y): 点积,各分量分别相乘 后 相加
cross(x, y): 差积: x[1]*y[2] - y[1]*x[2], 
                    x[2]*y[0] - y[2]*x[0], 
                    x[0]*y[1] - y[0]*x[1]
normalize(x): 归一化, length(x)=1;
faceforward(N, I, Nref): 如 dot(Nref, I)< 0则N, 否则 -N
reflect(I, N): I的反射方向, I -2*dot(N, I)*N, N必须先归一化
refract(I, N, eta): 折射,k=1.0-eta*eta*(1.0 - dot(N, I) * dot(N, I)); 
                    如k<0.0 则0.0,否则 eta*I - (eta*dot(N, I)+sqrt(k))*N
matrixCompMult(matX, matY): 矩阵相乘, 每个分量 自行相乘;
                            即 r[i][j] = x[i][j]*y[i][j];矩阵线性相乘,直接用 * 
lessThan(vecX, vecY): 向量 每个分量比较 x < y
lessThanEqual(vecX, vecY): 向量 每个分量比较 x<=y
greaterThan(vecX, vecY): 向量 每个分量比较 x>y
greaterThanEqual(vecX, vecY): 向量 每个分量比较 x>=y
equal(vecX, vecY): 向量 每个分量比较 x==y
notEqual(vecX, vexY): 向量 每个分量比较 x!=y
any(bvecX): 只要有一个分量是true, 则true
all(bvecX): 所有分量是true, 则true
not(bvecX): 所有分量取反
  • GLSL2.0 ES 与 GLSL3.0 ES 的区别

CocosCreator 支持 GLSL3.0 ES 标准,和2.0相比差异为:

  • attribute 和 varying 使用 in 和 out 来代替
  • 头文件多了个 #version 300 es
  • 纹理 texture2D 和 texture3D 统统改为 texture
  • 内置函数gl_FragColor和gl_FragData删除,如果片段着色器要输出用 out 声明字段输出。不过保留了 gl_Position
  • layout,可直接指定位置

三、CocosCreator 渲染简介

  • 纹理、材质、Effect

在Creator编辑器中,创建一个Sprite精灵,然后拖一个贴图到精灵的SpriteFrame属性中,纹理和材质的协同工作表现如下图:

纹理是所需要渲染的内容,材质是渲染所必须的配置,Effect则是渲染要呈现的效果所采用的方法;

项目中所有可用的Effect都会显示在材质面板的Effect属性下拉列表中,包括内置Effect。编辑器内置的 Effect 资源全部位于 Internal DB 的 assets/effects 目录下;

Effect定义代码块中 properties 部分会被解析显示到材质面板上,包括:宏定义、out 变量、UBO结构等等;

  • CocosCreator 渲染组件

Creator2.0版本以后,所有支持渲染的组件都从RenderComponent继承,

我们以cc.Sprite举例说明:

/* 该组件用于在场景中渲染精灵。 */
export class Sprite extends RenderComponent implements BlendFunc {

在RenderComponent组件定义中,定义了一个材质数组

/**所有支持渲染的组件的基类 */
    export class RenderComponent extends Component {        
        /** 
        !#en The materials used by this render component.
        !#zh 渲染组件使用的材质。 */
        sharedMaterials: Material[];        
        /**
        !#en Get the material by index.
        !#zh 根据指定索引获取材质
        @param index index 
        */
        getMaterial(index: number): MaterialVariant;        

        /**
        !#en Gets all the materials.
        !#zh 获取所有材质。 
        */
        getMaterials(): MaterialVariant[];      
        /**
        !#en Set the material by index.
        !#zh 根据指定索引设置材质
        @param index index
        @param material material 
        */
        setMaterial(index: number, material: Material): Material;   
    }   

当组件设置指定材质后(setMaterial方法),最终会通过 RenderFlow.visitRootNode 方法,调用到 RenderComponent._checkBacth 方法

RenderFlow.visitRootNode = function (rootNode) {
    RenderFlow.validateRenderers();    
    _cullingMask = 1 << rootNode.groupIndex;
    if (rootNode._renderFlag & WORLD_TRANSFORM) {
        _batcher.worldMatDirty ++;
        rootNode._calculWorldMatrix();
        rootNode._renderFlag &= ~WORLD_TRANSFORM;
        flows[rootNode._renderFlag]._func(rootNode);
        _batcher.worldMatDirty --;
    }
    else {
        flows[rootNode._renderFlag]._func(rootNode);
    }
};

_checkBacth 方法中,会将材质数组的第0个材质配置提交到渲染主流程中;

_checkBacth (renderer, cullingMask) {
        let material = this._materials[0];
        if ((material && material.getHash() !== renderer.material.getHash()) || 
            renderer.cullingMask !== cullingMask) {
            renderer._flush();

            renderer.node = material.getDefine('CC_USE_MODEL') ? this.node : renderer._dummyNode;
            renderer.material = material;
            renderer.cullingMask = cullingMask;
        }
}

在介绍完渲染基础和渲染组件后,我们终于可以正式的开始学习 Creator Effect 的语法和结构了;

四、Creator Effect 语法

官网有一个详细的 Effect 结构描述图,为了便于理解,我们可以简单的把Effect结构分成如下图展示的几部分;

image

  • CCEffect部分结构

CCEffect部分是基于YAML语言编写,用于声明特效的流程控制清单;YAML 是一种数据序列化语言,语法简洁直观,和Python类似使用缩进来表达语法的层次结构,更多应用于编写配置文件;

Creator官方表示支持符合 YAML 1.2 标准的解析器,意味着CCEffect部分可直接使用JSON格式编写;但是官方仍推荐使用YAML编写,因为更简单直观;

YAML语法简介:

  1. 大小写敏感;
Test: 1   // 两个不同的变量
test: 2
2. 用缩进表示层级关系,同级必须对齐;缩进只能使用空格,不能使用TAB字符;
# YAML
one:
  two: 2
  three:
    four: 4
    five: 5
// 以上的内容转成 JSON 后
"one": {
  "two": 2,
  "three": {
    "four": 4,
    "five": 5 
  }
}
3. 使用#表示注释
# 我是注释
4. 支持以键值对方式(key : value)的对象定义,使用“冒号+空格”来区分键与值;
# YAML
key:
  child-key1: value1
  child-key2: value2

// JSON
"key": {
  "child-key1": "value1",
  "child-key2": "value2",
}

# 另一种对象定义方法
# YAML
key: { child-key1: value1, child-key2: value2 }
// JSON
"key": { "child-key1": "value1", "child-key2": "value2" }

5. 支持一维或多 维数组;
# 一维数组定义
# YAML
values:
  - value1
  - value2
  - value3

// JSON
"values": [ "value1", "value2", "value3" ]

# 另一种一维数组定义
# YAML
values: [value1, value2, value3]

// JSON
"values": [ "value1", "value2", "value3" ]

# ■■数组定义(使用缩进表示层级关系)
# YAML
values:
  -
    - value1
    - value2
  -
    - value3
    - value4

// JSON
"values": [ [ "value1", "value2"], ["value3", "value4"] ]
6. 支持:String、Boolean、Integer、Float、Null 等数据类型;
# -------------字符串定义可支持使用单双引号包裹-------------
# YAML
strings:
  - Hello without quote # 不用引号包裹
  - 'He said: "Hello!"' # 单双引号支持嵌套"

// JSON
"strings":
  [ "Hello without quote", "He said: 'Hello!'" ]

# -------------Boolean 定义-------------
# YAML
boolean:
  - true # True、TRUE
  - yes # Yes、YES
  - false # False、FALSE
  - no # No、NO
// JSON
"boolean": [ true, true, false, false ]

# -------------Integer 定义-------------
# YAML
int:
  - 666
  - 0001_0000 # 二进制表示
// JSON
"int": [ 666, 4096 ]

# -------------Float 定义-------------
# YAML
float:
  - 3.14
  - 6.8523015e+5 # 使用科学计数法
// JSON
"float": [ 3.14, 685230.15 ]

# -------------Null 定义-------------
# YAML
nulls:
  - null
  - Null
  - ~
  -
// JSON
"nulls": [ null, null, null, null ]

本文仅介绍Creator Effect 编写过程中部分经常使用到的YAML语法,更详细的相关资料请点击参考部分链接;

CCEffect示例:

CCEffect %{             # 该部分定义体现在属性检查器上,以YAML格式编写
  techniques:           # 支持多段定义,在属性面板上通过选择框选择,参见下图1
  - name: test1         # 每段 technique 的名称
    passes:             # 每个 passes 部分都是完整的渲染管线定义,一个 technique 支持多个 passes
    - vert: vs:main1    # 顶点 Shader 的名称,格式:"Shader名称:入口函数名称",如果入口函数省略,则默认为 main
      frag: fs:main1    # 片元 Shader 的名称,格式定义与上面相同
      blendState:       # 混合模式设置,该部分参数详见官方文档 Pass Params 部分定义
        targets:
        - blend: true
      rasterizerState:
        cullMode: none
      properties:       # 属性定义,可以在属性检查器面板上体现并编辑的属性,一般用于配置uniform 
        texture: { value: white }
        alphaThreshold: { value: 0.5 }
        # inspector 用于属性检查器面板的显示定义,包括显示名称、提示、类型、设定区间;
        # 编辑器会根据不同类型显示并使用不同的输入设置
        outlineSize: { value: 1, inspector: { displayName: "描边大小", tooltip: "描边大小", range: [1.0, 10.0] } }
        outlineColor: { value: [0.5, 0.5, 0.5, 0.5], inspector: { displayName: "描边大小", tooltip: "描边颜色", type: color } }
        testCheck: { value: true, inspector: { displayName: "选择框", tooltip: "测试选择框", type: boolean }}
        testPos: { value: [1.0, 2.0], inspector: { displayName: "坐标", tooltip: "测试坐标" }}
  - name: test2         # 第二段 technique
    passes:
    - vert: vs_2        # 该段顶点和片元使用默认的 main 入口函数
      frag: fs_2
    # .... 以下省略   

passes中顶点片元的片段名可以是当前文件中的片段名称,也可以是引擎提供的标准头文件,但是片段名不能是main,因为main已经被指定为片段名的默认函数入口名称;

passes 部分可选配置的参数有很多,但只能和官方文档的材质->Pass Params 部分列表定义一一对应;更详细的相关内容请查看参考部分链接;

  • CCProgram部分结构

CCProgram部分是符合GLSL3 ES语法格式的Shader片段,一个Effect文件中可包含多段CCProgram定义;CCProgram包含顶点Shader和片元Shader两种,在CCEffect的Pass部分会定义当前引用的顶点和片元Shader ;

基础说明:

在CCProgram中可以使用类似 C/C++ 的头文件 include 机制引入其它的代码片段;

// 注:CCEffect 使用 # 注释,CCProgram 中的注释则是使用 //
// 引擎代码片段一般存放在路径: resources\engine\cocos2d\renderer\build\chunks

#include <cc-global>     // 引入引擎的cc-global代码
#include "../test.chunk" // 引入自定义的代码

Creator预先定义了一系列的:预处理宏、工具函数和内置 Uniform 变量;Uniform 变量定义的明细可查看官方文档,或在引擎工程下全局查找变量名称,可以获得详细定义;

所有的宏定义会默认在属性编辑器上体现,以虚线框标注,方便使用调整;

多次引用的头文件、宏和工具函数,只会在编译后的Shader文件中展开一次,在Creator编辑器中选中Effect文件后,属性检查器会显示Effect源代码,以及展开后的顶点和片元代码;

例如:Effect中使用的 texture() 函数,在展开后变成了 texture2D();

Creator 在加载 Effect 时,会默认填充传递顶点和纹理数据到相关的 Shader 片段中;所以我们不用手动定义和传递纹理相关的参数;

下图为顶点 Shader 中的:a_position、a_color、a_uv0 参数的定义和展开

下图为顶点Shader中 cc_matViewProj 参数的定义和展开

外部参数赋值过程,请查看引擎源码

UBO内存布局:

UBO (Uniform Block Object)是OpenGL 3.1 Core中引入的概念。基于UBO的技术实现,可以多个Shader共享一组缓存的 Uniform 变量,从而简化着色器中变量的传递和共享,减少参数传递的开销以达到节省性能的目的。

Creator规定在 Shader 中所有非 sampler 的 Uniform 都应遵循UBO并以 block 形式声明;如下图所示,CCEffect 部分 properties 中定义的 Uniform 外部参数,在片元Shader 中以UBO 形式申明后才能在片段代码中获得并使用;

同时,对于所有 UBO的定义有以下要求:

  • 不应出现 vec3 成员;
  • 对数组类型成员,每个元素 size 不能小于 vec4;
  • 不允许任何会引入 padding 的成员声明顺序。

对于不符合要求的定义,任意长度小于 vec4 类型的数组和结构体,都会逐元素补齐至 vec4,并且UBO成员偏移会按自身所占字节数进行对齐;

// 未对齐定义
uniform IncorrectUBOOrder {
  float f1_1; // 偏移量0,长度4(对齐到4字节)
  vec2 v2;    // 偏移量8,长度8(对齐到8字节) [成员 f1_1 隐式填充至8字节]
  float f1_2; // 偏移量16,长度4(对齐到4字节)
};            // 合计32个字节长度(与平台相关)
// 已对齐定义
uniform CorrectUBOOrder {
 float f1_1; // 偏移量0,长度4(对齐到4字节)
 float f1_2; // 偏移量4,长度4(对齐到4字节)
 vec2 v2;    // 偏移量8,长度8(对齐到8字节)
};           // 合计16个字节长度

代码示例:

// Creator自带的置灰的 Shader
// 顶点 Shader
CCProgram vs %{
  // 设置精度
  precision highp float;    
  // 引入引擎公共 Shader 代码片段
  #include <cc-global>
  // 引擎传入的顶点信息
  in vec3 a_position;
  in mediump vec2 a_uv0;   // 顶点uv
  out mediump vec2 v_uv0;  // uv数据需要传递到片元 Shader,所以要以out 定义; 
                           // 该定义必须与片元 Shader 中定义一一对应;
  // 引擎传入的顶点颜色信息
  in vec4 a_color;         // 顶点颜色
  out vec4 v_color;        // 与 v_uv0 一样,传递到片元的数据
  // 顶点 Shader 入口
  void main () {
    // 转换每个点的纹理坐标到GL
    gl_Position = cc_matViewProj * vec4(a_position, 1);
    // 将外部每个点的UV和颜色保存传递给片元 Shader 
    v_uv0 = a_uv0;         
    v_color = a_color;
  }
}%

// 片元 Shader,获得光栅化后的片元(像素)信息
CCProgram fs %{
  // 设置精度
  precision highp float;
  // 引入引擎纹理 Shader 代码片段
  #include <texture>
  // 外部传入的纹理数据
  uniform sampler2D texture;
  // 从顶点 Shader 传入的数据,该定义必须与顶点 Shader 中一一对应
  // 这里获得的是经过光栅化后片元(像素)的uv和Color插值数据
  in mediump vec2 v_uv0;
  in vec4 v_color;
  void main () {
    vec4 color = v_color;
    CCTexture(texture, v_uv0, color);
    // 将色值转换成灰度
    float gray = 0.2126*color.r + 0.7152*color.g + 0.0722*color.b;
    gl_FragColor = vec4(gray, gray, gray, color.a);
  }
}%

五、简单的示例

  • 描边 Shader 示例

实现效果

代码片段

// 实现描边效果  
CCEffect %{
  techniques:
  - passes:
      properties:
        # 描边开放参数,其它参数定义暂略
        outlineSize: { value: 2, inspector: { tooltip: "描边大小", range: [1.0, 10.0] } }
        outlineColor: { value: [0.5, 0.5, 0.5, 0.5], inspector: { tooltip: "描边颜色", type: color } }
}%
CCProgram vs %{
    // 主要基于片元Shader实现效果,顶点Shader代码略
}%
// 片元 Shader
CCProgram fs %{
  precision highp float;
  
  #include <alpha-test>
  #include <texture>
  #include <cc-global>
  #include <common>
  in vec4 v_color;
  #if USE_TEXTURE
    in vec2 v_uv0;
    uniform sampler2D texture;
  #endif
  uniform Constant {        // 开放参数
    vec4 outlineColor;      // 描边颜色
    float outlineSize;      // 描边宽度,以像素为单位
  };
  // 纹理大小(宽和高),为了计算周围各点的纹理坐标,必须传入它,
  // 因为纹理坐标范围是0~1,测试代码,省略从外部参数传入过程;
  const vec2 textureSize = vec2(200, 200); 
  // 判断在这个角度上距离为outlineSize那一点是不是透明
  int getIsStrokeWithAngel(float angel)
  {
    int stroke = 0;
    // 这个浮点数是 pi / 180,角度转弧度
    float rad = angel * 0.01745329252; 
    // outlineSize * cos(rad)可以理解为在x轴上投影,或取得当前距离uv坐标outlineSize远的一个点
    // 除以textureSize.x是因为texture2D接收的是一个0~1的纹理坐标,而不是像素坐标
    float sx = v_uv0.x + outlineSize * cos(rad) / textureSize.x;
    float sy = v_uv0.y + outlineSize * sin(rad) / textureSize.y;
    // 把alpha值大于0.5都视为不透明,小于0.5都视为透明
    float a = texture(texture, vec2(sx, sy)).a; 
    if (a >= 0.5)
    {
      stroke = 1;
    }
    return stroke;
  }
  void main () {
    vec4 texel = vec4(1);
    #if USE_TEXTURE
      CCTexture(texture, v_uv0, texel);
    #endif
    #if USE_TEXTURE
      // 小于0.5都视为透明
      if (texel.a < 0.5) {    
        // for循环在安卓某些机型(如小米4)上会直接崩溃,
        // 查找资料发现OpenGL es并不是很支持循环,低端机型 while和for 都不要用
        int strokeCount = 0;
        strokeCount += getIsStrokeWithAngel(0.0);
        strokeCount += getIsStrokeWithAngel(30.0);
        strokeCount += getIsStrokeWithAngel(60.0);
        strokeCount += getIsStrokeWithAngel(90.0);
        strokeCount += getIsStrokeWithAngel(120.0);
        strokeCount += getIsStrokeWithAngel(150.0);
        strokeCount += getIsStrokeWithAngel(180.0);
        strokeCount += getIsStrokeWithAngel(210.0);
        strokeCount += getIsStrokeWithAngel(240.0);
        strokeCount += getIsStrokeWithAngel(270.0);
        strokeCount += getIsStrokeWithAngel(300.0);
        strokeCount += getIsStrokeWithAngel(330.0);
        // 四周围至少有一个点是不透明的,这个点要设成描边颜色
        if (strokeCount > 0) 
        {
          texel = outlineColor;
        }
      }
    #endif
    texel *= v_color;
    gl_FragColor = texel;
  }
}%

六、ShaderToy 学习简介

ShaderToy 是一个基于WEBGL使用片元 Shader 编程、展示、分享的网站,ShaderToy 按字面意直接翻译过来就是着色器玩具。网站地址:

https://www.shadertoy.com/

如下图所示,代码入口函数为:mainImage,红色框中为 Shader 所需要依赖的参数:

常用参数常量定义:

uniform vec4 fragColor;             // 计算出的像素颜色
uniform vec4 fragCoord;             // 像素坐标
uniform vec3 iResolution;           // 窗口分辨率,单位像素
uniform float iTime;                // 程序运行的时间,单位秒
uniform float iTimeDelta;           // 渲染时间,单位秒
uniform float iFrame;               // 帧率
uniform vec4 iMouse;                // 鼠标位置
uniform vec4 iDate;                 // 日期(年,月,日,时)

基本上,只要做一些很小的改动,就能够在 Creator 里使用这些Shader,主要就是将上面介绍的 ShaderToy 定义的常量参数替换成 Creator 中的参数;

vec2 uv = fragCoord.xy / iResolution.xy; // 将坐标转换成UV坐标
vec2 uv = v_uv0;   // Creator 中直接使用 v_uv0 代替

vec2 size = iResolution;   
vec2 size = vec2(1080.0, 960.0); // 使用外部传入的屏幕大小或使写死代替
vec2 size =  cc_screenSize.xy;   // 也可以使用 Creator 中定义的常量代替

finalCol = texture(iChannel0, v_uv0);  // 获取iChannel0 纹理中的uv点颜色
CCTexture(texture, v_uv0, finalCol); // 使用 Creator 中定义的函数代替

vec2 fc = fragCoord.xy;
vec2 fc = gl_FragCoord.xy;   // 使用 Creator 中定义的 gl_FragCoord 代替

vec4 ff = fragColor;
vec4 ff = gl_FragColor;      // 使用 Creator 中定义的 gl_FragColor 代替

七、示例和参考

  • 示例DEMO

欢迎扫码关注我的公众号,获取webGL和Shader示例DEMO,以及更多Cocos干货:

image

  • 参考资料

LearnOpenGL CN

https://learnopengl-cn.github.io/

WebGL 理论基础

https://webglfundamentals.org/webgl/lessons/zh_cn/

WebGL GLSL ES语法基础

https://zhuanlan.zhihu.com/p/149853457

官网材质系统介绍,包括(YAML、Effect语法、Pass参数、内置 Uniform 等等)

https://docs.cocos.com/creator3d/manual/zh/material-system/overview.html

详细的YAML学习资料

ShaderToy系列:零

https://zhuanlan.zhihu.com/p/52287086

10赞

给力,zsbd

你这图挂了啊

不好意思,还没编辑好不小心就点了发布。现在看应该有了

1赞

markk

先mark再说

mark!

mark!

mark,好文!

mark一下

mark一下

mark!

mark!