前言

皮皮最近接到了一个小需求:

美术小姐姐:皮皮皮皮,你能不能做奶茶?

我:???

美术小姐姐:就是那种,奶茶的轮廓加上动态水波纹~

我:吓死我还以为让我做喝的奶茶...

美术小姐姐:炒鸡多图片都需要这种效果,用动画的话工作量太大了!

我:波浪效果是吧,小意思,一个月的奶茶就够了,或者扫码提需求~

美术小姐姐:皮?

我:卒~

俗话说:遇事不决,量子力学写虽得儿。

根据我多年喝奶茶的经验,像这种效果用 Shader 做就再简单不过了,最终的效果如下:

趁此机会,本篇文章就来与小伙伴们分享动态波浪 Shader 的原理和制作思路吧。

要注意的是,这是一篇偏入门的文章,写得会相对比较详细,尽量让不懂 Shader 的小白也可以看懂,这也是我写文章的一贯风格。

好了,话不多说,进入正题~


正文

整体思路

看到波浪的表现特点我第一时间想到的就是正弦曲线(或者说是正弦波,又让我想起了示波器)。

正弦曲线(Sinusoid)

正弦曲线是三角函数中的一种正弦(Sine)比例的曲线。正弦曲线表现为一条波浪线,形状犹如海上完美的波浪。

标准的正弦函数公式为:

\[y = \sin(x) \]

正弦函数属于周期函数,其值域为 [-1, 1]

如下图就是一个纯正标准的正弦曲线:

而一般我们常用的正弦曲线公式为:

\[y = A \cdot \sin(\omega x ± \phi) + k \]

这条公式比标准公式多了几个常数,含义如下:

  • A振幅(Amplitude),曲线最高点与最低点的差值,表现为曲线的整体高度
  • ω角速度(Angular Velocity),控制曲线的周期,表现为曲线的紧密程度
  • φ初相(Initial Phase),即当 x = 0 时的相位,表现为曲线在坐标系上的水平位置
  • k偏距(Offset),表现为曲线在坐标系上的垂直位置

相位(Phase):上方公式中的 ωx±φ 部分称为相位,相位发生在周期性的运动之中,最直接的理解就是角度。

稍加思索

有了公式之后,我们可以尝试调整其中的常数来改变函数曲线的形态。

在查看下方的示例时,请尝试将曲线形态的变化图中右上角公式的变化关联起来。

改变曲线的高度

我们可以调整常数 A(振幅)来改变曲线的值域(值域为 [-A, A]):

改变曲线的周期

我们可以调整常数 ω(角速度)来改变曲线的周期:

改变曲线的水平位置

我们可以调整常数 φ(初相)来改变曲线的水平位置:

多说一句

其实对于“曲线的水平位置”这个描述是不太准确的,因为初相实际上改变的是当 x = 0 时的相位,也就直接影响函数曲线在 x = 0 处的位置。

所以说曲线的位置并没有真正改变,而只是曲线的形态发生了改变。

但是由于正弦曲线的周期性特点,曲线的这种形态变化看起来像是曲线进行了位移。

改变曲线的垂直位置

我们可以调整常数 k(偏距)来改变曲线的垂直位置:

动手实现

明白了正弦曲线的特性之后,接下来我们需要做的就是在代码中运用正弦函数。

慢着!正弦曲线确实如海上完美的波浪般优美,但是正弦曲线是静态的,我们要的波浪是动态的啊!

如何让曲线动起来

别慌!还记得我们可以调整初相来改变曲线的“水平位置”吗?

既然如此,我们可以给初相加入时间因素,使得 y 值可以随着时间的增加发生周期性变化,看起来就像是曲线在进行“水平位移”。

就像这样:

得到新的公式

加入时间因素 t 后的曲线公式:

\[y = A \cdot \sin(\omega x ± \phi t) + k \]

On Shadertoy

小贴士:由于 GLSL ES 没有办法进行调试,所以写 Shader 时可以先在 Shadertoy 中编写并在线预览,显著提高效率。

一切尽在注释中,简单详细且直观。

主函数代码如下:

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
// 将像素坐标归一化(区间 [0.0, 1.0])
// iResolution 是 Shadertoy 提供的视口分辨率全局变量(类型:vec3)
vec2 uv = fragCoord / iResolution.xy; // 振幅(控制波浪顶端和底端的高度)
float amplitude = 0.05; // 角速度(控制波浪的周期)
float angularVelocity = 10.0; // 频率(控制波浪移动的速度)
float frequency = 10.0; // 偏距(设为 0.5 使得波浪垂直居中于屏幕)
float offset = 0.5; // 初相位(正值表现为向左移动,负值则表现为向右移动)
// iTime 是 Shadertoy 提供的运行时间全局变量(类型:float)
float initialPhase = frequency * iTime; // 代入正弦曲线公式计算 y 值
// y = Asin(ωx ± φt) + k
float y = amplitude * sin((angularVelocity * uv.x) + initialPhase) + offset; // 区分 y 值上下部分,设置不同颜色
vec4 color = uv.y > y ? vec4(0.0, 0.0, 0.0, 1.0) : vec4(0.0, 0.7, 0.9, 1.0); // 输出到屏幕
fragColor = color;
}

预览效果如下(是不是有内味儿了):

在线预览:https://www.shadertoy.com/view/ttSfRh

On Cocos Creator

我们主要关注片段着色器部分,这里就不展示整个 Effect 文件的代码了,直接上传送门吧。

Effect 文件:https://gitee.com/ifaswind/eazax-ccc/blob/master/resources/effects/eazax-sine-wave.effect

代码核心其实就是套用了公式,我们代码注释一起看吧。

一切尽在注释中,简单详细且直观。

片段着色器代码如下:

CCProgram fs %{
precision highp float; // 引入 Cocos Creator 内置的全部变量
#include <cc-global> // 顶点颜色(来自顶点着色器)
in vec4 v_color;
// UV 坐标(来自顶点着色器)
in vec2 v_uv0; // 纹理
uniform sampler2D texture; // 自定义属性
uniform Properties {
float amplitude; // 振幅
float angularVelocity; // 角速度
float frequency; // 频率
float offset; // 偏距
}; void main () {
// 保存顶点颜色
vec4 color = v_color; // 叠加纹理颜色
color *= texture(texture, v_uv0); // 直接丢弃原本就透明的像素
if (color.a == 0.0) discard; // 初相位(正值表现为向左移动,负值则表现为向右移动)
// cc_time 是 Cocos Creator 提供的运行时间全局变量(类型:vec4)
float initiaPhase = frequency * cc_time.x; // 代入正弦曲线公式计算 y 值
// y = Asin(ωx ± φt) + k
float y = amplitude * sin(angularVelocity * v_uv0.x + initiaPhase) + offset; // 丢弃 y 值以上的像素(左上角为原点 [0.0, 0.0])
if (v_uv0.y < y) discard; // 输出颜色
gl_FragColor = color;
}
}%

运行效果如下:

使用 cc.tween 动态改变高度(偏距)实现波浪进度条:

cc.tween(this.sineWave)
.to(3, { height: 1 })
.to(0.5, { amplitude: 0 })
.start();

在线预览:https://ifaswind.gitee.io/eazax-cases?case=sineWave

SineWave 组件:https://gitee.com/ifaswind/eazax-ccc/blob/master/components/effects/SineWave.ts


专题

《一起学 Shader》这个专题断更了一段时间,很对不起小伙伴们,是时候续上了

《一起学 Shader》

《Shader 入门:GLSL ES(简介和基本语法)》

《Shader 入门:GLSL ES(数据类型)》

《Shader 入门:GLSL ES(运算符和限定符)》


传送门

微信推文版本

个人博客:菜鸟小栈

开源主页:陈皮皮

Eazax-CCC 游戏开发脚手架

Eazax-CCC 示例在线预览


更多分享

《为什么选择使用 TypeScript ?》

《高斯模糊 Shader》

《一文看懂 YAML》

《Cocos Creator 性能优化:DrawCall》

《互联网运营术语扫盲》

《在 Cocos Creator 里画个炫酷的雷达图》


公众号

菜鸟小栈

我是陈皮皮,这是我的个人公众号,专注但不仅限于游戏开发、前端和后端技术记录与分享。

每一篇原创都非常用心,你的关注就是我原创的动力!

Input and output.

用 Shader 写个完美的波浪的更多相关文章

  1. 写出完美的snprintf

    平时公司的代码安全扫描会给出不安全代码的告警,其中会检查代码中间的strcpy和sprintf函数,而要求使用strncpy和snprintf.今天我们讨论一下怎样写出完美的snprintf. snp ...

  2. 在java中写出完美的单例模式

    1. 前言 单例(Singleton)应该是开发者们最熟悉的设计模式了,并且好像也是最容易实现的——基本上每个开发者都能够随手写出——但是,真的是这样吗? 作为一个Java开发者,也许你觉得自己对单例 ...

  3. 写出完美论文的十个技巧10 Tips for Writing the Perfect Paper

    10 Tips for Writing the Perfect Paper Like a gourmet meal or an old master painting, the perfect col ...

  4. JavaScript 原始值与包装对象

    前言 随着 JavaScript 越来越流行,越来越多地开发者开始接触并使用 JavaScript. 同时我也发现,有不少开发者对于 JavaScript 最基本的原始值和包装对象都没有很清晰的理解. ...

  5. Unity shader(CG) 写一个 散色、折射、反射、菲涅尔、gamma、简单后期屏幕特效

    http://www.lai18.com/content/506918.html 1.自生要求是很重要的,当然不是什么强迫工作之类的,而是自己有限的能力上不断的扩展兴趣上的内容. 2.用生活的眼光去发 ...

  6. 从 0 到 1 到完美,写一个 js 库、node 库、前端组件库

    之前讲了很多关于项目工程化.前端架构.前端构建等方面的技术,这次说说怎么写一个完美的第三方库. 1. 选择合适的规范来写代码 js 模块化的发展大致有这样一个过程 iife => commonj ...

  7. 从零开始写一个武侠冒险游戏-6-用GPU提升性能(1)

    从零开始写一个武侠冒险游戏-6-用GPU提升性能(1) ----把帧动画的实现放在GPU上 作者:FreeBlues 修订记录 2016.06.19 初稿完成. 2016.08.05 增加对 XCod ...

  8. Unity Shader——Writing Surface Shaders(0)

    从今天起,开始翻译Unity关于shader的官方文档.翻译水平比较一般,目的主要是通过翻译来提升对shader的见解,也让其他人更容易的了解shader.以下开始正文内容: 编写Surface Sh ...

  9. 写出优美代码的两个方式:一步到位VS迭代优化

    最近把手头这个安卓APP的所有事务性方法都写完了,有了以下体会,新手体会,老鸟轻拍   想写成优美代码的人一般都会有这样的想法: 一定要在写每一句代码,写每一个方法,构造每一个类的时候,都要记得优化: ...

随机推荐

  1. 如何有效防止sql注入

    SQL注入攻击是黑客对数据库进行攻击常用的手段之一,随着B/S模式应用开发的发展,使用这种模式编写应用程序的程序员也越来越多.但是由于程序员的水平及经验参差不齐,相当大一部分程序员在编写代码的时候,没 ...

  2. tableauRFM分析

    1.数据源 2.创建相关字段 2.1 购买点会员生命期 2.2 会员最后购买时间 2.3 最后购买点生命期 3.近一个月老客户的生命期情况 排除了当天创建当天购买的情况,可以看到超市的大部分用户是两年 ...

  3. LeetCode 646 最长数对链详解

    题目描述 给出 n 个数对. 在每一个数对中,第一个数字总是比第二个数字小. 现在,我们定义一种跟随关系,当且仅当 b < c 时,数对(c, d) 才可以跟在 (a, b) 后面.我们用这种形 ...

  4. Linux中.bashrc与.bash_profile的对比

    如果你平时在命令行上花费了大量时间,那么你可能会萌生出希望定制 Shell 环境的想法.今天本文就和大家讲解该如何实现你们的这个想法.其实可以通过创建别名.向环境变量 $PATH 添加新目录或更改 S ...

  5. SpringMVC的简介和工作流程

    一.简介 Spring MVC属于SpringFrameWork的后续产品,已经融合在Spring Web Flow里面.Spring 框架提供了构建 Web 应用程序的全功能 MVC 模块.Spri ...

  6. AdblockPlus自定义屏蔽广告

    AdblockPlus自定义屏蔽广告我推荐使用两种方法: 1. 使用CSS选择器 2. 使用样式选择器 屏蔽广告中,重要的一个问题就是识别广告. 我们要自己编写屏蔽就得将广告选出来,告诉Adblock ...

  7. offset range 查询

    offset range 查询 我们在实际使用过程中经常需要查询某个topic的某分区的offset的range 命令行: kafka-run-class.sh kafka.tools.GetOffs ...

  8. SpringSecurity权限管理系统实战—二、日志、接口文档等实现

    系列目录 SpringSecurity权限管理系统实战-一.项目简介和开发环境准备 SpringSecurity权限管理系统实战-二.日志.接口文档等实现 SpringSecurity权限管理系统实战 ...

  9. Android 本地缓存Acache的简单使用

    设置缓存数据: ACache mCache = ACache.get(this); mCache.put("key1", "value"); //保存6秒,如果 ...

  10. 简单的股票信息查询系统 1 程序启动后,给用户提供查询接口,允许用户重复查股票行情信息(用到循环) 2 允许用户通过模糊查询股票名,比如输入“啤酒”, 就把所有股票名称中包含“啤酒”的信息打印出来 3 允许按股票价格、涨跌幅、换手率这几列来筛选信息, 比如输入“价格>50”则把价格大于50的股票都打印,输入“市盈率<50“,则把市盈率小于50的股票都打印,不用判断等于。

    '''需求:1 程序启动后,给用户提供查询接口,允许用户重复查股票行情信息(用到循环)2 允许用户通过模糊查询股票名,比如输入“啤酒”, 就把所有股票名称中包含“啤酒”的信息打印出来3 允许按股票价格 ...