实践
在我们绘制第一个 2D 形状之前,让我们用 Shadertoy 进行更多练习。创建一个新的着色器并将起始代码替换为以下内容:
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = fragCoord/iResolution.xy; // <0,1>
vec3 col = vec3(0); // 从黑色开始
if (uv.x > .5) col = vec3(1); // 将画布的右半部分设为白色
// 输出到屏幕
fragColor = vec4(col,1.0);
}
由于我们的着色器在所有像素上并行运行,因此我们必须依靠if
语句根据像素在屏幕上的位置绘制不同颜色的像素。根据您的显卡和用于着色器代码的编译器,使用诸如step之类的内置函数可能会更高效。
让我们看一下相同的示例,但改用该step
函数:
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = fragCoord/iResolution.xy; // <0,1>
vec3 col = vec3(0); // 从黑色开始
col = vec3(step(0.5, uv.x)); // 将画布的右半部分设为白色
// 输出到屏幕
fragColor = vec4(col,1.0);
}
画布的左半部分为黑色,画布的右半部分为白色。
阶跃函数接受两个输入:阶跃函数的边缘和用于生成阶跃函数的值。如果函数参数中的第二个参数大于第一个参数,则返回值 1。否则,返回零值。
您也可以在向量中的每个分量上执行阶跃函数:
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = fragCoord/iResolution.xy; // <0,1>
vec3 col = vec3(0); // 从黑色开始
col = vec3(step(0.5, uv), 0); // 跨 uv 的 x 分量和 y 分量执行阶跃函数
// 输出到屏幕
fragColor = vec4(col,1.0);
}
由于 step 函数同时作用于画布的 X 分量和 Y 分量,您应该看到画布被分成四种颜色。
如何画圆圈
圆的方程由以下定义:
x^2 + y^2 = r^2
x = x-coordinate on graph
y = y-coordinate on graph
r = radius of circle
我们可以重新排列变量以使方程等于零:
x^2 + y^2 - r^2 = 0
要在图表上对此进行可视化,您可以使用Desmos 计算器绘制以下图表:
x^2 + y^2 - 4 = 0
如果您复制上面的代码片段并将其粘贴到 Desmos 计算器中,那么您应该会看到一个半径为 2 的圆图。圆心位于坐标 (0, 0) 处。
在 Shadertoy 中,我们可以使用该等式的左侧 (LHS) 来制作一个圆。让我们创建一个名为的函数,该函数sdfCircle
返回 XY 坐标上每个像素的颜色白色,使得方程大于零,否则颜色为蓝色。
函数的sdf
一部分是指一个叫做有符号距离函数(SDF)的概念,也就是有符号距离场。在 3D 中绘制时使用 SDF 更为常见,但我也会将这个术语用于 2D 形状。
我们将在函数中调用我们的新函数mainImage
来使用它。
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>
vec3 col = sdfCircle(uv, .2); // 在每个像素上调用此函数以检查坐标是在圆内还是在圆外
// 输出到屏幕
fragColor = vec4(col,1.0);
}
如果您想知道为什么我使用0.
而不是简单地0
不使用小数,那是因为在整数末尾添加小数会使其具有类型 float
而不是int
. 当您使用需要类型为 的数字的函数时float
,将小数放在整数末尾是满足编译器要求的最简单方法。
我们使用半径是0.2
因为我们的坐标系设置为仅具有介于 0 和 1 之间的 UV 值。当您运行代码时,您会注意到出现了一些错误。
画布的左下角似乎有四分之一的蓝点。为什么?因为我们的坐标系当前设置为原点位于左下角。我们需要将每个值平移,0.5
以获得位于画布中心的坐标系原点。
0.5
从 UV 坐标中减去:
vec2 uv = fragCoord/iResolution.xy; // <0,1>
uv -= 0.5; // <-0.5, 0.5>
现在范围在 x 轴和 y 轴之间-0.5
,0.5
这意味着坐标系的原点在画布的中心。然而,我们面临另一个问题......
我们的圆看起来有点拉长,所以它看起来更像一个椭圆。这是由画布的纵横比引起的。当画布的宽度和高度不匹配时,圆圈会显得拉伸。我们可以通过将 UV 坐标的 X 分量乘以画布的纵横比来解决这个问题。
vec2 uv = fragCoord/iResolution.xy; // <0,1>
uv -= 0.5; // <-0.5, 0.5>
uv.x *= iResolution.x/iResolution.y; // 固定纵横比
这意味着 X 分量不再介于-0.5
和之间0.5
。它将在与画布的纵横比成比例的值之间变化,这将由浏览器或网页的宽度决定(如果您使用 Chrome DevTools 之类的东西来改变宽度)。
您完成的代码应如下所示:
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);
}
运行代码后,您应该会看到一个完全成比例的蓝色圆圈
我们可以从中获得一些乐趣!我们可以使用全局iTime
变量随时间改变颜色。通过使用余弦 (cos) 函数,我们可以在同一组颜色中一遍又一遍地循环。由于余弦函数在 -1 和 1 之间振荡,我们需要将此范围调整为介于 0 和 1 之间的-1
值1
。
请记住,最终片段颜色中小于零的任何颜色值都将自动被限制为零。同样,任何大于 1 的颜色值都将被限制为 1。通过调整范围,我们可以获得更广泛的颜色。
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(0.) : 0.5 + 0.5 * cos(iTime + uv.xyx + vec3(0,2,4));
}
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);
}
运行代码后,您应该会看到圆圈在各种颜色之间变化。
我们可以使用变量的分量创建新向量。让我们看一个例子。
vec3 col = vec3(0.2, 0.4, 0.6);
vec3 col2 = col.xyx;
vec3 col3 = vec3(0.2, 0.4, 0.2);
在上面的代码片段中,col2
和col3
是相同的。
移动圆圈
要移动圆,我们需要对圆等式内的 XY 坐标应用偏移量。因此,我们的等式将如下所示:
(x - offsetX)^2 + (y - offsetY)^2 - r^2 = 0
x = x-coordinate on graph
y = y-coordinate on graph
r = radius of circle
offsetX = how much to move the center of the circle in the x-axis
offsetY = how much to move the center of the circle in the y-axis
您可以通过复制和粘贴以下代码再次在Desmos 计算器中进行试验:
(x - 2)^2 + (y - 2)^2 - 4 = 0
在 Shadertoy 中,我们可以调整sdfCircle
函数以允许偏移,然后将圆心移动0.2
.
vec3 sdfCircle(vec2 uv, float r, vec2 offset) {
float x = uv.x - offset.x;
float y = uv.y - offset.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; // 固定纵横比
vec2 offset = vec2(0.2, 0.2); // 将圆圈向右移动 0.2 个单位,向上移动 0.2 个单位
vec3 col = sdfCircle(uv, .2, offset);
// 输出到屏幕
fragColor = vec4(col,1.0);
}
您可以再次iTime
在某些地方使用全局变量来为画布赋予生命并为您的圆圈设置动画。
vec3 sdfCircle(vec2 uv, float r, vec2 offset) {
float x = uv.x - offset.x;
float y = uv.y - offset.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; // 固定纵横比
vec2 offset = vec2(sin(iTime*2.)*0.2, cos(iTime*2.)*0.2); // 顺时针移动圆圈
vec3 col = sdfCircle(uv, .2, offset);
// 输出到屏幕
fragColor = vec4(col,1.0);
}
上面的代码将沿顺时针方向沿圆形路径移动圆,就好像它围绕原点旋转一样。通过乘以iTime
一个值,您可以加快动画速度。通过将正弦或余弦函数的输出乘以一个值,您可以控制圆从画布中心移动的距离。您将大量使用正弦和余弦函数,iTime
因为它们会产生振荡。
结论
在本课中,我们学习了如何固定画布的坐标系、绘制圆以及沿圆形路径为圆设置动画。 在下一课中,我将向您展示如何在屏幕上绘制一个正方形。然后,我们将学习如何旋转它