Flutter 小技巧之不一样的思路实现炫酷 3D 翻页折叠动画

今天聊一个比较有意思的 Flutter 动画实现,如果需要实现一个如下图的 3D 折叠动画效果,你会选择通过什么方式?

相信可能很多人第一想法就是:在 Dart 里通过矩阵变换配合 Canvas 实现

因为这个效果其实也算「常见」,在目前的小说阅读器场景里,类似的翻页效果基本都是通过这个思路完成,而这个思路以前我也「折腾」过不少,比如 《炫酷的 3D 卡片和帅气的 360° 展示效果》 和 用纯代码实现立体 Dash 和 3D 掘金 Logo ,就是在 Dart 里利用矩阵变换实现的视觉 3D 效果。

但是今天通过一个叫 riveo_page_curl 的项目,提供了不一样的实现方式,那就是通过自定义 Fragment Shaders 实现动画 ,使用自定义 shaders 可以直接使用 GLSL 语言来进行编程,最终达到通过 GPU 渲染出更丰富图形效果。

解释这个项目之前,我们先聊聊 Fragment Shader ,Flutter 在 3.7 开始提供 Fragment Shader API ,顾名思义,它是一个作用于片段的着色器,也就是通过 Fragment Shader API ,开发者可以直接介入到 Flutter 渲染管道的渲染流程中。

那么直接使用 Fragment Shader 而不使用 Dart 矩阵变换的好处是什么?简单来说就是可以减少 CPU 的耗时,直接通过图形语言(GLSL)直接给 GPU 发送指令,从性能上无疑可以得到提升,并且实现会更简洁。

不过加载着色器这个行为的开销可能会比较大,所以必须在运行时将它编译为适当的特定于平台的着色器。

当然,在 Flutter 里使用 Fragment Shader 也是有条件限制的,例如一般都需要引入 #include <flutter/runtime_effect.glsl> 这个头文件,因为在编写着色器代码时,我们都需要知道当前片段的局部坐标的值,而 flutter/runtime_effect.glsl 里就提供了 FlutterFragCoord().xy; 来支持访问局部坐标,而这并不是标准 GLSL 的 API 。

另外, Fragment Shader 只支持 .frag 格式文件, 不支持顶点着色文件 .vert ,同时还有以下限制:

  • 不支持 UBO 和 SSBO
  • sampler2D 是唯一受支持的 sampler 类型
  • texture 仅支持( sampler 和 uv)的两个参数版本
  • 不能声明额外的可变输入
  • 不支持无符号整数和布尔值

所以如果需要搬运一些已有的 GLSL 效果,例如 shadertoy 上的代码时,那么一些必要的「代码改造」还是逃不掉的,例如下方代码是一段渐变动画的着色器:

void mainImage( out vec4 fragColor, in vec2 fragCoord ){float strength = 0.4;float t = iTime/3.0;vec3 col = vec3(0);vec2 fC = fragCoord;for(int i = -1; i <= 1; i++) {for(int j = -1; j <= 1; j++) {fC = fragCoord+vec2(i,j)/3.0;vec2 pos = fC/iResolution.xy;pos.y /= iResolution.x/iResolution.y;pos = 4.0*(vec2(0.5) - pos);for(float k = 1.0; k < 7.0; k+=1.0){ pos.x += strength * sin(2.0*t+k*1.5 * pos.y)+t*0.5;pos.y += strength * cos(2.0*t+k*1.5 * pos.x);}col += 0.5 + 0.5*cos(iTime+pos.xyx+vec3(0,2,4));}}col /= 9.0;col = pow(col, vec3(0.4545));fragColor = vec4(col,1.0);
}

而在 Flutter 里,就需要转化为如下代码所示:

  • 首先就是必不可少的 flutter/runtime_effect.glsl
  • 其次定义 main() 函数
  • 然后我们需要将 mainImage 里定义的 out vec4 fragColor; 移到全局声明
  • 因为在 GLSL 里 iResolution 用于表示画布像素高宽,iTime 是程序运行的时间,而这里通过 uniform 定义 resolutioniTime 直接用于接受 Dart 端的输入,其余逻辑不变
  • 对应 fragCoord 可以在 Flutter 里通过 FlutterFragCoord 获取坐标
#version 460 core
#include <flutter/runtime_effect.glsl>out vec4 fragColor;uniform vec2 resolution;
uniform float iTime;void main(){float strength = 0.25;float t = iTime/8.0;vec3 col = vec3(0);vec2 pos = FlutterFragCoord().xy/resolution.xy;pos = 4.0*(vec2(0.5) - pos);for(float k = 1.0; k < 7.0; k+=1.0){pos.x += strength * sin(2.0*t+k*1.5 * pos.y)+t*0.5;pos.y += strength * cos(2.0*t+k*1.5 * pos.x);}col += 0.5 + 0.5*cos(iTime+pos.xyx+vec3(0,2,4));col = pow(col, vec3(0.4545));fragColor = vec4(col,1.0);
}

第一行 #version 460 core 指定所用的 OpenGL 语言版本。

可以看到转换一段 GLSL 代码并不特别麻烦,主要是坐标和输入参数的变化,而通过这些已有的片段着色器,却可以给我们提供极其丰富的渲染效果,如下代码所示:

  • pubspec.yaml 里引入上面的 shaders 代码

  • 通过 ShaderBuilder 加载对应 'shaders/warp.frag' 文件,获得 FragmentShader

  • 利用 FragmentShadersetFloat 传递数据

  • 通过 Paint()..shader 添加着色器进行绘制,就可以完成渲染

flutter:shaders:- shaders/warp.frag·············late Ticker _ticker;Duration _elapsed = Duration.zero;void initState() {super.initState();_ticker = createTicker((elapsed) {setState(() {_elapsed = elapsed;});});_ticker.start();}Widget build(BuildContext context) => ShaderBuilder(assetKey: 'shaders/warp.frag',(BuildContext context, FragmentShader shader, _) => Scaffold(appBar: AppBar(title: const Text('Warp')),body: CustomPaint(size: MediaQuery.of(context).size,painter: ShaderCustomPainter(shader, _elapsed) ),),);class ShaderCustomPainter extends CustomPainter {final FragmentShader shader;final Duration currentTime;ShaderCustomPainter(this.shader, this.currentTime);void paint(Canvas canvas, Size size) {shader.setFloat(0, size.width);shader.setFloat(1, size.height);shader.setFloat(2, currentTime.inMilliseconds.toDouble() / 1000.0);final Paint paint = Paint()..shader = shader;canvas.drawRect(Offset.zero & size, paint);}bool shouldRepaint(CustomPainter oldDelegate) => true;
}

这里唯一需要解释的就是 shader.setFloat 流程,因为它其实是通过索引来对应到我们在 .frag 文件里的变量,简单来说:

这里我们在 GLSL 里定义了 uniform vec2 resolution;uniform float iTime; ,那么 vec2 resolution 就占据了索引 0 和 1 ,float iTime 就占据了索引 2 。

大概理解就是,vec2 就是两个 float 类型的值保存在了一起的意思,所以先声明的 vec2 resolution 就占据了 索引 0 和 1 ,举个例子,如下图所示,此时的 vec2 和 vec3 分了就占据了 0-4 的索引。

而通过 uniform 在 GLSL 着色器中定义值,然后在 Dart 中就可以通过 setFloat 的索引来传递对应数据过去,从而形成了数据交互的完整闭环。

这里的渐变动画在 Flutter 的完整代码可以参考 Github https://github.com/tbuczkowski/flutter_shaders 里的 warp.frag ,

同时针对前面整个渐变动画,作者在仓库内还提供了对应纯 Dart 代码实现一样效果的对比,通过数据可以看到,利用着色器的实现在性能上得到了巨大的提升。

image-20231031175152699

那么回过头来, riveo_page_curl 的项目里的折叠着色器如下所示,除了一堆不懂的矩阵变化,如 scale 缩放、translate 平移和 project 投影转换之外,就是各种看不明白的三角函数计算,简单的核心就是在矩阵变化时计算弯曲部分的弧度,以及增加阴影投影来提高视觉效果。

#include <flutter/runtime_effect.glsl>uniform vec2 resolution;
uniform float pointer;
uniform float origin;
uniform vec4 container;
uniform float cornerRadius;
uniform sampler2D image;const float r = 150.0;
const float scaleFactor = 0.2;#define PI 3.14159265359
#define TRANSPARENT vec4(0.0, 0.0, 0.0, 0.0)mat3 translate(vec2 p) {return mat3(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, p.x, p.y, 1.0);
}mat3 scale(vec2 s, vec2 p) {return translate(p) * mat3(s.x, 0.0, 0.0, 0.0, s.y, 0.0, 0.0, 0.0, 1.0) * translate(-p);
}vec2 project(vec2 p, mat3 m) {return (inverse(m) * vec3(p, 1.0)).xy;
}struct Paint {vec4 color;bool stroke;float strokeWidth;int blendMode;
};struct Context {vec4 color;vec2 p;vec2 resolution;
};bool inRect(vec2 p, vec4 rct) {bool inRct = p.x > rct.x && p.x < rct.z && p.y > rct.y && p.y < rct.w;if (!inRct) {return false;}// Top left cornerif (p.x < rct.x + cornerRadius && p.y < rct.y + cornerRadius) {return length(p - vec2(rct.x + cornerRadius, rct.y + cornerRadius)) < cornerRadius;}// Top right cornerif (p.x > rct.z - cornerRadius && p.y < rct.y + cornerRadius) {return length(p - vec2(rct.z - cornerRadius, rct.y + cornerRadius)) < cornerRadius;}// Bottom left cornerif (p.x < rct.x + cornerRadius && p.y > rct.w - cornerRadius) {return length(p - vec2(rct.x + cornerRadius, rct.w - cornerRadius)) < cornerRadius;}// Bottom right cornerif (p.x > rct.z - cornerRadius && p.y > rct.w - cornerRadius) {return length(p - vec2(rct.z - cornerRadius, rct.w - cornerRadius)) < cornerRadius;}return true;
}out vec4 fragColor;void main() {vec2 xy = FlutterFragCoord().xy;vec2 center = resolution * 0.5;float dx = origin - pointer;float x = container.z - dx;float d = xy.x - x;if (d > r) {fragColor = TRANSPARENT;if (inRect(xy, container)) {fragColor.a = mix(0.5, 0.0, (d-r)/r);}}elseif (d > 0.0) {float theta = asin(d / r);float d1 = theta * r;float d2 = (3.14159265 - theta) * r;vec2 s = vec2(1.0 + (1.0 - sin(3.14159265/2.0 + theta)) * 0.1);mat3 transform = scale(s, center);vec2 uv = project(xy, transform);vec2 p1 = vec2(x + d1, uv.y);s = vec2(1.1 + sin(3.14159265/2.0 + theta) * 0.1);transform = scale(s, center);uv = project(xy, transform);vec2 p2 = vec2(x + d2, uv.y);if (inRect(p2, container)) {fragColor = texture(image, p2 / resolution);} else if (inRect(p1, container)) {fragColor = texture(image, p1 / resolution);fragColor.rgb *= pow(clamp((r - d) / r, 0.0, 1.0), 0.2);} else if (inRect(xy, container)) {fragColor = vec4(0.0, 0.0, 0.0, 0.5);}}else {vec2 s = vec2(1.2);mat3 transform = scale(s, center);vec2 uv = project(xy, transform);vec2 p = vec2(x + abs(d) + 3.14159265 * r, uv.y);if (inRect(p, container)) {fragColor = texture(image, p / resolution);} else {fragColor = texture(image, xy / resolution);}}}

其实我知道大家并不关心它的实现逻辑,更多是如何使用,这里有个关键信息就是 uniform sampler2D image ,通过引入 sampler2D ,我们就可以在 Dart 通过 setImageSampler(0, image); ui.Image 传递到 GLSL 里,这样就可以对 Flutter 控件实现上述的折叠动画逻辑。

对应在 Dart 层,就是除了 ShaderBuilder 之外,还可以通过 flutter_shaders 的 AnimatedSampler 来实现更简洁的 shaderimagecanvas 的配合,其中 AnimatedSampler 的最大作用,就是将整个 child 通过 PictureRecorder 进行截图,转化成 ui.Image 传递给 GLSL,完成 UI 传递交互效果。

  Widget _buildAnimatedCard(BuildContext context, Widget? child) {return ShaderBuilder((context, shader, _) {return AnimatedSampler((image, size, canvas) {_configureShader(shader, size, image);_drawShaderRect(shader, size, canvas);},child: Padding(padding: EdgeInsets.symmetric(vertical: cornerRadius),child: widget.child,),);},assetKey: 'shaders/page_curl.frag',);void _configureShader(FragmentShader shader, Size size, ui.Image image) {shader..setFloat(0, size.width) // resolution..setFloat(1, size.height) // resolution..setFloat(2, _animationController.value) // pointer..setFloat(3, 0) // origin..setFloat(4, 0) // inner container..setFloat(5, 0) // inner container..setFloat(6, size.width) // inner container..setFloat(7, size.height) // inner container..setFloat(8, cornerRadius) // cornerRadius..setImageSampler(0, image); // image}void _drawShaderRect(FragmentShader shader, Size size, Canvas canvas) {canvas.drawRect(Rect.fromCenter(center: Offset(size.width / 2, size.height / 2),width: size.width,height: size.height,),Paint()..shader = shader,);}

完整项目可见:https://github.com/Rahiche/riveo_page_curl

所以可以看到,相比起在 Dart 层实现这样的 3D 翻页折叠,利用 FragmentShader 实现的代码会更简洁,并且性能体验上会更优于纯 Dart 实现,最重要的是,类似 ShaderToy 里的一些着色器代码,通过简单的移植适配,就可以在直接被运用到 Flutter 里,这对于 Flutter 在游戏场景的实现来无疑说非常友好。

最后,Flutter 3.10 之后, Flutter Web 同样支持了 fragment shaders,所以着色器在 Flutter 的实现目前已经相对成熟,那么如果是之前的我通过 Flutter 实现的《霓虹灯文本的「故障」效果的实现》的逻辑转换成 fragment shaders 来完成,是不是性能和代码简洁程度也会更高?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/127809.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

使用 PyTorch 构建自定义 GPT

一、介绍 介绍大模型&#xff0c;首先考虑一下使用 ChatGPT、Bing Chat 或 Bard 。您是否想过拥有自己的 ChatGPT 会是什么样子&#xff1f;想象一下创建自己的 GPT 模型的兴奋程度。这确实是一种难以置信的感觉&#xff01; 为了开始构建自定义 GPT 的旅程&#xff0c;让我们仔…

MATLAB和S7-1200PLC OPC通信(激活S7-1200PLC OPC UA服务器)

MATLAB和SMART PLC OPC通信请参考下面文章博客: MATLAB和西门子SMART PLC OPC通信-CSDN博客文章浏览阅读123次。西门子S7-200SMART PLC OPC软件的下载和使用,请查看下面文章Smart 200PLC PC Access SMART OPC通信_基于pc access smart的opc通信_RXXW_Dor的博客-CSDN博客OPC是…

通讯网关软件032——利用CommGate X2OPC实现OPC客户端访问Modbus TCP设备

本文介绍利用CommGate X2OPC实现OPC客户端连接Modbus TCP设备。CommGate X2OPC是宁波科安网信开发的网关软件&#xff0c;软件可以登录到网信智汇(http://wangxinzhihui.com)下载。 【案例】如下图所示&#xff0c;SCADA系统上位机、PLC、设备具备Modbus TCP通讯接口&#xff…

机器学习2:决策树--基于信息增益的ID3算法

1.决策树的简介 建立决策树的过程可以分为以下几个步骤: 计算每个特征的信息增益或信息增益比,选择最优的特征作为当前节点的划分标准。根据选择的特征将数据集划分为不同的子集。对每个子集递归执行步骤 1 和步骤 2,直到满足终止条件。构建决策树,并输出。基于信息增益的…

k8s调度约束

List-Watch Kubernetes 是通过 List-Watch的机制进行每个组件的协作&#xff0c;保持数据同步的&#xff0c;每个组件之间的设计实现了解耦。 List-Watch机制 工作机制&#xff1a;用户通过 kubectl请求给 APIServer 来建立一个 Pod。APIServer会将Pod相关元信息存入 etcd 中…

移动路由器Cellular Router命令执行漏洞复现 [附POC]

文章目录 移动路由器Cellular Router命令执行漏洞复现 [附POC]0x01 前言0x02 漏洞描述0x03 影响版本0x04 漏洞环境0x05 漏洞复现1.访问漏洞环境2.构造POC3.复现 0x06 修复建议 移动路由器Cellular Router命令执行漏洞复现 [附POC] 0x01 前言 免责声明&#xff1a;请勿利用文章…

SQL面试

#(1)请写出要查询员工J开头的名字其工号(EMPNO)及部门名称(DEPTNA)的 SQL语句SELECT e.emp,e.name,d.deptna FROM emp e left join dept d on d.deptno e.deptno where e.name like J%#(2)请写出要查询 Kevin 所在部门的部门代号(DEPTNO)及部门名称(DEPTNA)的 SQL 语句SELECT e…

Pure-Pursuit 跟踪双移线 Gazebo 仿真

Pure-Pursuit 跟踪双移线 Gazebo 仿真 主要参考学习下面的博客和开源项目 自动驾驶规划控制&#xff08;&#xff21;*、pure pursuit、LQR算法&#xff0c;使用c在ubuntu和ros环境下实现&#xff09; https://github.com/NeXTzhao/planning Pure-Pursuit 的理论基础见今年六月…

如何巧妙公布成绩

宝子们&#xff0c;来来来&#xff01;听说你们对如何公布学生成绩很头疼&#xff1f;别担心&#xff0c;今天就让我来给大家支支招&#xff01; 1在家长群内发公告&#xff0c;孩子的成绩已出&#xff0c;想知道具体成绩可以私信哦&#xff5e;简单粗暴&#xff01;关心孩子的…

频谱仪超外差和零中频架构

文章目录 超外差结构零中频结构接收机结构发射机结构 优缺点对比附录相关词汇多次变频的形象解释 参考文献 频谱仪的本质就是一个超宽带、超宽调谐范围、高动态范围的通信接收机&#xff0c; 频谱仪的原理即通信接收机的原理。 遇到高频率高带宽谐波成分复杂的通信信号的话&am…

【网络协议】聊聊HTTPS协议

前面的文章&#xff0c;我们描述了网络是怎样进行传输数据包的&#xff0c;但是网络是不安全的&#xff0c;对于这种流量门户网站其实还好&#xff0c;对于支付类场景其实容易将数据泄漏&#xff0c;所以安全的方式是通过加密&#xff0c;加密方式主要是对称加密和非对称加密。…

【蓝桥杯选拔赛真题08】C++最大值最小值平均值 青少年组蓝桥杯C++选拔赛真题 STEMA比赛真题解析

目录 C/C++最大值最小值平均值 一、题目要求 1、编程实现 2、输入输出 二、算法分析</

gcc/g++使用格式+各种选项,预处理/编译(分析树,编译优化,生成目标代码)/汇编/链接过程(函数库,动态链接)

目录 gcc/g--编译器 介绍 使用格式 通用选项 编译选项 链接选项 程序编译过程 预处理(宏替换) 编译 (生成汇编) 分析树(parse tree) 编译优化 删除死代码 寄存器分配和调度 强度削弱 内联函数 生成目标代码 汇编 (生成二进制代码) 链接(生成可执行文件) 函…

数据抽取+dataworks的使用+ADB的应用

一&#xff0c;大数据处理之数据抽取 1&#xff0c;什么是数据抽取 在大数据领域中&#xff0c;数据抽取是指从原始数据源中提取所需的数据子集或特定数据项的过程&#xff0c; 数据抽取是数据预处理的重要步骤&#xff0c;它为后续的数据分析和建模提供了基础。 2&#xff…

嵌入式linux常用的文件传输方式

做嵌入式就避免不了移植工作&#xff0c;所谓移植就是将交叉编译生成的可执行程序&#xff0c;库&#xff0c;配置文件等传输到开发板上进行工作。 常用传输方式有以下几种&#xff1a;1.串口传输 就是使用串口传输工具rz/sz; 该工具通过串口传输在SRT串口工具…

什么是用户体验测试? 为什么很重要?

在当今数字化时代&#xff0c;用户体验(User Experience&#xff0c;简称UX)已经成为产品成功的关键因素之一。无论是应用程序、网站、硬件设备还是软件&#xff0c;提供出色的用户体验不仅能够吸引更多用户&#xff0c;还能够增加用户满意度&#xff0c;提高品牌忠诚度&#x…

残差网络ResNet

残差网络的提出,是为了解决深度学习中的退化问题。 退化问题指的是随着神经网络层数的增加&#xff0c;网络性能反而逐渐降低的现象。换句话说&#xff0c;当我们不断增加神经网络的层数时&#xff0c;神经网络的训练误差可能会持续下降&#xff0c;但是验证集误差却不断增加&…

前端项目 index.html 中发请求 fetch

想要在前端项目 index.html文件中向后端发起请求&#xff0c;但是引入axios报错&#xff08;我这边会报错&#xff09;&#xff0c;可以使用fetch。 //window.location.origin----获取域名&#xff0c;包括协议、主机号、端口号fetch(window.location.origin "/api/pla…

MPLAB X IDE 仿真打断点提示已中断的断点?

这种中间带裂缝的是无效断点。 原因可能与XC编译器的优化有关&#xff0c;最后生成的汇编与C语言并不是一一对应的(官方给的解释是效率高)。所以这一行C语言转换的汇编代码可能并不在这个位置&#xff0c;也可能与其它汇编合并后根本就没有 我的解决方法是把优化等级调到最低&a…

2014年亚太杯APMCM数学建模大赛A题无人机创造安全环境求解全过程文档及程序

2014年亚太杯APMCM数学建模大赛 A题 无人机创造安全环境 原题再现 20 国集团&#xff0c;又称 G20&#xff0c;是一个国际经济合作论坛。2016 年第 11 届 20 国集团峰会将在中国召开&#xff0c;这是继 APEC 后中国将举办的另一个大型峰会。此类大型峰会&#xff0c;举办城市…