当前位置: 首页 > news >正文

用shader做一个柿子颜色的过场动画

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

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

先来看一下原效果:

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

效果概括(可跳过)

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

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

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

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

实现方案

运动轨迹

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

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更像是一个逻辑时间单位。

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坐标做离散化拆成“行”,然后对各“行”进行一定的偏移。

// 对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);

处理颜色交叉


本文一开始提到会有同屏出现三种颜色的情况。仔细观察效果可以发现在第一种颜色到达末端前第二种颜色已经出场了。要处理这种情况只需要对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再从头到尾看一遍,可以发现一些可以简化或者合并的部分。一开始我是采用 sin() 作为运动曲线,也尝试过映射到不同的屏幕坐标范围,后来都调整了。

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

Demo地址

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

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


技术交流,欢迎加我微信:ezglumes ,拉你入技术交流群。

推荐阅读:

音视频面试基础题

OpenGL ES 学习资源分享

一文读懂 YUV 的采样与格式

OpenGL 之 GPUImage 源码分析

推荐几个堪称教科书级别的 Android 音视频入门项目

觉得不错,点个在看呗~

相关文章:

  • 只需跟着Google学android:ViewModel篇
  • 我用 OpenGL 实现了那些年流行的相机滤镜
  • 有必要去研究Handler和Binder么?
  • 音视频交流群又来啦~~~
  • Android 手机如何拍摄RAW图
  • 华为手机刷微博体验更好?技术角度的分析和思考
  • 播放视频时如何调整音频的音量
  • 视频码控:CBR、VBR和ABR
  • OpenGL ES 相机 LUT 滤镜
  • Android 11 正式发布 | 开发者们的舞台已就绪
  • 刚刚,鸿蒙 OS 2.0 发布!HarmonyOS 将正式开源!
  • 如何给 FFmpeg 添加自定义 Codec 编码器
  • FFmpeg从入门到精通——进阶篇,SEI那些事儿
  • iOS音频采集技术解读:如何实现男女变声的音效?
  • MediaCodec 、x264、faac 实现音视频编码并通过 rtmp 协议实现推流
  • 【node学习】协程
  • canvas实际项目操作,包含:线条,圆形,扇形,图片绘制,图片圆角遮罩,矩形,弧形文字...
  • Consul Config 使用Git做版本控制的实现
  • java8-模拟hadoop
  • JavaScript 奇技淫巧
  • Javascript编码规范
  • leetcode388. Longest Absolute File Path
  • Redis的resp协议
  • 计算机常识 - 收藏集 - 掘金
  • 前端工程化(Gulp、Webpack)-webpack
  • 融云开发漫谈:你是否了解Go语言并发编程的第一要义?
  • 线上 python http server profile 实践
  • 学习Vue.js的五个小例子
  • 在electron中实现跨域请求,无需更改服务器端设置
  • 阿里云移动端播放器高级功能介绍
  • ​批处理文件中的errorlevel用法
  • ​软考-高级-系统架构设计师教程(清华第2版)【第12章 信息系统架构设计理论与实践(P420~465)-思维导图】​
  • (4)事件处理——(6)给.ready()回调函数传递一个参数(Passing an argument to the .ready() callback)...
  • (android 地图实战开发)3 在地图上显示当前位置和自定义银行位置
  • (Forward) Music Player: From UI Proposal to Code
  • (Python第六天)文件处理
  • (板子)A* astar算法,AcWing第k短路+八数码 带注释
  • (超详细)2-YOLOV5改进-添加SimAM注意力机制
  • (二)pulsar安装在独立的docker中,python测试
  • (蓝桥杯每日一题)love
  • (没学懂,待填坑)【动态规划】数位动态规划
  • (十二)devops持续集成开发——jenkins的全局工具配置之sonar qube环境安装及配置
  • (转)程序员疫苗:代码注入
  • (转)大道至简,职场上做人做事做管理
  • .NET DataGridView数据绑定说明
  • .NET MVC第五章、模型绑定获取表单数据
  • .NET/ASP.NETMVC 大型站点架构设计—迁移Model元数据设置项(自定义元数据提供程序)...
  • .Net各种迷惑命名解释
  • .net下简单快捷的数值高低位切换
  • .NET与 java通用的3DES加密解密方法
  • /dev/sda2 is mounted; will not make a filesystem here!
  • [ C++ ] STL---stack与queue
  • [ Linux Audio 篇 ] 音频开发入门基础知识
  • []我的函数库
  • [20150629]简单的加密连接.txt