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中的国际化日期格式化操作

    对于国际化功能来说,日期相关的格式化操作也是一块重头戏,毕竟不同的时区,不同的国家对于日期的表示方式都会有些不同.今天我们主要来学习的就是国际化地表示日期相关的信息内容. 日期格式化 首先就是最直接的 ...

  2. 手机端wap站网页播放腾讯视频代码

    <div class="detail-con clear"> <div id="mod_player_wrap" class="mo ...

  3. 使用Jmeter执行接口自动化测试-如何初始化清空旧数据

    需求分析: 每次执行完自动化测试,我们不会执行删除接口把数据删除,而需要留着手工测试,此时会导致下次执行测试有旧数据 我们手工可能也会新增数据,导致下次执行自动化测试有旧数据 下面介绍两种清空数据的方 ...

  4. FastAPI(38)- 模拟一个跨域场景

    同源策略 https://www.cnblogs.com/poloyy/p/15345184.html CORS https://www.cnblogs.com/poloyy/p/15345871.h ...

  5. Phalcon多模块如何实现连接不同数据库 《Phalcon入坑指南系列 五》

    本系列目录 一.Phalcon在Windows上安装 <Phalcon入坑指南系列 一> 二.Phalcon入坑必须知道的功能<Phalcon入坑指南系列 二> 三.Phalc ...

  6. mysql从零开始之MySQL DELETE 语句

    MySQL DELETE 语句 你可以使用 SQL 的 DELETE FROM 命令来删除 MySQL 数据表中的记录. 你可以在 mysql> 命令提示符或 PHP 脚本中执行该命令. 语法 ...

  7. HttpClient遭遇Connection Reset异常,如何正确配置?

    最近工作中使用的HttpClient工具遇到的Connection Reset异常.在客户端和服务端配置不对的时候容易出现问题,下面就是记录一下如何解决这个问题的过程. 出现Connection Re ...

  8. 开发函数计算的正确姿势——OCR 服务

    作者 | 杜万(倚贤) 阿里云技术专家 简介 首先介绍下在本文出现的几个比较重要的概念: OCR(光学字符识别):光学字符识别(Optical Character Recognition, OCR)是 ...

  9. Shell 编程 基础用法

    Shell 编程 更改shell脚本权限 chmod u+x shell.sh 标准头部写法 #! /bin/bash #! /bin/dash 变量使用 a=10 print $a 读取命令行参数 ...

  10. .jar文件没有Java(TM) Platform SE binary打开方式解决办法

    下面是我个人在打开.jar文件时候的一些小问题: 明明已经配置好了环境变量.jar文件却没有 Java(TM) Platform SE binary 的打开方式, 网上查了资料点明是环境变量的问题,后 ...