用280字符建模世界:探索代码、艺术与约束的微型着色器创作

本文深入探讨了如何用不超过280字符的GLSL代码创作出精美的实时着色器效果,涵盖了代码高尔夫技巧、着色器基础知识和创作过程,展示了图形编程与艺术创作的完美结合。

用280字符建模世界

一位图形程序员对创作微型、富有表现力的着色器的心态、方法和动机的探索,这些着色器结合了代码、艺术和约束。

引言

我是Xor。作为一名图形程序员,我的工作本质上是使用数学公式让像素变得更漂亮。我为游戏和软件中的动画背景处理视频效果,如光照、反射和后处理等。

为了娱乐,我喜欢通过编写能放入“推文”(280字符或更少)的紧凑小着色器程序来放松。你可能在X/Twitter上看到过一些这样的作品。在保持功能的同时缩小代码的过程被称为“代码高尔夫”。

以下是我用仅197个字符的GLSL代码编写的动画星系:

1
"Milky" #つぶやきGLSLfor(float i=-fract(t/.1),j;i++<1e2;o+=(cos((j=round(i+t/.1))*j+vec4(0,1,2,3))+1.)*exp(cos(j*j/.1)/.6)*min(1e3-i/.1+9.,i)/5e4/length((FC.xy-r*.5)/r.y+.05*cos(j*j/F4+vec2(0,5))*sqrt(i)));o=tanh(o*o);

这个小代码片段对屏幕上的每个像素实时运行,并使用一些花哨的数学和逻辑生成独特的输出颜色。我使用一个名为Twigl.app的工具构建这些演示,这是一个专为分享迷你着色器而设计的在线着色器编辑器。它使导出视频变得超级容易,在其“极客”模式下,它还处理通用头代码并缩短内置变量名。

我甚至成功地将一个体素DDA光线追踪器与边缘检测压缩到仅190个字符:

1
Down to 190 characters! #つぶやきGLSLvec3 p=vec3(t/.1,cos(t+r)),d=r.x/(r.xxy-round(FC.zxy)*2.),a;for(;o.w++<9e2&&snoise3D(a=ceil(p)/28.)+1.>length(a.yz);p+=min(a=fract(-p*sign(d))*abs(d)+1e-4,min(a.y,a.z)).x/d);o.rgb=fwidth(p);

今天,我想解释为什么我制作这些,分享我的创作过程,并展示如果你有兴趣如何自己尝试。让我们从“为什么”开始。

动机

我为什么写这些?嗯,有几个因素。既然我喜欢列表,我就按相关性顺序呈现它们:

  • 好奇心和热情:有时我会被一个新想法击中,只想玩玩。我喜欢Twigl,因为它有助于降低我的期望,让我开始涂鸦。过度规划的空间更少,而且超级容易上手。
  • 学习和发现:在约束下工作迫使我以不同的方式思考问题。通过优化代码大小,我经常找到简化或近似的方法。它并不总是导致更高性能的代码(但经常如此),我学会了如何榨取每个字节的最大价值。拥有非常少的代码使得实验公式和变体变得更容易,而不会感到不知所措。
  • 挑战:编写微小代码既具有挑战性又刺激。它让我的大脑保持敏锐,我不断开发新技能。这基本上成了我的游戏。在尝试解决这些技术问题时,我意外地学到了大量数学知识。
  • 社区:通过这个过程,我联系了许多有趣的人——艺术家、设计师、数学爱好者、游戏开发者、工程师、技术爱好者等等。分享我的作品导致了一些令人兴奋的相遇。(稍后更多关于一些著名人物!)

所以,简而言之,它有趣、发人深省、引人入胜,并且是激发对图形编程兴趣的好方法。现在,什么是着色器?

着色器介绍

如果你还没听说过着色器,它们是在GPU(图形处理单元)而不是CPU(中央处理单元)上运行的程序。CPU擅长复杂或分支操作,这些操作按顺序一次计算一个(我在这里简化了)。GPU设计用于每秒并行处理数十亿或数万亿的可预测操作。这听起来很多,但一个4K屏幕以60帧每秒输出近5亿像素每秒。每个像素可能有100或1000次操作,更不用说GPU可能用于的其他任何事情。

有几种不同类型的着色器:顶点着色器、片段着色器、计算着色器等,但这些推文着色器 specifically是片段着色器,也称为“像素着色器”,因为它们运行在每个像素上。本质上,片段着色器接受输入片段坐标并输出颜色和不透明度(或alpha)。片段坐标给你屏幕上每个像素中心的位置,所以(0.5, 0.5)是左下(或左上)。向右一个像素是(1.5, 0.5),依此类推到(宽度 – 0.5, 高度 – 0.5)。坐标变量在Twigl中称为“FC”。输出颜色“o”有4个RGBA分量:红、绿、蓝和alpha,每个范围从0.0到1.0。

(1.0, 1.0, 1.0, 1.0)是纯白色,(0.0, 0.0, 0.0, 1.0)是不透明黑色,(1.0, 0.0, 0.0, 1.0)是RGBA颜色格式中的纯红色。从这里,你已经可以制作简单的颜色渐变:

1
o = vec4(0.0, FC.y/100.0, 0.0, 1.0);

记住,这在每个像素上运行,所以每个像素都有一个唯一的片段坐标。该公式制作一个简单的渐变,在屏幕底部(FC.y = 0.0)开始为黑色,当FC.y达到100.0时,绿色输出值达到1.0。

所以你有一个输出颜色“o”,输入片段坐标“FC”,和四个“uniform”输入,它们在所有像素之间共享:“r”是着色器屏幕分辨率(像素),“t”是时间(秒),还有较少使用的鼠标位置“m”和后缓冲纹理“b”。这就是核心!从那里,是大量的数学和逻辑来控制输出颜色并生成酷炫的图像。

我要跳过一点,但如果你有兴趣了解更多,尝试从这里开始!

我的过程

人们经常问我是否从一开始就以紧凑形式编写着色器,或者是否先编写扩展版本然后减少代码。答案是前者。我练习代码高尔夫这么多,以至于我发现以紧凑形式原型化想法更容易,而且我倾向于不在微小着色器中迷失。代码高尔夫着色器需要在代码大小、渲染性能、艺术吸引力、设计和数学函数之间找到正确的平衡。这是一个微妙的平衡,绝对挑战我大脑的两侧。通过编写这些,我学到了大量关于数学、艺术和设计的知识!

开始一个,你需要一个想法。当编写“Milky”星星着色器时,我知道我想创建某种星系,所以那是我最初的灵感。我的着色器通常从居中和缩放开始,以便它们在各种分辨率和纵横比下看起来都好。对于星星,我循环了100个点光源围绕中心旋转。我喜欢发光效果,而且它们很容易创建。你只需要知道当前像素到光源的距离,并使用逆距离作为像素亮度(近像素更亮,远像素更暗)。

我使用一些三角函数玩弄粒子的位置,并给圆盘一个轻微的倾斜。对于着色,我喜欢使用一些带有相位偏移的正弦波用于RGB通道。正弦波也用于选择伪随机数,所以我这样选择每个星星的颜色。使用正弦公式,你可以得到这样的调色板:

我最终使用了左边第二个的轻微变体。它有一个很好的温度和亮度范围。我还添加了一些星星亮度的变化,这使得图像看起来更有趣。接下来,我应用了一些色调映射与双曲正切函数以控制大小。色调映射防止当颜色通道达到其最大亮度值时发生的 harsh 过度曝光和色调偏移(左边是原始,右边是带色调映射):

任何具有高动态范围光照的好着色器都应该应用一些色调映射,推文着色器也不例外!最后,我玩弄了动画。它本可以旋转或扭曲,但最终,我最喜欢收缩效果。我还创建了一个循环,以便当旧星星到达中心时新星星淡入。你可以在这里更详细地阅读我的设计过程!

代码高尔夫

正如你可以想象的,在缩小代码的过程中,我开发了(并继续发现)数百个小技巧,但我可以给你简略版本!我的通用代码高尔夫过程可以如下列出:

  • 减少名称:最初可能具有挑战性,但你可以习惯单字母变量和函数名。你有时可能忘记变量的用途,但这实际上有助于代码高尔夫。它迫使你重读代码,你经常会在这样做时找到更好的编写方式。像其他任何事情一样,你的记忆会随着练习改善,随着时间的推移,你会建立一些标准(对我来说:p = 位置,c = 颜色,O = 片段输出,I = 输入,等等)。
  • 减少数字:这很自解释。1.0 == 1.1000.0 == 1e3。不要忘记,使用向量构造函数,你可以使用任何数据类型作为输入,并且它被转换(“转换”)为新类型:vec4(1.0, 1.0, 1.0, 1.0) == vec4(1)。如果你乘以10.0,你可以改为除以.1
  • 最小化初始化:如果你有两个浮点数“x”和“y”,尝试像这样一起初始化它们:float x = 0., y = 1.;寻找共享数据类型的机会。如果你有一个颜色vec3和一个vec4,使它们都是vec4。避免float/int转换。
  • 避免if:GLSL中的if语句占用一些空间,特别是如果你需要else if。尝试使用三元运算符代替。例如:if (x>y) O = vec4(1,0,0,1); else O = vec4(0,1,0,1);变成O = x>y ? vec4(1,0,0,1) : vec4(0,1,0,1);。短得多,而且你可以用它做很多事情。你甚至可以在?:之间设置多个变量。
  • for(;;) > while()forwhile使用相同数量的字符,但for有一个用于初始化(在第一个分号之前)和一个用于每次迭代后的最终步骤(在最后一个分号之后)的位置。这些是免费槽,可以用于否则必须以分号结束的行。另外,避免使用break,并使用条件点代替!你也可以移除括号,如果每行以逗号结束(所以它不适用于嵌套for循环)。

除此之外,我使用一些函数替换来进一步减少代码。更多关于这里!

我整理了一个ShaderToy演示,带有一些额外的变量、格式化和注释以清晰。每个着色器都不同,需要使用不同的技术、近似和概念,但这正是让它对我如此有趣的原因!我几乎每天都在学习新东西!

问答

以下是我在X上被问到的一些问题。

你有最喜欢的“技巧”或“技术”吗?如果有,是什么?

我经历阶段。我喜欢Bokeh DoF,然后是体积阴影和分形,但目前,我的最爱必须是“湍流”。它可以用于一些 awesome 魔法效果、云或火。

你是如何发展相关数学的直觉的?

这需要很多时间和耐心。我不得不 push through 许多次当我认为一个主题超出我的理解时。如果你以小片段学习,休息,并睡在上面,你可以学到很多!我写了一些我多年来 pick up 的概念化技术。那可能节省你一些时间!

你开始编写着色器时是代码高尔夫模式,还是一个过程直到你达到最优化代码?哪个是正常着色器和代码高尔夫着色器的最佳编辑器?

是的,我以代码高尔夫模式编写,因为我已经 developed 了一种直觉,而且在这点上原型化感觉更快。当我找到一个我喜欢的外观时,我仍然需要精炼代码。我是Twigl.app的 big fan,但ShaderToy也很棒。ShaderToy最适合其社区和知识财富。我尝试在解释我的推文着色器时使用它。

你是如何开始编写酷炫着色器的,你用什么学习它?

嗯,我稍后会解释更多关于我的背景,但它始于对游戏开发的兴趣。着色器在视频游戏图形中有大量应用——那激发了我学习的好奇心。

你对牺牲可读性有遗憾吗?

没有。我更关心导致较慢代码的大小优化,但我不介意不可读的代码。对我来说,那是它的魔力的一部分。

你的背景是什么,让你能够有效地学习材料?

是故事时间…

我的故事

成长过程中,我对视频游戏感兴趣,尤其是那些带有“花哨”3D图形的游戏。当我大约10岁时,我的朋友向我展示了一个名为GameMaker的工具。我摆弄它并学习了一些拖放编程、变量和条件的基础知识。

随着时间的推移,我开始在GM中实验3D图形,即使它(现在仍然是) primarily 一个2D游戏引擎。它足以学习3D渲染如何工作和渲染管道的基础知识。后来,GameMaker引入了这个称为“着色器”的东西,允许开发者创建更高级的效果。当时,没有太多资源可用,所以我花了一些时间才 pick it up。我开始在GameMaker论坛上发布我的着色器,并从社区得到了一些有用的反馈(向“xygthop3”喊叫他的有用例子)!

游戏开发是学习着色器的好地方,因为你有性能约束(你不希望游戏 stutter),而且你在那个上下文中学习大量关于整个渲染过程的知识。2014年,我开始发布我最早的着色器教程,分享我学习的技术。早期的教程不是很好,但我很高兴我写了它们。2015年,我开始探索ShaderToy,那是我技能真正发展的地方。

有太多伟大的例子可以学习,而且是一个获得反馈的好地方。2021年,我为GameMaker与GLSL 1.00启动了一个新的入门教程系列。现在我发布更通用的教程,涵盖各种图形主题,从数学到艺术到设计到代码等等。这绝对是我最好的系列 yet,它们继续变得更好。如果你对视频游戏和图形感兴趣,我高度推荐从GameMaker或Godot开始。它们相对容易学习,同时仍然足够强大教你 ropes。如果软件或Web开发更是你的菜,你不能错 with ShaderToy或compute.toys。

以下是一些帮助过我的人,直接或间接, along the way:

  • xygthop3 – 这个家伙的免费着色器例子可能是 along the way 最大的帮助。他的例子是我理解各种图形技术的关键点,所以谢谢,Michael!
  • Inigo Quilez – Inigo是ShaderToy的作者和光线步进的国王。他的 Signed Distance Field 函数至今仍然是基础。一个绝对的传奇!
  • Fabrice Neyret – Fabrice可能是最好的着色器代码高尔夫球手,许多着色器受他的工作启发。他多年来教了我这么多技术。
  • Yonatan “zozuar” – 另一个对我的主要灵感。Yonatan的工作说服我尝试在Twitter上真正代码高尔夫,他的大脑惊人。
  • Yohei Nishitsuji – 这个家伙在微小分形方面是传奇。爱他的工作。Yohei还在Codrops上写了Rendering the Simulation Theory: Exploring Fractals, GLSL, and the Nature of Reality。

我肯定还有许多人 whose names are eluding me at the moment,但我想感谢整个着色器社区的反馈和鼓励。

武器库

我将以一些我最喜欢的推文着色器结束:

1
"Quantum" #GLSLvec3 p,q;for(float i=1.,j,z;i>0.;i-=.02)z=p.z+=sqrt(max(z=i-dot(p=vec3(FC.xy*2.-r,0)/r.y,p),-z/1e6)),p/=2.+p.z,p.xz*=rotate2D(t),o+=pow(z*z,.2)*i*pow(cos(j=cos(j)*dot(cos(q+=p),sin(q.yzx))/.3)*.5+.5,8.)*(sin(i*3e1+vec4(0,1,2,3))+1.)/8.;o*=o;
1
#つぶやきGLSLvec2 p,q;for(float i,a;i++<3e1;o+=5e-3/(abs(length(q=p*mat2(1,-1,2,2)/(r.y-p-p.yx))-i/4e1)+1./r.y)*clamp(cos(a=atan(q.y,q.x)*ceil(i*.1)+t*sin(i*i)+i*i),.0,.6)*(cos(a-i+vec4(0,1,2,0))+1.))p=FC.xy-r*.6;Demo: https://t.co/IhRk3HX4Kt
1
"Origami" #つぶやきGLSLo++;vec4 h;vec2 u;for(float A,l,a,i=.6;i>.1;i-=.1)a-=sin(a-=sin(a=(t+i)*4.)),u=(FC.xy*2.-r)/r.y,l=max(length(u-=rotate2D(a/=4.)*clamp(u*rotate2D(a),-i,+i)),.1),A=min((l-.1)*r.y*.2,1.),o=mix(h=sin(i/.1+a+vec4(1,3,5,0))*.2+.7,o,A)*mix(h/h,h+.5*A*u.y/l,.1/l);
1
195 chars of #GLSLvec2 p=(FC.xy*2.-r)/r.y,l,v=p*(1.-(l+=abs(.7-dot(p,p))))/.2;for(float i;i++<8.;o+=(sin(v.xyyx)+1.)*abs(v.x-v.y)*.2)v+=cos(v.yx*i+vec2(0,i)+t)/i+.7;o=tanh(exp(p.y*vec4(1,-1,-2,0))*exp(-4.*l.x)/o);
1
"Vortex"for(float i,z=fract(dot(FC,sin(FC))),d;i++<1e2;o+=(sin(z-t+vec4(6,2,4,0))+1.5)/d){vec3 p=z*normalize(FC.rgb*2.-r.xyy);p.z+=6.;for(d=1.;d<9.;d/=.8)p
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计