系列文章目录
- LearnOpenGL 笔记 - 入门 01 OpenGL
- LearnOpenGL 笔记 - 入门 02 创建窗口
- LearnOpenGL 笔记 - 入门 03 你好,窗口
- LearnOpenGL 笔记 - 入门 04 你好,三角形
- OpenGL - 如何理解 VAO 与 VBO 之间的关系
- LearnOpenGL - Android OpenGL ES 3.0 绘制三角形
- LearnOpenGL - Android OpenGL ES 3.0 绘制纹理
- LearnOpenGL - Android OpenGL ES 3.0 YUV 渲染
- LearnOpenGL - Android OpenGL ES 3.0 使用 FBO 进行离屏渲染
一、前言
本章我们学习下如何来写一个基础特效,本文所有代码你可以在 BasicEffect.kt 找到
二、GLSL 内置函数
通过 shader 来实现特效,GLSL 是我们使用的语言。了解 GLSL 内置函数对于清楚了解可以调用哪些函数和“子弹”有很大帮助。这方面的知识已经有很多资源可供参考,可以查阅以下文章:
- GLSL-内置函数
- OpenGL & Metal Shader 编程:GLSL 重要的内置函数
三、编写基础特效
2.1 片元着手器处理
在 OpenGL 中,我们可以通过片元着色器(Fragment Shader)对输入图像进行特效处理,并生成经过处理后的输出图像。一般来说,实现这种简单特效的主要逻辑都在片元着色器中,而顶点着色器是相对固定的。片元着色器处理的基本单元是片元,或者可以简单地认为是一个像素。
为了更好地理解这一点,我们可以回顾一下在 CPU 上进行图像处理的伪代码:
for(int i = 0; i < width; ++i) {
for (int j = 0; j < height; ++j) {
auto in = image(i, j); // 从图像中获取像素
auto out = process(in); // 对像素进行处理
image(i, j) = out; // 将处理后的像素写回到图像中
}
}
在 OpenGL 中进行类似处理时,我们使用 texture
函数从纹理坐标中获取图片的片元,即像素,这相当于伪代码中的变量 in
。然后,我们将处理后的结果输出到 fragColor
,这相当于伪代码中的 out
。
下面是一个简单的片元着色器示例:
#version 330 core
uniform sampler2D texture0; // 输入纹理
in vec2 v_texcoord; // 传递的纹理坐标
out vec4 fragColor; // 输出片元颜色
void main() {
vec4 in = texture(texture0, v_texcoord); // 获取输入片元(像素)
fragColor = process(in); // 对片元进行处理并输出
}
// 简单的例子处理函数,可以根据需求修改
vec4 process(vec4 color) {
// 示例处理:简单的反色效果
return vec4(1.0 - color.rgb, color.a);
}
在这个片元着色器中:
uniform sampler2D texture0
:定义了一个输入纹理。in vec2 v_texcoord
:接收从顶点着色器传递过来的纹理坐标。out vec4 fragColor
:定义输出的片元颜色。
函数 texture(texture0, v_texcoord)
从输入的纹理中获取指定纹理坐标处的颜色值,相当于 CPU 伪代码中的 image(i, j)
。然后,我们可以在 process
函数中对这个颜色值进行各种处理,最终将处理结果赋值给 fragColor
,相当于 image(i, j) = out
。
通过这种方式,我们可以非常灵活地定义各种图像处理效果,例如颜色转换、模糊、锐化等。只需修改 process
函数的实现即可。
2.2 最佳实践
推荐使用 KodeLife 来快速验证你想要的效果,然后再迁移到 OpenGL ES 上,例如下面这个片元着色器:
#version 330 core
uniform sampler2D texture0;
in vec2 v_texcoord;
out vec4 fragColor;
void main(void)
{
int numBlockX = 50;
int numBlockY = 50;
float stepX = 1.0 / numBlockX;
float stepY = 1.0 / numBlockY;
float indexBlockX = floor(v_texcoord.x / stepX);
float indexBlockY = floor(v_texcoord.y / stepY);
vec2 currentBlockLeftBottom = vec2(indexBlockX*stepX, indexBlockY*stepY);
fragColor = texture(texture0, currentBlockLeftBottom);
}
效果正确后,移植到 OpenGL ES 3.0 上只需要做很小的改动:
#version 300 es
precision mediump float;
uniform sampler2D texture0;
in vec2 v_texcoord;
out vec4 fragColor;
void main(void)
{
float numBlockX = 150.0;
float numBlockY = 150.0;
float stepX = 1.0 / numBlockX;
float stepY = 1.0 / numBlockY;
float indexBlockX = floor(v_texcoord.x / stepX);
float indexBlockY = floor(v_texcoord.y / stepY);
vec2 currentBlockLeftBottom = vec2(indexBlockX*stepX, indexBlockY*stepY);
fragColor = texture(texture0, currentBlockLeftBottom);
}
2.3 基础特效
在 BasicEffect.kt 一共有 8 个基础特效,参考了 PPT 中一些动画特效,接下来一一进行说明
2.3.1 动态网格
#version 300 es
precision mediump float;
uniform vec2 resolution;
uniform float offset;
uniform sampler2D texture0;
in vec4 v_position;
in vec2 v_texcoord;
out vec4 fragColor;
void main()
{
vec2 imgTextCoord = v_texcoord * resolution;
float sideLength = resolution.y / 6.0;
float maxOffset = 0.15 * sideLength;
float x = mod(imgTextCoord.x, floor(sideLength));
float y = mod(imgTextCoord.y, floor(sideLength));
float offsetLength = offset * maxOffset;
if(offsetLength <= x && x <= sideLength-offsetLength
&& offsetLength <= y && y <= sideLength-offsetLength)
{
fragColor = texture(texture0, v_texcoord);
}else{
fragColor = vec4(1.0, 1.0, 1.0, 1.0);
}
}
这个 OpenGL ES 3.0 Shader 程序用来实现一种效果,它将图像划分为若干个小正方形网格,并在每个小正方形中留下一个可以通过 offset
参数控制的可见区域。超出这个区域的边缘将会被填充为白色。下面对 Shader 程序逐行解释。
-
vec2 imgTextCoord = v_texcoord * resolution;
:将纹理坐标转换为图像的像素坐标。 -
float sideLength = resolution.y / 6.0;
:计算出每个小正方形的边长,6.0 表示将图像在垂直方向(y 方向)分成 6 等份。 -
float maxOffset = 0.15 * sideLength;
:计算出最大偏移量,为小正方形边长的 15%。 -
float x = mod(imgTextCoord.x, floor(sideLength));
:计算当前像素在所在小正方形中的相对 x 坐标。 -
float y = mod(imgTextCoord.y, floor(sideLength));
:计算当前像素在所在小正方形中的相对 y 坐标。 -
float offsetLength = offset * maxOffset;
:将offset
参数转换为具体的偏移量。 -
if (offsetLength <= x && x <= sideLength - offsetLength && offsetLength <= y && y <= sideLength - offsetLength)
:- 如果当前像素在每个小正方形的可见区域内,则从
texture0
中采样纹理颜色作为输出。 - 否则,输出白色 (
vec4(1.0, 1.0, 1.0, 1.0)
).
- 如果当前像素在每个小正方形的可见区域内,则从
这个 Shader 效果的核心思想是通过二维坐标系上的模运算来定位每个像素在小正方形中的位置,并根据 offset
定义的可见区域,决定是否显示纹理颜色。
总结来说,这个 Shader 实现了一种可以调整的小正方形镂空效果,通过一个 offset
参数控制每个小正方形中的可见区域大小,不在可见区域的部分填充为白色。
2.3.2 缩放的圆
#version 300 es
precision mediump float;
uniform vec2 resolution;
uniform float offset;
uniform sampler2D texture0;
in vec2 v_texcoord;
out vec4 fragColor;
void main(void)
{
float minR = 0.2;
float maxR = 1.0;
float r = (minR - maxR)*offset + maxR;
vec2 circlePoint = vec2(0.5, 0.5);
vec2 circlePointRes = circlePoint * resolution;
vec2 texcoordRes = v_texcoord * resolution;
float rRes = r * resolution.x * 0.5;
float dis = distance(circlePointRes, texcoordRes);
if(dis < rRes){
fragColor = texture(texture0, v_texcoord);
}else
{
fragColor = vec4(1,1,1,1);
}
}
这个片段着色器的主函数实现了一个效果,它根据一个可调的 offset
参数在图像上绘制一个可调节大小的圆形区域。圆形区域内显示图像,圆形区域外显示白色。下面对主函数的逻辑进行逐行解释:
-
定义最小和最大半径:
float minR = 0.2; float maxR = 1.0; float r = (minR - maxR) * offset + maxR;
- 定义了一个最小半径
minR
和最大半径maxR
(都相对于纹理坐标系)。 - 利用线性插值公式
(minR - maxR) * offset + maxR
计算当前圆形的半径r
。offset
是一个从 0 到 1 的值,大多数情况下由外部应用设定,控制着从最小半径到最大半径的变化。
- 定义了一个最小半径
-
定义圆心点:
vec2 circlePoint = vec2(0.5, 0.5);
- 圆心的定义使用标准化坐标
(0.5, 0.5)
,表示纹理的中心。
- 圆心的定义使用标准化坐标
-
将标准化坐标转换为分辨率坐标:
vec2 circlePointRes = circlePoint * resolution; vec2 texcoordRes = v_texcoord * resolution;
- 将圆心
circlePoint
和当前像素的纹理坐标v_texcoord
转换为相对于图像分辨率的实际像素坐标。
- 将圆心
-
计算圆的实际半径:
float rRes = r * resolution.x * 0.5;
- 将标准化半径
r
转换为实际像素坐标系下的半径rRes
。这里用图像宽度的一半作为转换系数。
- 将标准化半径
-
计算当前像素到圆心的距离:
float dis = distance(circlePointRes, texcoordRes);
- 计算当前像素坐标到圆心坐标的距离。
-
判断当前像素是显示图像还是白色:
if(dis < rRes) { fragColor = texture(texture0, v_texcoord); } else { fragColor = vec4(1, 1, 1, 1); // 白色 }
- 如果当前像素到圆心的距离
dis
小于实际半径rRes
,则此像素位于圆形区域内,显示图像纹理。 - 否则,此像素在圆形区域外,显示白色 (
vec4(1, 1, 1, 1)
).
- 如果当前像素到圆心的距离
这个片段着色器通过计算每个像素到图像中心的距离,并与动态调整的半径进行比较,决定是否显示图像纹理或白色,进而实现一个可调循环形区域显示的效果。offset
参数控制圆的大小,从0到1之间变化。
2.3.3 分屏
#version 300 es
precision mediump float;
uniform vec2 resolution;
uniform sampler2D texture0;
in vec2 v_texcoord;
out vec4 fragColor;
void main(void)
{
float N = 2.0;
vec2 uv = v_texcoord;
uv *= N;
uv = fract(uv);
fragColor = texture(texture0, uv);
}
这个片段着色器的实现逻辑是将输入纹理进行瓦片化处理。它将纹理坐标范围缩放并通过 fract
函数实现周期性的重复图像。下面对主函数进行逐行解释:
-
定义平铺次数:
float N = 2.0;
- 定义一个浮点数
N
来控制纹理平铺的次数。在这里,N
被设定为 2.0,意味着纹理会在水平方向和垂直方向上各重复两次。
- 定义一个浮点数
-
获取当前的纹理坐标:
vec2 uv = v_texcoord;
- 将输入的纹理坐标
v_texcoord
赋值给局部变量uv
。
- 将输入的纹理坐标
-
缩放纹理坐标:
uv *= N;
- 将纹理坐标
uv
按照平铺次数N
进行缩放,使得纹理坐标范围从[0.0, 1.0]
变为[0.0, N]
。当N
为 2.0 时,纹理坐标范围变为[0.0, 2.0]
。
- 将纹理坐标
-
应用周期性重复:
uv = fract(uv);
- 使用
fract
函数将纹理坐标限制在[0.0, 1.0]
的范围内。fract
函数返回输入数值的小数部分,因此当uv
大于 1.0 时,它会返回uv
自身的小数部分,从而实现纹理的重复平铺。
- 使用
-
采样并输出纹理颜色:
fragColor = texture(texture0, uv);
- 使用调整后的纹理坐标
uv
采样纹理texture0
,并将采样到的颜色赋值给fragColor
作为输出。
- 使用调整后的纹理坐标
这个片段着色器通过缩放和 fract
函数实现了对输入纹理的平铺效果。当 N
为 2 时,纹理会在水平方向和垂直方向上分别重复两次。通过调整 N
的值,可以控制纹理的重复次数,使得纹理在屏幕上呈现出周期性平铺的效果。
2.3.4 百叶窗
#version 300 es
precision mediump float;
uniform vec2 resolution;
uniform float offset;
uniform sampler2D texture0;
in vec2 v_texcoord;
out vec4 fragColor;
void main(void)
{
float shuttersMaxH = 1.0 / 10.0;
float shuttersH = -shuttersMaxH*offset + shuttersM
float y = mod(v_texcoord.y, shuttersMaxH);
if(y < shuttersH)
{
fragColor = vec4(1.0,1.0,1.0,1.0);
}else
{
fragColor = texture(texture0, v_texcoord);
}
}
这个片段着色器的主函数实现了一种百叶窗效果。通过一个可调的 offset
参数,控制纹理中水平条纹之间的透明区域。透明区域显示为白色,而其余部分显示纹理。下面对主函数进行逐行解释:
-
定义百叶窗各条纹的最大高度:
float shuttersMaxH = 1.0 / 10.0;
- 定义百叶窗条纹的最大高度
shuttersMaxH
。这里设定为1.0 / 10.0
,表示条纹在纹理坐标系中占据的垂直高度为 1/10,即整体被水平分成10等份。
- 定义百叶窗条纹的最大高度
-
计算当前条纹的高度:
float shuttersH = -shuttersMaxH * offset + shuttersMaxH;
- 通过
offset
参数动态计算当前条纹的高度shuttersH
。offset
的值在 0 到 1 之间变化。- 当
offset
为 0 时,shuttersH
为shuttersMaxH
,即条纹高度最大(占据1/10的总高度)。 - 当
offset
为 1 时,shuttersH
为 0,条纹高度为 0,即没有显示条纹。
- 当
- 通过
-
计算当前像素所在的条纹位置:
float y = mod(v_texcoord.y, shuttersMaxH);
- 通过
mod
函数计算当前像素的y
纹理坐标在条纹条纹高度shuttersMaxH
内的位置。结果y
将在0
到shuttersMaxH
之间循环,表示当前像素在条纹中的相对位置。
- 通过
-
根据位置决策显示内容:
if(y < shuttersH) { fragColor = vec4(1.0, 1.0, 1.0, 1.0); } else { fragColor = texture(texture0, v_texcoord); }
- 如果相对位置
y
小于当前条纹高度shuttersH
,则设置该像素的颜色为白色 (vec4(1.0, 1.0, 1.0, 1.0)
),表示这个区域是”透明”的。 - 否则,从纹理
texture0
中采样颜色,并将其赋值给fragColor
,来显示该像素的纹理颜色。
- 如果相对位置
这个片段着色器通过将纹理按垂直方向分成多个水平条纹,并根据 offset
参数调整每个条纹的高度,实现了一种类似百叶窗的效果。offset
参数动态控制条纹的高度,从而能够模拟百叶窗关闭及打开的过程。当offset
值最大(1.0)时,条纹高度最小(0),表示完全打开状态;当offset
值最小(0.0)时,条纹高度最大(分辨率的 1/10),表示完全关闭状态。
2.3.5 溶解渐入
#version 300 es
precision mediump float;
uniform sampler2D texture0;
uniform float offset;
in vec2 v_texcoord;
out vec4 fragColor;
float rand2(vec2 co){
return fract(sin(dot(co.xy, vec2(12.9898, 78.233))) * 43758.5453);
}
void main(void)
{
float randomValue = rand2(v_texcoord);
if(randomValue < offset){
fragColor = texture(texture0, v_texcoord);
}else{
fragColor = vec4(1.0,1.0,1.0,1.0);
}
}
这个片段着色器的主函数实现了一种基于随机性的效果。通过一个可调的 offset
参数来控制显示纹理像素的概率,其余像素显示为白色。下面对主函数进行逐行解释:
-
生成随机值:
float randomValue = rand2(v_texcoord);
- 调用自定义的
rand2
函数,生成一个基于当前纹理坐标v_texcoord
的随机值randomValue
。 rand2
函数通过对纹理坐标应用一组固定的常数并使用sin
和fract
函数来生成一个伪随机数。
- 调用自定义的
-
根据随机值和 offset 决策显示内容:
if(randomValue < offset){ fragColor = texture(texture0, v_texcoord); }else{ fragColor = vec4(1.0, 1.0, 1.0, 1.0); }
- 比较生成的随机值
randomValue
和offset
参数:- 如果
randomValue
小于offset
,则显示纹理中的颜色值,由texture
函数采样自texture0
。 - 否则,显示白色 (
vec4(1.0, 1.0, 1.0, 1.0)
)。
- 如果
- 比较生成的随机值
-
rand2
函数解释:
float rand2(vec2 co){
return fract(sin(dot(co.xy, vec2(12.9898, 78.233))) * 43758.5453);
}
rand2
函数接受一个二维向量co
作为输入。- 通过
dot
函数计算co.xy
和一个固定向量(12.9898, 78.233)
的点积。 - 将点积结果传递给
sin
函数并乘以一个大常数43758.5453
,目的是打破周期性并增加随机性。 - 最后通过
fract
函数提取小数部分,生成一个范围在[0.0, 1.0)
的伪随机数。
这个片段着色器通过伪随机数生成,实现了一种基于随机性的纹理透明效果。offset
参数控制当前像素显示纹理颜色的概率:
- 当
offset
为 1.0 时,所有像素都会显示纹理颜色。 - 当
offset
为 0.0 时,所有像素都显示为白色。 - 在
0.0
和1.0
之间的值,会根据生成的随机数决定部分像素显示纹理颜色,部分像素显示白色,形成一种随机遮挡的效果。
2.3.6 劈裂
#version 300 es
precision mediump float;
uniform float offset;
uniform sampler2D texture0;
in vec2 v_texcoord;
out vec4 fragColor;
void main(void)
{
float w = -offset + 1.0;
float dis = abs(v_texcoord.x - 0.5);
if(dis < w/2.0){
fragColor = vec4(1.0,1.0,1.0,1.0);
}else{
fragColor = texture(texture0, v_texcoord);
}
}
这个片段着色器的主函数实现了一种基于水平遮罩的效果。根据一个可调的 offset
参数,决定一部分图像在水平方向上被遮挡为白色,其余部分显示纹理。下面对主函数逐行解释:
-
计算遮罩区域的宽度:
float w = -offset + 1.0;
- 定义一个浮点数
w
来表示遮罩区域的宽度。offset
参数决定了遮罩区域的宽度:- 当
offset
为 0 时,w
为 1,遮罩区域覆盖整个图像。 - 当
offset
为 1 时,w
为 0,没有遮罩区域。 offset
为 0 到 1 之间的值,将线性控制遮罩区域的宽度。
- 当
- 定义一个浮点数
-
计算当前像素到中心的水平距离:
float dis = abs(v_texcoord.x - 0.5);
- 计算当前像素的水平纹理坐标
v_texcoord.x
到中心(0.5
)的绝对距离dis
,用来决定当前像素是否在遮罩区域内。
- 计算当前像素的水平纹理坐标
-
决策显示内容:
if(dis < w / 2.0){ fragColor = vec4(1.0, 1.0, 1.0, 1.0); } else { fragColor = texture(texture0, v_texcoord); }
- 判断当前像素的水平距离
dis
是否小于遮罩区域的一半宽度w / 2.0
:- 如果小于
w / 2.0
,表示该像素在遮罩区域内,设置该像素的颜色为白色 (vec4(1.0, 1.0, 1.0, 1.0)
)。 - 否则,从纹理
texture0
中采样颜色,并将其赋值给fragColor
。
- 如果小于
- 判断当前像素的水平距离
这个片段着色器实现了一个水平遮罩效果,offset
参数控制遮罩区域的宽度:
- 当
offset
为 0 时,遮罩区域最宽,图像完全被遮罩为白色。 - 当
offset
为 1 时,没有遮罩区域,显示整个图像纹理。 - 在
0.0
和1.0
之间的值,会线性控制遮罩区域的宽度,中间区域被遮挡为白色,两边显示纹理,使得遮罩效果渐进变化。
2.3.7 轮子
#version 300 es
precision mediump float;
uniform float offset;
uniform sampler2D texture0;
in vec2 v_texcoord;
out vec4 fragColor;
void main(void)
{
vec2 circlePos = vec2(0.5, 0.5); // 圆心位置
vec2 direction = v_texcoord - circlePos; // 从圆心指向当前片元的向量
// 计算当前片元相对于圆心的角度
float angle = atan(direction.y, direction.x);
// 将角度范围从 [-π, π] 映射到 [0, 2π]
if (angle < 0.0) {
angle += 2.0 * 3.14159265358979323846;
}
// 当前阈值角度,offset 控制动画进度,范围为 [0, 2π]
float curAngle = offset * 2.0 * 3.14159265358979323846;
// 根据当前片元的角度和阈值角度决定片元颜色
if (angle < curAngle) {
fragColor = texture(texture0, v_texcoord); // 显示纹理
} else {
fragColor = vec4(1.0, 1.0, 1.0, 1.0); // 显示白色
}
}
这个片段着色器实现了一个基于角度动画效果,即通过一个中心点(圆心)逐渐从白色向纹理过渡显示的效果。offset
参数用来控制动画进度。
-
定义圆心位置并计算向量方向:
vec2 circlePos = vec2(0.5, 0.5); // 圆心位置 vec2 direction = v_texcoord - circlePos; // 从圆心指向当前片元的向量
circlePos
定义了圆心的位置,在纹理坐标(0.5, 0.5)
,即中心位置。direction
计算从圆心到当前片元位置的向量。
-
计算片元相对于圆心的角度:
float angle = atan(direction.y, direction.x);
- 使用
atan
函数计算当前片元相对于圆心方向的角度,范围在[-π, π]
。
- 使用
-
将角度范围映射到
[0, 2π]
:if (angle < 0.0) { angle += 2.0 * 3.14159265358979323846; }
- 如果角度
angle
小于 0,则将其加上2π
,使得角度范围变为[0, 2π]
。
- 如果角度
-
计算当前阈值角度:
float curAngle = offset * 2.0 * 3.14159265358979323846;
- 通过
offset
参数控制动画进度,计算当前的阈值角度curAngle
,范围从 0 到2π
。offset
的值应在[0, 1.0]
之间变化。
- 通过
-
判断并设置片元颜色:
if (angle < curAngle) { fragColor = texture(texture0, v_texcoord); // 显示纹理 } else { fragColor = vec4(1.0, 1.0, 1.0, 1.0); // 显示白色 }
- 如果当前片元的角度
angle
小于当前阈值角度curAngle
,则显示纹理颜色。 - 否则,显示白色 (
vec4(1.0, 1.0, 1.0, 1.0)
)。
- 如果当前片元的角度
这个片段着色器通过计算每个片元相对于圆心的角度,并与由 offset
参数定义的阈值角度进行比较,实现一种从中心开始展开的动画效果。offset
参数在 [0, 1.0]
之间变化,逐渐揭示从圆心扩展到整个纹理的过程。
2.3.8 马赛克
#version 300 es
precision mediump float;
uniform sampler2D texture0;
in vec2 v_texcoord;
out vec4 fragColor;
void main(void)
{
float numBlockX = 150.0;
float numBlockY = 150.0;
float stepX = 1.0 / numBlockX;
float stepY = 1.0 / numBlockY;
float indexBlockX = floor(v_texcoord.x / stepX);
float indexBlockY = floor(v_texcoord.y / stepY);
vec2 currentBlockLeftBottom = vec2(indexBlockX*stepX, indexBlockY*stepY);
fragColor = texture(texture0, currentBlockLeftBottom);
}
这个片段着色器的主函数实现了一种像素化效果。将图像划分成若干个小块,然后对每个小块仅采样其左下角的颜色,从而实现一种块状化的视觉效果。下面对主函数的逻辑进行逐行解释:
-
定义块数和每个块的尺寸:
float numBlockX = 150.0; float numBlockY = 150.0; float stepX = 1.0 / numBlockX; float stepY = 1.0 / numBlockY;
numBlockX
和numBlockY
分别定义了图像在水平方向和垂直方向上划分的块数,这里设定为 150 块。stepX
和stepY
计算每个块在纹理坐标系中的宽度和高度。由于纹理坐标范围是[0.0, 1.0]
,所以步长stepX
和stepY
分别为1.0 / 150.0
。
-
计算当前片元的块索引:
float indexBlockX = floor(v_texcoord.x / stepX); float indexBlockY = floor(v_texcoord.y / stepY);
indexBlockX
计算当前片元在水平方向上位于哪个块中。通过将当前片元的x
坐标除以stepX
,并用floor
函数取整。- 类似地,
indexBlockY
计算当前片元在垂直方向上位于哪个块中。
-
计算当前块的左下角坐标:
vec2 currentBlockLeftBottom = vec2(indexBlockX * stepX, indexBlockY * stepY);
currentBlockLeftBottom
计算当前片元所在块的左下角的纹理坐标。通过将块索引分别乘以步长stepX
和stepY
得到。
-
采样并输出纹理颜色:
fragColor = texture(texture0, currentBlockLeftBottom);
- 使用
texture
函数从纹理texture0
中采样左下角坐标点的颜色,并将其赋值给fragColor
作为输出。这意味着当前块中的所有片元都会显示块左下角的颜色,从而实现像素化效果。
- 使用
这个片段着色器通过将图像划分成多个小块,并对每个块仅采样其左下角的颜色,达到了像素化的效果。numBlockX
和 numBlockY
控制块的数量,所以可以通过调整这两个参数来改变块的大小,从而获得不同程度的像素化效果:
- 较大的
numBlockX
和numBlockY
值,将导致更多更小的块,像素化程度更高。 - 较小的
numBlockX
和numBlockY
值,将导致更少更大的块,像素化程度更低。
总结
本文介绍了 8 中基础特效的实现逻辑,所有代码可以在 BasicEffect.kt 找到。
参考
- BasicEffect.kt
- android-gpuimage
- KodeLife