前言

WebGL Fundamentals: 适合初学者,包含详细的教程和示例代码

基础知识

什么是Shader

Shader是运行在GPU上的一段程序,主要负责控制图形渲染管线中的一部分。主要类型

  • 顶点着色器 (Vertex Shader): 确定图形的每一个顶点的位置
  • 片元着色器 (Fragment Shader): 渲染图形的每一个片元(像素)的颜色

Shader和其他技术

Shader是前端图形技术中的重要一环,它与常见的图形技术如CSSSVGCanvas2DWebGL都有着密切的关系,每一种技术都在不同层面提供了图形渲染的能力,但Shader的独特之处在于它直接操作GPU,能够实现高度复杂的视觉效果

  • CSS: CSS的主要功能是网页布局,但它也可以通过一些属性(如animationtransform)实现简单的动画效果。它的局限性在于无法很好地处理复杂的动态变化,尤其是像素级的精细操作

  • SVG: SVG提供了矢量图形的能力,允许开发者通过路径(path标签)定义任意形状,并结合属性(如 stroke-dasharraystroke-dashoffset)实现动画。相比CSSSVG更适合处理复杂的几何图形。然而,它仍然难以实现动态粒子效果或像素级控制

  • Canvas2D: Canvas2D提供了对每个像素的直接控制,开发者可以通过数学运算和随机函数实现动态效果,例如粒子拖尾动画。但它的设计初衷是处理2D图形,对3D图形的支持相对较弱

  • WebGL: WebGL是在浏览器中渲染3D图形的标准,它通过“渲染管线”处理图形数据。WebGL的强大之处在于既可以处理2D图形,也可以实现3D场景。ShaderWebGL的核心组成部分,用于自定义渲染管线中的特定部分,提供开发者完全的渲染控制能力


Shader基础知识

Shader编程语言选择

目前主要的Shader编程语言有以下几种

  • GLSL: 主要用于OpenGL平台的图形API
  • HLSL: 由微软开发,主要用于DirectX平台的图形API
  • Cg: 由Nvidia开发,可被编译为GLSLHLSL,主要被用于Unity平台
  • WGSL: WebGPU的专用语言,适用于未来的浏览器图形开发

本学习笔记重点是Web环境中的Shader开发,并基于WebGL平台,选择GLSL作为主要的学习语言

WebGL渲染管线流程

WebGL的渲染管线的核心流程(一个物体是如何被渲染到屏幕上的)主要包括以下几个步骤

  • 顶点着色器: 处理顶点数据(如位置、颜色、纹理坐标)
  • 图元装配: 将顶点数据组合成图元(如点、线、三角形)
  • 光栅化: 将图元转换为屏幕上的像素
  • 片元着色器: 计算每个像素的颜色

整个流程是: 在JS中提供顶点的数据(通常是Float32Array类型的数组,包含了顶点的位置等信息),将这些数据传递给顶点着色器,让它计算每个顶点的位置,然后WebGL将顶点装配成图元(如三角形),图元再被转换成屏幕上的空像素(光栅化),让片元着色器来计算每个像素的颜色并填充上去,最终将物体渲染到屏幕上

  • 图元装配和光栅化是WebGL自带的操作,无法进行额外的定制,但顶点着色器和片元着色器则是完全可通过编程来定制化的

Shader开发环境

在线开发工具

  • ShaderToy: 一个实时运行和分享Shader的在线平台
  • codesandbox: 偏向工程化的Template
  • jsfiddle: Fork下面的fiddle然后编辑JS (当然也可以在codepen里写)

编辑器开发工具

  • VSCode: 安装以下插件 Shader language support, Shader Toy, Live Preview, glsl-canvas

实现第一个Shader

GLSL语言的Shader文件后缀名是.glsl

  • 下面定义了一个mainImage函数,它不返回任何值,故返回类型为void。它接受2个参数,一个是4维的fragColor,代表输出的像素颜色。另一个是2维的fragCoord,代表输入的像素坐标
void mainImage(out vec4 fragColor,in vec2 fragCoord){
vec3 color = vec3(1.0, 0.0, 0.0); // 定义红色
fragColor = vec4(color, 1.0); // 输出红色
}
  • 上面代码定义了一个名为color3维变量,将它的值设置为红色,红色的RGB颜色值为(255,0,0),在GLSL中,需要先将颜色原先的值进行归一化操作(除以255)后才能将它正确地输出,因此将红色的值归一化后就得到了(1,0,0)这个值,将它转换为3维变量vec3(1.,0.,0.)赋给color变量

  • 最后给输出颜色fragColor赋值一个4维变量,前3维就是color这个颜色变量,最后一维是透明度,由于纯红色并不透明,直接将其设为1即可

  • 当然也可以通过判断像素坐标范围,给不同区域填充不同的颜色

  • 这里就要用到fragCoord变量,它代表了输入的像素坐标,有2个维度xy,它们的大小取决于画面本身的大小。假设画面当前的大小为1536x864,那么每一个像素的fragCoordx坐标值将会分布在(0,1536)之间,y坐标值则分布在(0,864)之间

  • 在当前的Shader开发环境内,还有个内置的变量iResolution,代表了画面整体的大小,使用它时一般会取它的xy维度

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec3 color1 = vec3(1.0, 0.0, 0.0); // 红色
vec3 color2 = vec3(0.0, 1.0, 0.0); // 绿色
vec3 color3 = vec3(0.0, 0.0, 1.0); // 蓝色
vec3 color4 = vec3(1.0, 1.0, 0.0); // 黄色

if (fragCoord.x < iResolution.x * 0.25) {
fragColor = vec4(color1, 1.0);
} else if (fragCoord.x < iResolution.x * 0.5) {
fragColor = vec4(color2, 1.0);
} else if (fragCoord.x < iResolution.x * 0.75) {
fragColor = vec4(color3, 1.0);
} else {
fragColor = vec4(color4, 1.0);
}
}
  • 之所以能够显示出四种颜色,是因为GPU会对屏幕上的每一个像素点进行独立的并行计算

GLSL基础知识

GLSL是一种类似C的语言,用于控制GPU的渲染逻辑,如果有C/C++的基础会更容易理解,以下内容将逐步介绍GLSL的核心知识

变量

  • 一维变量(标量): 比如intfloatbool。浮点型float可以说是GLSL里最常用的类型,浮点型变量必须要有小数点,不能省略,而且语句结尾一定要加一个分号;
float foo = 1.0; // 浮点型
int bar = 10; // 整型
bool flag = true; // 布尔型
  • 向量变量: 向量类型支持3种维度,二维vec2、三维vec3和四维vec4,通过.x,.y,.z,.w(或.r,.g,.b,.a)访问。如果想对多维度变量进行取值或赋值操作,就要用到.符号,并且四个维度可以任意组合(称为Swizzling
vec2 a = vec2(1.0, 0.0);  // 二维向量

vec3 b = vec3(1.0, 0.5, 0.0); // 三维向量
vec2 d = b.xy; // 提取 b 的 x 和 y 分量,构成二维向量 d (1.0, 0.5)
d.y = 2.0; // 修改 d 的 y 分量为 2.0

vec4 c = vec4(1.0, 0.5, 0.0, 1.0); // 四维向量
vec3 e = c.yxy; // 重组 c 的分量,形成一个新的三维向量 (0.5, 1.0, 0.5)
e.zx = vec2(1.0); // 修改 e 的 z 和 x 分量为 1.0
  • 矩阵变量: 矩阵用于线性变换,mat2类型代表了一个大小是2x2的矩阵,mat3类型则代表了一个3x3的矩阵、mat4类型是4x4的矩阵
mat2 m1 = mat2(1.0, 0.0, 0.0, 1.0);  // 2x2 矩阵
mat3 m2 = mat3(1.0, 2.0, 0.0, 0.0, 1.0, 1.0, 0.0, 2.0, 1.0); // 3x3 矩阵
  • 结构体: 如果想把多个变量捆绑到一个变量上,可以使用结构体struct
struct Ray {
vec3 ro; // 起点
vec3 rd; // 方向
};

Ray ray = Ray(vec3(0.0), vec3(1.0, 0.0, 0.0));

运算符

  • GLSL支持常见的算术运算符和赋值运算符

  • 要注意的一点是,运算一定要保证维度的匹配,比如不能将一个vec2的变量与一个vec3的变量相加,要加的话得选取vec3变量的其中2个维度,转成vec2变量才能与另一个vec2变量相加

  • 也有一种特殊的情况,当一个向量和一个标量进行运算时,GLSL会将标量广播(broadcast)到向量的每一个分量上

vec2 v = vec2(1.0, 2.0);
v += 1.0; // v 的值为 vec2(2.0, 3.0)

控制流

  • 和常规的编程语言一样,GLSL有基本的控制流结构
if (condition) {
// 条件满足时执行
} else {
// 条件不满足时执行
}
  • 这里要注意一点,Shader是针对整个屏幕的像素进行处理的,因此if的所有分支只要满足一定的条件,都会被执行。这是由于GPU并行特性像素级独立处理的工作方式
if (fragCoord.x < iResolution.x * 0.25)
fragColor = vec4(1.0, 0.0, 0.0, 1.0); // 红色
else if (fragCoord.x < iResolution.x * 0.5)
fragColor = vec4(0.0, 1.0, 0.0, 1.0); // 绿色
else
fragColor = vec4(0.0, 0.0, 1.0, 1.0); // 蓝色
  • GPU会为屏幕上的每个像素单独运行一遍Fragment Shader代码,代码中的if条件根据每个像素的位置(fragCoord.x)来决定应该执行哪一段代码

变量限定符

变量限定符是用来描述变量的存储和使用方式的关键词

  • GLSL中常用的变量限定符有以下几种,uniformconstvaryingattribute
uniform vec3 uColor; // 定义统一的颜色变量
varying vec3 vColor; // 顶点着色器向片元着色器传递的颜色 (旧版 GLSL)
attribute vec3 aPosition; // 传递顶点位置 (旧版 GLSL)
  • uniform: 全局变量,一旦定义后会同时存在于顶点着色器与片元着色器中,并且它在每一个顶点和片元上的值都是相同的,是一个“统一”的值
  • const: 定义常量,它是无法被改变的一个值
  • iTime: 表示Shader从开始到现在执行所经过的时间,用于创作动画效果
  • iResolution: 表示Shader所在画布的大小,默认是占满整个屏幕
  • iMouse: 表示用户鼠标当前所在的位置

宏定义

宏(macros)是一种预处理指令,用于在编译时进行文本的替换,常用于定义常量、函数、条件编译等

  • 宏定义的格式是#define 宏的名称 宏的值,语句结尾没有分号。下面代码定义了一个名为PI的宏,Shader编译时会将所有的PI替换为3.14159265359这个值
#define PI 3.14159265359
  • 宏也可以带有参数。下面代码定义了一个名为add的宏,接受2个参数ab,对它们应用相加的运算,并且无需指定明确的类型,调用这个宏时只要参数的类型相匹配,就能正确执行宏定义的运算
#define add(a,b) a+b
  • 宏也可以条件编译。下面代码会在编译时通过#if判断宏USE_COLOR的值是否为1,如果USE_COLOR == 1,代码会保留红色的定义,反之,代码会保留黑色的定义
#define USE_COLOR 1

#if USE_COLOR == 1
vec3 color = vec3(1.0, 0.0, 0.0);
#else
vec3 color = vec3(0.0, 0.0, 0.0);
#endif

UV基础与核心概念

UV坐标

  • UV坐标是一种将像素坐标归一化到[0.0, 1.0]范围的坐标系,其中U代表水平方向(x坐标),V代表垂直方向(y坐标)
  • Shader编程中,UV坐标用于描述画布上的归一化像素位置,通常由片元着色器中的fragCoordiResolution计算得出,下面是归一化公式
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord / iResolution.xy;
}

UV坐标的分布特性

  • 先看下x坐标的分布情况
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord / iResolution.xy;
fragColor = vec4(uv.x, 0.0, 0.0, 1.0);
}
  • 现在默认颜色的第3个值是0,只看前2个值。可以看到x坐标从左边开始是黑色,值是(0,0),到最右边是纯红色,值是(1,0),而中间则是分布在(0,1)之间的值。从整体上看,得到了一个横向的渐变图案
  • 再看下y坐标的分布情况
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord / iResolution.xy;
fragColor = vec4(0.0, uv.y, 0.0, 1.0);
}
  • 可以看到y坐标从底下开始是黑色,值是(0,0),到最上面是纯绿色,值是(0,1),而中间则是分布在(0,1)之间的值。从整体上看,得到了一个纵向的渐变图案
  • 接下来同时输出x坐标和y坐标的分布
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord / iResolution.xy;
fragColor = vec4(uv, 0.0, 1.0);
}
  • 左下角原点是黑色,值是(0,0),右下角是红色,值是(1,0),左上角是绿色,值是(0,1),右上角是黄色,值是(1,1),中间的所有值在(0,0)(1,1)2个区间分布。从整体上看,得到了一个有多种颜色的渐变图案
  • 上面就是所谓的UV坐标,它代表了图像(这里指画布)上所有像素的归一化后的坐标位置,其中U代表水平方向,V代表垂直方向

图形绘制 (圆形)

  • 先计算UV坐标上的点到原点的距离,然后根据这些距离的值来设定对应点的颜色
  • 为了计算UV上点到原点的距离,可以用GLSL的内置函数length函数来实现
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord / iResolution.xy;
float d = length(uv);
fragColor = vec4(vec3(d), 1.0);
}
  • 左下角原点是黑色,值是(0,0),从原点向右上方向辐射的径向渐变,上面每个点的值代表的就是该点到原点的距离,越靠近原点距离越小,越接近黑色,反之越远离原点距离越大,越接近白色
  • 目前图形的位置在左下角,把它挪到中间,将UV的坐标减去0.5,再整体乘上2,这一步被称为“UV的居中处理”
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord / iResolution.xy;
uv = (uv - 0.5) * 2.0;
fragColor = vec4(uv, 0.0, 1.0);
}
  • 看下操作前后的对比图

  • 之前的坐标系原点是第一幅图左下角的(0,0),通过整体减去0.5,将原点变成了(-0.5,-0.5),也就是第二幅图左下角的那个点的位置,第一幅图的中点(0.5,0.5)就变成了第二幅图的中点(0,0),然后,将坐标整体乘上2,将0.5变成了1,这样归一化后能方便后续的计算

  • 理解UV的居中处理后,将UV坐标输出的代码注释掉,换回之前的距离代码

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord / iResolution.xy;
uv = (uv - 0.5) * 2.0;
float d = length(uv);
fragColor = vec4(vec3(d), 1.0);
}
  • 可以看到图形确实被挪到了中间。然而,图形目前的形状是一个椭圆,这是为什么呢?因为UV坐标的值并不会自动地适应画布的比例,导致了图形被拉伸这一现象
  • 为了修正这一点,需要计算画布的比例,将画布长除以画布宽就能算出,再将UVx坐标与比例相乘即可
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord / iResolution.xy;
uv = (uv - 0.5) * 2.0;
uv.x *= iResolution.x / iResolution.y;
float d = length(uv);
fragColor = vec4(vec3(d), 1.0);
}
  • 上面代码得到了一个完整的圆形径向渐变。中点的值是(0,0),颜色是纯黑色,然而从中点开始向四周辐射的那些区域,它们的值都大于0,不是纯黑色。我们的目标,是要把其中的一片区域也变成纯黑色,也就是说要把分布在这片区域上面的点的值也变成0
  • Shader中,值的显示范围只会是[0,1]之间,小于0的负数实际显示的值还是0(黑色),大于1的数实际显示的值还是1(白色)。可以利用这一点,给距离d减去一个值(这里取0.5),制造出一片负数的区域,而这片区域就是黑色
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord / iResolution.xy;
uv = (uv - 0.5) * 2.0;
uv.x *= iResolution.x / iResolution.y;
float d = length(uv);
d -= 0.5;
fragColor = vec4(vec3(d), 1.0);
}
  • 中间确实出现了纯黑色的圆形区域,然后只需把周围的渐变给消除,就能得到真正的圆形
  • 先定义一个中间变量c,用if语句来判断距离d的大小,如果大于0,代表的是除了中间纯黑区域外的渐变区域,将它们的值设为1(白色)。反之,就代表的是中间的纯黑区域,将它们的值设为0(黑色),最后将中间变量直接作为结果输出即可
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord / iResolution.xy;
uv = (uv - 0.5) * 2.0;
uv.x *= iResolution.x / iResolution.y;
float d = length(uv);
d -= 0.5;
float c = 0.0;
if (d > 0.0) {
c = 1.0;
} else {
c = 0.0;
}
fragColor = vec4(vec3(c), 1.0);
}
  • Shader的编写中,应当尽量避免使用if语句。因为GPU是并行处理结果的,而if语句会让处理器进行分支切换这一操作,处理多个分支会降低并行处理的性能。
  • 可以用GLSL其中的一个内置函数来优化掉if语句,这个内置函数是 step函数,也被称作“阶梯函数”,是因为它的图像是阶梯的形状

  • 它的代码表示形式是这样的
step(edge, x)
  • 它接受2个参数,边界值edge和目标值x,如果目标值x大于边界值edge,则返回1,反之返回0
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord / iResolution.xy;
uv = (uv - 0.5) * 2.0;
uv.x *= iResolution.x / iResolution.y;
float d = length(uv);
d -= 0.5;
float c = step(0.0, d);
fragColor = vec4(vec3(c), 1.0);
}
  • 尽管圆形是画出来了,但仔细一看,就会发现图形的周围有锯齿,比较影响美观,要消除它们
  • 再来认识一个GLSL的内置函数——smoothstep函数,它也被称作“平滑阶梯函数”,是因为它的函数图像是一个平滑过的阶梯的形状

  • 它的代码表示形式是这样的
smoothstep(edge1, edge2, x)
  • 它的边界值比step函数要多一个,可以将它的边界值定为edge1edge2。如果目标值x小于边界值edge1,则返回0。如果目标值x大于边界值edge2,则返回1。如果目标值x2个边界值之间,则返回从01平滑过渡的值
  • 把之前代码里的step函数的语句注释掉,改成用smoothstep函数来实现,再将第2个边界值设定为一个比0稍微大一点的值。这里取了0.02
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord / iResolution.xy;
uv = (uv - 0.5) * 2.0;
uv.x *= iResolution.x / iResolution.y;
float d = length(uv);
d -= 0.5;
float c = smoothstep(0.0, 0.02, d);
fragColor = vec4(vec3(c), 1.0);
}
  • 这样,就得到了一个边缘是平滑的,没有锯齿的圆形

图形效果

  • 尽管Shader的绘图步骤确实要比传统的绘图方式要繁琐一点,但是也带来了很多意想不到的可能性,比方说,它能实现一些特殊的图形效果

模糊效果

  • 上面代码用到了smoothstep函数来绘制圆形,它的第二个参数用的是一个很小的值0.02,尝试把这个值改大一点,比如0.2
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord / iResolution.xy;
uv = (uv - 0.5) * 2.0;
uv.x *= iResolution.x / iResolution.y;
float d = length(uv);
d -= 0.5;
float c = smoothstep(0.0, 0.2, d);
fragColor = vec4(vec3(c), 1.0);
}
  • 随着渐变区域的扩大,圆形的边缘变得模糊了起来,这是因为两个边界值的差变大了,渐变的区域也就随着变大了,这样就营造出了一种模糊的效果

发光效果

  • 这里不用smoothstep函数来绘制图形,取距离d的倒数,并且乘上一个比较小的值
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord / iResolution.xy;
uv = (uv - 0.5) * 2.0;
uv.x *= iResolution.x / iResolution.y;
float d = length(uv);
float c = 0.25 / d;
fragColor = vec4(vec3(c), 1.0);
}
  • 画面上出现了一个美丽的光球,它是怎么形成的呢

  • 这是个反比例函数的图像,目前输入值范围是(0.,1.),在这段范围内,输入值位于(0.,.25)时,输出值都大于1Shader中比1大的值输出的还是白色,因此能看到中间的白色圆形部分。输入值位于(.25,1.)时,输出的值开始变成了比1小的值,而且是逐渐变化的,因此会产生一种渐变的效果

  • 目前光的辐射范围太大了,要稍微缩小一些

  • 再来认识一个新的内置函数——pow函数,它用于计算数字的指数幂,比如pow(4.,3.),返回的值就是43次方——64,也就是说,pow这个函数能让数值指数般地增长

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord / iResolution.xy;
uv = (uv - 0.5) * 2.0;
uv.x *= iResolution.x / iResolution.y;
float d = length(uv);
float c = 0.25 / d;
c = pow(c, 1.6);
fragColor = vec4(vec3(c), 1.0);
}
  • 为了理解这一步,依旧来看图

  • 函数图像比之前要往下“躺”了一些,输出值总体变小了,这样光的辐射也稍微缩小了一点

SDF函数

  • 圆形是众多几何图形中的其中一种,既然已经通过上面的方式将它画了出来,那肯定也能用类似的手段来把其他图形给画出来
  • 绘制圆形时,在调用smoothstep函数之前做了如下的操作
float d = length(uv);
d -= 0.5;
  • 其实,可以把这些操作抽象成一个函数,叫sdCircle
float sdCircle(vec2 p, float r) {
return length(p) - r;
}

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord / iResolution.xy;
float d = sdCircle(uv, 0.5);
fragColor = vec4(vec3(d), 1.0);
}
  • 尽管这个函数的调用结果跟之前写的一模一样,但它有一个特殊的含义,它是一个SDF函数
  • SDF函数(Signed Distance Function),中文译作“符号距离函数”,它用于描述这么一个函数,它将空间里的一个位置作为输入,并返回该位置到给定形状的距离,它的前面还有个“符号”,是因为在形状外的距离为正数(“+”号),在形状内的距离为负数(“-”号),边界处的值恰好为0
  • 下图是圆形SDF函数的可视化图,可以更形象地理解它的意义

图形学大咖Inigo Quilez(后文简称iq)的博客上有篇文章把常用的2D图形的SDF函数都列了出来,如果有需要可以随时查阅

  • 知道SDF函数的概念后,绘制其他图形将会变得非常轻松,比如想要画一个长方形,那么只需找到长方形的SDF函数(sdBox),调用它获取距离,再用stepsmoothstep函数勾画出图形即可
float sdBox(in vec2 p, in vec2 b)
{
vec2 d = abs(p) - b;
return length(max(d, vec2(0.))) + min(max(d.x, d.y), 0.);
}

void mainImage(out vec4 fragColor, in vec2 fragCoord)
{
vec2 uv = fragCoord / iResolution.xy;
uv = (uv - vec2(0.5)) * 2.0;
uv.x *= iResolution.x / iResolution.y;

float d = sdBox(uv, vec2(0.6, 0.3));
float c = smoothstep(0.0, 0.02, d);
fragColor = vec4(vec3(c), 1.0);
}
  • 上面的代码画出了一个半尺寸长为0.6、宽为0.3(实际长为1.2、宽为0.6)的长方形
  • SDF中,矩形的sdBox函数定义的b参数(如vec2(0.6, 0.3))表示的是矩形的半尺寸。这是因为SDF函数设计时,通常使用半尺寸来计算方便

UV变换

  • 基于已经画好的这个长方形,来学习一些基本的UV变换操作

平移

  • 先尝试给UVxy坐标加上想移动的值
float sdBox(in vec2 p, in vec2 b)
{
vec2 d = abs(p) - b;
return length(max(d, vec2(0.0))) + min(max(d.x, d.y), 0.0);
}

void mainImage(out vec4 fragColor, in vec2 fragCoord)
{
vec2 uv = fragCoord / iResolution.xy;
uv = (uv - vec2(0.5)) * 2.0;
uv.x *= iResolution.x / iResolution.y;
uv.x += 0.2;
uv.y += 0.4;

float d = sdBox(uv, vec2(0.6, 0.3));
float c = smoothstep(0.0, 0.02, d);
fragColor = vec4(vec3(c), 1.0);
}
  • 明明是给坐标加上了值,为什么图形的坐标并未朝右上移动,而是朝相反的左下方向移动了呢
void mainImage(out vec4 fragColor, in vec2 fragCoord)
{
vec2 uv = fragCoord / iResolution.xy;
uv = (uv - vec2(0.5)) * 2.0;
uv.x *= iResolution.x / iResolution.y;
uv.x += 0.2;
uv.y += 0.4;
fragColor = vec4(uv, 0.0, 1.0);
}

  • 之前位于中间的原点值是(0,0),现在则变成了(0.2,0.4),上一个(0,0)移动到了当前中间点的左下方,SDF函数输入的坐标值的原点值是(0,0),正好对应左下方的那个点,因此图形才会整体往左下方移动
  • 为了确定SDF图形位置的变化,要看目前(0,0)这个点的位置变化。如果要平移符合正方向的移动(右上方),把之前的加法操作改成与其相反的减法操作即可
float sdBox(in vec2 p, in vec2 b)
{
vec2 d = abs(p) - b;
return length(max(d, vec2(0.0))) + min(max(d.x, d.y), 0.0);
}

void mainImage(out vec4 fragColor, in vec2 fragCoord)
{
vec2 uv = fragCoord / iResolution.xy;
uv = (uv - vec2(0.5)) * 2.0;
uv.x *= iResolution.x / iResolution.y;
uv -= vec2(0.2, 0.4);

float d = sdBox(uv, vec2(0.6, 0.3));
float c = smoothstep(0.0, 0.02, d);
fragColor = vec4(vec3(c), 1.0);
}

缩放

  • 先尝试给UVxy坐标乘上想缩放的值
float sdBox(in vec2 p, in vec2 b)
{
vec2 d = abs(p) - b;
return length(max(d, vec2(0.0))) + min(max(d.x, d.y), 0.0);
}

void mainImage(out vec4 fragColor, in vec2 fragCoord)
{
vec2 uv = fragCoord / iResolution.xy;
uv = (uv - vec2(0.5)) * 2.0;
uv.x *= iResolution.x / iResolution.y;
uv *= vec2(2.0, 2.0);

float d = sdBox(uv, vec2(0.6, 0.3));
float c = smoothstep(0.0, 0.02, d);
fragColor = vec4(vec3(c), 1.0);
}
  • 果不其然,图形并非扩大,而是缩小了相应的倍数
  • 当坐标范围从[-1.0, 1.0]扩大到[-2.0, 2.0]时,矩形的顶点坐标虽然变大了(例如从0.6变成1.2),但由于画布的整体范围也变大了,矩形在整个画布中的相对比例缩小了。因此,矩形看起来变小了。简单来说,矩形的“绝对大小”没有改变,但它在画布中的“相对大小”变小了
  • 同样地,如果要符合正方向的缩放(扩大),把之前的乘法操作改成与其相反的除法操作即可

翻转


附录