likes
comments
collection
share

【Flutter&GLSL】用Fragment Shader来实现高性能的动画效果——翻页动画的阴影实现

作者站长头像
站长
· 阅读数 8

前文回顾:

在之前的文章中,已经实现了最基本的翻页动画,不过距离适配与可用,还差两个小目标:

  • 翻页阴影的实现
  • 翻页限制和对应手势信息的处理转换

这次就接着对这两部进行处理,首先就来看下阴影部分的实现。

首先还是奉上这次实现后的效果:

【Flutter&GLSL】用Fragment Shader来实现高性能的动画效果——翻页动画的阴影实现

现在回到正题,看下阴影效果应该怎么实现出来。

如何通过GLSL来实现一个阴影效果

阴影效果的实现,说白了就是一个黑色渐变为透明的过程,在glsl中,只要提供一个渐变的数值,那么阴影就可以根据这个渐变数值来实现出来,因此第一步就需要找到这个渐变数值。

这里首先还是回顾下原理:

【Flutter&GLSL】用Fragment Shader来实现高性能的动画效果——翻页动画的阴影实现

如图中所示,各个区域的绘制是根据目标纹理位置在鼠标方向上映射,减去鼠标和计算原点的距离所得的dist(当然现稍有区别,会再减去一部分来让翻页部分跟随手指)来判断划分各个区域;

  • 如果dist大于卷曲效果的半径,那么视为已经翻过去的部分;
  • 如果dist大于0小于半径,那么视为正在卷曲的部分;
  • 如果dist小于0,则视为还未卷曲正常展示的部分;

按照这个划分逻辑,可以看到dist是一个从大到小的渐变数值,在包含了鼠标方向之外,还结合上了纹理位置这个因素,是一个相对比较合适的判断依据。


我们在获得了当前渐变数值的同时,还需要得知阴影延申的方向,以及阴影的范围大小

在已翻页部分,这个问题比较好解决:

由于dist是其在鼠标方向上的映射距离,自然包含了鼠标方向,这个就是阴影的方向,因此我们只需要根据dist的大小来规定其渐变效果即可,比如说像这样,根据dist的值修改一个黑色颜色的透明度:

vec4(0.0, 0.0, 0.0, (1.0 - pow(clamp((dist - radius)*pi, 0.0, 1.0), 0.2)));

比较麻烦的是正在翻页的部分和已经翻页的部分:

由于翻页部分的阴影应该围绕翻起页脚,且方向同书页方向,这样dist这种包含鼠标方向的数值就不能使用了。我们需要寻找一个新的数值。

在翻起页的计算过程中,会计算当前像素位置对应在纹理上的位置,其中就有一个判断在蜷曲轴上是否有多个映射点的过程。因此可以这么想象一下,阴影就是一个书页纹理未蜷曲前外面围绕的一圈,如果这包裹的一圈能随着书页一起蜷曲起来,那不就实现了阴影效果么?

【Flutter&GLSL】用Fragment Shader来实现高性能的动画效果——翻页动画的阴影实现

根据这个设想,可以给书页纹理在增加一些映射范围,如果存在多个映射点,那说明正好是需要绘制阴影的部分:

if (p2.x <= aspect+shadowWidth && p2.y <= 1.0+shadowWidth&& p2.x > 0.0-shadowWidth && p2.y > 0.0-shadowWidth){
   /// todo;判断阴影颜色
}

对于阴影颜色效果的实现,那就根据当前映射位置距离书页纹理位置的大小判断就行,类似这样:

if (targetPoint.y>=1.0){ 
   return max(pow(clamp((targetPoint.y-1.0)/shadowWidth, 0.0, 0.9), 0.2),pow(clamp((targetPoint.x-aspect)/shadowWidth, 0.0, 0.9), 0.2));
} else { 
   return max(pow(clamp((0.0-targetPoint.y)/shadowWidth, 0.0, 0.9), 0.2),pow(clamp((targetPoint.x-aspect)/shadowWidth, 0.0, 0.9), 0.2)); 
}

由于实际翻页角度并不像上图所示,完全横向的那种,因此需要判断两个方向上的颜色,这里直接取两个方向上较大的作为阴影的渐变数值。

shader完整代码

#include <flutter/runtime_effect.glsl>

uniform vec2 resolution;
uniform vec4 iMouse;
uniform sampler2D image;

#define pi 3.14159265359
#define radius 0.05
#define shadowWidth 0.02
#define TRANSPARENT vec4(0.0, 0.0, 0.0, 0.0)

out vec4 fragColor;

float calShadow(vec2 targetPoint, float aspect){
    if (targetPoint.y>=1.0){
        return max(pow(clamp((targetPoint.y-1.0)/shadowWidth, 0.0, 0.9), 0.2), pow(clamp((targetPoint.x-aspect)/shadowWidth, 0.0, 0.9), 0.2));
    } else {
        return max(pow(clamp((0.0-targetPoint.y)/shadowWidth, 0.0, 0.9), 0.2), pow(clamp((targetPoint.x-aspect)/shadowWidth, 0.0, 0.9), 0.2));
    }
}

void main() {
    vec2 fragCoord = FlutterFragCoord().xy;

    float aspect = resolution.x / resolution.y;

    vec2 uv = fragCoord * vec2(aspect, 1.0) / resolution.xy;
    // 归一化鼠标坐标
    vec2 mouse = iMouse.xy  * vec2(aspect, 1.0) / resolution.xy;

    vec2 cornerFrom = (iMouse.w<resolution.y/2)?vec2(resolution.x, 0.0):vec2(resolution.x, resolution.y);

    // 鼠标方向的向量
    vec2 mouseDir = normalize(abs(cornerFrom) - iMouse.xy);
    // 翻页原点的计算,可以视为转换为横轴下的x轴起点位置
    vec2 origin = clamp(mouse - mouseDir * mouse.x / mouseDir.x, 0.0, 1.0);
    // 鼠标距离
    float mouseDist = distance(mouse, origin);
    //    float mouseDist = clamp(length(mouse - origin)
    //    + (aspect - (abs(cornerFrom.x) / resolution.x) * aspect) / mouseDir.x, 0.0, aspect / mouseDir.x);
    // 如果鼠标方向向左,那么鼠标拖动距离就是鼠标到原点的距离
    if (mouseDir.x < 0.0) {
        mouseDist = distance(mouse, origin);
    }

    float proj = dot(uv - origin, mouseDir);
    float dist = proj - mouseDist;
    vec2 curlAxisLinePoint = uv - dist * mouseDir;

    if (distance(mouse, cornerFrom* vec2(aspect, 1.0) / resolution.xy)>=pi*radius) {
        float params = (distance(mouse, cornerFrom* vec2(aspect, 1.0) / resolution.xy)-pi*radius)/2;
        curlAxisLinePoint = uv - dist * mouseDir +params*mouseDir;
        dist -=params;
    }

    if (dist > radius) {
        fragColor = vec4(0.0, 0.0, 0.0, (1.0 - pow(clamp((dist - radius)*pi, 0.0, 1.0), 0.2)));

    } else if (dist >= 0.0) {
        // map to cylinder point
        float theta = asin(dist / radius);
        vec2 p2 = curlAxisLinePoint + mouseDir * (pi - theta) * radius;
        vec2 p1 = curlAxisLinePoint + mouseDir * theta * radius;
        if (p2.x <= aspect && p2.y <= 1.0 && p2.x > 0.0 && p2.y > 0.0){
            uv = p2;
            fragColor = texture(image, uv * vec2(1.0 / aspect, 1.0));
            fragColor.rgb *= pow(clamp((radius - dist) / radius, 0.0, 1.0), 0.2);
        } else {
            uv = p1;
            fragColor = texture(image, uv * vec2(1.0 / aspect, 1.0));
            if (p2.x <= aspect+shadowWidth && p2.y <= 1.0+shadowWidth&& p2.x > 0.0-shadowWidth && p2.y > 0.0-shadowWidth){
                float shadow = calShadow(p2, aspect);
                fragColor = vec4(fragColor.r*shadow, fragColor.g*shadow, fragColor.b*shadow, fragColor.a);
            }
        }
    } else {
        vec2 p = curlAxisLinePoint + mouseDir * (abs(dist) + pi * radius);

        if (p.x <= aspect && p.y <= 1.0 && p.x > 0.0 && p.y > 0.0){
            uv = p;
            fragColor = texture(image, uv * vec2(1.0 / aspect, 1.0));
        } else {
            fragColor = texture(image, uv * vec2(1.0 / aspect, 1.0));
            if (p.x <= aspect+shadowWidth && p.y <= 1.0+shadowWidth&& p.x > 0.0-shadowWidth && p.y > 0.0-shadowWidth){
                float shadow = calShadow(p, aspect);
                fragColor = vec4(fragColor.r*shadow, fragColor.g*shadow, fragColor.b*shadow, fragColor.a);
            }
        }
    }
}

小结

现在已经完成了阴影部分的效果,再剩下的部分就是增加翻页范围的限制,以应对过度翻页。根据目前的结果来看,性能方面还是非常满意的,基本能保证FPS>=50的比率高于95%,没有BigJank的情况,基本可以视为不会出现卡顿的情况了。

在完成了翻页范围的限制之后,就需要将这个翻页动画应用到小说阅读器上,在应用小说阅读器的过程中,或许可以再次审视一下分页功能和负责手势处理的ListView?