Note: This series blog was translated from Nathan Vaughn's Shaders Language Tutorial and has been authorized by the author. If reprinted or reposted, please be sure to mark the original link and description in the key position of the article after obtaining the author’s consent as well as the translator's. If the article is helpful to you, click this Donation link to buy the author a cup of coffee.

说明:该系列博文翻译自Nathan Vaughn着色器语言教程。文章已经获得作者翻译授权,如有转载请务必在取得作者译者同意之后在文章的重点位置标明原文链接以及说明。如果你觉得文章对你有帮助,点击此打赏链接请作者喝一杯咖啡。

更新提示:本篇博文于2021年5月3日被重新修改过。我用更加简洁的解决方案替换了很多代码片段绘制2D图形。

朋友们,你们好!在前面的一系列教程中,我们学会了如何使用Shadertoy在画布上绘制2D图形。在这节课中,我将会讨论一些其他更好的绘制2D图形的方法。这样我们就能更加方便地增加各种图形了。我们也将学到如何独立地为每个形状改变颜色。

Mix(混入) 函数

在继续学习之前,我们需要先看看mix函数。这个函数在2D场景中绘制多个图形会尤其重要。

mix函数会在两个值之间进行差值处理。在其他着的色器语言中,这个函数被命名为lerp。

线性差值函数,mix(x,y,z),它是基于以下公式:


x * (1 - a) + y * a
x = first value
y = second value
a = value that linearly interpolates between x and y

仔细思考第三个参数 a,它作为一个滑块器能够让你选择一个介于xy之间的值。

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); // Output to screen
fragColor = vec4(col,1.0);
}

上面的代码中,我们使用mix函数在屏幕上沿着x轴生产出了一个内插值。给红色,绿色和蓝色通道赋一个相同的值就会产生从黑到白的渐变效果,其中间地带为灰色。

我们也可以在y轴上使用此方法:

  float interpolatedValue = mix(0., 1., uv.y);

运用此知识,我们就在像素着色器中创建了一个渐变色。让我们定义一个特殊的方法设置背景色吧。

  vec3 getBackgroundColor(vec2 uv) {
uv += 0.5; // remap uv from <-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); // gradient goes from bottom to top
} 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; // fix aspect ratio vec3 col = getBackgroundColor(uv); // Output to screen
fragColor = vec4(col,1.0);
}

上面这段代码让我们得到了一个从紫色到青蓝色的渐变效果。

当我们对向量使用mix函数,它会根据第三个参数去给向量中的每个元素元素进行差值。它通过为红色元素进行gradientStartColor插值,然后又给gradientEndColor进行向量的插值。同样的策略会运用到绿色元素之上和蓝色元素之上。

我们给uv的值添加了0.5个单位,因为在大多数情况下,我们会使用到uv坐标范围是介于正负数之间。如果给fragColor传递一个负数值,它会变为0。

绘制2D形状的另一种方式

在之前的教程中,我们学会了如何使用2D符号距离场函数(SDF)创建2D形状,例如:圆形和正方形。sdfCirclesdfSquare函数的返回值的类型是一个vec3

但是,符号距离场函数返回值的类型的是一个浮点数而非vec3。记住SDF就是符号距离场的缩写,因此我们预期它们返回的就是一个浮点类型的距离。在3D符号距离场函数中,它通常是正确的,但是在2D符号距离场函数中,根据像素点是否在图形内而返回1或者0也许会对我们来说显得更有用,我们等下就能看到了。

距离是相对于某点来说的,尤其是形状的中心点。如果一个圆的中心点在(0, 0),那么在圆周长上的任何点到这个圆的的距离就是这个圆的半径,因此就有了以下的等式:

  x^2 + y^2 = r^2

  Or, when rearranged,
x^2 + y^2 - r^2 = 0 where x^2 + y^2 - r^2 = distance = d

如果距离大于0,则我们就知道它在圆之外,如果距离小于0,我就知道它在圆内。如果距离等于0,则它就正好在圆的边缘之上。这是就是符号距离场中(sign)符号的概念来源。距离可以是正数或者负数,取决于当前像素坐标是在圆内还是圆外。

在第二部分教程中,我们用下面的代码创建了一个蓝色的圆形:

  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.);
// draw background color if outside the shape
// draw circle color if inside the shape
} void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = fragCoord/iResolution.xy; // <0,1>
uv -= 0.5;
uv.x *= iResolution.x/iResolution.y; // fix aspect ratio vec3 col = sdfCircle(uv, .2); // Output to screen
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; // fix aspect ratio vec3 col = drawScene(uv); // Output to screen
fragColor = vec4(col,1.0);
}

以上代码,我们抽象出来了一些东西。我们有一个drawScene函数用来渲染场景,一个sdfCircle函数返回符号距离在屏幕上的像素和圆点上。

我们在第二章教程中使用了step函数,他返回了一个根据第二个参数的值介于1和0之间的值。实际上,下面的代码也是相等的:

  float result = step(0., circle);
float result = circle > 0. ? 1. : 0.;

在符号距离场函数当中,如果它大于0,意味着,所有的点在圆中,如果小于护着等于0,说明点在圆之外或者圆的边缘上。

drawScene函数时,我们使用mix函数混合了背景色白色和蓝色。circle返回的值会决定当前的像素是白色抑或蓝色。在此场景中,我们可以用mix函数作为toogle方法,在形状颜色和背景颜色中间来回的切换,只需要根据第三个参数的值即可。

使用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; // fix aspect ratio vec3 col = drawScene(uv); // Output to screen
fragColor = vec4(col,1.0);
}

使用mix函数,我们就可以轻易地在同一个场景中绘制出2D图形了。

自定义背景和多个2D图形

运用我们学习到的知识,我们就可以轻易的定制我们的背景颜色和形状。让我们添加一个返回渐变的函数吧,然后在drawScene函数的顶部调用:

  vec3 getBackgroundColor(vec2 uv) {
uv += 0.5; // remap uv from <-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); // gradient goes from bottom to top
} 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; // fix aspect ratio vec3 col = drawScene(uv); // Output to screen
fragColor = vec4(col,1.0);
}

是不是很神奇?

这个简单的由抽象的数字组成的作品是否能像non-fungible token一样赚钱呢。也许不行,但我们希望如此。

总结

本节课中,我们创建了一件电子艺术作品。我们学会了如何使用mix函数创建渐变色以及如何使用它去渲染图形。在下节课中,我们会讨论其他2D形状例如心形和星形。

资源

Shadertoy 教程 Part 4 - 绘制多个2D图形和混入的更多相关文章

  1. Shadertoy 教程 Part 5 - 运用SDF绘制出更多的2D图形

    Note: This series blog was translated from Nathan Vaughn's Shaders Language Tutorial and has been au ...

  2. Shadertoy 教程 Part 2 - 圆和动画

    Note: This series blog was translated from Nathan Vaughn's Shaders Language Tutorial and has been au ...

  3. Shadertoy 教程 Part 3 - 矩形和旋转

    Note: This series blog was translated from Nathan Vaughn's Shaders Language Tutorial and has been au ...

  4. iOS10 UI教程视图的绘制与视图控制器和视图

    iOS10 UI教程视图的绘制与视图控制器和视图 iOS10 UI视图的绘制 iOS10 UI教程视图的绘制与视图控制器和视图,在iOS中,有很多的绘图应用.这些应用大多是在UIView上进行绘制的. ...

  5. VB6 GDI+ 入门教程[4] 文字绘制

    http://vistaswx.com/blog/article/category/tutorial/page/2 VB6 GDI+ 入门教程[4] 文字绘制 2009 年 6 月 18 日 7条评论 ...

  6. emwin之2D图形绘制问题

    @2018-09-03 [问题] 在 WM_PAINT 消息分支里绘制2D图形可以正常显示,而在外部函数或按钮按下事件的响应消息分支下等处,绘制2D图形则不显示. [解决] 在除消息WM_PAINT分 ...

  7. WebGL简易教程(三):绘制一个三角形(缓冲区对象)

    目录 1. 概述 2. 示例:绘制三角形 1) HelloTriangle.html 2) HelloTriangle.js 3) 缓冲区对象 (1) 创建缓冲区对象(gl.createBuffer( ...

  8. 【STM32H7教程】第55章 STM32H7的图形加速器DMA2D的基础知识和HAL库API

    完整教程下载地址:http://www.armbbs.cn/forum.php?mod=viewthread&tid=86980 第55章       STM32H7的图形加速器DMA2D的基 ...

  9. 【Android】21.2 2D图形图像处理(Canvas和Paint)

    分类:C#.Android.VS2015: 创建日期:2016-03-19 一.Canvas对象简介 画布(Canvas对象)是与System.Drawing或iOS核心图形等传统框架非常类似的另一种 ...

随机推荐

  1. 如何获取PHP命令行参数

    使用 PHP 开发的同学多少都会接触过 CLI 命令行.经常会有一些定时任务或者一些脚本直接使用命令行处理会更加的方便,有些时候我们会需要像网页的 GET . POST 一样为这些命令行脚本提供参数. ...

  2. PHP中的对象比较

    在之前的文章中,我们讲过PHP中比较数组的时候发生了什么?.这次,我们来讲讲在对象比较的时候PHP是怎样进行比较的. 首先,我们先根据PHP文档来定义对象比较的方式: 同一个类的实例,比较属性大小,根 ...

  3. webpack4 使用babel处理ES6语法的一些简单配置

    一,安装包 npm install --save-dev babel-loader @babel/corenpm install @babel/preset-env --save-devnpm ins ...

  4. P4100-[HEOI2013]钙铁锌硒维生素【矩阵求逆,最大匹配】

    正题 题目链接:https://www.luogu.com.cn/problem/P4100 题目大意 给出\(n\)个线性无关的向量\(A_i\),然后给出\(n\)个向量\(B_i\),求一个字典 ...

  5. P6775-[NOI2020]制作菜品【贪心,dp】

    正题 题目链接:https://www.luogu.com.cn/problem/P6775 题目大意 \(n\)种原材料,第\(i\)个有\(d_i\)个,\(m\)道菜品都需要\(k\)个原料而且 ...

  6. P1251-餐巾计划问题【费用流】

    正题 题目链接:https://www.luogu.com.cn/problem/P1251 题目大意 \(N\)天,第\(i\)天需要\(a_i\)个餐巾. 每个餐巾价格为\(p\),使用完后有两种 ...

  7. Python接口自动化测试概念以及意义

    接口定义: 接口普遍有两种意思,一种是API(Application Program Interface),应用编程接口,它是一组定义.程序及协议的集合,通过API接口实现计算机软件之间的相互通信.而 ...

  8. JVM类加载器的分类

    类加载器的分类 JVM支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader). 从概念上来讲,自定 ...

  9. 当一个 Pod 被调度时,Kubernetes 内部发生了什么?

    在 Kubernetes 中,调度是指将 Pod 放置到合适的 Node 上,然后对应 Node 上的 Kubelet 才能够运行这些 Pod . kube-scheduler 是集群控制平面的主要组 ...

  10. Postman快速入门

        Postman是一款非常流行的支持HTTP/HTTPS协议的接口调试与测试工具,其功能非常强大,易用. 1 基础知识 1.1 下载与安装     Postman的安装步骤,本例以Windows ...