Flutter & GLSL 系列文章:
- 《Flutter & GLSL - 壹 | Shader 让绘制无限强大》
- 《Flutter & GLSL - 贰 | 从坐标到颜色》
- 《Flutter & GLSL - 叁 | 变量传参》
- 《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;
}
本篇通过图形区域,让我们又多了一种对像素坐标控制颜色输出的手段。除了圆形之外,还有其他很多的基础图形区域,将在后面继续介绍。那本文就到这里,谢谢观看~