OpenGL Shaders

文章目录[x]
  1. 1:为什么 shaders 运行特别快
  2. 2:Shader 分类
  3. 2.1:Vertex Shader(顶点着色器)
  4. 2.2:Fragment Shader(片段着色器 / 像素着色器)
  5. 2.3:Geometry Shader(几何着色器)(可选)
  6. 2.4:Tessellation Shader(细分着色器)(高级功能)
  7. 2.5:Compute Shader(计算着色器)
  8. 3:Shader 工作流程
  9. 4:Shader 编写语言
  10. 5:Shader 的作用与优势
  11. 6:Fragment Shader 基本结构
  12. 7:Fragment Shader 语法详解
  13. 7.1:#version 指令
  14. 7.2:precision mediump float;
  15. 7.3:in 输入变量
  16. 7.4:out 输出变量
  17. 7.5:uniform 变量
  18. 8:内置函数与变量
  19. 8.1:gl_FragCoord
  20. 9:main() 函数
  21. 10:Fragment Shader 常见代码模式
  22. 10.1:固定颜色
  23. 10.2:纹理映射
  24. 10.3:光照计算(Lambert Diffuse)
  25. 10.4:丢弃透明片段
  26. 11:注意事项
  27. 12:Flutter渲染引擎的发展
  28. 12.1:早期阶段:Skia 渲染引擎
  29. 12.2:关键改进:Shader Warm-up (预编译)
  30. 12.3:重大突破:Impeller 渲染引擎
  31. 13:Flutter渲染引擎下可编写的Shader
  32. 14:在Flutter项目下编写Fragment Shader
  33. 14.1:Flutter 支持的Uniforms
  34. 14.2:Flutter中获取当前位置
  35. 14.3:Flutter中的采样器(Samplers)
  36. 14.4:Flutter Skia为后端的性能问题
  37. 14.5:Flutter实现烟雾飘动的背景

OpenGL(Open Graphics Library)是一个跨语言、跨平台的图形渲染API,主要用于二维和三维图形的开发。它自1992年发布以来,经历了多个重大版本的演进,逐步从固定管线架构过渡到可编程管线,适应了现代图形硬件的发展。以下是OpenGL的发展历程和趋势概览:

版本 时间 特点 说明
1.x 1992\~1997 固定功能管线 开发者无法控制渲染流程细节,只能使用内置函数(如glBegin/glEnd)进行图形绘制。
2.0 2004 可编程管线引入 加入GLSL(OpenGL Shading Language),开发者可以自定义顶点/片元着色器。
3.x 2008\~2010 弃用固定功能管线 引入VAO、VBO、FBO等对象模型,提升性能,同时标记旧API为弃用。
4.x 2010\~2023 高级渲染特性 支持Tessellation(细分着色器)、Compute Shader(计算着色器)、多重采样、延迟渲染等。
OpenGL ES 2003\~现今 移动设备图形API 为嵌入式设备设计的轻量级OpenGL子集,是Android/iOS等平台的主流图形API。

虽然OpenGL仍然存在并在一些跨平台应用中被使用,但其在桌面图形开发中的地位正逐渐被以下技术替代:

图形API 是否支持 Shader 使用语言 特点
OpenGL 2.0+ ✅ 支持 GLSL(OpenGL Shading Language) 首个引入可编程 Shader 的 OpenGL 版本
OpenGL ES 2.0+ ✅ 支持 GLSL ES 面向嵌入式设备,语法简化
Vulkan ✅ 必须使用 GLSL/SPIR-V/HLSL 必须使用 Shader,没有默认固定功能;更接近硬件
Metal ✅ 必须使用 Metal Shading Language (MSL) 苹果自定义语言,基于C++14
DirectX 10+ ✅ 支持 HLSL(High Level Shader Language) 微软专用语言
WebGL 1.0(基于 OpenGL ES 2.0) ✅ 支持 GLSL ES Web环境中的图形编程
WebGPU ✅ 必须使用 WGSL 全新语言,类似 Rust 风格

什么是 Shaders?

如果你曾经有用计算机绘图的经验,你就知道在这个过程中你需要画一个圆,然后一个长方形,一条线,一些三角形……直到画出你想要的图像。这个过程很像用手写一封信或一本书 —— 都是一系列的指令,需要你一件一件完成。

Shaders 也是一系列的指令,但是这些指令会对屏幕上的每个像素同时下达。也就是说,你的代码必须根据像素在屏幕上的不同位置执行不同的操作。就像活字印刷,你的程序就像一个 function(函数),输入位置信息,输出颜色信息,当它编译完之后会以相当快的速度运行。

为什么 shaders 运行特别快

不得不给大家介绍并行处理(parallel processing)的神奇之处。

想象你的 CPU 是一个大的工业管道,然后每一个任务都是通过这个管道的某些东西 —— 就像一个生产流水线那样。有些任务要比别的大,也就是说要花费更多时间和精力去处理。我们就称它要求更强的处理能力。由于计算机自身的架构,这些任务需要串行;即一次一个地依序完成。现代计算机通常有一组四个处理器,就像这个管道一样运行,一个接一个地处理这些任务,从而使计算机流畅运行。每个管道通常被称为线程。
视频游戏和其他图形应用比起别的程序来说,需要高得多的处理能力。因为它们的图形内容需要操作无数像素。想想看,屏幕上的每一个像素都需要计算,而在 3D 游戏中几何和透视也都需要计算。

让我们回到开始那个关于管道和任务的比喻。屏幕上的每个像素都代表一个最简单的任务。单独来看完成任何一个像素的任务对 CPU 来说都很容易,那么问题来了,屏幕上的每一个像素都需要解决这样的小任务!也就是说,哪怕是对于一个老式的屏幕(分辨率 800x600)来说,都需要每帧处理480000个像素,即每秒进行14400000次计算!是的,这对于微处理器就是大问题了!而对于一个现代的 2800x1800 视网膜屏,每秒运行60帧,就需要每秒进行311040000次计算。图形工程师是如何解决这个问题的?

这个时候,并行处理就是最好的解决方案。比起用三五个强大的微处理器(或者说“管道”)来处理这些信息,用一大堆小的微处理器来并行计算,就要好得多。这就是图形处理器(GPU : Graphic Processor Unit)的来由。

设想一堆小型微处理器排成一个平面的画面,假设每个像素的数据是乒乓球。14400000个乒乓球可以在一秒内阻塞几乎任何管道。但是一面800x600的管道墙,每秒接收30波480000个像素的信息就可以流畅完成。这在更高的分辨率下也是成立的 —— 并行的处理器越多,可以处理的数据流就越大。

另一个 GPU 的魔法是特殊数学函数可通过硬件加速。非常复杂的数学操作可以直接被微芯片解决,而无须通过软件。这就表示可以有更快的三角和矩阵运算 —— 和电流一样快。

Shader 分类

在 OpenGL 中,最常用的 Shader 有以下几种:

Vertex Shader(顶点着色器)

作用:对每个顶点进行处理(如变换、法线计算、纹理坐标传递等)。

输入:顶点属性(如位置、法线、纹理坐标)。

输出:传递给下一个阶段的数据(如齐次坐标、varying 变量)。

#version 330 core
layout (location = 0) in vec3 aPos; // 顶点位置

void main() {
    gl_Position = vec4(aPos, 1.0); // 变换到裁剪空间
}

Fragment Shader(片段着色器 / 像素着色器)

作用:对屏幕上的每个片段(像素)进行着色(如颜色计算、光照、纹理映射)。

输入:从顶点着色器插值而来的数据(如颜色、纹理坐标)。

输出:最终写入到 Framebuffer 的颜色。

#version 330 core
out vec4 FragColor; // 最终颜色输出

void main() {
    FragColor = vec4(1.0, 0.5, 0.2, 1.0); // 橙色
}

Geometry Shader(几何着色器)(可选)

作用:对图元(点、线、三角形)进行处理,可以动态生成新的顶点。

一般用于复杂几何效果(如草丛、爆炸)。

Tessellation Shader(细分着色器)(高级功能)

作用:将图形细分为更小的片段,主要用于高精度的曲面细分。

包含:Tessellation Control Shader、Tessellation Evaluation Shader。

Compute Shader(计算着色器)

作用:通用 GPU 计算,不走图形管线(GPGPU),用于粒子系统、图像处理、物理模拟等。

Shader 工作流程

CPU 传递顶点数据到 GPU。

Vertex Shader 处理顶点,将其转换到屏幕坐标。

经过装配与光栅化,生成片段(Fragment)。

Fragment Shader 处理每个片段的颜色输出。

最终输出到 Framebuffer 显示在屏幕上。

Shader 编写语言

OpenGL 使用 GLSL(OpenGL Shading Language) 编写 Shader 代码。

语法类似 C 语言,但针对 GPU 并行处理优化。

Shader 的作用与优势

传统固定管线 可编程 Shader
功能固定 灵活编程自定义效果
光照模型单一 支持各种光照/后处理特效
无法扩展 可实现水面、火焰、阴影、后期特效等

Fragment Shader

Fragment Shader 基本结构

#version 330 core  // GLSL版本声明
precision mediump float; //声明浮点精度

// Uniform变量(全局变量,由CPU设置)
uniform sampler2D texture1;
uniform vec3 lightColor;

// 输入变量(从 Vertex Shader 传入,必须与 Vertex Shader 的 out 匹配)
in vec3 FragPos;
in vec3 Normal;
in vec2 TexCoord;

// 输出变量
out vec4 FragColor;

void main() {
    vec4 texColor = texture(texture1, TexCoord); // 采样纹理颜色
    vec3 lighting = lightColor * texColor.rgb;   // 简单的乘法光照
    FragColor = vec4(lighting, texColor.a);      // 输出到屏幕
}

Fragment Shader 语法详解

#version 指令

#version 330 core
  • 必须在第一行,指定 GLSL 版本。
  • 常见:330 core、300 es(OpenGL ES)、450 core。

precision mediump float;

precision mediump float;
  • float类型在 shaders 中非常重要,所以精度非常重要。
  • 更低的精度会有更快的渲染速度,但是会以质量为代价。
  • 你可以选择每一个浮点值的精度。在第二行(precision mediump float;) 我们就是设定了所有的浮点值都是中等精度。但我们也可以选择把这个值设为“低”(precision lowp float;)或者“高”(precision highp float;)

in 输入变量

in vec2 TexCoord;
  • 从顶点着色器(Vertex Shader)的 out 传递过来的变量。
  • Fragment Shader 中可以进行插值使用。

out 输出变量

out vec4 FragColor;

最终输出颜色,OpenGL 默认 framebuffer 只接受 vec4(RGBA)。

uniform 变量

uniform sampler2D texture1;
uniform vec3 lightColor;
  • 这些输入值叫做 uniform (统一值),它们的数据类型通常为:float, vec2, vec3, vec4, mat2, mat3, mat4, sampler2D and samplerCube。
  • 由 CPU 传递给 GPU,全局共享变量。
  • 因为显卡的架构,所有线程的输入值必须统一(uniform),而且必须设为只读。
  • uniform 值需要数值类型前后一致。且在 shader 的开头,在设定精度之后,就对其进行定义。
  • 常用于传递矩阵、颜色、光照参数、纹理等。
  • 应该按业界传统应在 uniform 值的名字前加 u_ ,这样一看即知是 uniform。
precision mediump float;
uniform vec2 u_resolution; // 画布尺寸(宽,高)
uniform vec2 u_mouse;      // 鼠标位置(在屏幕上哪个像素)
uniform float u_time;     // 时间(加载后的秒数)

内置函数与变量

类型 说明
gl_FragCoord 当前片段的屏幕坐标(vec4)
texture(sampler, uv) 纹理采样
normalize(vec3) 单位向量
dot(a, b) 点乘
mix(a, b, t) 线性插值
discard; 丢弃该片段(透明效果)

gl_FragCoord

fragment shader有一个默认输入值( vec4 gl_FragCoord )。gl_FragCoord存储了活动线程正在处理的像素或屏幕碎片的坐标。

有了它我们就知道了屏幕上的哪一个线程正在运转。

为什么我们不叫 gl_FragCoord uniform (统一值)呢?因为每个像素的坐标都不同,所以我们把它叫做 varying(变化值)。

#version version 330 core
precision mediump float;

uniform vec2 u_resolution;
uniform vec2 u_mouse;
uniform float u_time;

out vec4 FragColor;
void main() {
    vec2 st = gl_FragCoord.xy/u_resolution;
    FragColor = vec4(st.x,st.y,0.0,1.0);
}

上述代码中我们用 gl_FragCoord.xy 除以 u_resolution,对坐标进行了规范化。这样做是为了使所有的值落在 0.0 到 1.0 之间,这样就可以轻松把 X 或 Y 的值映射到红色或者绿色通道。

main() 函数

void main() {
    // 颜色计算逻辑
    FragColor = vec4(1.0, 0.0, 0.0, 1.0);  // 输出红色
}
  • Fragment Shader 的入口函数,必须定义。
  • 主要编写片段颜色、透明度、光照等效果。

Fragment Shader 常见代码模式

固定颜色

void main() {
    FragColor = vec4(1.0, 0.0, 0.0, 1.0); // 红色
}

纹理映射

uniform sampler2D texture1;
in vec2 TexCoord;
void main() {
    FragColor = texture(texture1, TexCoord);
}

光照计算(Lambert Diffuse)

in vec3 Normal;
uniform vec3 lightDir;
void main() {
    float diff = max(dot(normalize(Normal), normalize(lightDir)), 0.0);
    FragColor = vec4(vec3(diff), 1.0);
}

丢弃透明片段

void main() {
    vec4 texColor = texture(texture1, TexCoord);
    if(texColor.a < 0.1)
        discard;
    FragColor = texColor;
}

注意事项

  • Fragment Shader 必须输出 vec4。
  • 输入的 in 变量必须和 Vertex Shader 的 out 名称与类型匹配。
  • Uniform 是全局变量,只能 CPU 传值,Shader 内不能修改。
  • 浮点数需要写 .0,如 1.0。
void main() {
    gl_FragColor = vec4(1,0,0,1);   // 出错
}

Flutter 与 Shader

Flutter渲染引擎的发展

Flutter 渲染引擎的发展经历了几个关键阶段,从最早的 Skia 渲染架构,到引入 Impeller 渲染器,逐步优化了跨平台性能、视觉效果和 GPU 适配性。

早期阶段:Skia 渲染引擎

Skia 是 Google 开源的 2D 图形库,Chrome、Android、Firefox 都用它。

Flutter 最初的渲染核心完全基于 Skia,配合 Dart VM 实现跨平台 UI 渲染。

层级 技术
Framework层 Flutter Widgets / Dart
Engine层 Skia (C++)
Platform层 OpenGL / Vulkan / Metal

它的优点主要如下:
- 跨平台性强(iOS/Android/Windows/Linux/Mac)。
- 高效的 2D 渲染性能。

它的缺点主要如下:
- 对于 GPU Overdraw、抗锯齿、实时Shader编译抖动(Shader Jank) 不够友好。
- 针对 Metal(iOS)与 Vulkan(Android)有性能瓶颈(Shader Warm-up 必须手动管理)。

关键改进:Shader Warm-up (预编译)

Flutter 团队曾通过 Shader Warm-up(着色器预编译) 方式缓解首帧卡顿的问题。
手动预加载 shader,但依然无法彻底解决 “Shader Jank”。
需要开发者自己维护 shader 列表,繁琐易错。

重大突破:Impeller 渲染引擎

Flutter 从 3.10 开始引入 Impeller 渲染器,意图取代 Skia。

Impeller 是 新一代 GPU 渲染器,专为 Flutter 优化,目标是“Shader Jank Zero”。
- Flutter 3.10 开始 iOS 上默认启用。
- Android 端仍在逐步测试中(Vulkan为主)。
- 仍保留 Skia 引擎作为回退方案(兼容性考虑)。

Flutter引入Impeller带来的好处:

优势 说明
去除 JIT Shader 编译卡顿 Skia 在运行时实时编译着色器,导致首次渲染卡顿(Jank);Impeller 使用预编译 Pipeline,无需运行时编译 Shader。
预编译 Shader 管线 Shader 使用 GLSL/SPIR-V 等编译为各平台(Metal、Vulkan)目标语言,构建时就完成,提升首帧体验和稳定性。
提高帧率一致性 减少掉帧、卡顿问题,适合高刷新率设备(90Hz/120Hz)
更易实现高级图形功能 Impeller 拥有更明确的图形抽象,便于未来集成高级特效(模糊、阴影、滤镜、粒子等)
更强的跨平台一致性 提供一致的行为和视觉输出,无论底层是 Metal、Vulkan、OpenGL
构建 Flutter GPU 的基础 Impeller 提供构建 Flutter 自定义渲染器(Flutter GPU)的图形接口,以Flutter GPU实现的通用3D渲染库flutter_Scene,未来可扩展为更强大的图形平台。

Flutter渲染引擎下可编写的Shader

在传统 Flutter 中:
- 官方文档明确指出Flutter 不支持顶点着色器 (.vert 文件),仅支持 .frag 格式的 Fragment Shader。

在 Impeller + Flutter GPU 环境下:
- Impeller 渲染引擎是 Flutter 的新一代图形后端,支持 GPU 全流程优化,包括支持将 GLSL 顶点和片段着色器预编译到不同平台专有格式(如 SPIR-V、Metal Shader)以提升性能。
- Flutter GPU 是建立在 Impeller 之上的低级 API,允许开发者直接使用 Dart + GLSL 构建自定义渲染管线,并支持自定义顶点着色器

在Flutter项目下编写Fragment Shader

Flutter支持从 460 到 100 的任何 GLSL 版本,但部分可用功能受到限制。

着色器与 Flutter 一起使用时会受到以下限制:

  • 不支持 UBO 和 SSBO
  • sampler2D是唯一受支持的采样器类型
  • texture仅支持 (sampler 和 uv)的双参数版本
  • 不能声明额外的变化输入
  • 以 Skia 为目标时,所有精度提示都会被忽略
  • 不支持无符号整数和布尔值

Flutter 支持的Uniforms

可以通过uniform在 GLSL 着色器源中定义值,然后在 Dart 中为每个片段着色器实例设置这些值来配置片段程序。

GLSL 类型为float、vec2、vec3和 的浮点型统一变量vec4使用 方法来设置FragmentShader.setFloat。使用 类型的 GLSL 采样器值sampler2D使用 方法来设置FragmentShader.setImageSampler。

每个值的正确索引uniform由统一值在片段程序中定义的顺序决定。对于由多个浮点数组成的数据类型(例如 )vec4,您必须FragmentShader.setFloat为每个值调用一次。

uniform float uScale;
uniform sampler2D uTexture;
uniform vec2 uMagnitude;
uniform vec4 uColor;

初始化这些uniform值的相应 Dart 代码如下:

void updateShader(FragmentShader shader, Color color, Image image) {
  shader.setFloat(0, 23);  // uScale
  shader.setFloat(1, 114); // uMagnitude x
  shader.setFloat(2, 83);  // uMagnitude y

  // Convert color to premultiplied opacity.
  shader.setFloat(3, color.red / 255 * color.opacity);   // uColor r
  shader.setFloat(4, color.green / 255 * color.opacity); // uColor g
  shader.setFloat(5, color.blue / 255 * color.opacity);  // uColor b
  shader.setFloat(6, color.opacity);                     // uColor a

  // Initialize sampler uniform.
  shader.setImageSampler(0, image);
 }

请注意, sampler2D的索引不会按照FragmentShader.setFloat中使用的索引计算。该sampler2D是使用单独设置的索引从 0 开始。

Flutter中获取当前位置

着色器可以访问varying包含正在计算的特定片段的局部坐标的值。使用此功能可以计算依赖于当前位置的效果,可以通过导入flutter/runtime_effect.glsl库并调用FlutterFragCoord函数来访问当前位置。例如:

#include <flutter/runtime_effect.glsl>

void main() {
  vec2 currentPos = FlutterFragCoord().xy;
}

FlutterFragCoord返回的值与gl_FragCoord不同。gl_FragCoord提供屏幕空间坐标,以确保着色器在各个后端保持一致,通常应避免使用当以 Skia 后端为目标时, 调用gl_FragCoord会被重写以访问本地坐标,但 Impeller 无法进行这种重写。

Flutter中的采样器(Samplers)

采样器提供对 dart:ui Image 对象的访问

#include <flutter/runtime_effect.glsl>

uniform vec2 uSize;
uniform sampler2D uTexture;

out vec4 fragColor;

void main() {
  vec2 uv = FlutterFragCoord().xy / uSize;
  fragColor = texture(uTexture, uv);
}

Flutter Skia为后端的性能问题

当以 Skia 后端为目标时,加载着色器可能会很昂贵,因为必须在运行时将其编译为特定于平台的相应着色器。如果打算在动画期间使用一个或多个着色器,应在开始动画之前预先缓存片段程序对象。

Flutter实现烟雾飘动的背景

  • 在项目目录下创建shaders/smoke.frag定义shader:
#include <flutter/runtime_effect.glsl>
precision highp float;
uniform vec2 u_resolution;
uniform float u_time;
out vec4 fragColor;
float random (in vec2 _st) {
    return fract(sin(dot(_st.xy,
                         vec2(12.9898,78.233)))*
                 43758.5453123);
}
float noise (in vec2 _st) {
    vec2 i = floor(_st);
    vec2 f = fract(_st);

    // Four corners in 2D of a tile
    float a = random(i);
    float b = random(i + vec2(1.0, 0.0));
    float c = random(i + vec2(0.0, 1.0));
    float d = random(i + vec2(1.0, 1.0));

    vec2 u = f * f * (3.0 - 2.0 * f);

    return mix(a, b, u.x) +
    (c - a)* u.y * (1.0 - u.x) +
    (d - b) * u.x * u.y;
}
#define NUM_OCTAVES 5
float fbm ( in vec2 _st) {
    float v = 0.0;
    float a = 0.5;
    vec2 shift = vec2(100.0);
    // Rotate to reduce axial bias
    mat2 rot = mat2(cos(0.5), sin(0.5),
    -sin(0.5), cos(0.50));
    for (int i = 0; i < NUM_OCTAVES; ++i) {
        v += a * noise(_st);
        _st = rot * _st * 2.0 + shift;
        a *= 0.5;
    }
    return v;
}
void main() {
    vec2 st = FlutterFragCoord().xy/u_resolution.xy*3.;
    // st += st * abs(sin(u_time*0.1)*3.0);
    vec3 color = vec3(0.0);

    vec2 q = vec2(0.);
    q.x = fbm( st + 0.00*u_time);
    q.y = fbm( st + vec2(1.0));

    vec2 r = vec2(0.);
    r.x = fbm( st + 1.0*q + vec2(1.7,9.2)+ 0.15*u_time );
    r.y = fbm( st + 1.0*q + vec2(8.3,2.8)+ 0.126*u_time);

    float f = fbm(st+r);

    color = mix(vec3(0.101961,0.619608,0.666667),
                vec3(0.666667,0.666667,0.498039),
                clamp((f*f)*4.0,0.0,1.0));

    color = mix(color,
                vec3(0,0,0.164706),
                clamp(length(q),0.0,1.0));

    color = mix(color,
                vec3(0.666667,1,1),
                clamp(length(r.x),0.0,1.0));

    fragColor = vec4((f*f*f+.6*f*f+.5*f)*color,1.);
}
  • pubspec.yaml配置shaders
flutter:
  uses-material-design: true
  shaders:
    - shaders/smoke.frag
  • Flutter加载Fragment Shader
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
void main() => runApp(MaterialApp(home: ShaderWidget(),));
class ShaderWidget extends StatefulWidget {
  const ShaderWidget({super.key});

  @override
  State<ShaderWidget> createState() => _ShaderWidgetState();
}
class _ShaderWidgetState extends State<ShaderWidget>
    with SingleTickerProviderStateMixin {
  late final Ticker _ticker;
  double _elapsed = 0;

  late final Future<ui.FragmentProgram> _shaderFuture;
  @override
  void initState() {
    super.initState();
    _shaderFuture = ui.FragmentProgram.fromAsset('shaders/smoke.frag');
    _ticker = createTicker((Duration elapsed) {
      setState(() {
        _elapsed = elapsed.inMilliseconds / 1000.0;
      });
    })..start();
  }

  @override
  void dispose() {
    _ticker.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<ui.FragmentProgram>(
      future: _shaderFuture,
      builder: (context, snapshot) {
        if (!snapshot.hasData) return const SizedBox.shrink();
        final shader = snapshot.data!.fragmentShader();
        return CustomPaint(
          painter: _ShaderPainter(shader, _elapsed),
          size: MediaQuery.of(context).size,
        );
      },
    );
  }
}
class _ShaderPainter extends CustomPainter {
  final ui.FragmentShader shader;
  final double time;

  _ShaderPainter(this.shader, this.time);
  @override
  void paint(Canvas canvas, Size size) {
    shader.setFloat(0, size.width); // u_resolution.x
    shader.setFloat(1, size.height); // u_resolution.y
    shader.setFloat(2, time); // u_time

    final paint = Paint()..shader = shader;

    canvas.drawRect(Offset.zero & size, paint);
  }
  @override
  bool shouldRepaint(_ShaderPainter oldDelegate) {
    return oldDelegate.time != time;
  }
}

这样就实现了背景动态烟雾效果Fragment shader的加载与渲染。

如果想实现更多效果,参考:
- Fragment Shader在线学习 The Book of Shaders
- 在线预览Shader效果和提供GLSL源码 shadertoy

点赞

发表评论

昵称和uid可以选填一个,填邮箱必填(留言回复后将会发邮件给你)
tips:输入uid可以快速获得你的昵称和头像

Title - Artist
0:00