GLSL教程4-多个 2D 形状和混合

在本文中,我想讨论一种更好的绘制 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 轴上每个像素的插值。通过在红色、绿色和蓝色通道上使用相同的值,我们得到了从黑色到白色的渐变,中间有灰色阴影。

image.png

我们还可以使用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);
}
复制代码

image.png 这将产生介于紫色和青色之间的冷色渐变。

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函数用作“切换”,根据第三个参数的值在形状颜色或背景颜色之间切换。

image.png

以这种方式使用 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 形状渲染到场景中!

image.png

自定义背景和多个 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);
}
复制代码

image.png

结论

在本课中,我们学习了如何使用该mix函数创建颜色渐变,以及如何使用它在彼此之上或在背景层之上渲染形状。在下一课中,我将讨论我们可以绘制的其他 2D 形状,例如心形和星星。

Guess you like

Origin juejin.im/post/7055857056720355359