前言
WebGL Fundamentals: 适合初学者,包含详细的教程和示例代码
基础知识
什么是Shader
Shader
是运行在GPU
上的一段程序,主要负责控制图形渲染管线中的一部分。主要类型
- 顶点着色器 (
Vertex Shader
): 确定图形的每一个顶点的位置 - 片元着色器 (
Fragment Shader
): 渲染图形的每一个片元(像素)的颜色
Shader
和其他技术
Shader
是前端图形技术中的重要一环,它与常见的图形技术如CSS
、SVG
、Canvas2D
和WebGL
都有着密切的关系,每一种技术都在不同层面提供了图形渲染的能力,但Shader
的独特之处在于它直接操作GPU
,能够实现高度复杂的视觉效果
CSS
: CSS
的主要功能是网页布局,但它也可以通过一些属性(如animation
和transform
)实现简单的动画效果。它的局限性在于无法很好地处理复杂的动态变化,尤其是像素级的精细操作
SVG
: SVG
提供了矢量图形的能力,允许开发者通过路径(path
标签)定义任意形状,并结合属性(如 stroke-dasharray
和stroke-dashoffset
)实现动画。相比CSS
,SVG
更适合处理复杂的几何图形。然而,它仍然难以实现动态粒子效果或像素级控制
Canvas2D
: Canvas2D
提供了对每个像素的直接控制,开发者可以通过数学运算和随机函数实现动态效果,例如粒子拖尾动画。但它的设计初衷是处理2D
图形,对3D
图形的支持相对较弱
WebGL
: WebGL
是在浏览器中渲染3D
图形的标准,它通过“渲染管线”处理图形数据。WebGL
的强大之处在于既可以处理2D
图形,也可以实现3D
场景。Shader
是WebGL
的核心组成部分,用于自定义渲染管线中的特定部分,提供开发者完全的渲染控制能力
Shader
基础知识
Shader
编程语言选择
目前主要的Shader
编程语言有以下几种
GLSL
: 主要用于OpenGL
平台的图形API
HLSL
: 由微软开发,主要用于DirectX
平台的图形APICg
: 由Nvidia
开发,可被编译为GLSL
和HLSL
,主要被用于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); }
|
上面代码定义了一个名为color
的3
维变量,将它的值设置为红色,红色的RGB
颜色值为(255,0,0)
,在GLSL
中,需要先将颜色原先的值进行归一化操作(除以255
)后才能将它正确地输出,因此将红色的值归一化后就得到了(1,0,0)
这个值,将它转换为3
维变量vec3(1.,0.,0.)
赋给color
变量
最后给输出颜色fragColor
赋值一个4
维变量,前3
维就是color
这个颜色变量,最后一维是透明度,由于纯红色并不透明,直接将其设为1
即可
当然也可以通过判断像素坐标范围,给不同区域填充不同的颜色
这里就要用到fragCoord
变量,它代表了输入的像素坐标,有2
个维度xy
,它们的大小取决于画面本身的大小。假设画面当前的大小为1536x864
,那么每一个像素的fragCoord
的x
坐标值将会分布在(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
的核心知识
变量
- 一维变量(标量): 比如
int
,float
,bool
。浮点型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; d.y = 2.0;
vec4 c = vec4(1.0, 0.5, 0.0, 1.0); vec3 e = c.yxy; e.zx = vec2(1.0);
|
- 矩阵变量: 矩阵用于线性变换,
mat2
类型代表了一个大小是2x2
的矩阵,mat3
类型则代表了一个3x3
的矩阵、mat4
类型是4x4
的矩阵
mat2 m1 = mat2(1.0, 0.0, 0.0, 1.0); mat3 m2 = mat3(1.0, 2.0, 0.0, 0.0, 1.0, 1.0, 0.0, 2.0, 1.0);
|
- 结构体: 如果想把多个变量捆绑到一个变量上,可以使用结构体
struct
struct Ray { vec3 ro; vec3 rd; };
Ray ray = Ray(vec3(0.0), vec3(1.0, 0.0, 0.0));
|
运算符
vec2 v = vec2(1.0, 2.0); v += 1.0;
|
控制流
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
中常用的变量限定符有以下几种,uniform
、const
、varying
、attribute
uniform vec3 uColor; varying vec3 vColor; attribute vec3 aPosition;
|
uniform
: 全局变量,一旦定义后会同时存在于顶点着色器与片元着色器中,并且它在每一个顶点和片元上的值都是相同的,是一个“统一”的值const
: 定义常量,它是无法被改变的一个值iTime
: 表示Shader
从开始到现在执行所经过的时间,用于创作动画效果iResolution
: 表示Shader
所在画布的大小,默认是占满整个屏幕iMouse
: 表示用户鼠标当前所在的位置
宏定义
宏(macros
)是一种预处理指令,用于在编译时进行文本的替换,常用于定义常量、函数、条件编译等
- 宏定义的格式是
#define 宏的名称 宏的值
,语句结尾没有分号。下面代码定义了一个名为PI
的宏,Shader
编译时会将所有的PI
替换为3.14159265359
这个值
- 宏也可以带有参数。下面代码定义了一个名为
add
的宏,接受2
个参数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
坐标用于描述画布上的归一化像素位置,通常由片元着色器中的fragCoord
和iResolution
计算得出,下面是归一化公式
void mainImage(out vec4 fragColor, in vec2 fragCoord) { vec2 uv = fragCoord / iResolution.xy; }
|
UV
坐标的分布特性
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)
之间的值。从整体上看,得到了一个横向的渐变图案
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)
之间的值。从整体上看,得到了一个纵向的渐变图案
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
坐标的值并不会自动地适应画布的比例,导致了图形被拉伸这一现象
- 为了修正这一点,需要计算画布的比例,将画布长除以画布宽就能算出,再将
UV
的x
坐标与比例相乘即可
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
函数,也被称作“阶梯函数”,是因为它的图像是阶梯的形状
- 它接受
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
函数要多一个,可以将它的边界值定为edge1
和edge2
。如果目标值x
小于边界值edge1
,则返回0
。如果目标值x
大于边界值edge2
,则返回1
。如果目标值x
在2
个边界值之间,则返回从0
到1
平滑过渡的值 - 把之前代码里的
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)
时,输出值都大于1
,Shader
中比1
大的值输出的还是白色,因此能看到中间的白色圆形部分。输入值位于(.25,1.)
时,输出的值开始变成了比1
小的值,而且是逐渐变化的,因此会产生一种渐变的效果
目前光的辐射范围太大了,要稍微缩小一些
再来认识一个新的内置函数——pow
函数,它用于计算数字的指数幂,比如pow(4.,3.)
,返回的值就是4
的3
次方——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
),调用它获取距离,再用step
或smoothstep
函数勾画出图形即可
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
变换操作
平移
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); }
|
缩放
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
),但由于画布的整体范围也变大了,矩形在整个画布中的相对比例缩小了。因此,矩形看起来变小了。简单来说,矩形的“绝对大小”没有改变,但它在画布中的“相对大小”变小了 - 同样地,如果要符合正方向的缩放(扩大),把之前的乘法操作改成与其相反的除法操作即可
翻转
附录