查看原文
其他

炫酷的 3D 特效 Shader 是如何炼成的?

麒麟子TM COCOS 2022-06-10
上周我接到了一位开发者的技术咨询,她说:她需要使用Cubemap多层纹理混合UV流动以及边缘光等技术组合出一个渲染效果。然而网上相关教程都不是采用 Cocos Creator 来进行教学,导致她需要投入大量的时间去反复摸索验证,无法高效地制作出酷炫的 3D 特效 Shader。
大家都知道,我此前分享过 Cocos Shader 入门系列教程,在编写的过程中,我也收到了诸如上述等问题反馈,这让我意识到,基于 Cocos Creator 来编写 Shader 的教程存在刚需。基于此,我希望通过此文,能够为大家厘清相关概念和基础知识,解析Cocos Shader着色器(Effect)文件语法以及Cocos Creator材质系统,帮助大家通过简单的几行代码,就能够实现下述 Shader 效果:


二、内容规划

开发环境:Cocos Creator 3.4+

  • Cocos Shader 中的纹理
    • 纹理相关基础知识
    • Cocos Shader中使用smapler2D
    • Cocos Shader中使用smaplerCube
  • 纹理基本属性
    • 采样方式
    • 过滤方式
    • 寻址方式
  • 常见效果实现
    • 纹理UV流动动画
    • 纹理UV切换动画
    • 多重纹理混合
    • 纹理扰动效果
  • 一个能量环绕的 3D 角色效果

三、Cocos Shader 中的纹理

3.1 纹理相关基础知识

什么是纹理?

GPU渲染流程中使用的图片,我们称之为纹理,美术领域称其为贴图

纹理的英文为texture贴图的英文为map

所以,当你在Shader中看到参数为mainTexture或者albedoMap的时候,请记得它们就是纹理

什么是纹理坐标?

试想一下,当我们要渲染一个三角面的时候,三角面中某个位置与纹理的颜色如何对应呢?

图形学前辈们发明了纹理坐标这个概念,它用于指定每个顶点与纹理的对应关系,而顶点之间,则使用插值进行处理。

纹理坐标是一个顶点上属性,它和顶点位置、顶点法线、顶点颜色一样,属于常见的顶点信息。

有了纹理纹理坐标,就可以对纹理进行采样,获得纹理上对应贴图位置的信息。

Shader中,通常使用uv来表示纹理坐标,u表示水平方向,v表示竖直方向。

纹理坐标纹理的关系是什么呢?请看下图:

如上图所示,我们使用[0.0,0.0]来表示一张纹理的左上角,使用[1.0,1.0]来表示一张纹理的右下角。

纹理坐标纹理的实际尺寸无关,这样可使相关运算与纹理分辨率脱离关系。

不难发现:

  • uv为(0.5,0.5)的时候,刚好获取到中间的像素。
  • uv为(0.0,0.0)的时候,刚好取到左上角的像素。
  • uv为(1.0,1.0)的时候,刚好取到右下角的像素。

什么是纹理采样?

纹理采样是指获取给定纹理坐标处的纹理颜色的过程。

纹理采样是一个非常复杂且耗时的操作 ,会涉及到放大采样缩小采样Mipmap采样等内容,本文后面会讲相关知识。

虽然过程很复杂,但这些事情都是GPU硬件做的,完全不需要操心。

Shader中,我们能够感知到的纹理采样只有下面这个函数:

vec4 texture(sampler,uv)

若想要在Shader对一个纹理进行采样,只需要像下面这样的代码即可:

vec4 color = texture(textur,euv);

Cocos Shader 支持哪些纹理类型?

打开Cocos Creator官方文档https://docs.cocos.com/creator/manual/zh/

进入图形渲染->材质系统->Pass Params页面,并滚动到底部。

可以找到Cocos Shader编写时所支持的Uniform类型,如下图所示:

从文档中可以看出,Cocos Shader支持sampler2DsamplerCube

3.2 在 Cocos Shader 中使用 smapler2D

smapler2D顾名思义,它可以用来声明一个2D纹理采样器

Cocos Shader中,如果声明了这个类型的uniform,就可以将一个2D纹理资源传递给它。

2D纹理资源理解起来非常简单,常见的JPG,PNG图片,都是2D纹理资源。

也可以简单的理解为普通纹理

下面来看看,如何在Cocos Shader中,使用2D纹理

步骤一:创建一个默认的着色器(Effect)

新建一个Cocos Creator 3.4的项目,并在assets目录下,点击鼠标右键,在弹出的菜单中选择创建着色器(Effect)。如下图所示:

麒麟小贴士
新建项目的时候,建议选择HelloWorld模板。
此模板提供了静态模型带动画的模型Cubemap等素材,方便大家学习使用。如下图所示:

步骤二:创建一个材质,并使用刚创建的着色器(Effect)

如下图所示,使用右键菜单创建一个材质

有朋友问物理材质是不是基于物理渲染的材质?
答: 不是的,物理材质物理系统使用的材质,用于描述物体的物理属性。

选中新建的材质,在右边的属性面板(Inspector)中,将Effect切换为刚刚新建的。如下图所示:

麒麟小贴士: 
切换后,右边的材质面板会多出一个绿色的勾,点击将保存更改。

步骤三:使用 2D 纹理

将一个2D纹理拖到MainTexture参数,会发现材质预览中的模型也会跟着改变外观。

本文 DEMO 使用了HelloWorld模板项目中自带的盾牌贴图,如下图所示:

Shader 解析

仔细阅读刚刚创建的着色器(Effect),可以发现,在其properties区域定义了一个mainTexture属性,此属性会出现在属性面板(Inspector)上。如下图所示:

mainTexture的默认值为white,表示在不被赋值的时候,它将是白色。

unlit-fs中,可以找到如下语句:

uniform sampler2D mainTexture

它定义了一个类型为sampler2Duniform

这个就是标准2D纹理属性定义,这个mainTexture就是一个2D纹理

frag函数中,可以找到如下语句:

vec4 col = mainColor * texture(mainTexture,v_uv)

这句话中的texture函数,用于获取纹理在指定uv坐标处的像素颜色值(RGBA),也就是我们所说的纹理采样。如下图所示:

可能有人会疑惑,v_uv 是什么?它从哪里来的?

这个v_uv就是纹理坐标,它是vs传递给fs的。

可以查看本文下一节中的general-vs.chunk内容看看v_uv的来源。

可能还有朋友会疑惑,为什么新建的默认Effect没有unlit-vs呢?

这是因为,大部分情况下,不需要改动vs,所以Cocos Creator引擎提供了内置的general-vs供大家使用。

general-vs的路径为 internal/chunks/general-vs.chunk

新建的着色器(Effect)general-vs:vert #builtin header就是对它的引用。如下图所示:

如果想要自定义vs,只需将其复制过来,再按需修改即可。

为了照顾 “没有时间” 的朋友们,麒麟子还是展示一下general-vs.chunk的内容。


precision highp float;
#include <input-standard>
#include <cc-global>
#include <cc-local-batch>
#include <input-standard>
#include <cc-fog-vs>
#include <cc-shadow-map-vs>

in vec4 a_color;
#if HAS_SECOND_UV
  in vec2 a_texCoord1;
#endif

out vec3 v_position;
out vec3 v_normal;
out vec3 v_tangent;
out vec3 v_bitangent;
out vec2 v_uv;
out vec2 v_uv1;
out vec4 v_color;

vec4 vert () {
  StandardVertInput In;
  CCVertInput(In);

  mat4 matWorld, matWorldIT;
  CCGetWorldMatrixFull(matWorld, matWorldIT);

  vec4 pos = matWorld * In.position;

  v_position = pos.xyz;
  v_normal = normalize((matWorldIT * vec4(In.normal, 0.0)).xyz);
  v_tangent = normalize((matWorld * vec4(In.tangent.xyz, 0.0)).xyz);
  v_bitangent = cross(v_normal, v_tangent) * In.tangent.w; // note the cross order

  v_uv = a_texCoord;
  #if HAS_SECOND_UV
    v_uv1 = a_texCoord1;
  #endif
  v_color = a_color;

  CC_TRANSFER_FOG(pos);
  CC_TRANSFER_SHADOW(pos);

  return cc_matProj * (cc_matView * matWorld) * In.position;
}

general-vs.chunk的代码中可以看出,引擎内置的这个vs默认就对tangentbitangent进行了计算。

基于对性能的讲究,如果有项目确实用不上的,请记得移除。

同时,还可以发现,默认的vs对于常见的positionnormaluvuv1color顶点信息都进行了输出,在fs中可直接使用。如下图所示:

3.3 在 Cocos Shader 中使用 smaplerCube

smaplerCube对应的是Cubemap(立方体贴图),用一个更贴切的描述叫:立方体盒子贴图

可以想象一下,使用6张正方形的2D纹理,拼接成一个中空的立方体盒子,就得到了一个Cubemap

为了便于查看,Cubemap的显示方式,通常是以六面展开方式出现的。如下图所示:

这个特征非常好记,下次再看到这样的图的时候,大家肯定都能识别啦。

接下来,我们看看在Cocos Shader中,如何使用Cubemap

步骤一:在properties中新增一个cubeTexture属性

使用上一节讲到的方法,创建一个着色器(Effect)

mainTexture下方添加一个cubeTexture属性,如下图所示:

步骤二:在unlit-fs中新增一个samplerCube类型的uniform

uniform sampler2D mainTexture的下方,增加uniform samplerCube cubeTexture,见下图中红色方框标记:

步骤三:引入法线数据

在前面的知识介绍中,我们讲到了,Cubemap其实就是一个空心的立方体盒子。那如何获取它的信息呢?

假如有一个很大的立方体盒子,在盒子中的某个点,朝某个方向发射出一条射线。

射线最终会与立方体盒子形成交点,而交点处的颜色值,就是我们想要的颜色值。

不难发现,如果将一个物体放入环境中,如果想让这个物体的表面,映射出Cubemap的内容,所使用的射线肯定和物体的面朝向是分不开的。

在 3D 渲染流程中,决定物体面朝向的信息,叫法线(Normal)。因此,需要先引入法线信息

由于general-vs已经输出了v_normal,所以在unlit-fs中只需要引入就可使用。

in vec3 v_position;下方添加一行in vec3 v_normal;即可引入并使用v_normal。请见下图绿色方框标记。

步骤四:采样 Cubemap

Cubemap采样函数的原型为vec4 texture(samperCube cubemap,vec3 coord)

麒麟小贴士: 
Cocos Shader中的texture函数有许多个重载,它会根据传入的数据类型挑选适合的函数原型。
所以采样2D纹理Cube纹理均使用texture函数就行了,只是传入的参数不同。

最终采样函数见下图黄色标记。

为了测试效果,我们先新建一个材质。

选择刚写好的着色器(Effect)作为Effect

assets/skybox/sunnySkyBox拖到材质的CubeTexture参数上。如下图所示:

在场景中新建一个球,并使用此材质,最终得到的效果如下:

步骤五:正确的反射计算

当你在编辑器场景中转动摄像机的时候会发现,天空盒与球面的关系是固定的。

而现实世界中,物体反射的内容在环境不变的情况下,与物体转动是无关的。

常见的Cubemap采样,一般是采用视线基于法平面的反射方向去采样。

因此,需要根据normal计算出视线的反射方向,并利用反射方向进行采样。

设视线方向为V,法线方向为N,反射方向为R(V,N,R均为单位向量)。

R推导过程如下图所示:

最终可得 R = V - 2*dot(V,N)*N

根据上述公式,我们修改一下Shader中对CubeTexture的采样,如下图所示:

在未转动的情况下,很难感受到与直接使用v_normal进行采样的区别。

但当我们转动摄像机的时候,可以明显感知到反射带来的变化。

如果把球体换成其他非规则模型这个感受会更明显。

步骤六:反射强度控制

完成步骤五后,我们可以得到一个全反射的Shader,但这个Shader的适用性不高。

在现实世界中,全反射的情况非常少见,因此需要添加一些控制反射强弱的因子,使物体表面的反射更加真实。

首先,在properties中,添加两个属性:

  • maskTexture用于控制物体表面某个部位的反射强弱
  • reflectionStrengthen 用于控制整体强弱

然后添加相应的uniform,如下图所示:

最后,在frag中使用刚刚添加的属性,如下图所示:

红框中的代码,用于采样maskTexture

蓝框中的代码,我们将maskTexturer通道作为反射强度因子,并与全局强度因子相乘,得到最终的反射因子。

麒麟小贴士: 
这里简单的使用了r通道,是非常常见的mask纹理做法。r通道即便是在单通道纹理或者灰度纹理的情况下也是生效的。当然,也可以根据实际情况,使用gba通道或者通过各类公式计算得出反射强度。

绿框中的代码,是将底色与反射做一个线性插值。当反射越强的时候,底 色越弱,当反射越弱的时候,底色越强。

黄框中的代码,会在材质面板上会出现一个SHOW_REFECTION_STRENGTHEN的开关,开启这个开关就可以查看反射强度。

DEMO 中渲染了4个球用于展示效果,如下图所示:

  • 第1个球使用了盾牌图作为maskTexture。可以看到越亮的地方,反射强度越大(比如椰子头部),越暗的地方反射强度越小(比如蓝色部分)。
  • 第2个球是显示的第1个球的材质中最终的各部分的反射强度。整体偏 暗是因为全局强度reflectionStrengthen设置为了0.5
  • 第3个球是maskTexture为空的情况。由于maskTexture默认值为white,所以maskColor.r总是为1.0,即全反射。而reflectionStrengthen为0.5,所以原色与反射各占一半,且各部分反射都一样。
  • 第4个球是显示的第3个球的材质中最终的各部分的反射强度。整体为灰色是因为全局强度reflectionStrengthen设置为了0.5

DEMO 中本小节相关资源

Effect:assets/tutorial/effect-cubemap.effect

Scene:assets/tutorial/tutorial-cubemap.scene

四、纹理属性

assets窗口中,选择任意纹理,可在右边属性窗口(Inspector)中看到纹理相关的属性。如下图所示:

4.1 纹理采样方式

下图中MinFilterMagFilterMipFilter就是需要关注的采样方式,它们能够有效的提升渲染效果。

它们的含义如下:

MinFilter

  • 缩小采样,用于纹理分辨率大于实际所需时

MagFilter

  • 放大时采样,用于纹理分辨经小于实际所需时

MipFilter

  • Mipmap采样,用于在Mipmap开启时,从各级Mipmap中获取像素信息时的使用。

然而,光有概念依然是较难理解的,接下来用一个示例让大家细细体会一下它们的作用。

下图中:

  • 图1 - 正常视角
  • 图2 - 摄像机拉远的视角
  • 图3 - 摄像机拉近的视角。

现在大家把目光集中到角色上,可以很明显的发现,图2中,角色变小了,图3中角色变大了。

但是,图2图3的角色,使用的是同一张贴图。

也就是说,同样的纹理区域,在屏幕上显示大小是不同的。

假如角色纹理尺寸为512x512图1是它的最佳视角。

那么在图2中,由于纹理所占屏幕比例缩小了,为了保证较好的效果,纹理采样的时候就需要做缩小采样处理。

图3中,纹理所占屏幕比例变大了,纹理采样的时候就需要放大采样处理。

4.2  纹理过滤方式

纹理过滤方式有两种

  • 最近点过滤(nearest)
  • 线性过滤(linear)。

最近点过滤就是选择最近的像素使用。如下图所示:

最近点过滤(图片来源于网络)

可以把纹理想象成一个由像素组成的格子,采样的时候,uv坐标落到哪个格子,就取哪个格子。

最近点过滤采样的优点是:性能好,没有额外运算。

有得必有失,最近点过滤采样的缺点是:不管是放大采还是缩小采,效果都不好。

线性过滤就是选择最近像素的2x2像素区域,进行加权混合。

线性过滤(图片来源于网络)

线性过滤(Linear)相较于最近点过滤(Nearest)的优缺点,刚好相反:线性过滤效果较好,但性能较低。

麒麟小贴士: 
虽然线性过滤比最近点过滤性能低一点,但由于最近点过滤在很多时候是满足不了视觉要求,所以大部分情况下都是采用线性过滤方式。

两种过滤方式比较(图片来源于网络)

麒麟小贴士: 
以上内容仅针对纹理放大和缩小采样,即MinFilterMagFilter
Mipmap采样过滤请看下面内容。

既然有了MinFilterMagFilter存在,那Mipmap的意义何在呢?

说来也是尴尬,Mipmap出现的主要原因,是因为缩小过滤采样不能达到很好的效果。

当缩小不多的时候,MinFilter可以工作得很好。

但当缩小达到一定比例的时候,MinFilter就无能为力了。

缩小过多的像素会因为‘挤压’而产生摩尔纹。

Mipmap则是通过预先的较好的过滤采样算法,逐级生成小分辨率的纹理,从而避免像素‘挤压’问题。

下图展示了Mipmap过滤方式分别为NoneNearestLinear三种情况下的效果对比:

从上图中可以看出,不管Mipmap是否开启,近处的像素都是可以接受的。这是因为近处的像素不会被挤压

  • Mipmap FilterNone时,不开启Mipmap
  • Mipmap FilterNearest时,会先选择最接近的Mipmap层级。
  • Mipmap FilterLinear时,会选择接近的两个Mipmap层级,并做线性插值。

麒麟小贴士: 
一般用于2D Sprite的纹理不开启Mipmap,用于3D模型渲染的纹理需要开启Mipmap

4.3  纹理寻址方式

纹理寻址方式,主要是为uv坐标大于1.0的时候,提供相应的寻址策略。

Cocos Creator引擎提供了以下三种纹理寻址方式:

  • 重复(repeat
    • 大于1.0的部分会被模除,仅保留小数部分。
  • 边缘约束(clamp-to-edge
    • 小于0.0时取0.0。
    • 大于1.0时取1.0。
  • 镜像重复(mirrored-repeat)
    • 整数部分为偶数时,取小数部分。
    • 整数部分为奇数时,取 1.0减去小数部分。

三种纹理寻址的效果如下图所示:

左:repeat,中:clamp-to-edge,右:mirrored-repeat

麒麟小贴士:
1、当需要大面积的地砖、玻璃窗等重复效果时。使用repeat模式,并且调节uv tiling,可以有效降低纹理分辨率,节省显存。
2、clamp-to-edge可确保边缘干净。
3、mirrred-repeat是一个特殊的repeat,使用场合不多,可按需使用。
4、3D纹理默认为repeat
5、repeat在一些低端API上(如WebGL 1.0)要求纹理尺寸为2幂。所以,3D模型渲染尽可能使用2幂纹理,以增强兼容性。

这里补充说明一个标准材质上与寻址有关的属性TilingOffset,如下图所示:

材质面板上的 TilingOffset

TilingOffset顾名思义,它负责调节UVTiling(缩放)Offset(偏移)

其中,xy分量用于调节Tilingxy的值会和uv相乘,使得uv值变大或者变小。

如上图所示,将它们设置为3,在repeat模式下,就会重复3次。

zw分量用于调节Offsetzw的值会和uv相加,值得uv值产生偏移。

internal/effects/builtin-standard中可看到具体的计算公式,如下图所示:

下图是x=10,y=10,z=0.3,w=0.3的效果。

可能一些认真学习的小伙伴就要问了:那在Cocos Creator中,如何修改一个纹理的寻址方式呢?

如果是用于3D模型的纹理,在assets中选中某张纹理后,右边的属性窗口(Inspector)就会显示出纹理相关属性。

在属性窗口中修改Wrap Mode SWrap Mode T即可。

这个ST就是我们常说的UV,如下图所示:

有朋友可能会问,UVST是什么关系呢?

  • UVW空间中,U表示水平方向,V表示竖直方向 ,W表示深度方向(其实除了2DCube纹理,还有3D纹理。只是使用得较少,本文没有展开讲。)
  • UVW是一种用于均匀纹理坐标,在3D建模领域中被广泛应用。
  • STQ空间中,S表示水平方向,T表示竖直方向 ,Q表示深度方向
  • STQ可以表示非均匀纹理坐标,STQ能够用于需要处理纹理透视矫正的情况。因此,在纹理相关领域 ,STQ被广泛应用。
  • U=S/QV=T/Q。因此,当Q=1.0时(即不需要透视矫正),UV=ST
  • UVWSTQXYZ的三个分量都分别表示正交坐标系中的水平竖直深度。只是在不同的领域,为了更好的区分概念,而采用了不同的字母表示。

麒麟小贴士: 
如果想要修改sprite的纹理寻址方式,只需在assets窗口中展开sprite的资源内容,并选中sprite内容中的texture即可。

五、常见效果实现

5.1  纹理UV流动动画

终于,可以开始让Shader从静态走向动态了。

Shader上的一小步,效果上的一大步。

麒麟小贴士: 
要想实现纹理UV流动动画,有一个前提是:纹理寻址方式为repeat或者mirroed-repeat

上面的动画中,左边的是repeat,右边的是mirrored-repeat,可以从Cocos Logo头顶的方向看出差异。

纹理UV流动的原理非常简单,只需要在纹理采样的时候,给uv加一个与时间相关的偏移量即可。如下图所示:

其中 cc_time 是引擎提供的内置Shader变量,x分量表示从项目启动开始到现在经过的时间。 yzw分量暂时未使用。

更多内置变量可直接查看:引擎官方文档->图形渲染->材质系统->Builtin Shader Uniforms

麒麟小贴士: 
若想在Cocos Creator编辑器内查看纹理流动效果,只需要在编辑器窗口中同时按下鼠标左键和右键即可。没有鼠标的笔记本,长按触摸板即可。

5.2  纹理UV切换动画

像上面这样的图片,大部分人应该不陌生了,它是一个24列的纹理图集。

有两种方式实现纹理UV切换动画:

  • 方式一、在TypeScript中计算出对应的纹理坐标,然后修改材质中TilingOffset属性的xyzw参数。
  • 方式二、直接在Shader中计算对应的纹理坐标。

对于24列的纹理图集,可以很容易算出,每一个子图占用的UV比例。水平方向每个子图占0.25,竖直方向每个子图占0.5。因此,将TilingOffsetx设置为0.25y设置为0.5即可。

对于此纹理图集,也可以很容易的算出所有子图的偏移参数:

第一行:[0.0,0.0],[0.25,0.0],[0.5,0.0],[0.75,0.0]
第二行:[0.0,0.5],[0.25,0.5],[0.5,0.5],[0.75,0.5]

row=2col=4,可得:

  • 每一个子图在水平方向的占比为1.0 / col (即0.25)。
  • 每一个子图在竖直方向的占比为1.0 / row (即0.5)。

计算公式为:

  • z = (index % col) * 1.0 / col
  • w = floor(index / col) * 1.0 / row

大家可带入验算一下。

有了上面的公式,新建一个脚本并写下对应的代码即可。这个使用本系列的上一篇教程内容就可以搞定,在此不再敷述。

下面我们就来一步步实现基于Shader的纹理UV切换效果。

步骤一:新建一个着色器(Effect)

参考本文3.1中的步骤二,新建一个着色器,命名为:effect-texture-anim.effect

步骤二:在properties中添加属性

properties区域,添加两个属性:

  • cells: 类型为vec2,用于标记纹理图集中水平和竖直方向的子图数量。
  • fps: 类型为float,用于控制子图切换速度。

步骤三:添加Uniforms

unlit-fs中,添加两个uniform,如下图所示:

步骤四:根据上述公式,编写Shader

  vec4 frag () {
    float index = floor(cc_time.x * fps);

    float row = cells.x;
    float col = cells.y;

    vec2 offset = vec2(mod(index,col)/col,floor(index/col)/row);

    vec4 color = mainColor * texture(mainTexture, v_uv / cells.yx + offset);

    float gray = color.r * 0.299 + color.g * 0.587 + color.b * 0.114;
    color.a = gray; 

    CC_APPLY_FOG(color, v_position);
    return CCFragOutput(color);
  }

步骤五:新建一个材质,使用此Shader

在场景中创建一个Plane或者Quad

新建一个材质,使用此Effect,并设置好对应的纹理参数,最终可看到如下效果:

DEMO 中本小节相关资源:

Effect:assets/tutorial/effect-texture-anim.effect

Scene:assets/tutorial/tutorial-anim.scene

5.3  多重纹理混合

多重纹理混合在Shader编写中,是一个使用频率非常高的点。

常见的应用场景有:多层地表混合物体表面细节流光特效消散特效镂空特效石化效果等等。

多重纹理的混合的实现,在Shader中只需要三步:

步骤一:添加必要的纹理参数

可以基于之前的着色器(Effect)来改,也可以新建一个。

首先在properties里面添加一个纹理属性

然后在unlit-fs中添加相应的sampler2D

步骤二:取出所有需要参与的纹理像素值

这个太简单了,简单得就像普通纹理采样。如下图所示:

步骤三:根据想要的混合方式混合

公式是可以随便造的,但有一些用于实现常见效果的公式可以快速使用。如下图所示:

对应渲染结果如下图所示:

如果结合UV切换动画,就可以呈现文章开头的效果:

DEMO 中本小节相关资源 :

Effect:assets/tutorial/effect-texture-anim-blending.effect

Scene:assets/tutorial/tutorial-anim-blending.scene

5.4  纹理扰动效果

纹理扰动效果的核心思想,就是在纹理采样前对uv进行干扰。

干扰的方式一般是使用另一张纹理作为干扰纹理。

面为了更直观的看出扰动效果,肯定是要和纹理动画结合的。

接下来我们分别实现基于UV流动动画UV切换动画的两种纹理扰动效果。

5.4.1 基于纹理 UV 流动的扰动效果

基于纹理流动的扰动效果,效果如下所示:

基于纹理流动的扰动效果.GIF

要想达到上面的效果,需要实现以下几个功能:

  • 使用一张干扰图干扰底图的采样
  • 干扰图 UV 流动
  • 控制干扰强度
  • 干扰图与底图的混合时的比例

接下来,手把手教大家做一次。

步骤一:新建一个着色器(Effect)

给这个着色器文件命名为:effect-texture-move-distortion(大家也可根据爱好自行命名)。

步骤二:在properties中添加参数

  • detailTexture 细节图,在这里也充当扰动图
  • strengthen 扰动强度控制因子
  • speed 纹理UV流动速度,这里用了vec2,用于单独调节uv的流动速度
  • detailColorFactor 细节图在向原图叠加时的混合比例

步骤三:在unlit-fs中添加相关的uniform

没看错,这里只加了一个叫paramsuniform

大家请注意看,在properties中,添加strengthen等属性时,比之前多加了一个target

target的意义在于重定向,它可以决定属性使用哪一个uniform的哪几个分量。

也可以反过来解释:target可以使我们给uniform的分量在属性面板上起一个别名,方便我们理解各个分量的含义。

比如,在本例中:

  • params.x的别名为strengthen
  • params.yz的别名为speed
  • params.w的别名为detailColorFactor

麒麟小贴士:
出现这个机制的主要原因,是由于uniform要占用内存空间的。

而不同的平台,在编译Shader的时候,uniform内存布局规则不同,可能会造成内存浪费。

因此建议大家在写Shader的时候,uniform尽量以vec4为主。

步骤四:让干扰图流动起来

黄框标记的部分,是为了方便Shader的编写,我们使用了几个临时变量将params的分量根据properties中的定义存储下来。

红框标记部分则是对uv做一个时间相关的偏移。这是本文前面纹理UV流动动画中讲过的内容 ,不再敷述。

最后采样得到的detailColor就是一个不停流动的纹理。

步骤五:扰动处理

灰度计算

上图中,黄色方框标记的语句:

float gray = detailColor.r * 0.299 + detailColor.g * 0.587 + detailColor.b * 0.114

是由于本示例中用的细节图片没有 ALPHA 通道,所以这里使用了心理学灰度公式来计算出图片的灰度值,用于参与强度运算。

如果是带有 ALPHA 通道的图片,直接使用 ALPHA 通道值即可。

也可以简单使用detailColor.r作为扰动强度去算。

麒麟小贴士: 
在表示灰色的时候,大家应该会发现有的人用gray,有的人用grey
不必纠结,在表示颜色时,两个都是可以的。
gray是美式写法,grey是英式写法。

偏移计算

大家注意上图中的红框标记的代码,如下所示:

vec2 offset = (detailColor.rg - 0.5) * 2.0 * strengthen * gray

这里使用了rg通道来作为uv扰动因子,但rg是一个[0.0,1.0]区间的值。

想让uv在一个范围内扰动,而不是朝某一个方向偏转,期望的扰动因子就需要在[-1.0,1.0]这个区间。

大家一起来回忆一下中学数学的内容:

  • 若函数f(x)的值域为[0.0,1.0]

  • g(x)=(f(x) - 0.5) * 2.0

  • 可得g(x)的值域为[-1.0,1.0]

最终我们可以得到一个x,y分量在[-1.0,1.0]区间的偏移量。

强度控制

句末的 * strengthen * gray,是对扰动的强度进行控制。

  • strengthen是对offset整体的缩放,用于整体调节扰动强度。
  • gray是对单个像素进行强度控制,从而产生波纹效果。

采样与混合

有了之前的准备工作,只需要将offset参与到mainTexture的采样即可。如下所示:

vec4 color = mainColor * texture( mainTexture, v_uv + offset );

如果只做到这一步的话,我们会发现,像素会被一个看不见的东西扰动,如下所示:

不管是游戏、动漫还是电影里,带空间扰动的特效或多或少都有一些颜色。所以我们还需要将扰动特效的颜色叠加到底图上,代码如下:

color.rgb += detailColor.rgb * detailColorFactor;

其中detailColorFactor用于控制特效图显示的强弱。

最终的效果如下所示:

DEMO 中本小节相关资源

Effect:assets/tutorial/effect-texture-move-distortion.effect

Scene:assets/tutorial/tutorial-move-distortion.scene

5.4.2 基于纹理 UV 切换的扰动效果

基于纹理UV切换动画的扰动效果,与上一节基于UV流动的扰动效果大同小异。本质的区别就是detailTexture的运动方式问题。

本节要实现的UV切换动画,只需要将UV流动动画部分,改为UV切换动画即可。

接下来,我们说一下差异的地方。

差异一:properties

  • speed替换为了cells,用于标记图集中的子图数量
  • 新增fps,用于控制子图切换频率

差异二:uniforms

fps是新增的属性,而params4个分量都用完了,所以只能给fps新增一个uniform

差异三:animUV

animUV的计算方式替换为了UV切换方式。

其余部分均与上一小节保持一致,最终效果如下所示:

DEMO 中本小节相关资源

Effect:assets/tutorial/effect-texture-anim-distortion.effect

Scene:assets/tutorial/tutorial-anim-distortion.scene

六、一个能量环绕的 3D 角色效果

通过本文的 Shader 组合,即可实现上面的效果,编辑好材质参数就行。

DEMO 中本小节相关资源

Scene:assets/tutorial/tutorial-soldier.scene

七、结束

呼~~~,终于写完了!本想着是一篇简单的入门文章,结果内容越讲越多,码字器统计已9500多字。

是时候收手了!

本文 DEMO 源码可在Cocos Store免费获取,地址如下:

https://store.cocos.com/app/detail/3521

也可点击阅读原文直接跳转到Cocos Store页面。

八、本系列其他文章

Cocos Shader入门基础一:前言

Cocos Shader入门基础二:初识Cocos Shader

Cocos Shader入门基础三:可编程管线浅析与Hello World

Cocos Shader入门基础四:Uniform与材质参数控制


往期精彩

 


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存