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. PTA——c++类与对象

    对于给定的一个字符串,统计其中数字字符出现的次数. 类和函数接口定义: 设计一个类Solution,其中包含一个成员函数count_digits,其功能是统计传入的string类型参数中数字字符的个数 ...

  2. git实战-linux定时监控github更新状态(二)

    系列文章 git介绍-常用操作(一)✓ git实战-linux定时监控github更新状态(二)✓ 本文主要内容 如何查看github的本地仓库和远程仓库的同步情况 linux服务器定时监控githu ...

  3. 【PHP数据结构】图的概念和存储结构

    随着学习的深入,我们的知识也在不断的扩展丰富.树结构有没有让大家蒙圈呢?相信我,学完图以后你就会觉得二叉树简直是简单得没法说了.其实我们说所的树,也是图的一种特殊形式. 图的概念 还记得我们学习树的第 ...

  4. CentOS下安装libmcrypt失败

    libmcrypt是什么?? 是加密算法扩展库---支持DES, 3DES, RIJNDAEL, Twofish, IDEA, GOST, CAST-256, ARCFOUR, SERPENT, SA ...

  5. gin 源码阅读(2) - http请求是如何流入gin的?

    推荐阅读: gin 源码阅读(1) - gin 与 net/http 的关系 本篇文章是 gin 源码分析系列的第二篇,这篇文章我们主要弄清一个问题:一个请求通过 net/http 的 socket ...

  6. jqGride的基本使用

    1. 定义:jqGrid是一个在jQuery基础上封装一个表格控件,以ajax的方式和服务器端通信. 2. 使用方式: 2.1 项目中引入jqGride的文件: 2.2 搭建开发页面:  2.3 构建 ...

  7. linux中创建公私钥

    linux中创建公私钥要再~(root)目录下ssh-keygencd /root/.ssh/lsid_rsa 是私钥id_rsa.pub 是公钥把 authorized_keys删除掉,重新建aut ...

  8. 牛客练习赛71E-神奇的迷宫【点分治,NTT】

    正题 题目链接:https://ac.nowcoder.com/acm/contest/7745/E 题目大意 给出\(n\)个点的一棵树,每个点有一个选择权重\(a_i\)(有\(\frac{a_i ...

  9. IE浏览器设置兼容性

    因为IE浏览器不兼容高版本的Jquery.Bootstrap等JS框架,导致页面在Google浏览器和在IE的显示完全不一样,所以需要对页面进行兼容性设置 <!--设置兼容性--> < ...

  10. Spring系列之Redis的两种集成方式

    在工作中,我们用到分布式缓存的时候,第一选择就是Redis,今天介绍一下SpringBoot如何集成Redis的,分别使用Jedis和Spring-data-redis两种方式. 一.使用Jedis方 ...