前言
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平台的图形APIHLSL: 由微软开发,主要用于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),但由于画布的整体范围也变大了,矩形在整个画布中的相对比例缩小了。因此,矩形看起来变小了。简单来说,矩形的“绝对大小”没有改变,但它在画布中的“相对大小”变小了 - 同样地,如果要符合正方向的缩放(扩大),把之前的乘法操作改成与其相反的除法操作即可
翻转
附录