OpenGL ES 案例复刻(二) 一个律动的爱心

本文正在参与「金石方案 . 分割6万现金大奖」

What

这次咱们来做点有趣的事,完成一个律动的爱心,先来看一下作用图

OpenGL ES 案例复刻(二) 一个律动的爱心

这个特效来源于SahderToy,我也是从这篇博客了解到的,可是说实话原博客对于完成进程剖析得不够清晰,我是没看明白,所以才写下这篇。

咱们先完成原始作用,然后逐渐了解完成进程,终究咱们能够尝试进行一些改动,比方我这儿改了改动画,让动效看起来像是心跳

OpenGL ES 案例复刻(二) 一个律动的爱心

本文一切代码都在这儿

How

shaderToy上面的代码直接拿过来用就能够了,可是它仅仅一个片段着色器,需求修改一下,主要是有一些uniform参数需求传入着色器

#version 300 es
precision highp float;
out vec4 fragColor;
uniform vec2 layerSize;
uniform float delta;
const float e = 2.718281828459045;
const float PI = 3.141593;
void main() {
    // 坐标转化,转化为以短轴长度为基准,以中心点为原点的相对坐标
    // (xy - wh/2) / (短轴/2) =>(2xy - wh)/ 短轴
    vec2 p = (2.0 * gl_FragCoord.xy - layerSize) / min(layerSize.y, layerSize.x);
    // background color
    vec3 bcol = vec3(1.0,0.8,0.7-0.07*p.y)*(1.0-0.25*length(p));
    // animate
    float tt = mod(delta,1.5)/1.5;
    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)/3.141593;
    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.4*r,0.3)*s;
    vec3 col = mix( bcol, hcol, smoothstep( -0.01, 0.01, d-r) );
    fragColor = vec4(col,1.0);
}

接下来还需求Renderer和极点着色器,非常简单就不多讲了,仅仅用两个三角形填充整个屏幕

#version 300 es
layout(location = 0) in vec2 mPosition;
void main() {
    gl_Position = vec4(mPosition, 0.0, 1.0);
}
#include <glm/glm.hpp>
#include <chrono>
#include "core/advanced/HeartRenderer.h"
void HeartRenderer::onSurfaceCreated() {
    shader = Shader("shader/heart/heart.vert", "shader/heart/heart2.frag");
    float vertices[] = {
            -1.0f, -1.0f,
            1.0f, -1.0f,
            -1.0f, 1.0f,
            1.0f, -1.0f,
            1.0f, 1.0f,
            -1.0f, 1.0f,
    };
    glGenBuffers(1, &VBO);
    glGenVertexArrays(1, &VAO);
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
    glBindVertexArray(VAO);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 2, GL_FLOAT, false, 2 * sizeof(float), nullptr);
    shader.use();
}
void HeartRenderer::onDraw() {
    using namespace std::chrono;
    auto t = system_clock::now();
    auto tt = duration_cast<duration<long long, std::ratio<1, 1000>>>(t.time_since_epoch()).count();
    // 2秒为一个动画周期
    auto delta = static_cast<float>(fmod(tt, 2000) / 2000);
    shader.setVec2("layerSize", glm::vec2(surfaceWidth, surfaceHeight));
    shader.setFloat("delta", delta);
    glClear(GL_COLOR_BUFFER_BIT);
    glClearColor(0.1f, 0.1f, 0.1f, 0.0f);
    glBindVertexArray(VAO);
    glDrawArrays(GL_TRIANGLES, 0, 6);
}
HeartRenderer::~HeartRenderer() {
    glDeleteBuffers(1, &VBO);
    glDeleteVertexArrays(0, &VAO);
    shader.release();
}

这就完成了,现在你能够运转看看作用

Why

现在咱们来一同看看这么有趣的作用是怎样用短短几十行代码完成的,可是在那之前,先做点别的工作,先来画一个圆,假如你还不知道该怎样做的话。

制作一个圆

创立一个circle.frag片段着色器,代码如下

#version 300 es
precision highp float;
out vec4 fragColor;
uniform vec2 layerSize;
// 画一个圆
void main() {
    // 坐标转化,转化为以短轴长度为基准,以中心点为原点的相对坐标
    // (xy - wh/2) / (短轴/2) =>(2xy - wh)/ 短轴
    vec2 uv = (2.0 * gl_FragCoord.xy - layerSize) / min(layerSize.y, layerSize.x);
    float r = length(uv);
    // 间隔原点小于0.5的片段烘托为赤色,smoothstep做边际平滑
    // t = clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0);
    // smoothstep(edge0, edge1, x) = (3.0 - 2.0 * t) * t * t;
    // c = r < 0.5 => 1  r > 0.51 > 0
    float c = smoothstep(0.51, 0.5, r);
    // float c = (r > 0.4 && r < 0.5) ? 1.0 : 0.0;
    fragColor = vec4(.8,.2,0.3,0.0) * c;
}
  • gl_FragCoord是以屏幕左上角为原点的,所以先把片段坐标转化为以屏幕中心为原点的的坐标系,并且把肯定坐标转化为[-1, 1]的相对坐标,以宽度为基准
  • 判别该片段是否在圆的内部,这儿咱们想要画一个半径为0.5的圆,所以只需求判别和原点的间隔小于0.5就好了,在这之外的片段烘托成黑色

这儿用到了smoothstep函数,它用来做边际平滑,半径在[0.5, 0.51]范围内的片段和背景做一个混合,让边际看起来更自然。

来用desmos看看smoothstep函数的作用

OpenGL ES 案例复刻(二) 一个律动的爱心

限制一个上界t1一个下界t2,输入 < t1时输出为1,> t2时输出为0,在[t1, t2]内时平滑过渡,用在这儿正好对应了圆内、圆外、边际。

制作爱心

上面咱们试着制作了一个圆,接下来咱们来制作一个初步的爱心,代码如下

#version 300 es
precision highp float;
out vec4 fragColor;
uniform vec2 layerSize;
uniform float delta;
const float PI = 3.141593;
void main() {
    // 坐标转化,转化为以短轴长度为基准,以中心点为原点的相对坐标
    // (xy - wh/2) / (短轴/2) =>(2xy - wh)/ 短轴
    vec2 uv = (2.0 * gl_FragCoord.xy - layerSize) / min(layerSize.y, layerSize.x);
    // 因为本来的uv坐标左上角为(0, 0),所以这儿的y轴是反的,咱们纠正一下
    uv.y = - uv.y;
    // 背景色从中心到四周由浅到深
    vec3 bgColor = vec3(1.0, 0.8, 0.7 - 0.07 * -uv.y) * (1.0 - 0.25 * length(uv));
    // 上移
    uv.y += 0.25;
    // 片段坐标所在角的弧度,同一个方向上的片段atan值相同
    // atan2(x, y) [-PI,PI] => [-1, 1]
    float a = atan(uv.x, -uv.y) / PI;
    float h = abs(a);
    float r = length(uv);
    // 爱心颜色
    vec3 hcol = vec3(1.0, 0.4, 0.3);
    vec3 col = r < h ? hcol : bgColor;
    // 边际平滑
    // vec3 col = mix(bgColor, hcol, smoothstep(-0.01, 0.01, h - r));
    fragColor = vec4(col, 1.0);
}

运转结果如下

OpenGL ES 案例复刻(二) 一个律动的爱心

心形图案主要是靠atan2函数制作出来的,atan2(x, -y)/得到的值在每个方向上面都相同,假如咱们以原点为中心,以每个方向求得的h值为长度画出一条条线,它们刚好就能构成一个心形。

OpenGL ES 案例复刻(二) 一个律动的爱心

现在问题就变得简单处理了,咱们能够像之前画圆相同来判别当时片段的坐标和原点的间隔r是否小于h,来断定该片段是否在爱心的内部,从而把它画出来。

让爱心变得愈加修长

现在的爱心有点太饱满了,咱们来给它瘦身一下,只需求一行代码对h值做一个非线性改换

void main() {
    ......
    float d = (13.0 * h - 22.0 * h * h + 10.0 * h * h * h) / (6.0 - 5.0 * h);
    ......
    vec3 col = mix(bgColor, hcol, smoothstep(-0.01, 0.01, d - r));
    fragColor = vec4(col, 1.0);
}

还是来看一下这个函数的曲线

OpenGL ES 案例复刻(二) 一个律动的爱心
本来的h值函数曲线是平滑递加的(因为desmos不支持atan2函数,能够在geogebra上面试试看原函数曲线),那么想要爱心曲线有一些改变就需求对原函数进一步改换。把h值代入为x,进行进一步的改换,能够看到曲线后半段改变量放缓然后又加快,这样子就能够在心形的下半部产生洼陷,得到想要的结果。

OpenGL ES 案例复刻(二) 一个律动的爱心

添加立体感

让每个片段的明暗跟r、d、x坐标联系起来,具体作用写在注释里了

// 颜色插值,让爱心更有立体感
// 让x轴正半轴方向更亮
float s = 0.75 + 0.75 * uv.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.4 * r, 0.3) * s;

OpenGL ES 案例复刻(二) 一个律动的爱心

动起来

先来观察一下这个动画,类似一个横向的挤压回弹作用,那么咱们需求在x方向上向内偏移,y方向上向外偏移;因为有几次回弹作用,所以偏移量还需求正负替换,并且因为动画起伏是渐弱的,所以偏移量还需求逐渐变小;终究,偏移量周期性改变就根据当时帧数来核算。

uniform float delta;
......
float ss = pow(delta, .2) * 0.5 + 0.5;
ss = 1.0 + ss * 0.5 * sin(delta * 6.2831 * 3.0 -uv.y * 0.5) * exp(-delta * 4.0);
uv *= (vec2(0.5, 1.5) + ss * vec2(0.5, -0.5));
auto t = system_clock::now();
auto tt = duration_cast<duration<long long, std::ratio<1, 1000>>>(t.time_since_epoch()).count();
// 2秒为一个动画周期
auto delta = static_cast<float>(fmod(tt, 2000) / 2000);

将动画周期设为两秒,改变量值在[0, 1]区间,将每帧的时间改变量传入shader,再经过函数核算改换为片段的偏移量

相同来看一下这个函数的曲线,这儿咱们把y值设为滑块a,然后别离看看y和x偏移后的值随delta的改变,红线是x,黑线是y

OpenGL ES 案例复刻(二) 一个律动的爱心

OpenGL ES 案例复刻(二) 一个律动的爱心
能够看到函数曲线和咱们剖析的相同,x、y偏移量相反,偏移量相对于初始量增减替换,并且渐弱,这样就完成了终究的作用

Extra

剖析完了原完成,咱们来尝试改改动画,完成心跳作用

其实很简单,咱们只需求一个渐弱的缩小动画,也便是片段向内偏移,代码就两行

float ss = (1.0 - 0.3/(delta + 0.3));
uv *= 0.85 + ss * 0.15;

它的函数曲线如下,在[0.85-1.0]递加,先快后慢。

OpenGL ES 案例复刻(二) 一个律动的爱心

xy变小会让心形变大,所以这儿其实是呈现一个逐渐缩小的动画,缩到最小后又突然扩大回原尺度,看起来就像是心跳了,注意将动画周期缩短为一秒看起来愈加真实

End

大多数创造都从模仿开端,着手尝试才干学到东西,我这儿仅仅简单地改了改动画,假如完全了解了完成思路,想做别的改动或者是用相同的套路做其他类似的作用也不难。 本文正在参与「金石方案 . 分割6万现金大奖」