想要提高编写shader的水平,需要不断学习和练习。

跪着看完大神们的shadertoy作品后打算自己找个软柿子捏一捏。想了半天打算实现一下Nintendo Switch主机进eShop时的过场动画,仔细一看个过场的颜色和柿子还有点像,用shader做一个柿子颜色的过场动画_音视频。本文将各个技术点整理分享给大家。

先来看一下原效果:


(eShop禁用主机录屏,视频为手机录制)

效果概括(可跳过)

原效果中有四种颜色轮流出现,并且互相覆盖,在视觉上有一种层次感。

在第一层播放过程中,第二层就已经出现,最多同时出现三种颜色

四种颜色轮播完毕后动画暂停一小段时间,接着重新播放。第四种颜色和一开始的背景色相同,所以动画首尾连接

入场的形状是阶梯造型,并且阶梯的距离在屏幕两边时比较窄,运行到屏幕中间时最大。

实现方案

运动轨迹

从原效果上看,运动时有缓入和缓出。
先简化处理,只控制某个颜色出场时第一个像素的 ​​​x​​​ 位置,选择 ​​-cos(t)​​​  作为运动的速度曲线。
对应地,将屏幕的x范围映射到(-1, 1)区间,x = 0的位置在屏幕中下方。

用shader做一个柿子颜色的过场动画_技术交流_02用shader做一个柿子颜色的过场动画_离散化_03


const float PI = 3.14159;
const float TOTAL_TIME = PI * 4.; // 一遍动画的总时间
const vec3 C0 = vec3(1., 0.4667, 0.); // 背景色
const vec3 C1 = vec3(1., 0.5882, 0.0784); // 第一个颜色

void mainImage(out vec4 fragColor, in vec2 fragCoord)
{
vec2 uv = fragCoord / iResolution.xy;
uv.x = uv.x * 2.0 - 1.0; // x居中,范围为(-1, 1)

float t = mod(iTime * 2.5, TOTAL_TIME); // 全局时间2.5倍速度播放。对全局时间取模,保证t总是在(0, TOTAL_TIME)范围,实现时间循环
float mask = 1.0 - step(-cos(t), uv.x); // -cos(t)像是一个“游标”,左侧为C1,右侧为C0
vec3 col = mix(C0, C1, mask); // 根据mask选择颜色
fragColor = vec4(col, 1.0); // 输出颜色
return;
}

时间分片

控制某个颜色的动画是否显示的逻辑,采用“遮罩”的方式。原理和上一篇的“带通”类似。只不过这里的“遮罩”不是处理空间,而是处理时间

可以理解为四个颜色的动画无时无刻都在自己运行,当时间处于某个区间内时,对应的颜色才会被画出来

按顺序分配各颜色的出场时间,第一种颜色出场时间是0,第二种颜色是T1 = PI,第三种是T2 = 2PI,以此类推,第四种颜色播放完毕后是4PI。

实际运行时间不是4PI也没有关系,对全局时间 ​​iTime​​​ 进行缩放可以很方便控制整体动画的节奏,所以4PI更像是一个逻辑时间单位。
用shader做一个柿子颜色的过场动画_离散化_04


void mainImage(out vec4 fragColor, in vec2 fragCoord)
{
vec2 uv = fragCoord / iResolution.xy;
uv.x = uv.x * 2.0 - 1.0;

vec3 col = C0;
float t = mod(iTime * 2.5, TOTAL_TIME);
float mask = 1.0;

float cursor = step(-cos(t), uv.x);
mask = 1.0 - cursor;
col = mix(col, C1, mask);

mask = cursor * step(T1, t); // T1之后才显示第2种颜色,时间t < T1时受step()函数影响mask = 0
col = mix(col, C2, mask); // 根据mask选择颜色,mask = 0时选择老的颜色

mask = (1.0 - cursor) * step(T2, t); // T2之后才显示第3种颜色
col = mix(col, C3, mask);

mask = cursor * step(T3, t); // T3之后才显示第4种颜色
col = mix(col, C4, mask);

fragColor = vec4(col, 1.0);
}

阶梯造型

阶梯造型的规律性很强,可以看出是对y坐标做离散化拆成“行”,然后对各“行”进行一定的偏移。

用shader做一个柿子颜色的过场动画_音视频_05


// 对y离散化
float offsety = floor(uv.y * GRID_COUNT) / GRID_COUNT;

// 时间t进行offsety偏移,为了避免在t = 0时出现负数导致三角函数“反弹”,做了clamp处理
mask = 1.0 - step(-cos(clamp(t - offsety, 0., TOTAL_TIME)), uv.x);
col = mix(col, C1, mask);

处理颜色交叉

用shader做一个柿子颜色的过场动画_离散化_06
本文一开始提到会有同屏出现三种颜色的情况。仔细观察效果可以发现在第一种颜色到达末端前第二种颜色已经出场了。要处理这种情况只需要对t进行偏移使下一个动画提前播放即可。


float offsety = floor(uv.y * GRID_COUNT) / GRID_COUNT;
mask = 1.0 - step(-cos(clamp(t - offsety, 0., TOTAL_TIME)), uv.x);
col = mix(col, C1, mask);

t += h; // 对t进行偏移,h是下一个动画出场的时间提前量

mask = step(-cos(clamp(t - offsety, T1, TOTAL_TIME)), uv.x) * step(T1, t);
col = mix(col, C2, mask);

末尾动画停留

本来规划的4PI时间,但是由于上面将每个颜色的播放提前了,导致4PI长度的时间末尾会有一段空白时间,这段时间就刚好用来模拟原效果里的停留效果。想要调整停留时间可以修改 ​​TOTAL_TIME​​ 。

最后调整一下屏宽和动画速度,完工!

用shader做一个柿子颜色的过场动画_音视频_07

总结

老实说这柿子有点硬,我肝了一整天。
每个基本功能单独实现都很简单,但是合并到一起后经常出现牵一发动全身的情况。

写完shader再从头到尾看一遍,可以发现一些可以简化或者合并的部分。一开始我是采用 ​​sin()​​ 作为运动曲线,也尝试过映射到不同的屏幕坐标范围,后来都调整了。

目前的代码没有经过深度调优,尽量保持了和自己的思路比较匹配的写法。
完整代码可从下方领取。

Demo地址

shadertoy
​https://www.shadertoy.com/view/ttfBDf​

Cocos Creator
​https://github.com/caogtaa/CCBatchingTricks​
内含各种Cocos Creator编程技巧Demo
本文Demo可直接访问场景文件 SceneEnterEShop

用shader做一个柿子颜色的过场动画_离散化_08