OpenGL ES之深入解析如何绘制“跳动的心“特效

时间:2024-10-05 18:11:39
  • 最近在浏览技术网站时,偶然间发现如下这个"跳动的心"特效,觉得蛮好玩的,当得知这个特效是用纯代码实现(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() {
	// move to center
	vec2 fragCoord = gl_FragCoord.xy;
	vec2 p = (2.0*fragCoord-u_screenSize.xy)/min(u_screenSize.y,u_screenSize.x);

	// background color
	vec3 bcol = vec3(1.0,0.8,0.8)*(1.0-0.38*length(p));

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

	// shape
	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);
	
	// color
	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 就是屏幕中心与屏幕像素点坐标之间的方向向量:
// move to center
vec2 fragCoord = gl_FragCoord.xy;
vec2 p = (2.0 * fragCoord - u_screenSize.xy) / min(u_screenSize.y,u_screenSize.x);
  • 1
  • 2
  • 3
  • 屏幕中心与屏幕像素点坐标之间的方向向量:

在这里插入图片描述

  • 接下来计算背景颜色,length§ 表示计算当前片元(像素)与屏幕中心点的距离,背景颜色以 vec3(1.0,0.8,0.8) 该颜色为基础,距离屏幕越远颜色越暗:
// background color
vec3 bcol = vec3(1.0,0.8,0.8)*(1.0-0.38*length(p));
  • 1
  • 2
  • 把背景颜色渲染出来:

在这里插入图片描述

  • 接着绘制心形,主要利用反正切函数值和当前片元(像素)与屏幕中心点的距离相比较,来确定心形状的边界。GLES 中的反正切函数 atan(,) 取值范围是[-π, π],然后除以 PI 后,取值范围变成了 [-1, 1] :
// shape
p.y -= 0.25;// 向屏幕下方偏移 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);// 主要使心的形状更加扁平化,暂时先忽略
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

在这里插入图片描述

  • 通过上图来理解心形的绘制过程,每条直线上像素点得到的 a 值都是相同的,用黄点表示距离屏幕中心的远近,然后通过 d-r 的值来确定心形的边界:
vec3 col = mix(bcol, hcol, smoothstep(-0.06, 0.06, d-r));
  • 1
  • 以上是绘制心形的关键函数,hcol 是心的颜色,bcol 是背景色:

在这里插入图片描述

  • smoothstep 是一个很常用的平滑过渡函数,当第三个参数比 -0.06 小时,返回 0,比0.06 大时返回 1,如果在 -0.06 和 0.06 之间,则返回 0 到 1 之间的值,其作用是用于平缓 d-r 的值在正负交界处的突变:

在这里插入图片描述

  • mix 函数用于加权混合心的颜色和背景色,根据 smoothstep 函数特性,在心形内用心的颜色,在心形外用背景色,而边界则是两种颜色之间的模糊过渡。
  • 再说说心形扁平化函数的作用,当不使用扁平化函数,而是直接用 h-r 来控制心的形状,得到的图像是一个又胖又肥的心形,这样大概可以得知这个函数的作用:
// float d = (13.0*h - 22.0*h*h + 10.0*h*h*h)/(6.0-5.0*h);// 主要使心的形状更加扁平化,暂时先忽略
vec3 col = mix(bcol, hcol, smoothstep(-0.06, 0.06, h-r));
  • 1
  • 2
  • 不使用扁平化函数时得到的心形:

在这里插入图片描述

  • 然后看看心的颜色生成,由表达式 vec3(1.0,0.5*r,0.3) 可以看出心的颜色是红色,且由屏幕中心向四周红色逐渐减弱,然后产生一系列渐变,最后分出心形内外的区域颜色:
// color
float s = 0.75 + 0.75*p.x;//在 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 );//借助变量 r/d 分出了心形内外的区域颜色
vec3 hcol = vec3(1.0,0.5*r,0.3)*s;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 直接输出心的颜色 hcol ,看看是什么效果:

在这里插入图片描述

  • 最后是跳动效果的实现,其原理就是对屏幕像素在 x、y 方向进行周期性偏移,偏移幅度由特殊的函数来控制:
// animate
float tt = u_time; // 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);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 通过下面的代码控制输入时间周期为 2000ms:
float time = static_cast<float>(fmod(GetSysCurrentTime(), 2000) / 2000);
glUniform1f(m_TimeLoc, time);
  • 1
  • 2
  • 振幅控制函数的模拟曲线如下图所示,大致可以看出心跳动时幅度变化情况:

在这里插入图片描述

  • 最后还有一点需要注意的是 GLSL 脚本中精度的声明,这里使用的是 highp 精度,但是当使用 mediump 精度时,会出现由于精度不够导致的毛刺现象,如下图所示:

在这里插入图片描述