在本教程中,我将讨论如何使用 2D SDF 操作从原始形状创建更复杂的形状,并将讨论如何绘制更原始的 2D 形状,包括心形和星星。我将帮助您使用2D SDF列表
我们可以新建一个shadertoy程序
组合 2D SDF 操作
在之前的教程中,我们已经了解了如何绘制原始 2D 形状,例如圆形和正方形,但是我们可以使用 2D SDF 操作通过将原始形状组合在一起来创建更复杂的形状。
让我们从一些简单的 2D 形状样板代码开始:
vec3 getBackgroundColor(vec2 uv) {
uv = uv * 0.5 + 0.5; // 调整uv区间 <-0.5,0.5> to <0.25,0.75>
vec3 gradientStartColor = vec3(1., 0., 1.);
vec3 gradientEndColor = vec3(0., 1., 1.);
return mix(gradientStartColor, gradientEndColor, uv.y); // 渐变从下到上
}
float sdCircle(vec2 uv, float r, vec2 offset) {
float x = uv.x - offset.x;
float y = uv.y - offset.y;
return length(vec2(x, y)) - r;
}
float sdSquare(vec2 uv, float size, vec2 offset) {
float x = uv.x - offset.x;
float y = uv.y - offset.y;
return max(abs(x), abs(y)) - size;
}
vec3 drawScene(vec2 uv) {
vec3 col = getBackgroundColor(uv);
float d1 = sdCircle(uv, 0.1, vec2(0., 0.));
float d2 = sdSquare(uv, 0.1, vec2(0.1, 0));
float res; // 结果
res = d1;
res = step(0., res); // 等同 > 0. ? 1. : 0.;
col = mix(vec3(1,0,0), col, res);
return col;
}
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = fragCoord/iResolution.xy; // <0, 1>
uv -= 0.5; // <-0.5,0.5>
uv.x *= iResolution.x/iResolution.y; // 固定纵横比
vec3 col = drawScene(uv);
fragColor = vec4(col,1.0); // 输出到屏幕
}
请注意我现在如何使用sdCircle函数名称而不是sdfCircle(在以前的教程中使用过)。Inigo Quilez 的网站通常在sd形状名称前面使用,但我用sdf来帮助明确这些是有符号距离场 (SDF)。
运行代码时,您应该会看到一个带有渐变背景颜色的红色圆圈,类似于我们在第四章中学到的内容。
注意我们使用mix函数的地方:
col = mix(vec3(1,0,0), col, res);
该行表示获取结果并根据res(结果)的值选择红色或col(当前背景颜色)的值。
现在,让我们讨论可以执行的各种 SDF 操作。我们将看看圆形和正方形之间的相互作用。
vec3 drawScene(vec2 uv) {
vec3 col = getBackgroundColor(uv);
float d1 = sdCircle(uv, 0.1, vec2(0., 0.));
float d2 = sdSquare(uv, 0.1, vec2(0.1, 0));
float res; // 结果
res = min(d1, d2); //将两个形状组合在一起
res = step(0., res); // 等同 > 0. ? 1. : 0.;
col = mix(vec3(1,0,0), col, res);
return col;
}
vec3 drawScene(vec2 uv) {
vec3 col = getBackgroundColor(uv);
float d1 = sdCircle(uv, 0.1, vec2(0., 0.));
float d2 = sdSquare(uv, 0.1, vec2(0.1, 0));
float res; // 结果
res = max(d1, d2); // 只取两个形状相交的部分
res = step(0., res); // 等同 > 0. ? 1. : 0.;
col = mix(vec3(1,0,0), col, res);
return col;
}
vec3 drawScene(vec2 uv) {
vec3 col = getBackgroundColor(uv);
float d1 = sdCircle(uv, 0.1, vec2(0., 0.));
float d2 = sdSquare(uv, 0.1, vec2(0.1, 0));
float res; // 结果
res = max(-d1, d2); // 从 d2 中减去 d1
res = step(0., res); // 等同 > 0. ? 1. : 0.;
col = mix(vec3(1,0,0), col, res);
return col;
}
vec3 drawScene(vec2 uv) {
vec3 col = getBackgroundColor(uv);
float d1 = sdCircle(uv, 0.1, vec2(0., 0.));
float d2 = sdSquare(uv, 0.1, vec2(0.1, 0));
float res; // 结果
res = max(d1, -d2); // 从 d1 中减去 d2
res = step(0., res); // 等同 > 0. ? 1. : 0.;
col = mix(vec3(1,0,0), col, res);
return col;
}
vec3 drawScene(vec2 uv) {
vec3 col = getBackgroundColor(uv);
float d1 = sdCircle(uv, 0.1, vec2(0., 0.));
float d2 = sdSquare(uv, 0.1, vec2(0.1, 0));
float res; // 结果
res = max(min(d1, d2), -max(d1, d2)); // 异或运算将取两个形状中不相交的部分
res = step(0., res); // 等同 > 0. ? 1. : 0.;
col = mix(vec3(1,0,0), col, res);
return col;
}
我们还可以创建“平滑”的 2D SDF 操作,平滑地融合形状相交处的边缘。当我讨论 3D 形状时,您会发现这些操作更适用,但它们也适用于 2D!
将以下函数添加到代码顶部:
// 平滑最小
float smin(float a, float b, float k) {
float h = clamp(0.5+0.5*(b-a)/k, 0.0, 1.0);
return mix(b, a, h) - k*h*(1.0-h);
}
// 平滑最大
float smax(float a, float b, float k) {
return -smin(-a, -b, k);
}
vec3 drawScene(vec2 uv) {
vec3 col = getBackgroundColor(uv);
float d1 = sdCircle(uv, 0.1, vec2(0., 0.));
float d2 = sdSquare(uv, 0.1, vec2(0.1, 0));
float res; // 结果
res = smin(d1, d2, 0.05); // 将两个形状组合在一起,但平滑地融合它们相遇的边缘
res = step(0., res); // 等同 > 0. ? 1. : 0.;
col = mix(vec3(1,0,0), col, res);
return col;
}
vec3 drawScene(vec2 uv) {
vec3 col = getBackgroundColor(uv);
float d1 = sdCircle(uv, 0.1, vec2(0., 0.));
float d2 = sdSquare(uv, 0.1, vec2(0.1, 0));
float res; // 结果
res = smax(d1, d2, 0.05); // 只取两个形状相交的两个部分,但平滑地融合它们相交的边缘
res = step(0., res); // 等同 > 0. ? 1. : 0.;
col = mix(vec3(1,0,0), col, res);
return col;
}
可以在下面找到完成的代码。取消注释您想要查看的任何组合 2D SDF 操作的行:
// 平滑最小
float smin(float a, float b, float k) {
float h = clamp(0.5+0.5*(b-a)/k, 0.0, 1.0);
return mix(b, a, h) - k*h*(1.0-h);
}
// 平滑最大
float smax(float a, float b, float k) {
return -smin(-a, -b, k);
}
vec3 getBackgroundColor(vec2 uv) {
uv = uv * 0.5 + 0.5; // 调整uv区间 <-0.5,0.5> to <0.25,0.75>
vec3 gradientStartColor = vec3(1., 0., 1.);
vec3 gradientEndColor = vec3(0., 1., 1.);
return mix(gradientStartColor, gradientEndColor, uv.y); // 渐变从下到上
}
float sdCircle(vec2 uv, float r, vec2 offset) {
float x = uv.x - offset.x;
float y = uv.y - offset.y;
return length(vec2(x, y)) - r;
}
float sdSquare(vec2 uv, float size, vec2 offset) {
float x = uv.x - offset.x;
float y = uv.y - offset.y;
return max(abs(x), abs(y)) - size;
}
vec3 drawScene(vec2 uv) {
vec3 col = getBackgroundColor(uv);
float d1 = sdCircle(uv, 0.1, vec2(0., 0.));
float d2 = sdSquare(uv, 0.1, vec2(0.1, 0));
float res; // 结果
res = d1;
//res = d2;
//res = min(d1, d2); // 合并
//res = max(d1, d2); // 相交
//res = max(-d1, d2); // 从 d2 中减去 d1
//res = max(d1, -d2); // 从 d1 中减去 d2
//res = max(min(d1, d2), -max(d1, d2)); // 异或运算将取两个形状中不相交的部分
//res = smin(d1, d2, 0.05); // 平滑合并
//res = smax(d1, d2, 0.05); // 平滑相交
res = step(0., res); // 等同 > 0. ? 1. : 0.;
col = mix(vec3(1,0,0), col, res);
return col;
}
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = fragCoord/iResolution.xy; // <0, 1>
uv -= 0.5; // <-0.5,0.5>
uv.x *= iResolution.x/iResolution.y; // 固定纵横比
vec3 col = drawScene(uv);
fragColor = vec4(col,1.0); // 输出到屏幕
}
位置二维 SDF 操作
3D SDF 页面描述了一组位置 3D SDF 操作,但我们也可以在 2D 中使用这些操作。稍后讨论 3D SDF 操作。在本教程中,我将介绍位置 2D SDF 操作,这些操作可以帮助我们在绘制 2D 形状时节省时间并提高性能。
如果您正在绘制对称场景,那么使用opSymX操作可能会很有用。此操作将使用您提供的 SDF 沿 x 轴创建一个重复的 2D 形状。如果我们在 vec2(0.2, 0) 的偏移量处画一个圆,那么将在 vec2(-0.2, 0) 处画一个等价的圆。
我们也可以沿 y 轴执行类似的操作。使用opSymY操作,如果我们在 vec2(0, 0.2) 的偏移处绘制一个圆,那么将在 vec2(0, -0.2) 处绘制一个等效的圆。
vec3 getBackgroundColor(vec2 uv) {
uv = uv * 0.5 + 0.5; // 调整uv区间 <-0.5,0.5> to <0.25,0.75>
vec3 gradientStartColor = vec3(1., 0., 1.);
vec3 gradientEndColor = vec3(0., 1., 1.);
return mix(gradientStartColor, gradientEndColor, uv.y); // 渐变从下到上
}
float sdCircle(vec2 uv, float r, vec2 offset) {
float x = uv.x - offset.x;
float y = uv.y - offset.y;
return length(vec2(x, y)) - r;
}
float opSymX(vec2 p, float r)
{
p.x = abs(p.x);
return sdCircle(p, r, vec2(0.2, 0));
}
vec3 drawScene(vec2 uv) {
vec3 col = getBackgroundColor(uv);
float res; // 结果
res = opSymX(uv, 0.1);
res = step(0., res);
col = mix(vec3(1,0,0), col, res);
return col;
}
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = fragCoord/iResolution.xy; // <0, 1>
uv -= 0.5; // <-0.5,0.5>
uv.x *= iResolution.x/iResolution.y; // 固定纵横比
vec3 col = drawScene(uv);
fragColor = vec4(col,1.0); // 输出到屏幕
}
我们也可以沿 y 轴执行类似的操作。使用opSymY操作,如果我们在 vec2(0, 0.2) 的偏移处绘制一个圆,那么将在 vec2(0, -0.2) 处绘制一个等效的圆。
float opSymY(vec2 p, float r)
{
p.y = abs(p.y);
return sdCircle(p, r, vec2(0, 0.2));
}
vec3 drawScene(vec2 uv) {
vec3 col = getBackgroundColor(uv);
float res; // 结果
res = opSymY(uv, 0.1);
res = step(0., res);
col = mix(vec3(1,0,0), col, res);
return col;
}
如果要沿两个轴而不是一个轴绘制圆,则可以使用opSymXY操作。这将沿 x 轴和 y 轴创建一个副本,从而产生四个圆圈。如果我们画一个偏移量为 vec2(0.2, 0) 的圆,那么会在 vec2(0.2, 0.2)、vec2(0.2, -0.2)、vec2(-0.2, -0.2) 和 vec2( -0.2,0.2)。
float opSymXY(vec2 p, float r)
{
p = abs(p);
return sdCircle(p, r, vec2(0.2));
}
vec3 drawScene(vec2 uv) {
vec3 col = getBackgroundColor(uv);
float res; // 结果
res = opSymXY(uv, 0.1);
res = step(0., res);
col = mix(vec3(1,0,0), col, res);
return col;
}
有时,您可能希望在一个或多个轴上创建无限数量的 2D 对象。您可以使用opRep操作沿您选择的轴重复圆圈。参数 c是一个向量,用于控制 2D 对象沿每个轴的间距。
float opRep(vec2 p, float r, vec2 c)
{
vec2 q = mod(p+0.5*c,c)-0.5*c;
return sdCircle(q, r, vec2(0));
}
vec3 drawScene(vec2 uv) {
vec3 col = getBackgroundColor(uv);
float res; // 结果
res = opRep(uv, 0.05, vec2(0.2, 0.2));
res = step(0., res);
col = mix(vec3(1,0,0), col, res);
return col;
}
如果您只想重复 2D 对象一定次数而不是无限量,您可以使用opRepLim操作。参数c现在是一个浮点值,并且仍然控制每个重复的 2D 对象之间的间距。参数l是一个向量,可让您控制形状应沿给定轴重复多少次。例如vec2(2, 2)的值将沿正负 x 轴和 y 轴绘制一个额外的圆。
float opRepLim(vec2 p, float r, float c, vec2 l)
{
vec2 q = p-c*clamp(round(p/c),-l,l);
return sdCircle(q, r, vec2(0));
}
vec3 drawScene(vec2 uv) {
vec3 col = getBackgroundColor(uv);
float res; // 结果
res = opRepLim(uv, 0.05, 0.15, vec2(2, 2));
res = step(0., res);
col = mix(vec3(1,0,0), col, res);
return col;
}
您还可以通过操纵p、uv 的值坐标并将其添加到从 SDF 返回的值来对 SDF 执行变形或扭曲。在opDisplace运算内部,您可以创建任何类型的数学运算来替换其值p然后将该结果添加到您从 SDF 返回的原始值中。
float opDisplace(vec2 p, float r)
{
float d1 = sdCircle(p, r, vec2(0));
float s = 0.5; // 比例因子
float d2 = sin(s * p.x * 1.8); // 一些任意值
return d1 + d2;
}
vec3 drawScene(vec2 uv) {
vec3 col = getBackgroundColor(uv);
float res; // 结果
res = opDisplace(uv, 0.1); // 有点像鸡蛋
res = step(0., res);
col = mix(vec3(1,0,0), col, res);
return col;
}
您可以在下面找到完成的代码。取消注释您想要查看的任何位置 2D SDF 操作的行。
vec3 getBackgroundColor(vec2 uv) {
uv = uv * 0.5 + 0.5; // 调整uv区间 <-0.5,0.5> to <0.25,0.75>
vec3 gradientStartColor = vec3(1., 0., 1.);
vec3 gradientEndColor = vec3(0., 1., 1.);
return mix(gradientStartColor, gradientEndColor, uv.y); // 渐变从下到上
}
float sdCircle(vec2 uv, float r, vec2 offset) {
float x = uv.x - offset.x;
float y = uv.y - offset.y;
return length(vec2(x, y)) - r;
}
float opSymX(vec2 p, float r)
{
p.x = abs(p.x);
return sdCircle(p, r, vec2(0.2, 0));
}
float opSymY(vec2 p, float r)
{
p.y = abs(p.y);
return sdCircle(p, r, vec2(0, 0.2));
}
float opSymXY(vec2 p, float r)
{
p = abs(p);
return sdCircle(p, r, vec2(0.2));
}
float opRep(vec2 p, float r, vec2 c)
{
vec2 q = mod(p+0.5*c,c)-0.5*c;
return sdCircle(q, r, vec2(0));
}
float opRepLim(vec2 p, float r, float c, vec2 l)
{
vec2 q = p-c*clamp(round(p/c),-l,l);
return sdCircle(q, r, vec2(0));
}
float opDisplace(vec2 p, float r)
{
float d1 = sdCircle(p, r, vec2(0));
float s = 0.5; // 比例因子
float d2 = sin(s * p.x * 1.8); // 一些任意值
return d1 + d2;
}
vec3 drawScene(vec2 uv) {
vec3 col = getBackgroundColor(uv);
float res; // 结果
res = opSymX(uv, 0.1);
//res = opSymY(uv, 0.1);
//res = opSymXY(uv, 0.1);
//res = opRep(uv, 0.05, vec2(0.2, 0.2));
//res = opRepLim(uv, 0.05, 0.15, vec2(2, 2));
//res = opDisplace(uv, 0.1);
res = step(0., res);
col = mix(vec3(1,0,0), col, res);
return col;
}
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = fragCoord/iResolution.xy; // <0, 1>
uv -= 0.5; // <-0.5,0.5>
uv.x *= iResolution.x/iResolution.y; // 固定纵横比
vec3 col = drawScene(uv);
fragColor = vec4(col,1.0); // 输出到屏幕
}
抗锯齿
如果要添加任何抗锯齿,则可以使用smoothstep函数来平滑形状的边缘。该函数接受三个参数,并在时在 0 和 1 之间smoothstep(edge0, edge1, x)执行Hermite 插值edge0 < x < edge1 。
edge0: 指定 Hermite 函数下边缘的值。
edge1: 指定 Hermite 函数上边缘的值。
x: 指定插值的源值。
t = clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0);
return t * t * (3.0 - 2.0 * t);
文档会说如果
edge0大于或等于edge1,那么该函数smoothstep将返回 的值undefined。但是,这是不正确的。smoothstep即使edge0大于,函数的结果仍然由 Hermite 插值函数确定edge1。
如果您仍然感到困惑,那么来自The Book of Shaders 的这个页面可能会帮助您可视化smoothstep功能。从本质上讲,它的行为类似于step带有一些额外步骤的函数
让我们将step函数替换为函数smoothstep,看看圆形和方形联合的结果如何表现
vec3 getBackgroundColor(vec2 uv) {
uv = uv * 0.5 + 0.5; // 调整uv区间 <-0.5,0.5> to <0.25,0.75>
vec3 gradientStartColor = vec3(1., 0., 1.);
vec3 gradientEndColor = vec3(0., 1., 1.);
return mix(gradientStartColor, gradientEndColor, uv.y); // 渐变从下到上
}
float sdCircle(vec2 uv, float r, vec2 offset) {
float x = uv.x - offset.x;
float y = uv.y - offset.y;
return length(vec2(x, y)) - r;
}
float sdSquare(vec2 uv, float size, vec2 offset) {
float x = uv.x - offset.x;
float y = uv.y - offset.y;
return max(abs(x), abs(y)) - size;
}
vec3 drawScene(vec2 uv) {
vec3 col = getBackgroundColor(uv);
float d1 = sdCircle(uv, 0.1, vec2(0., 0.));
float d2 = sdSquare(uv, 0.1, vec2(0.1, 0));
float res; // 结果
res = min(d1, d2); // 合并
res = smoothstep(0., 0.02, res); // 抗锯齿
col = mix(vec3(1,0,0), col, res);
return col;
}
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = fragCoord/iResolution.xy; // <0, 1>
uv -= 0.5; // <-0.5,0.5>
uv.x *= iResolution.x/iResolution.y; // 固定纵横比
vec3 col = drawScene(uv);
fragColor = vec4(col,1.0); // 输出到屏幕
}
我们最终得到一个边缘稍微模糊的形状
smoothstep功能可以帮助我们在颜色之间创建平滑的过渡,这对于实现抗锯齿很有用。您可能还会看到人们用smoothstep来创建发光物体或霓虹发光效果。它在着色器中经常使用
画一颗心❤️
在本节中,我将教你如何使用 Shadertoy 绘制一颗心。请记住,有多种风格的心。我将向您展示如何使用Wolfram MathWorld中的一个方程来创建一种特定的心型
如果我们想对这条心形曲线应用偏移量,那么我们需要在对它们进行任何类型的运算(例如求幂)之前从 x 分量和 y 分量中减去它。
s = x - offsetX
t = y - offsetY
(s^2 + t^2 - 1)^3 - s^2 * t^3 = 0
x = 图上的 x 坐标
y = 图上的 y 坐标
您可以使用我在Desmos上创建的图表来调整心形曲线上的偏移量。
现在,我们如何在 Shadertoy 中为心脏创建 SDF?我们简单地将方程的左侧 (LHS) 设置为等于距离 d。然后,这与我们在第 4 章中学习的过程相同。
float sdHeart(vec2 uv, float size, vec2 offset) {
float x = uv.x - offset.x;
float y = uv.y - offset.y;
float xx = x * x;
float yy = y * y;
float yyy = yy * y;
float group = xx + yy - size;
float d = group * group * group - xx * yyy;
return d;
}
vec3 drawScene(vec2 uv) {
vec3 col = vec3(1);
float heart = sdHeart(uv, 0.04, vec2(0));
col = mix(vec3(1, 0, 0), col, step(0., heart));
return col;
}
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = fragCoord/iResolution.xy; // <0, 1>
uv -= 0.5; // <-0.5,0.5>
uv.x *= iResolution.x/iResolution.y; // 固定纵横比
vec3 col = drawScene(uv);
// 输出到屏幕
fragColor = vec4(col,1.0);
}
理解 pow 函数
你可能想知道为什么我以sdHeart这种奇怪的方式创建了这个函数。为什么不使用我们可用的pow功能呢?pow(x,y)函数接受一个值x并将其提升到 y的幂。
如果您尝试使用pow功能,您会立即看到心脏的行为有多奇怪。
float sdHeart(vec2 uv, float size, vec2 offset) {
float x = uv.x - offset.x;
float y = uv.y - offset.y;
float group = pow(x,2.) + pow(y,2.) - size;
float d = pow(group,3.) - pow(x,2.) * pow(y,3.);
return d;
}
好吧,这看起来不对。那么为什么这个pow(x,y)函数的行为如此奇怪呢?如果您仔细查看此函数的文档,您会发现此函数undefined在x小于零或两者都x等于零且y小于或等于零时返回。
请记住pow函数的实现因编译器和硬件而异,因此在为 Shadertoy 之外的其他平台开发着色器时可能不会遇到此问题,或者您可能会遇到不同的问题。
x因为我们的坐标系设置为和具有负值y,所以我们有时会得到函数undefined的结果。pow在 Shadertoy 中,编译器将undefined在数学运算中使用这将导致混乱的结果。
我们可以通过使用颜色调试画布来试验不同算术运算的行为方式。让我们尝试添加一个数字undefined:
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = fragCoord/iResolution.xy; // <0, 1>
uv -= 0.5; // <-0.5,0.5>
vec3 col = vec3(pow(-0.5, 1.));
col += 0.5;
fragColor = vec4(col,1.0);
// 屏幕是灰色的,这意味着未定义被视为零
}
我们可以得出结论,undefined在算术运算中(尝试加减乘除)使用时将其视为零值。但是,这仍可能因编译器和图形硬件而异。因此,您需要注意如何pow在着色器代码中使用该函数。
如果你想对一个值求平方,一个常见的技巧是使用dot函数来计算向量与其自身之间的点积。这让我们可以将sdHeart函数重写为更简洁:
float sdHeart(vec2 uv, float size, vec2 offset) {
float x = uv.x - offset.x;
float y = uv.y - offset.y;
float group = dot(x,x) + dot(y,y) - size;
float d = group * dot(group, group) - dot(x,x) * dot(y,y) * y;
return d;
}
vec3 drawScene(vec2 uv) {
vec3 col = vec3(1);
float heart = sdHeart(uv, 0.04, vec2(0));
col = mix(vec3(1, 0, 0), col, step(0., heart));
return col;
}
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = fragCoord/iResolution.xy; // <0, 1>
uv -= 0.5; // <-0.5,0.5>
uv.x *= iResolution.x/iResolution.y; // 固定纵横比
vec3 col = drawScene(uv);
// 输出到屏幕
fragColor = vec4(col,1.0);
}
调用
dot(x,x)与对 的值求平方相同x,但您不必处理pow函数的麻烦。
使用 sdStar5 SDF
Inigo Quilez创建了许多 Shadertoy 开发人员使用的 2D SDF 和 3D SDF。在本节中,我将讨论如何使用他的2D SDF 列表
在使用 SDF 创建形状时,它们通常被称为“基元”,因为它们构成了用于创建更抽象形状的构建块。对于 2D,在画布上绘制形状非常简单,但当我们讨论 3D 形状时,它会变得更加复杂。
让我们用星星 SDF 练习,因为画星星总是很有趣。导航到 Inigo Quilez 的网站并向下滚动到名为“Star 5 - exact”的 SDF。它应该具有以下定义:
float sdStar5(in vec2 p, in float r, in float rf)
{
const vec2 k1 = vec2(0.809016994375, -0.587785252292);
const vec2 k2 = vec2(-k1.x,k1.y);
p.x = abs(p.x);
p -= 2.0*max(dot(k1,p),0.0)*k1;
p -= 2.0*max(dot(k2,p),0.0)*k2;
p.x = abs(p.x);
p.y -= r;
vec2 ba = rf*vec2(-k1.y,k1.x) - vec2(0,1);
float h = clamp( dot(p,ba)/dot(ba,ba), 0.0, r );
return length(p-ba*h) * sign(p.y*ba.x-p.x*ba.y);
}
不要担心函数中的in限定符。如果需要,您可以删除它们,因为in如果未指定,则为默认限定符。
让我们使用以下代码创建一个新的 Shadertoy 着色器:
float sdStar5(in vec2 p, in float r, in float rf)
{
const vec2 k1 = vec2(0.809016994375, -0.587785252292);
const vec2 k2 = vec2(-k1.x,k1.y);
p.x = abs(p.x);
p -= 2.0*max(dot(k1,p),0.0)*k1;
p -= 2.0*max(dot(k2,p),0.0)*k2;
p.x = abs(p.x);
p.y -= r;
vec2 ba = rf*vec2(-k1.y,k1.x) - vec2(0,1);
float h = clamp( dot(p,ba)/dot(ba,ba), 0.0, r );
return length(p-ba*h) * sign(p.y*ba.x-p.x*ba.y);
}
vec3 drawScene(vec2 uv) {
vec3 col = vec3(0);
float star = sdStar5(uv, 0.12, 0.45);
col = mix(vec3(1, 1, 0), col, step(0., star));
return col;
}
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = fragCoord/iResolution.xy; // <0, 1>
uv -= 0.5; // <-0.5,0.5>
uv.x *= iResolution.x/iResolution.y; // 固定纵横比
vec3 col = drawScene(uv);
// 输出到屏幕
fragColor = vec4(col,1.0);
}
当我们运行这段代码时,你应该可以看到一颗明亮的黄色星星
但是缺少一件事。我们需要通过稍微移动 UV 坐标在函数的开头添加一个偏移量。我们可以添加一个新参数offset,我们可以从向量p中减去这个偏移量,它表示我们传递给这个函数的 UV 坐标。
我们完成的代码应该是这样的:
float sdStar5(in vec2 p, in float r, in float rf, vec2 offset)
{
p -= offset; // 这将从 p.x 中减去 offset.x 并从 p.y 中减去 offset.y
const vec2 k1 = vec2(0.809016994375, -0.587785252292);
const vec2 k2 = vec2(-k1.x,k1.y);
p.x = abs(p.x);
p -= 2.0*max(dot(k1,p),0.0)*k1;
p -= 2.0*max(dot(k2,p),0.0)*k2;
p.x = abs(p.x);
p.y -= r;
vec2 ba = rf*vec2(-k1.y,k1.x) - vec2(0,1);
float h = clamp( dot(p,ba)/dot(ba,ba), 0.0, r );
return length(p-ba*h) * sign(p.y*ba.x-p.x*ba.y);
}
vec3 drawScene(vec2 uv) {
vec3 col = vec3(0);
float star = sdStar5(uv, 0.12, 0.45, vec2(0.2, 0)); // 添加偏移量以移动星星的位置
col = mix(vec3(1, 1, 0), col, step(0., star));
return col;
}
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = fragCoord/iResolution.xy; // <0, 1>
uv -= 0.5; // <-0.5,0.5>
uv.x *= iResolution.x/iResolution.y; // 固定纵横比
vec3 col = drawScene(uv);
// 输出到屏幕
fragColor = vec4(col,1.0);
}
使用 sdBox SDF
绘制框/矩形很常见,因此我们将选择标题为“Box - exact”的 SDF。它有以下定义:
float sdBox( in vec2 p, in vec2 b )
{
vec2 d = abs(p)-b;
return length(max(d,0.0)) + min(max(d.x,d.y),0.0);
}
我们将在函数声明中添加一个偏移参数。
float sdBox( in vec2 p, in vec2 b, vec2 offset )
{
p -= offset;
vec2 d = abs(p)-b;
return length(max(d,0.0)) + min(max(d.x,d.y),0.0);
}
现在,我们应该能够毫无问题地渲染 box 和 star:
float sdBox( in vec2 p, in vec2 b, vec2 offset )
{
p -= offset;
vec2 d = abs(p)-b;
return length(max(d,0.0)) + min(max(d.x,d.y),0.0);
}
float sdStar5(in vec2 p, in float r, in float rf, vec2 offset)
{
p -= offset; // 这将从 p.x 中减去 offset.x 并从 p.y 中减去 offset.y
const vec2 k1 = vec2(0.809016994375, -0.587785252292);
const vec2 k2 = vec2(-k1.x,k1.y);
p.x = abs(p.x);
p -= 2.0*max(dot(k1,p),0.0)*k1;
p -= 2.0*max(dot(k2,p),0.0)*k2;
p.x = abs(p.x);
p.y -= r;
vec2 ba = rf*vec2(-k1.y,k1.x) - vec2(0,1);
float h = clamp( dot(p,ba)/dot(ba,ba), 0.0, r );
return length(p-ba*h) * sign(p.y*ba.x-p.x*ba.y);
}
vec3 drawScene(vec2 uv) {
vec3 col = vec3(0);
float box = sdBox(uv, vec2(0.2, 0.1), vec2(-0.2, 0));
float star = sdStar5(uv, 0.12, 0.45, vec2(0.2, 0));
col = mix(vec3(1, 1, 0), col, step(0., star));
col = mix(vec3(0, 0, 1), col, step(0., box));
return col;
}
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = fragCoord/iResolution.xy; // <0, 1>
uv -= 0.5; // <-0.5,0.5>
uv.x *= iResolution.x/iResolution.y; // 固定纵横比
vec3 col = drawScene(uv);
// 输出到屏幕
fragColor = vec4(col,1.0);
}
只需进行一些小的调整,我们就可以从 Inigo Quilez 的网站上挑选许多 2D SDF,并将它们绘制到带有偏移的画布上。
但是请注意,一些 SDF 需要在他的3D SDF页面上定义的函数:
float dot2( in vec2 v ) { return dot(v,v); }
float dot2( in vec3 v ) { return dot(v,v); }
float ndot( in vec2 a, in vec2 b ) { return a.x*b.x - a.y*b.y; }
使用 sdSegment SDF
Inigo Quilez网站上的一些 2D SDF用于分段或曲线,因此我们可能需要稍微改变我们的方法。让我们看一下名为“Segment - exact”的 SDF。它有以下定义:
float sdSegment( in vec2 p, in vec2 a, in vec2 b )
{
vec2 pa = p-a, ba = b-a;
float h = clamp( dot(pa,ba)/dot(ba,ba), 0.0, 1.0 );
return length( pa - ba*h );
}
让我们尝试使用这个 SDF,看看会发生什么。
float sdSegment( in vec2 p, in vec2 a, in vec2 b )
{
vec2 pa = p-a, ba = b-a;
float h = clamp( dot(pa,ba)/dot(ba,ba), 0.0, 1.0 );
return length( pa - ba*h );
}
vec3 drawScene(vec2 uv) {
vec3 col = vec3(0);
float segment = sdSegment(uv, vec2(0, 0), vec2(0, 0.2));
col = mix(vec3(1, 1, 1), col, step(0., segment - 0.02)); // 从返回的段的“有符号距离”值中减去 0.02
return col;
}
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = fragCoord/iResolution.xy; // <0, 1>
uv -= 0.5; // <-0.5,0.5>
uv.x *= iResolution.x/iResolution.y; // 固定纵横比
vec3 col = drawScene(uv);
// 输出到屏幕
fragColor = vec4(col,1.0);
}
现在,我们可以看到我们的片段出现了!它从坐标 (0, 0) 开始,到 (0, 0.2) 结束。在对函数的调用中使用输入向量a和b,sdSegment以移动片段并以不同的方式拉伸它。如果要使分段更细或更宽,可以用另一个数字替换。
您还可以使用smoothstep功能使片段的边缘看起来模糊。
float sdSegment( in vec2 p, in vec2 a, in vec2 b )
{
vec2 pa = p-a, ba = b-a;
float h = clamp( dot(pa,ba)/dot(ba,ba), 0.0, 1.0 );
return length( pa - ba*h );
}
vec3 drawScene(vec2 uv) {
vec3 col = vec3(0);
float segment = sdSegment(uv, vec2(0, 0), vec2(0, .2));
col = mix(vec3(1, 1, 1), col, smoothstep(0., 0.02, segment));
return col;
}
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = fragCoord/iResolution.xy; // <0, 1>
uv -= 0.5; // <-0.5,0.5>
uv.x *= iResolution.x/iResolution.y; // 固定纵横比
vec3 col = drawScene(uv);
// 输出到屏幕
fragColor = vec4(col,1.0);
}
使用 sdBezier SDF
Inigo Quilez 的网站也有Bézier 曲线的 SDF 。更具体地说,他有一个二次贝塞尔曲线的 SDF。查找名为“Quadratic Bezier - exact”的 SDF。它有以下定义:
float sdBezier( in vec2 pos, in vec2 A, in vec2 B, in vec2 C )
{
vec2 a = B - A;
vec2 b = A - 2.0*B + C;
vec2 c = a * 2.0;
vec2 d = A - pos;
float kk = 1.0/dot(b,b);
float kx = kk * dot(a,b);
float ky = kk * (2.0*dot(a,a)+dot(d,b)) / 3.0;
float kz = kk * dot(d,a);
float res = 0.0;
float p = ky - kx*kx;
float p3 = p*p*p;
float q = kx*(2.0*kx*kx-3.0*ky) + kz;
float h = q*q + 4.0*p3;
if( h >= 0.0)
{
h = sqrt(h);
vec2 x = (vec2(h,-h)-q)/2.0;
vec2 uv = sign(x)*pow(abs(x), vec2(1.0/3.0));
float t = clamp( uv.x+uv.y-kx, 0.0, 1.0 );
res = dot2(d + (c + b*t)*t);
}
else
{
float z = sqrt(-p);
float v = acos( q/(p*z*2.0) ) / 3.0;
float m = cos(v);
float n = sin(v)*1.732050808;
vec3 t = clamp(vec3(m+m,-n-m,n-m)*z-kx,0.0,1.0);
res = min( dot2(d+(c+b*t.x)*t.x),
dot2(d+(c+b*t.y)*t.y) );
// 第三个根不能是最近的
// res = min(res,dot2(d+(c+b*t.z)*t.z));
}
return sqrt( res );
}
这是一个相当大的功能!请注意,此函数使用效用函数dot2. 这是在他的3D SDF页面上定义的。
float dot2( in vec2 v ) { return dot(v,v); }
二次贝塞尔曲线接受三个控制点。在 2D 中,每个控制点都是vec2具有 x 分量和 y 分量的值。您可以使用我在Desmos上创建的图表来调整控制点。
与sdSegment 一样,我们必须从返回的“有符号距离”中减去一个小值才能正确查看曲线。让我们看看如何使用 GLSL 代码绘制二次贝塞尔曲线:
试着调试控制点!记住!您可以使用我的Desmos 图来提供帮助!
您可以将 2D 操作与 Bézier 曲线一起使用来创建有趣的效果。我们可以从一个圆中减去两条贝塞尔曲线来得到某种网球。您可以探索使用提供给您的工具可以创建的所有内容!
您可以在下面找到用于制作网球的完成代码:
vec3 getBackgroundColor(vec2 uv) {
uv = uv * 0.5 + 0.5; // 调整uv区间 <-0.5,0.5> to <0.25,0.75>
vec3 gradientStartColor = vec3(1., 0., 1.);
vec3 gradientEndColor = vec3(0., 1., 1.);
return mix(gradientStartColor, gradientEndColor, uv.y); // 渐变从下到上
}
float sdCircle(vec2 uv, float r, vec2 offset) {
float x = uv.x - offset.x;
float y = uv.y - offset.y;
return length(vec2(x, y)) - r;
}
float dot2( in vec2 v ) { return dot(v,v); }
float sdBezier( in vec2 pos, in vec2 A, in vec2 B, in vec2 C )
{
vec2 a = B - A;
vec2 b = A - 2.0*B + C;
vec2 c = a * 2.0;
vec2 d = A - pos;
float kk = 1.0/dot(b,b);
float kx = kk * dot(a,b);
float ky = kk * (2.0*dot(a,a)+dot(d,b)) / 3.0;
float kz = kk * dot(d,a);
float res = 0.0;
float p = ky - kx*kx;
float p3 = p*p*p;
float q = kx*(2.0*kx*kx-3.0*ky) + kz;
float h = q*q + 4.0*p3;
if( h >= 0.0)
{
h = sqrt(h);
vec2 x = (vec2(h,-h)-q)/2.0;
vec2 uv = sign(x)*pow(abs(x), vec2(1.0/3.0));
float t = clamp( uv.x+uv.y-kx, 0.0, 1.0 );
res = dot2(d + (c + b*t)*t);
}
else
{
float z = sqrt(-p);
float v = acos( q/(p*z*2.0) ) / 3.0;
float m = cos(v);
float n = sin(v)*1.732050808;
vec3 t = clamp(vec3(m+m,-n-m,n-m)*z-kx,0.0,1.0);
res = min( dot2(d+(c+b*t.x)*t.x),
dot2(d+(c+b*t.y)*t.y) );
// 第三个根不能是最近的
// res = min(res,dot2(d+(c+b*t.z)*t.z));
}
return sqrt( res );
}
vec3 drawScene(vec2 uv) {
vec3 col = getBackgroundColor(uv);
float d1 = sdCircle(uv, 0.2, vec2(0., 0.));
vec2 A = vec2(-0.2, 0.2);
vec2 B = vec2(0, 0);
vec2 C = vec2(0.2, 0.2);
float d2 = sdBezier(uv, A, B, C) - 0.03;
float d3 = sdBezier(uv*vec2(1,-1), A, B, C) - 0.03;
float res; // 结果
res = max(d1, -d2); // 减法 - 从 d1 中减去 d2
res = max(res, -d3); // 减法 - 从结果中减去 d3
res = smoothstep(0., 0.01, res); // 抗锯齿整个结果
col = mix(vec3(.8,.9,.2), col, res);
return col;
}
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = fragCoord/iResolution.xy; // <0, 1>
uv -= 0.5; // <-0.5,0.5>
uv.x *= iResolution.x/iResolution.y; // 固定纵横比
vec3 col = drawScene(uv);
fragColor = vec4(col,1.0); // 输出到屏幕
}
结论
在本教程中,我们学习了如何通过绘制心形❤️和其他形状来表达对着色器的更多爱。我们学习了如何绘制星形、线段和二次贝塞尔曲线。当然,我使用 2D SDF 绘制形状的技术只是个人喜好。我们可以通过多种方式在画布上绘制 2D 形状。我们还学习了如何将原始形状组合在一起以创建更复杂的形状。在下一篇文章中,我们将开始学习如何使用 raymarching 绘制 3D 形状和场景!