炫酷的 3D 特效 Shader 是如何炼成的?
Cubemap
、多层纹理混合
、UV流动
以及边缘光
等技术组合出一个渲染效果。然而网上相关教程都不是采用 Cocos Creator 来进行教学,导致她需要投入大量的时间去反复摸索验证,无法高效地制作出酷炫的 3D 特效 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
支持sampler2D
和samplerCube
。
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
。
它定义了一个类型为sampler2D
的uniform
。
这个就是标准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
默认就对tangent
和bitangent
进行了计算。
基于对性能的讲究,如果有项目确实用不上的,请记得移除。
同时,还可以发现,默认的vs
对于常见的position
、normal
、uv
、uv1
、color
等顶点信息
都进行了输出,在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
。
蓝框中的代码,我们将maskTexture
的r
通道作为反射强度因子,并与全局强度因子相乘,得到最终的反射因子。
麒麟小贴士:
这里简单的使用了r
通道,是非常常见的mask
纹理做法。r
通道即便是在单通道
纹理或者灰度
纹理的情况下也是生效的。当然,也可以根据实际情况,使用g
、b
、a
通道或者通过各类公式计算得出反射强度。
绿框中的代码,是将底色与反射做一个线性插值。当反射越强的时候,底 色越弱,当反射越弱的时候,底色越强。
黄框中的代码,会在材质面板上会出现一个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 纹理采样方式
下图中MinFilter
、MagFilter
和MipFilter
就是需要关注的采样方式,它们能够有效的提升渲染效果。
它们的含义如下:
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)
的优缺点,刚好相反:线性过滤效果较好,但性能较低。
麒麟小贴士:
虽然线性过滤
比最近点过滤性能低一点,但由于最近点过滤
在很多时候是满足不了视觉要求,所以大部分情况下都是采用线性过滤
方式。
麒麟小贴士:
以上内容仅针对纹理放大和缩小采样,即MinFilter
和MagFilter
。Mipmap
采样过滤请看下面内容。
既然有了MinFilter
和MagFilter
存在,那Mipmap
的意义何在呢?
说来也是尴尬,Mipmap
出现的主要原因,是因为缩小过滤
采样不能达到很好的效果。
当缩小不多的时候,MinFilter
可以工作得很好。
但当缩小达到一定比例
的时候,MinFilter
就无能为力了。
缩小过多的像素会因为‘挤压’而产生摩尔纹。
Mipmap
则是通过预先的较好的过滤采样算法,逐级生成小分辨率的纹理,从而避免像素‘挤压’问题。
下图展示了Mipmap
过滤方式分别为None
、Nearest
、Linear
三种情况下的效果对比:
从上图中可以看出,不管Mipmap
是否开启,近处的像素都是可以接受的。这是因为近处的像素不会被挤压
。
当 Mipmap Filter
为None
时,不开启Mipmap
。当 Mipmap Filter
为Nearest
时,会先选择最接近的Mipmap
层级。当 Mipmap Filter
为Linear
时,会选择接近的两个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减去小数部分。
三种纹理寻址的效果如下图所示:
麒麟小贴士:
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
顾名思义,它负责调节UV
的Tiling(缩放)
和Offset(偏移)
。
其中,xy
分量用于调节Tiling
,xy
的值会和uv
相乘,使得uv
值变大或者变小。
如上图所示,将它们设置为3
,在repeat
模式下,就会重复3
次。
zw
分量用于调节Offset
,zw
的值会和uv
相加,值得uv
值产生偏移。
在internal/effects/builtin-standard
中可看到具体的计算公式,如下图所示:
下图是x=10
,y=10
,z=0.3
,w=0.3
的效果。
可能一些认真学习的小伙伴就要问了:那在Cocos Creator
中,如何修改一个纹理的寻址方式呢?
如果是用于3D模型
的纹理,在assets
中选中某张纹理后,右边的属性窗口(Inspector)
就会显示出纹理相关属性。
在属性窗口中修改Wrap Mode S
和Wrap Mode T
即可。
这个S
和T
就是我们常说的UV
,如下图所示:
有朋友可能会问,UV
和ST
是什么关系呢?
UVW
空间中,U
表示水平方向,V
表示竖直方向 ,W
表示深度方向(其实除了2D
和Cube
纹理,还有3D
纹理。只是使用得较少,本文没有展开讲。)UVW
是一种用于均匀纹理坐标,在3D建模
领域中被广泛应用。STQ
空间中,S
表示水平方向,T
表示竖直方向 ,Q
表示深度方向STQ
可以表示非均匀纹理坐标,STQ
能够用于需要处理纹理透视矫正的情况。因此,在纹理相关领域 ,STQ
被广泛应用。U=S/Q
,V=T/Q
。因此,当Q=1.0
时(即不需要透视矫正),UV=ST
。UVW
,STQ
,XYZ
的三个分量都分别表示正交坐标系中的水平
,竖直
,深度
。只是在不同的领域,为了更好的区分概念,而采用了不同的字母表示。
麒麟小贴士:
如果想要修改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切换动画
像上面这样的图片,大部分人应该不陌生了,它是一个2
行4
列的纹理图集。
有两种方式实现纹理UV切换
动画:
方式一、在 TypeScript
中计算出对应的纹理坐标,然后修改材质中TilingOffset
属性的xy
和zw
参数。方式二、直接在Shader中计算对应的纹理坐标。
对于2
行4
列的纹理图集,可以很容易算出,每一个子图占用的UV
比例。水平方向每个子图占0.25
,竖直方向每个子图占0.5
。因此,将TilingOffset
的x
设置为0.25
,y
设置为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=2
, col=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 流动的扰动效果
基于纹理流动的扰动效果,效果如下所示:
要想达到上面的效果,需要实现以下几个功能:
使用一张干扰图干扰底图的采样 干扰图 UV 流动 控制干扰强度 干扰图与底图的混合时的比例
接下来,手把手教大家做一次。
步骤一:新建一个着色器(Effect)
给这个着色器文件命名为:effect-texture-move-distortion
(大家也可根据爱好自行命名)。
步骤二:在properties
中添加参数
detailTexture
细节图,在这里也充当扰动图strengthen
扰动强度控制因子speed
纹理UV流动速度,这里用了vec2
,用于单独调节u
和v
的流动速度detailColorFactor
细节图在向原图叠加时的混合比例
步骤三:在unlit-fs
中添加相关的uniform
没看错,这里只加了一个叫params
的uniform
。
大家请注意看,在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
扰动因子,但r
和g
是一个[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
是新增的属性,而params
的4
个分量都用完了,所以只能给fps
新增一个uniform
。
差异三:animUV
animUV
的计算方式替换为了UV切换
方式。
其余部分均与上一小节保持一致,最终效果如下所示:
DEMO 中本小节相关资源:
Effect:assets/tutorial/effect-texture-anim-distortion.effect
Scene:assets/tutorial/tutorial-anim-distortion.scene
六、一个能量环绕的 3D 角色效果
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入门基础三:可编程管线浅析与Hello World
Cocos Shader入门基础四:Uniform与材质参数控制
往期精彩