学习一个马斯克转发的Grok效果(glsl)

3,543 阅读5分钟

写在开头

最近在学习glsl,在网上找相关开源代码效果学习时看到了这个被马斯克转发,并且只用四行就完成的效果!自我学习之后分享出来,因为本人也是一个glsl新手,有错误的地方欢迎大家斧正!本篇文章只会涉及到片段着色器,下面代码如果不特殊提醒均为片段着色器

效果原作者Xor

elonmusk.png

效果欣赏!🔑

入门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所以我们这个代码的效果是一个红色的填充。

工作原理

  1. GPU为屏幕上的每个像素都调用一次这个片元着色器
  2. 每次调用时,main()函数执行
  3. gl_FragColor被设置为红色
  4. 所有像素都得到相同的红色值
  5. 最终屏幕显示为一片红色

就像每个像素都调用了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.50.1 / 0.5 = 0.2暗灰色
0.20.1 / 0.2 = 0.5中等亮度
0.10.1 / 0.1 = 1.0比较亮
0.050.1 / 0.05 = 2.0很亮!
0.010.1 / 0.01 = 10.0超亮!!
0.0010.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实在是太有意思了!同时也太难啦!!!

参考资料

Xor

The Book of Shaders