- 最近在浏览技术网站时,偶然间发现如下这个"跳动的心"特效,觉得蛮好玩的,当得知这个特效是用纯代码实现(GLSL 实现)的,确实又被惊到:
- 追溯该特效最初的来源,最终在 ShaderToy 网站看到它的原始实现,另外在 ShaderToy 上还发现了无数类似惊人的特效,并且这些特效的实现代码完全公开。
- ShaderToy 是一个跨浏览器的在线社区,并且是用于通过 WebGL 创建和共享着色器的工具,用于在 Web 浏览器中学习和教授 3D 计算机图形学。在 SahderToy 网站上浏览了一番,感觉仿佛发现了新大陆,该网站支持在线编写并运行 GLSL 脚本,堪称 GL 界的 Github 。
- 把网站上"跳动的心"特效的脚本转换为 OpenGLES 对应的 GLSL 脚本在手机上运行,并对整个脚本进行一一解析,完整的代码如下:
#version 300 es
precision highp float;
layout(location = 0) out vec4 outColor;
uniform float u_time;
uniform vec2 u_screenSize;
const float PI = 3.141592653;
void main() {
vec2 fragCoord = gl_FragCoord.xy;
vec2 p = (2.0*fragCoord-u_screenSize.xy)/min(u_screenSize.y,u_screenSize.x);
vec3 bcol = vec3(1.0,0.8,0.8)*(1.0-0.38*length(p));
float tt = u_time;
float ss = pow(tt,.2)*0.5 + 0.5;
ss = 1.0 + ss*0.5*sin(tt*6.2831*3.0 + p.y*0.5)*exp(-tt*4.0);
p *= vec2(0.5,1.5) + ss*vec2(0.5,-0.5);
p.y -= 0.25;
float a = atan(p.x,p.y) / PI;
float r = length(p);
float h = abs(a);
float d = (13.0*h - 22.0*h*h + 10.0*h*h*h)/(6.0-5.0*h);
float s = 0.75 + 0.75*p.x;
s *= 1.0-0.4*r;
s = 0.3 + 0.7*s;
s *= 0.5+0.5*pow(1.0-clamp(r/d, 0.0, 1.0 ), 0.1);
vec3 hcol = vec3(1.0,0.5*r,0.3)*s;
vec3 col = mix(bcol, hcol, smoothstep( -0.06, 0.06, d-r));
outColor = vec4(col,1.0);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 关于内建变量 gl_FragCoord ,在我之前的博客OpenGL ES之“深度测试”与“模板测试”的使用流程中可以知道:与屏幕空间坐标相关的视区是由视口设置函数 glViewport 函数给定,并且可以通过片段着色器中内置的 gl_FragCoord 变量访问。
- gl_FragCoord 的 x 和 y 表示该片段的屏幕空间坐标 ((0,0) 在左下角),其取值范围由 glViewport 函数决定,屏幕空间坐标原点位于左下角。
- 如下一段代码主要作用是调整坐标系,将原点从左下角移至屏幕坐标系*,这样所有片元的向量 gl_FragCoord.xy 均以屏幕中心为起点,则向量 p 就是屏幕中心与屏幕像素点坐标之间的方向向量:
vec2 fragCoord = gl_FragCoord.xy;
vec2 p = (2.0 * fragCoord - u_screenSize.xy) / min(u_screenSize.y,u_screenSize.x);
- 接下来计算背景颜色,length§ 表示计算当前片元(像素)与屏幕中心点的距离,背景颜色以 vec3(1.0,0.8,0.8) 该颜色为基础,距离屏幕越远颜色越暗:
vec3 bcol = vec3(1.0,0.8,0.8)*(1.0-0.38*length(p));
- 接着绘制心形,主要利用反正切函数值和当前片元(像素)与屏幕中心点的距离相比较,来确定心形状的边界。GLES 中的反正切函数 atan(,) 取值范围是[-π, π],然后除以 PI 后,取值范围变成了 [-1, 1] :
p.y -= 0.25;
float a = atan(p.x,p.y) / PI;
float r = length(p);
float h = abs(a);
- 通过上图来理解心形的绘制过程,每条直线上像素点得到的 a 值都是相同的,用黄点表示距离屏幕中心的远近,然后通过 d-r 的值来确定心形的边界:
vec3 col = mix(bcol, hcol, smoothstep(-0.06, 0.06, d-r));
- 以上是绘制心形的关键函数,hcol 是心的颜色,bcol 是背景色:
- smoothstep 是一个很常用的平滑过渡函数,当第三个参数比 -0.06 小时,返回 0,比0.06 大时返回 1,如果在 -0.06 和 0.06 之间,则返回 0 到 1 之间的值,其作用是用于平缓 d-r 的值在正负交界处的突变:
- mix 函数用于加权混合心的颜色和背景色,根据 smoothstep 函数特性,在心形内用心的颜色,在心形外用背景色,而边界则是两种颜色之间的模糊过渡。
- 再说说心形扁平化函数的作用,当不使用扁平化函数,而是直接用 h-r 来控制心的形状,得到的图像是一个又胖又肥的心形,这样大概可以得知这个函数的作用:
vec3 col = mix(bcol, hcol, smoothstep(-0.06, 0.06, h-r));
- 然后看看心的颜色生成,由表达式 vec3(1.0,0.5*r,0.3) 可以看出心的颜色是红色,且由屏幕中心向四周红色逐渐减弱,然后产生一系列渐变,最后分出心形内外的区域颜色:
float s = 0.75 + 0.75*p.x;
s *= 1.0-0.4*r;
s = 0.3 + 0.7*s;
s *= 0.5+0.5*pow( 1.0-clamp(r/d, 0.0, 1.0 ), 0.1 );
vec3 hcol = vec3(1.0,0.5*r,0.3)*s;
- 最后是跳动效果的实现,其原理就是对屏幕像素在 x、y 方向进行周期性偏移,偏移幅度由特殊的函数来控制:
float tt = u_time;
float ss = pow(tt,.2)*0.5 + 0.5;
ss = 1.0 + ss*0.5*sin(tt*6.2831*3.0 + p.y*0.5)*exp(-tt*4.0);
p *= vec2(0.5,1.5) + ss*vec2(0.5,-0.5);
float time = static_cast<float>(fmod(GetSysCurrentTime(), 2000) / 2000);
glUniform1f(m_TimeLoc, time);
- 振幅控制函数的模拟曲线如下图所示,大致可以看出心跳动时幅度变化情况:
- 最后还有一点需要注意的是 GLSL 脚本中精度的声明,这里使用的是 highp 精度,但是当使用 mediump 精度时,会出现由于精度不够导致的毛刺现象,如下图所示: