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的着色器语言教程。文章已经获得作者翻译授权,如有转载请务必在取得作者和译者同意之后在文章的重点位置标明原文链接以及说明。如果你觉得文章对你有帮助,点击此打赏链接请作者喝一杯咖啡。

朋友们,你们好!今天,我们将会学习如何用一个像素着色器在Shadertoy上画出一个圆并且使它动起来。

练习

在我们画出第一个2D图形之前,让我们先用Shadertoy来做一些练习。新建一个着色器然后把下面的代码复制进去:

  void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = fragCoord/iResolution.xy; // <0,1> vec3 col = vec3(0); // start with black if (uv.x > .5) col = vec3(1); // make the right half of the canvas white // Output to screen
fragColor = vec4(col,1.0);
}

因为我们的着色器同时运行在每个像素上,所以我们需要用到if条件分支,根据每个像素的位置来绘制不同的颜色。你的着色器代码依赖于图形显卡和编译器,使用内置的函数如step会更合适。

让我们看看用step函数作为替换的例子吧:

  void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = fragCoord/iResolution.xy; // <0,1> vec3 col = vec3(0); // start with black col = vec3(step(0.5, uv.x)); // make the right half of the canvas white // Output to screen
fragColor = vec4(col,1.0);
}

画布的左侧会变成黑色,而右侧会变成白色。

step函数接收两个参数:一个边界值和对比值。如果函数中的第二个参数大于第一个,它返回1,否则返回0。你也可以把step的返回值作为一个向量中的若干个元素值应用:

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = fragCoord/iResolution.xy; // <0,1> vec3 col = vec3(0); // start with black col = vec3(step(0.5, uv), 0); // perform step function across the x-component and y-component of uv // Output to screen
fragColor = vec4(col,1.0);
}

因为step函数同时处理canvas上的 X和Y元素,所以你可以看到canvas被分成了四个颜色块:

绘制圆形

下面是定义一个圆的基础公式:

x^2 + y^2 = r^2

  x = x- 图像上的坐标值
y = y- 图像上的坐标值
r = 圆的半径

我们重新调整变量的位置,让这个等式为0:

  x^2 + y^2 - r^2 = 0

想要用图形的方式展示这套公式,你可以使用 Desmos calculator 帮助你完成公式的图像化:

  x^2 + y^2 - 4 = 0

把上面这段代码粘贴之后复制到Desmos calculator中,你就能看到一个半径为2的圆形,圆的中心点的位置是坐标原点(0, 0);

在Shadertoy中,我们使用上面的公式来画一个圆。首先创建一个sdfCircle函数,这个函数会在XY坐标大于0的时候返回白色,反之则返回蓝色。

sdf函数引用了符号距离场函数(signed distance functions[SDF])的概念。SDF经常用来绘制3D图形,但我们这里也可以用它来绘制2D图形。我们在新建的mainImage函数中调用它们:

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> vec3 col = sdfCircle(uv, .2); // Call this function on each pixel to check if the coordinate lies inside or outside of the circle // Output to screen
fragColor = vec4(col,1.0);
}

也许你会觉得奇怪,为什么我要在数字0之后加上一个小数点呢?这是因为加一个小数点可以把整数型的数字转换成浮点型的数字。当我们在调用需要传入浮点型参数的函数时,在整数后面添加一个“点”能够确保编译器会正确编译我们的代码。

我们把半径设置为0.2因为坐标系统的UV值是在0到1之间,运行代码,你会注意到我们的图形好像有点问题:

看起来像只是一个处在画布左下角的四分之一的圆形。造成这种现象的原因是我们的画布初始原点坐标是在左下角。我们需要把XY坐标偏移0.5,使其能够位于画布的正中心:

UV坐标值减去0.5

  vec2 uv = fragCoord/iResolution.xy; // <0,1>
uv -= 0.5; // <-0.5, 0.5>

现在,画布上X轴和Y轴的坐标范围为-0.5和0.5,意味着坐标的原点就在画布的中心。但是,我们又遇到了一个新的问题:

我们的圆有点被拉伸了,看起来更像一个椭圆。造成这种现象的原因是画布的宽高比率。当画布的宽度和高度不同时,我们的圆就会被拉伸。解决方案是用UV中的X坐标乘以画布宽高比率:

  vec2 uv = fragCoord/iResolution.xy; // <0,1>
uv -= 0.5; // <-0.5, 0.5>
uv.x *= iResolution.x/iResolution.y; // fix aspect ratio

这意味着X轴的范围不再是-0.5到0.5之间了,而是根据你的浏览器和界面(界面的宽度有可能因为开启了调试模式而窄)的宽度,自动适应画布的宽高比率的一个比例值。最后的代码应该是这个样子的:

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

运行以上代码,你就可以看到一个完美比例的蓝色的圆了!

提示: 这仅仅是绘制一个圆形方式之一。在第四节教程中,我们将会学习到另外一个方法,这种方法不仅仅能够帮我们绘制圆,也可以绘制其他图形。

现在我们来点乐子吧!我们可以使用全局变量iTime来变换圆的颜色。使用余弦(cos)函数,我们就能重复地变化一组颜色。因为余弦函数的波幅是在-1到1之间,所以我们需要将其进行调整到0到1之间。

别忘了,片着色器最后输出的颜色值都会在0到1之间。如果颜色值小于0,它会自动转化为0,如果大于1,它会为我们自动转化为1。通过调整区间范围,我们就能得到一个比较宽泛的颜色值范围。

  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(0.) : 0.5 + 0.5 * cos(iTime + uv.xyx + vec3(0,2,4));
} 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);
}

运行以上的代码,你就能看到一个在多个颜色之间变化圆。

你是不是对uv.xyx这种语法感到困惑?这种语法叫做Swizzling。我们可以声明一个包含多个元素的向量。如下:

  vec3 col = vec3(0.2, 0.4, 0.6);
vec3 col2 = col.xyx;
vec3 col3 = vec3(0.2, 0.4, 0.2);

在上面这段代码片段中,col2和 col3 是一样的效果。

让圆动起来

要让圆动起来,我们需要在圆的公式里面为X和Y坐标设置一个偏移值。因此,将等式修改成下面这样:

  (x - offsetX)^2 + (y - offsetY)^2 - r^2 = 0
x = x-coordinate on graph
y = y-coordinate on graph
r = radius of circle
offsetX = how much to move the center of the circle in the x-axis
offsetY = how much to move the center of the circle in the y-axis

我们把代码复制到 Desmos calculator 中去,就可以看到图形化的结果了:

在Shadertoy中,我们通过调整sdfCircle函数适应偏移值,然后使圆向右上角移动了0.2:

  vec3 sdfCircle(vec2 uv, float r, vec2 offset) {
float x = uv.x - offset.x;
float y = uv.y - offset.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; // fix aspect ratio vec2 offset = vec2(0.2, 0.2); // move the circle 0.2 units to the right and 0.2 units up vec3 col = sdfCircle(uv, .2, offset); // Output to screen
fragColor = vec4(col,1.0);
}

使用全局变量iTime让画布获得生机,让圆动起来。

  vec3 sdfCircle(vec2 uv, float r, vec2 offset) {
float x = uv.x - offset.x;
float y = uv.y - offset.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; // fix aspect ratio vec2 offset = vec2(sin(iTime*2.)*0.2, cos(iTime*2.)*0.2); // move the circle clockwise vec3 col = sdfCircle(uv, .2, offset); // Output to screen
fragColor = vec4(col,1.0);
}

以上代码会让圆沿着一个圆形的轨道,以画布原点为中心顺时钟旋转。给iTime乘以一个值,就可以调整动画的速率。给正弦和余弦返回的值乘以一个值,就可以调整运动的幅度。后面我们将会经常会将正弦、余弦函数和iTime变量一起使用。因为它们会产生共鸣!

总结

本节教程,我们学会了如果修正画布的坐标系统,画一个圆形,然后让它沿着圆形轨道动起来。圆!圆!圆!。在下一节课中,我将会向你展示如何绘制一个方块,然后让它旋转起来!

Shadertoy 教程 Part 2 - 圆和动画的更多相关文章

  1. Web动画API教程1:创建基本动画

    本人转载自: Web动画API教程1:创建基本动画

  2. css3基础教程十六变形与动画animation

    前面我们讲过的变形与动画一般都是通过鼠标的单击.获得焦点,被点击或对元素进行一定改变后以后触发效果的,那么有没有像Flash一样自动播放的动画效果呢?答案当然是肯定的,这就是我们今天要讲到的anima ...

  3. XamarinAndroid组件教程设置自定义子元素动画(二)

    XamarinAndroid组件教程设置自定义子元素动画(二) (9)打开MainActivity.cs文件,为RecylerView的子元素设置添加和删除时的透明动画效果.代码如下: …… usin ...

  4. XamarinAndroid组件教程设置自定义子元素动画(一)

    XamarinAndroid组件教程设置自定义子元素动画(一) 如果在RecyclerViewAnimators.Animators中没有所需要的动画效果,就可以自定义一个.此时,需要让自定义的动画继 ...

  5. Shadertoy 教程 Part 1 - 介绍

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

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

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

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

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

  8. Shadertoy 教程 Part 4 - 绘制多个2D图形和混入

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

  9. Android开发教程AnimationDrawable逐帧播放动画

    下面我们一起来看篇Android开发AnimationDrawable控制逐帧播放动画实现过程,希望文章对各位朋友带不一些帮助. 当我们点击按钮时,该图片会不停的旋转,当再次点击按钮时,会停止在当前的 ...

随机推荐

  1. [第六篇]——云服务器之Spring Cloud直播商城 b2b2c电子商务技术总结

    云服务器 云服务器(Elastic Compute Service, ECS)是一种简单高效.安全可靠.处理能力可弹性伸缩的计算服务. 云服务器管理方式比物理服务器更简单高效,我们无需提前购买昂贵的硬 ...

  2. 机器学习——EM算法

    1 数学基础 在实际中,最小化的函数有几个极值,所以最优化算法得出的极值不确实是否为全局的极值,对于一些特殊的函数,凸函数与凹函数,任何局部极值也是全局极致,因此如果目标函数是凸的或凹的,那么优化算法 ...

  3. 洛谷P1125——笨小猴(简易模拟)

    https://www.luogu.org/problem/show?pid=1125 题目描述 笨小猴的词汇量很小,所以每次做英语选择题的时候都很头疼.但是他找到了一种方法,经试验证明,用这种方法去 ...

  4. 性能再提升70%?大咖前瞻带你揭开.NET6的神秘面纱!

    本月初微软官宣.NET 6 的RC1即将在11月正式发布,这意味着.NET6正式版跟我们见面的时间又近了一步.在之前的.NET6预览版本中,微软加入了大量新功能特性,而在最终版本中将不再额外加入新的内 ...

  5. Linux系列(32) - rpm命令管理之RPM查询(4)

    RPM包默认安装位置 RPM包默认安装路径 /etc/ 配置文件安装目录 /usr/bin/ 可执行的命令安装目录 /usr/lib/ 程序所使用的函数库保存位置 /usr/share/doc/ 基本 ...

  6. spring入门2-aop和集成测试

    1.AOP开发 1.1.简述 作用:面向切面编程:在程序运行期间,在不修改源码的情况下对代码进行增强 优势:减少代码重复,提高开发效率,便于维护 底层:动态代理实现(jdk动态代理,cglib动态代理 ...

  7. css 常用语法

    1.禁止某个元素内的任何选中操作: .classname{ -webkit-user-select: none; -moz-user-select: none; -ms-user-select: no ...

  8. P6800-[模板]Chirp Z-Transform【NTT】

    正题 题目链接:https://www.luogu.com.cn/problem/P6800 题目大意 给出一个\(n\)此多项式\(P\),对于\(k\in[0,m-1]\)所有的求\(P(c^k) ...

  9. Unittest 框架之断言,你学会了吗??

    unittest断言 Python在 unittest.TestCase 类中提供了很多断言方法.断言方法检查你认为应该满足的条件是否确实满足.如果该条件确实满足,你对程序行为的假设就得到了确认,你就 ...

  10. Python3入门系列之-----函数

    什么是函数 函数是组织好的,可重复使用的,用来实现单一,或相关联功能的代码段. 函数能提高应用的模块性,和代码的重复利用率.你已经知道Python提供了许多内建函数,比如print().但你也可以自己 ...