持续进阶!暴改一个带纹理贴图的 Shader

共 7330字,需浏览 15分钟

 ·

2021-10-19 11:07

上一章,我们了解了基础绘图组件的渲染流程。本章将继续深入,选取一个带纹理贴图的 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 基础入门系列


(一)WebGL & GLSL 基础

(二)顶点着色器与片元着色器

(三)绘制多个三角形并给顶点换颜色

(四)纹理映射

(五)在 Creator 中画一个矩形

···更新中···


该系列教程视频版

已更新至 EP06

欢迎点击【阅读原文】

前往 B 站观看交流 ↓

B 站关注「Cocos 引擎官方」

https://www.bilibili.com/video/BV1Cq4y1d726


往期精彩

浏览 103
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报