在本文中,我想讨论一种更好的绘制 2D 形状的方法,这样我们就可以轻松地将多个形状添加到画布上。我们还将学习如何独立于形状颜色更改背景颜色。
混合功能
在继续之前,让我们看一下混合功能。当我们将多个 2D 形状渲染到场景中时,此功能对我们特别有用。
该mix
函数在两个值之间进行线性插值。在其他着色器语言(例如HLSL)中,此函数被称为lerp。
函数 的线性插值mix(x, y, a)
基于以下公式:
x * (1 - a) + y * a
x = 第一个值
y = 第二个值
a = 在 x 和 y 之间线性插值的值
复制代码
将第三个参数a
视为一个滑块,可让您在x
和之间选择值y
。
您将看到mix
是着色器中大量使用的函数。这是创建颜色渐变的好方法。让我们看一个例子:
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = fragCoord/iResolution.xy; // <0, 1>
float interpolatedValue = mix(0., 1., uv.x);
vec3 col = vec3(interpolatedValue);
// 输出到屏幕
fragColor = vec4(col,1.0);
}
复制代码
在上面的代码中,我们使用该mix
函数来获取屏幕上 x 轴上每个像素的插值。通过在红色、绿色和蓝色通道上使用相同的值,我们得到了从黑色到白色的渐变,中间有灰色阴影。
我们还可以使用mix
沿 y 轴的函数:
float interpolatedValue = mix(0., 1., uv.y);
复制代码
利用这些知识,我们可以在像素着色器中创建彩色渐变。让我们定义一个专门用于设置画布背景颜色的函数。
vec3 getBackgroundColor(vec2 uv) {
uv += 0.5; // 调整uv区间 <-0.5,0.5> to <0,1>
vec3 gradientStartColor = vec3(1., 0., 1.);
vec3 gradientEndColor = vec3(0., 1., 1.);
return mix(gradientStartColor, gradientEndColor, uv.y); // 渐变从下到上
}
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 = getBackgroundColor(uv);
// 输出到屏幕
fragColor = vec4(col,1.0);
}
复制代码
这将产生介于紫色和青色之间的冷色渐变。
mix
在向量上使用该函数时,它将使用第三个参数以分量为基础对每个向量进行插值。它将运行gradientStartColor
向量的红色分量(或 x 分量)和向量的红色分量的插值器函数gradientEndColor
。相同的策略将应用于每个向量的绿色(y 分量)和蓝色(z 分量)通道。
我们调整了uv
的值,因为在大多数情况下,我们将使用uv
介于负数和正数之间的值。如果我们将负值传递给fragColor
,那么它将被钳制为零。为了在整个范围内显示颜色,我们将范围从负值移开。
绘制 2D 形状的另一种方法
在之前的教程中,我们学习了如何使用 2D SDF 创建 2D 形状,例如圆形sdfCircle
和正方形sdfSquare
。但是,函数以向量vec3
的形式返回颜色。
通常,SDF 返回 float
而不是vec3
。请记住,“SDF”是“有符号距离场”的首字母缩写词。因此,我们希望它们返回一个距离类型float
。在 3D SDF 中,这通常是正确的,但在 2D SDF 中,我发现返回 1 或 0 更有用,具体取决于像素是在形状内部还是在形状外部,我们稍后会看到。
距离是相对于某个点的,通常是形状的中心。如果一个圆的中心在原点 (0, 0),那么我们知道圆的边缘上的任何点都等于圆的半径,因此等式:
x^2 + y^2 = r^2
或者,重新排列他们
x^2 + y^2 - r^2 = 0
x^2 + y^2 - r^2 = distance = d
复制代码
如果距离大于零,那么我们知道我们在圆外。如果距离小于零,那么我们在圆内。如果距离正好等于零,那么我们就在圆的边缘。这就是“有符号距离场”的“有符号”部分发挥作用的地方。距离可以是负数或正数,具体取决于像素坐标是在形状内部还是外部。
在本教程系列的第二章中,我们使用以下代码绘制了一个蓝色圆圈:
vec3 sdfCircle(vec2 uv, float r) {
float x = uv.x;
float y = uv.y;
float d = length(vec2(x, y)) - r;
return d > 0. ? vec3(1.) : vec3(0., 0., 1.);
// 如果在形状之外绘制背景颜色
// 如果在形状内,则绘制圆形颜色
}
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = fragCoord/iResolution.xy; // <0,1>
uv -= 0.5;
uv.x *= iResolution.x/iResolution.y; // 固定纵横比
vec3 col = sdfCircle(uv, .2);
// 输出到屏幕
fragColor = vec4(col,1.0);
}
复制代码
这种方法的问题是我们不得不用蓝色绘制一个圆,用白色绘制一个背景。
我们需要让代码更抽象一点,这样我们就可以相互独立地绘制背景和形状颜色。这将允许我们在场景中绘制多个形状,并为每个形状和背景选择我们想要的任何颜色。
让我们看一下绘制蓝色圆圈的另一种方法:
float sdfCircle(vec2 uv, float r, vec2 offset) {
float x = uv.x - offset.x;
float y = uv.y - offset.y;
return length(vec2(x, y)) - r;
}
vec3 drawScene(vec2 uv) {
vec3 col = vec3(1);
float circle = sdfCircle(uv, 0.1, vec2(0, 0));
col = mix(vec3(0, 0, 1), col, step(0., circle));
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);
}
复制代码
在上面的代码中,我们现在抽象出一些东西。我们有一个drawScene
负责渲染场景的函数,sdfCircle
现在返回一个float
代表屏幕上一个像素和圆上一个点之间的“有符号距离”的函数。 第二章中了解了阶跃函数。它根据第二个参数的值返回值 1 或 0。事实上,以下是等价的:
float result = step(0., circle);
float result = circle > 0. ? 1. : 0.;
复制代码
如果“有符号距离”值大于零,则表示该点位于圆内。如果该值小于或等于 0,则该点位于圆的外部或边缘。
在drawScene
函数内部,我们使用mix
函数将白色背景颜色与蓝色混合。的值circle
将确定像素是白色(背景)还是蓝色(圆圈)。从这个意义上说,我们可以将该mix
函数用作“切换”,根据第三个参数的值在形状颜色或背景颜色之间切换。
以这种方式使用 SDF 基本上可以让我们仅在像素位于形状内的坐标处时才绘制形状。否则,它应该绘制之前的颜色。
让我们添加一个稍微偏离中心的正方形。
float sdfCircle(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 sdfSquare(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 = vec3(1);
float circle = sdfCircle(uv, 0.1, vec2(0, 0));
float square = sdfSquare(uv, 0.07, vec2(0.1, 0));
col = mix(vec3(0, 0, 1), col, step(0., circle));
col = mix(vec3(1, 0, 0), col, step(0., square));
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);
}
复制代码
通过这种方法使用该mix
函数可以让我们轻松地将多个 2D 形状渲染到场景中!
自定义背景和多个 2D 形状
借助我们所学的知识,我们可以轻松自定义背景,同时保持形状颜色不变。让我们添加一个返回背景渐变颜色的函数,并在函数顶部使用它drawScene
。
vec3 getBackgroundColor(vec2 uv) {
uv += 0.5; // 调整uv区间 <-0.5,0.5> to <0,1>
vec3 gradientStartColor = vec3(1., 0., 1.);
vec3 gradientEndColor = vec3(0., 1., 1.);
return mix(gradientStartColor, gradientEndColor, uv.y); // 渐变从下到上
}
float sdfCircle(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 sdfSquare(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 circle = sdfCircle(uv, 0.1, vec2(0, 0));
float square = sdfSquare(uv, 0.07, vec2(0.1, 0));
col = mix(vec3(0, 0, 1), col, step(0., circle));
col = mix(vec3(1, 0, 0), col, step(0., square));
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);
}
复制代码
结论
在本课中,我们学习了如何使用该mix
函数创建颜色渐变,以及如何使用它在彼此之上或在背景层之上渲染形状。在下一课中,我将讨论我们可以绘制的其他 2D 形状,例如心形和星星。