写在开头
最近在学习glsl
,在网上找相关开源代码效果学习时看到了这个被马斯克转发,并且只用四行就完成的效果!自我学习之后分享出来,因为本人也是一个glsl
新手,有错误的地方欢迎大家斧正!本篇文章只会涉及到片段着色器,下面代码如果不特殊提醒均为片段着色器
效果原作者Xor
效果欣赏!🔑
入门glsl
首先我们应该了解到之前我们使用的canvas 2D
绘制和glsl
绘制的一些差异性:
Canvas 2D的绘制方式:
- 类似传统绘画,按顺序一笔一笔画
- 从左上角(0,0)开始,y轴向下
- 命令式编程:moveTo(), lineTo(), fill()
- 时间顺序执行,后画的覆盖先画的
GLSL的绘制方式:
- 每个像素点同时计算,高度并行
- 坐标系通常以中心为(0,0),y轴向上
- 函数式编程:给定坐标,返回颜色
- 所有像素"瞬间"同时渲染
这是学习glsl
初期相当让人不适应的一点,glsl
非常类似中国古代的活字印刷技术,所有像素点同时渲染,几何形状需要依靠数学计算来排列组合!下面举得例子中都要谨记这个概念瞬间/同时渲染
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
gl_FragColor 是GLSL片元着色器(Fragment Shader)中的一个内置输出变量,它决定了当前像素最终显示的颜色。
gl_FragColor的核心概念
本质:它是每个像素的"最终答案" - 告诉GPU这个像素应该显示什么颜色。
vec4的四个分量含义
gl_FragColor = vec4(r, g, b, a);
// ↑ ↑ ↑ ↑
// 红 绿 蓝 透明度
- r (Red):红色分量,范围 0.0-1.0
- g (Green):绿色分量,范围 0.0-1.0
- b (Blue):蓝色分量,范围 0.0-1.0
- a (Alpha):透明度,0.0完全透明,1.0完全不透明
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
因为r=1.0所以我们这个代码的效果是一个红色的填充。
工作原理
- GPU为屏幕上的每个像素都调用一次这个片元着色器
- 每次调用时,main()函数执行
- gl_FragColor被设置为红色
- 所有像素都得到相同的红色值
- 最终屏幕显示为一片红色
就像每个像素都调用了main方法得到的命令是都给我渲染成红色,结果就是整个画布展示为红色。
更多例子
void main() {
vec2 pos = gl_FragCoord.xy / u_resolution.xy;
gl_FragColor = vec4(pos.x, 0.0, 0.0, 1.0);
}
gl_FragCoord - 像素的绝对坐标
gl_FragCoord
是GLSL的内置变量,表示当前像素在屏幕上的绝对位置(以像素为单位)。
// 假设画布是 800x600 像素,不考虑采样精度
// gl_FragCoord.xy 的取值范围是:
// x: 0 到 800
// y: 0 到 600
u_resolution - 画布的总尺寸
u_resolution 是一个变量,表示画布的宽高。
归一化坐标的计算
vec2 pos = gl_FragCoord.xy / u_resolution.xy;
这行代码将绝对像素坐标转换为归一化坐标(0.0到1.0):
举例:画布800x600,当前像素在(400, 300)
pos.x = 400 / 800 = 0.5 (画布中央)
pos.y = 300 / 600 = 0.5 (画布中央)
渐变效果的实现
gl_FragColor = vec4(pos.x, 0.0, 0.0, 1.0);
// ↑
// 红色分量由x坐标决定
- 最左边:pos.x = 0.0 → 黑色 (0, 0, 0, 1)
- 最右边:pos.x = 1.0 → 纯红 (1, 0, 0, 1)
- 中间:pos.x = 0.5 → 半红 (0.5, 0, 0, 1)
这个效果是纵向渐变,我们举一反三实现一个垂直渐变
void main() {
vec2 pos = gl_FragCoord.xy / u_resolution.xy;
gl_FragColor = vec4(pos.y, 0.0, 0.0, 1.0);
}
从这两个渐变效果我们就能了解到glsl
坐标系是从左到右/从下到上
0,1 - - - - - 1,1
| |
| |
0,0 - - - - - 1,0
径向渐变
线性渐变我们已经掌握了接下来我们来挑战一下径向渐变!
void main() {
vec2 pos = gl_FragCoord.xy / u_resolution.xy;
pos = pos * 2.0 - 1.0;
float dist = length(pos);
gl_FragColor = vec4(dist, dist, dist, 1.0);
}
pos = pos * 2.0 - 1.0;
在于把中心点变换到[0,0]
,vec2 pos = gl_FragCoord.xy / u_resolution.xy;
是归一化坐标也就是坐标范围此时为[0,1]
此时坐标中心点是[0.5,0.5]
, 经过 pos * 2.0 - 1.0;
之后我们的坐标范围变成了[-1,1]
,此时的坐标中心点就是[0,0]
啦。
length()
是GLSL中的内置数学函数,用来计算向量的欧几里得长度,也就是我们熟知的勾股定理,对于对于2D向量vec2(x, y)
来说!
length(vec2(x, y)) = sqrt(x*x + y*y)
这就是我们为什么需要把作用中心点转换到[0,0]
的原因!我们的dist
的值靠近中心点就约等于0,越靠近外层就约等于1
发光的圆环
void main() {
vec2 pos = gl_FragCoord.xy / u_resolution.xy;
pos = pos * 2.0 - 1.0;
float dist = length(pos);// 计算与圆环的距离
float ring = abs(dist - 0.5);// 距离越近越亮
float brightness = 0.1 / ring;
gl_FragColor = vec4(brightness, brightness, brightness, 1.0);
}
OK Fine,现在我们提升一点难度,开始我最不喜欢的数学!首先是dist
可以产生一个径向渐变中心是黑色往外扩散白色,但是我们的目的是一个圆环,也就是中心要是 黑色->白色->黑色
,通过float ring = abs(dist - 0.5)
就可以产生一个明显的分层效果!
假设:
dist = 0; ring = 0.5;
dist = 0.5; ring = 0;
dist = 1; ring = 0.5;
float brightness = 0.1 / ring;
的作用就是提亮!
ring值 | brightness = 0.1 / ring | 效果 |
---|---|---|
0.5 | 0.1 / 0.5 = 0.2 | 暗灰色 |
0.2 | 0.1 / 0.2 = 0.5 | 中等亮度 |
0.1 | 0.1 / 0.1 = 1.0 | 比较亮 |
0.05 | 0.1 / 0.05 = 2.0 | 很亮! |
0.01 | 0.1 / 0.01 = 10.0 | 超亮!! |
0.001 | 0.1 / 0.001 = 100.0 | 极亮!!! |
数学规律:当分母越接近0,结果越接近无穷大! 这就是为什么圆环会发光 - 在半径0.5的位置,分母最小,亮度最大!
完成Grok效果
看完上述几个例子后,我们已经可以完成一个发亮的圆环,现在距离最终的效果只差一个对角线!
🎯 对角线的数学定义
对角线是所有满足 x = y 的点
在这些点上:p.x - p.y = x - x = 0
- 数值分布
左上角:p.x < p.y,所以 p.x - p.y < 0(负值)
右下角:p.x > p.y,所以 p.x - p.y > 0(正值)
对角线:p.x = p.y,所以 p.x - p.y = 0(零值)
- 除法放大
0.01 / (p.x - p.y)
当 p.x - p.y 接近 0 时(对角线附近)
除法结果趋向无穷大 → 产生极亮的线
我们加上这个对角线后为了让对角线在圆环中不发光需要反转亮度,想想我们之前的数学规律当分母越接近0,结果越接近无穷大!
,反过来如果分母越大结果自然也就越小!!
结语
glsl
实在是太有意思了!同时也太难啦!!!