Shadertoy 教程 Part 4 - 绘制多个2D图形和混入
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,它作为一个滑块器能够让你选择一个介于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);
// 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形状,例如:圆形和正方形。sdfCircle和sdfSquare函数的返回值的类型是一个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图形和混入的更多相关文章
- Shadertoy 教程 Part 5 - 运用SDF绘制出更多的2D图形
Note: This series blog was translated from Nathan Vaughn's Shaders Language Tutorial and has been au ...
- Shadertoy 教程 Part 2 - 圆和动画
Note: This series blog was translated from Nathan Vaughn's Shaders Language Tutorial and has been au ...
- Shadertoy 教程 Part 3 - 矩形和旋转
Note: This series blog was translated from Nathan Vaughn's Shaders Language Tutorial and has been au ...
- iOS10 UI教程视图的绘制与视图控制器和视图
iOS10 UI教程视图的绘制与视图控制器和视图 iOS10 UI视图的绘制 iOS10 UI教程视图的绘制与视图控制器和视图,在iOS中,有很多的绘图应用.这些应用大多是在UIView上进行绘制的. ...
- VB6 GDI+ 入门教程[4] 文字绘制
http://vistaswx.com/blog/article/category/tutorial/page/2 VB6 GDI+ 入门教程[4] 文字绘制 2009 年 6 月 18 日 7条评论 ...
- emwin之2D图形绘制问题
@2018-09-03 [问题] 在 WM_PAINT 消息分支里绘制2D图形可以正常显示,而在外部函数或按钮按下事件的响应消息分支下等处,绘制2D图形则不显示. [解决] 在除消息WM_PAINT分 ...
- WebGL简易教程(三):绘制一个三角形(缓冲区对象)
目录 1. 概述 2. 示例:绘制三角形 1) HelloTriangle.html 2) HelloTriangle.js 3) 缓冲区对象 (1) 创建缓冲区对象(gl.createBuffer( ...
- 【STM32H7教程】第55章 STM32H7的图形加速器DMA2D的基础知识和HAL库API
完整教程下载地址:http://www.armbbs.cn/forum.php?mod=viewthread&tid=86980 第55章 STM32H7的图形加速器DMA2D的基 ...
- 【Android】21.2 2D图形图像处理(Canvas和Paint)
分类:C#.Android.VS2015: 创建日期:2016-03-19 一.Canvas对象简介 画布(Canvas对象)是与System.Drawing或iOS核心图形等传统框架非常类似的另一种 ...
随机推荐
- ATR吊灯止损策略 (含有tbquant源码)
ATR吊灯止损策略定义: 做多,止损放在最高价之下N个ATR. 做空,止损放在最低价之上N个ATR. 该策略生成的止损点就像是从市场最高价的"天花板"上悬挂下来的吊灯.所以命名为A ...
- 使用Jmeter过程中遇到的问题
学习接口自动化测试框架或工具,UI自动化测试框架或工具,有时会觉得知识似乎比较零散,死记硬背不是一个好方法.一个学习的思路是思考使用这些框架或工具的时候,可能会遇到什么问题,遇到这些问题可以通过什么方 ...
- python学习笔记(十一)-python程序目录工程化
在一个程序当中,一般都会包含文件夹:bin.conf.lib.data.logs,以及readme文件. 所写程序存放到各自的文件夹中,如何进行串联? 首先,通过导入文件导入模块方式,引用其他人写好的 ...
- springboot 运行出现错误 Unable to start ServletWebServerApplicationContext due to missing ServletWebServerFactory bean.
原因是我将springboot启动类换到了另外一个方法中 出现了一个异常 后来发现因为我换了类但是忘记了换类名所以才报错 @ComponentScan @EnableAutoConfiguration ...
- 鸿蒙内核源码分析(进程管理篇) | 谁在管理内核资源 | 百篇博客分析OpenHarmonyOS | v2.07
百篇博客系列篇.本篇为: v02.xx 鸿蒙内核源码分析(进程管理篇) | 谁在管理内核资源 | 51.c.h .o 进程管理相关篇为: v02.xx 鸿蒙内核源码分析(进程管理篇) | 谁在管理内核 ...
- CF438E-The Child and Binary Tree【生成函数】
正题 题目链接:https://www.luogu.com.cn/problem/CF438E 题目大意 每个节点有\(n\)个权值可以选择,对于\(1\sim m\)中的每个数字\(k\),求权值和 ...
- 未能加载文件或程序集“System.Net.Http
前言 简单说先事情的起因吧,之前的程序写了有一段时间了,最近要添加新的功能.顺手就把NuGet包全部更新到最新版.随之问题就出现了. 开始以为是.NET Framework 库的原因,之前是4.6.1 ...
- IDEA破解方法:重新刷新到30天【支持正版】
IDEA破解方法:重新刷新到30天[支持正版] 步骤: 导入plugins.zhile.io 进入File-->Settings-->Plugins 点设置(齿轮符号)-->Mana ...
- 一文学会Java事件机制
本文同时发布于个人网站 https://ifuyao.com/blog/java-event/ 相信做 Java 开发的朋友,大多都是学习过或至少了解过 Java GUI 编程的,其中有大量的事件和控 ...
- 生成base64图片验证码
github.com/mojocn/base64Captcha func GetCaptcha(c *gin.Context){ driver := base64Captcha.NewDriverDi ...