【Flutter&GLSL】用Fragment Shader来实现高性能的动画效果——翻页动画的阴影实现在前面的文章中,
前文回顾:
在之前的文章中,已经实现了最基本的翻页动画,不过距离适配与可用,还差两个小目标:
- 翻页阴影的实现
- 翻页限制和对应手势信息的处理转换
这次就接着对这两部进行处理,首先就来看下阴影部分的实现。
首先还是奉上这次实现后的效果:
现在回到正题,看下阴影效果应该怎么实现出来。
如何通过GLSL来实现一个阴影效果
阴影效果的实现,说白了就是一个黑色渐变为透明的过程,在glsl中,只要提供一个渐变的数值,那么阴影就可以根据这个渐变数值来实现出来,因此第一步就需要找到这个渐变数值。
这里首先还是回顾下原理:
如图中所示,各个区域的绘制是根据目标纹理位置在鼠标方向上映射,减去鼠标和计算原点的距离所得的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这种包含鼠标方向的数值就不能使用了。我们需要寻找一个新的数值。
在翻起页的计算过程中,会计算当前像素位置对应在纹理上的位置,其中就有一个判断在蜷曲轴上是否有多个映射点的过程。因此可以这么想象一下,阴影就是一个书页纹理未蜷曲前外面围绕的一圈,如果这包裹的一圈能随着书页一起蜷曲起来,那不就实现了阴影效果么?
根据这个设想,可以给书页纹理在增加一些映射范围,如果存在多个映射点,那说明正好是需要绘制阴影的部分:
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?
转载自:https://juejin.cn/post/7337941065020801065