Shader 动画

4,344 阅读3分钟

Shader 动画和 Canvas 动画原理是一样的,通过定时器循环渲染,并改变画布中图形的属性来实现动画。

一些 Shader 编辑器都已经实现好了定时器的功能,同时会传递一些跟时间相关的值给到着色器代码中,如 ShaderToy 中与时间相关的属性是 iTime/iTimeDelta,gl-transition 中与时间相关的属性是 progress。通过将着色器代码中的变量与时间相结合,就可以让动画产生。

一、位移动画

在之前的文章中讲到了坐标的运算,其中加减就是位移:

那常规的位移动画就不说,下面讲些复杂的运动:

1. 圆周运动

st += vec(cos(u_time), sin(u_time)) 就是圆周运动了:

二、旋转

旋转可以通过矩阵来轻松完成:

但你会发现,当我们把矩阵和坐标相乘,得到的确实上面的圆周运动,这是因为旋转坐标在左下角,如果想旋转矩形,则必须把中心点挪到矩形中心,或者换个说法,把矩形中心挪到左下角。

封装好的代码:

mat2 rotate2d(float _angle){
    return mat2(cos(_angle),-sin(_angle),
                sin(_angle),cos(_angle));
}

当我们在非正方形的画布中对材质进行旋转的时候,会遇到一个难以避开的问题,就是拉伸问题。举个例子,当画布是矩形时,暴露了旋转时拉伸的问题:

// 部分代码
void main() {
    vec2 st = textureCoordinate;
    float ratio = inputImageTextureSize.x / inputImageTextureSize.y;
    float animationTime = getAnimationTime();
    float easingTime = animationTime;

    float bigRotation = 30./180.*3.14159;
    st = rotateUv(st, bigRotation*easingTime, vec2(.5, .5), 1.);

    gl_FragColor = texture2D(inputImageTexture, st);
}

这篇文章也提到了这个问题,之所以会出现这个问题,是因为由于宽高的比例不是 1:1,说明虽然 x 和 y 的坐标都是 0~1,但是它们表示的长度是不一样的,正因为这个长度不一样,导致在旋转的时候,像素点的运动并不符合画布的比例:

而解决方案也比较简单,我们在旋转的时候,对 y 轴进行画布比例的拉伸即可:

void main() {
    vec2 st = textureCoordinate;
    float ratio = inputImageTextureSize.x / inputImageTextureSize.y;
    float animationTime = getAnimationTime();
    float easingTime = animationTime;

    float bigRotation = 30./180.*3.14159;

    st.y *= 1./ratio;
    st = rotateUv(st, bigRotation*easingTime, vec2(.5, .5), 1.);
    st.y *= ratio;

    gl_FragColor = texture2D(inputImageTexture, st);
}

三、缩放

缩放就是坐标的乘除运算,同样也可以通过矩形来实现:

同理,如果不对坐标系进行转换,缩放的中心还是在左下角:

两个动画组合一起:

封装好的代码:

mat2 scale(vec2 _scale){
    return mat2(_scale.x,0.0,
                0.0,_scale.y);
}

四、正弦运动

正弦余弦是非常优美的动画曲线,我们在讲函数可视化也展示过下面这张图(图片来源于bookofshader):

所以通过这张图你应该知道正弦余弦函数可以做出什么样的效果了,我们来试试:

这是笛卡尔坐标系的运动,假设我们把坐标系变成极坐标系,大家能脑补大概的效果了吗?

假如我想做一个外向扩散的动画,类似雷达那样的效果呢?去掉 sin() 函数即可:

相关链接: