LOADING

加载过慢请开启缓存 浏览器默认开启

LunarEclipse's Home

GLSL Loading Shaders in Flutter

2024/5/24

1. 定义 ShaderPainter

在 Flutter 中使用自定义着色器需要创建一个 CustomPainter 的子类并重写 paint 方法,该方法会再需要重新绘制时被调用。然后在该方法中将 FragmentShader 着色器实例传递给 Paint 类实例来进行绘制,例如:

class ShaderPainter extends CustomPainter {  
  final FragmentShader shader;  

  ShaderPainter(this.shader);  

  @override void paint(Canvas canvas, Size size) {  
    canvas.drawRect(  
      Rect.fromLTWH(0, 0, size.width, size.height),  
      Paint()..shader = shader,  
    );  
  }  

  @override bool shouldRepaint(covariant CustomPainter old) {  
    return old != this;  
  }  
}

2. 准备 Animation Widget

创建一个StatefulWidget 来显示自定义的 ShaderPainter
由于需要向着色器程序传递时间,因此需要一个表示时间的变量和一个 AnimationController

class _LoadingWidgetState extends State<LoadingWidget> with TickerProviderStateMixin {
    int _startTime = 0;  
    double get _elapsedTimeInSeconds => (DateTime.now().millisecondsSinceEpoch - _startTime) / 1000;  

    late final AnimationController _controller;

    @override void initState() {  
        super.initState();  
        _controller = AnimationController(  
            duration: const Duration(seconds: 10),  
            vsync: this,  
        )..repeat();
    }

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

    @override Widget build(BuildContext context) { ... }
}

3. 通过 FutureBuilder 异步加载像素着色器

使用以下代码加载着色器 GLSL 程序,并将其传递给 CustomPaint 控件:

FragmentProgram program = await FragmentProgram.fromAsset('shaders/hellow.frag');
FragmentShader shader = program.fragmentShader();

由于着色器的加载是一个异步操作,可以将其与加载状态一起放在一个 FutureBuilder 中,同时通过 FragmentShadersetFloat 方法为着色器传递参数,例如时间、分辨率等:

SizedBox(
  width: MediaQuery.of(context).size.width,
  height: MediaQuery.of(context).size.height,
  child: FutureBuilder<FragmentShader>(
    future: FragmentProgram.fromAsset('shaders/loading.frag').then((program) {
      return program.fragmentShader();
    }),
    builder: (context, snapshot) {
      if (snapshot.hasData) {
        final shader = snapshot.data!;
        shader
          ..setFloat(1, width)   // iResolution.x
          ..setFloat(2, height); // iResolution.y

        return AnimatedBuilder(
            animation: _controller,
            builder: (context, child) {
              shader.setFloat(0, _elapsedTimeInSeconds); // iTime
              return CustomPaint(
                painter: ShaderPainter(shader),
              );
            });
      } else {
        return const CircularProgressIndicator();
      }
    },
  ),
);

4. Shader 程序

out vec4 fragColor;  
precision highp float;  
  
uniform float     iTime;                 // shader playback time (in seconds)  
uniform vec3      iResolution;           // viewport resolution (in pixels)  
  
// Constants  
#define PI 3.1415925359  
#define TAU 6.2831852  
  
// Parameters  
#define STEPS 5.0  
#define INTERVAL 0.06  
#define POSITION_Y 0.5  
#define HEIGHT 1.0  
#define AMPLITUDE 1.0  
#define FREQUENCY 0.2  
  
void main()  
{  
    vec4 color =  vec4(0.0, 0.476, 1.0, 1.0);  
  
    // Normalized pixel coordinates (from 0 to 1)  
    vec2 uv = gl_FragCoord.xy / iResolution.xy;  
    
    // Create a periodic function   
    float px = mod(uv.x, 1.0/STEPS);  
    // Create a step function  
    float sy = floor(uv.x * STEPS) / STEPS;   
    
    // The output is 1 if the pixel is in the interval [0.5/STEPS - INTERVAL, 0.5/STEPS + INTERVAL]  
    color *= step(px, 0.5/STEPS + INTERVAL) - step(px, 0.5/STEPS - INTERVAL);  
  
    // Change the opacity of the x axis periodically  
    // Opacity oscillates between -1 and 1 by time    
    float fade_by_x_time = sin(iTime + uv.x);   
    color *= fade_by_x_time;
  
    // Change the height of the wave periodically  
    float oscillate_y_by_time = abs(mod(iTime * FREQUENCY, AMPLITUDE) - AMPLITUDE * 0.5) * (0.5 * sin((sy + iTime) * TAU) + 0.5);   
        
    // Move position of the wave  
   color *= step(abs(uv.y - POSITION_Y) + oscillate_y_by_time, HEIGHT * 0.5);  
 fragColor = color;  
}

1 声明两个变量iTimeiResolution,作为 flutter 侧传递给着色器程序的外部变量:

uniform float iTime;         // shader playback time (in seconds)  
uniform vec3  iResolution;   // viewport resolution  (in pixels)  

2 归一化坐标系

GL 的坐标系默认以像素为单位,所以需要将原本的坐标除以视口大小,以得到 uv 坐标系:

vec2 uv = gl_FragCoord.xy / iResolution.xy; 

3 绘制周期性的条纹

// Create a periodic function   
float px = mod(uv.x, 1.0/STEPS);  

// The output is 1 if the pixel is in the interval:
// [0.5/STEPS - INTERVAL, 0.5/STEPS + INTERVAL]  
color *= step(px, 0.5/STEPS + INTERVAL) - step(px, 0.5/STEPS - INTERVAL);  

以下两个函数为例:

(橙色的函数)是一个周期函数,以 mod 的第二个参数 1 为周期,通常可以通过该方法周期性地出现同样的图形。
中,以区间 [0, 1] 内为例:

  • 则是代表在区间 [0, 0.8] 内为1,其他为0
  • 则是代表在区间 [0, 0.2] 内为1,其他为0
    两个函数相减 就是在区间 [0.2 + n, 0.8 + n] 内为1,也就是以下黄色的图形:

这样就可以的到一排周期性排列的条纹:

4 横向渐变

// Change the opacity of the x axis periodically  
// Opacity oscillates between -1 and 1 by time    
float fade_by_x_time = sin(iTime + uv.x);   
color *= fade_by_x_time;

此处通过让 sin 函数随着时间 iTime 在 x 轴上移动,并与原来条纹的颜色相乘,这样一来颜色就有了横向动态渐变的效果了。

5 条纹的纵向变化

// Change the height of the wave periodically  
float oscillate_y_by_time = abs(mod(iTime * FREQUENCY, AMPLITUDE) - AMPLITUDE * 0.5) * (0.5 * sin((sy + iTime) * TAU) + 0.5);   

以下两个函数为例:

这里用了一个阶梯函数 floor 作为 sin 函数的输入,这样得出来的函数图形就会像一个阶梯状的sin函数。

6 最后三种效果相乘叠加就有了最后的效果:

Example: LoadingWidget

class LoadingWidget extends StatefulWidget {  
  final double? width;  
  final double? height;  
  
  const LoadingWidget({  
    super.key,  
    this.width,  
    this.height,  
  });  
  
  @override  
  State<LoadingWidget> createState() => _LoadingWidgetState();  
}  
  
class _LoadingWidgetState extends State<LoadingWidget>  
    with TickerProviderStateMixin {  
  int _startTime = 0;  
  
  double get _elapsedTimeInSeconds =>  
      (DateTime.now().millisecondsSinceEpoch - _startTime) / 1000;  
  
  late final AnimationController _controller = AnimationController(  
    duration: const Duration(seconds: 10),  
    vsync: this,  
  )..repeat();  
  
  @override  
  void initState() {  
    super.initState();  
    _controller;  
  }  
  
  @override  
  void dispose() {  
    _controller.dispose();  
    super.dispose();  
  }  
  
  @override  
  Widget build(BuildContext context) {  
    _startTime = DateTime.now().millisecondsSinceEpoch;  
    var width = widget.width ?? 100.0;  
    var height = widget.height ?? 100.0;  
    
      return SizedBox(  
      width: width,  
      height: height,  
      child: FutureBuilder<FragmentShader>(  
          future: _load(),  
          builder: (context, snapshot) {  
            if (snapshot.hasData) {  
              final shader = snapshot.data!;  
              shader  
                ..setFloat(1, width)   // iResolution.x  
                ..setFloat(2, height); // iResolution.y  
  
              return AnimatedBuilder(  
                  animation: _controller,  
                  builder: (context, _) {  
                    shader.setFloat(0, _elapsedTimeInSeconds); // iTime  
                    return CustomPaint(  
                      painter: ShaderPainter(shader),  
                    );  
                  });  
            } else {  
              return const CircularProgressIndicator();  
            }  
          }),  
    );  
  }  
  
  Future<FragmentShader> _load() async {  
    FragmentProgram program =  
        await FragmentProgram.fromAsset('shaders/loading.frag');  
    return program.fragmentShader();  
  }  
}

Links:
Writing and using fragment shaders | Flutter
Shady Flutter: Using GLSL Shaders in Flutter | Codemagic Blog
javascript - What does `precision mediump float` mean? - Stack Overflow

阅读全文

Test

2023/9/10

Image

pocket.gl

Data Type HLSL GLSL
Float float float
Int int int
Bool bool bool
Uint uint uint
Double double double
Vector2 float2 vec2
Vector3 float3 vec3
Vector4 float4 vec4
Matrix float4x4 mat4
Sampler Texture2D sampler2D
Texture3D sampler3D
TextureCube samplerCube
Texture2DArray sampler2DArray
Sampler SamplerState sampler2D, sampler3D
SamplerComparisonState sampler2DShadow, samplerCubeShadow
Texture Texture1D N/A (not available in GLSL)
Texture2DMS N/A
Texture2DMSArray N/A
TextureCubeArray N/A
Buffer StructuredBuffer buffer
RWStructuredBuffer buffer, imageStore
ByteAddressBuffer N/A
RWByteAddressBuffer N/A
AppendStructuredBuffer N/A
ConsumeStructuredBuffer N/A
Constant cbuffer uniform
tbuffer buffer
Attribute attribute in
Varying n/a varying
Uniform uniform uniform
ConstantBuffer uniform buffer
TextureBuffer N/A
RWTexture1D image1D, uimage1D
RWTexture1DArray image1DArray, uimage1DArray
RWTexture2D image2D, uimage2D
RWTexture2DArray image2DArray, uimage2DArray
RWTexture3D image3D, uimage3D
RWTextureCube imageCube, uimageCube
Image n/a image1D, image2D, image3D
Atomic Interlocked* atomic*


傾きが k,点を通る直線の方程式 より,
関数 y=f(x) の x=a での接線の方程式は
互いに垂直である二直線の傾きの積は -1 から,
関数 y=f(x) の x=a での法線の方程式は

上の点 における接線を求める
より での接線は

Example:
  • 系統性
  • 一般化
  • 可驗證
//Load text from a JSON file (Assets/Resources/Text/jsonFile01.json)
var jsonTextFile = Resources.Load<TextAsset>("Text/jsonFile01").ToString();
//Load text from a JSON file (Assets/Resources/Text/jsonFile01.json)
var jsonTextFile = Resources.Load<TextAsset>("Text/jsonFile01").ToString();
阅读全文

Hello World

2023/9/10

这是咱新的博客捏。

阅读全文
1
avatar
Lunar Eclipse

咱的小窝