Flutter & GLSL - 伍 | 图形区域控制

Flutter & GLSL 系列文章:

案例代码开源地址 【skeleton】


1、从圆形与 step 函数

有时我们需要通过着色器来表现图形,那如何通过坐标控制颜色值的输出,得到基本图形呢?之前一直强调:

shader 的奥义在于 通过坐标控制像素的颜色信息

想要展示一个半径为 r 的黑色圆形,只需要计算 像素点 距原点距离 len , 对于所有 len <= r 的像素点着为黑色;反之着为白色:

这个逻辑由下面的 circle 方法进行处理:当 len <= r 时返回 0 ,着色为 vec4(0, 0, 0, 1) 即黑色;反之返回 1 , 着色为 vec4(1, 1, 1, 1) 即白色。这样就通过圆的性质,通过对坐标点,控制像素的表现,形成图形。

内置函数 length(vec2) : 用于计算 vec2 坐标到原点的距离。

c 复制代码
#version 460 core
#include <flutter/runtime_effect.glsl>
precision mediump float;

out vec4 fragColor;
uniform vec2 uSize;

float circle(vec2 coo, float r) {
    float len = length(coo);
    if (len <= r) {
        return 0;
    } else {
        return 1;
    }
}

void main() {
    vec2 coo = FlutterFragCoord() / uSize;
    float radius = 0.5;
    float ret = circle(coo, radius);
    fragColor = vec4(ret, ret, ret, 1);
}

在 GLSL 中内置了一个用于生成阶梯的 step 函数:

内置函数 step(a,b) : 比较两个值 a, b ; 如果 a < b,则返回 0.0、否则返回 1.0。

也就是说,上面 circle 函数的比较逻辑可以简化成如下形式,效果是完全等价的:

c 复制代码
float circle(vec2 coo, float r) {
    float len = length(coo);
    return step(r, len);
}

2、坐标系的转变

目前坐标系的原点在左上角(下图左),x,y 的取值范围在 [0,1]。所以上面画的圆形只显示了四分之一。如何变化,可以使坐标系的原点在画板中心(下图右),并且横纵坐标取值范围在 [-1,1] 呢?

其实很简单,左侧坐标系值放大两倍,即 坐标 *2 可以得到 x,y 的取值范围在 [0,2]的坐标系;然后坐标轴右移 1 个单位,即可得到 x,y 的取值范围在 [-1,1]的目标坐标系。

此时距离原点小于 0.5 的点被着为黑色,就可以得到如下的圆形:

c 复制代码
---->[shaders/base_01_circle_step2.frag]----
#version 460 core
#include <flutter/runtime_effect.glsl>
precision mediump float;

out vec4 fragColor;
uniform vec2 uSize;

float circle(vec2 coo, float r) {
    float len = length(coo);
    return step(r, len);
}

void main() {
    vec2 coo = FlutterFragCoord() / uSize;
    /// 变换坐标系,以中心为原点
    coo = coo * 2 - 1;
    float ret = circle(coo, 0.5);
    fragColor = vec4(ret, ret, ret, 1);
}

3. 多个圆形联合

现在想一个小问题:如何将圆形呈白色,周围是黑色呢?很简单,用 1 - step(r, len) 即可,这样原来的黑色 1 就会变为白色 1-1 = 0 ; 原来的白色 0 就会变为白色 1-0 = 1

根据 step 的作用,不难推出: 1 - step(r, len) = step(len, r)

c 复制代码
---->[shaders/base_01_circle_step3.frag]----
#version 460 core
#include <flutter/runtime_effect.glsl>
precision mediump float;

out vec4 fragColor;
uniform vec2 uSize;

float circle(vec2 coo, float r) {
    float len = length(coo);
    return step(len, r);
}

void main() {
    vec2 coo = FlutterFragCoord() / uSize;
    coo = coo * 2 - 1;
    float ret = circle(coo, 0.5);
    fragColor = vec4(ret, ret, ret, 1);
}

现在再想一想,如何在界面上显示多个圆呢?如下所示:

代码中有两个半径为 0.2 的小圆 c1 和 c2 ,可以自己思考一下 ret = c0 + c1 + c2 为什么可以把小圆展示出来?

  • 对每个像素操作 的视角来看,返回 1 表示该像素点是白色,返回 0 表示黑色;
  • c0 + c1 表示每个像素点的值是两个圆的结果累加值。如果每个坐标计算的结果啊: c0 是 0 (黑色); c1 是 1 (白色),两张相加 0+1 = 1 。就表示当前像素为白色。这样 c1 的白色就会出现在屏幕上。以此类推。
  • 当白色重叠时,即两个圆相交的地方,累加值是 1+1=2; 最后让 ret = min(ret, 1.0),就可以使结果中大于 1 时取 1 值:
c 复制代码
---->[shaders/base_01_circle_step4.frag]----
#version 460 core
#include <flutter/runtime_effect.glsl>
precision mediump float;

out vec4 fragColor;
uniform vec2 uSize;

float circle(vec2 coo, float r) {
    float len = length(coo);
    return step(len, r);
}

void main() {
    vec2 coo = FlutterFragCoord() / uSize;
    coo = coo * 2 - 1;
    float ret = 0;
    float c0 = circle(coo, 0.5);
    vec2 offset = vec2(-0.6, -0.6);
    float c1 = circle(coo + offset, 0.2);
    float c2 = circle(coo - offset*0.7, 0.2);
    ret = c0 + c1 + c2;
    ret = min(ret, 1.0);
    fragColor = vec4(ret, ret, ret, 1);
}

如下,我们将累加后的 ret 值减半,就更能说明问题。这样:

  • 未相交的圆 ret = (1+0)*0.5 = 0.5 ;颜色就是 vec4(0.5, 0.5, 0.5, 1) 为灰色。
  • 相交时 ret = (1+1) * 0.5 = 1 ;颜色就是 vec4(1, 1, 1, 1) 为白色。

于是,界面上可以呈现出叠合处更亮的效果:

c 复制代码
---->[shaders/base_01_circle_step5.frag]----
void main() {
    /// 略同...
    ret = c0 + c1 + c2;
    ret *=0.5;
    ret = min(ret, 1.0);
    fragColor = vec4(ret, ret, ret, 1);
}

4. 区域控制与贴图

我们可以根据 circle 计算的结果是 0 还是 1 来控制纹理贴图对应坐标的像素颜色。这样就很容易实现对贴图 区域控制 的效果, 如下所示,当像素点位于白色圆区域时展示图片颜色。以此可以实现类似图片裁剪的效果:

c 复制代码
#version 460 core
#include <flutter/runtime_effect.glsl>
precision mediump float;

out vec4 fragColor;
uniform vec2 uSize;
uniform sampler2D uTexture;

float circle(vec2 coo, float r) {
    float len = length(coo);
    return step(len, r);
}

void main() {
    vec2 coo = FlutterFragCoord() / uSize;
    coo = coo * 2 - 1;
    float ret = 0;
    float c0 = circle(coo, 0.5);
    vec2 offset = vec2(-0.6, -0.6);
    float c1 = circle(coo + offset, 0.2);
    float c2 = circle(coo - offset * .7, 0.2);
    ret = c0 + c1 + c2;
    ret = min(ret, 1.0);

    vec2 picCoo = (coo + 1) / 2;
    vec4 color = texture(uTexture, picCoo);
    /// 白色圆区域展示图片颜色
    color = ret == 1 ? color : vec4(0, 1, 1, 1);
    fragColor = color;
}

有了区域控制的手段,也可以很轻松地实现 区域贴图特效,如下所示,将原本黑色的区域施加马赛克的效果,就可以实现区域外的局部马赛克:

c 复制代码
#version 460 core
#include <flutter/runtime_effect.glsl>
precision mediump float;

out vec4 fragColor;
uniform vec2 uSize;
uniform sampler2D uTexture;

float circle(vec2 coo, float r) {
    float len = length(coo);
    return step(len, r);
}

void main() {
    vec2 coo = FlutterFragCoord() / uSize;
    coo = coo * 2 - 1;
    float ret = 0;
    float c0 = circle(coo, 0.5);
    vec2 offset = vec2(-0.6, -0.6);
    float c1 = circle(coo + offset, 0.2);
    float c2 = circle(coo - offset * .7, 0.2);
    ret = c0 + c1 + c2;
    ret = min(ret, 1.0);
    vec2 picCoo = (coo + 1) / 2;
    vec4 color = texture(uTexture, picCoo);

    if (ret == 0) {
        /// 局部马赛克
        float rowCount = 40.0;
        float x = floor(picCoo.x * rowCount) / rowCount;
        float y = floor(picCoo.y * rowCount) / rowCount;
        color = texture(uTexture, vec2(x, y));
    }
    fragColor = color;
}

本篇通过图形区域,让我们又多了一种对像素坐标控制颜色输出的手段。除了圆形之外,还有其他很多的基础图形区域,将在后面继续介绍。那本文就到这里,谢谢观看~

相关推荐
Frank_HarmonyOS4 小时前
Android MVVM(Model-View-ViewModel)架构
android·架构
PineappleCoder6 小时前
SVG 适合静态图,Canvas 适合大数据?图表库的场景选择
前端·面试·canvas
stringwu6 小时前
Flutter 开发者必备:WebSocket 实用指南
flutter
小林的技术分享6 小时前
关于排查 Flutter 3.27.0 版本Android端无法禁用Impeller引擎的过程记录
前端·flutter
新子y8 小时前
【操作记录】我的 MNN Android LLM 编译学习笔记记录(一)
android·学习·mnn
lincats10 小时前
一步一步学习使用FireMonkey动画(1) 使用动画组件为窗体添加动态效果
android·ide·delphi·livebindings·delphi 12.3·firemonkey
想想吴11 小时前
Android.bp 基础
android·安卓·android.bp
写点啥呢17 小时前
Android为ijkplayer设置音频发音类型usage
android·音视频·usage·mediaplayer·jikplayer
coder_pig1 天前
🤡 公司Android老项目升级踩坑小记
android·flutter·gradle
死就死在补习班1 天前
Android系统源码分析Input - InputReader读取事件
android