Shader 魔法的学习之路 #1

作者:mnikn
2022-05-20
10 7 2

简介

这系列的教程我们会从零讲解 shader 编程,如何用 shader 做一些酷炫的效果。

不过在此之前,我们先简单介绍一下 shader 是什么。


Shader 是什么?能吃吗?

我不想讲那些太学术官方的解释,一句话概括:Shader 是系统给你的一系列钩子函数,主要用在图形上,你可以在上面的钩子函数基于像素级别改变甚至创造图形。


Shader 的使用场景

可以看下别人用 shader 创造的东西:https://www.shadertoy.com/ 有人可能会问,这些东西为什么不直接通过绘图软件或者动画软件搞出来,而非要用 shader 实现呢? 举个例子:大家有玩过游戏吧(没玩过那就去玩),一些游戏中当鼠标移到可交互的物品,为了看起来更显眼,这些物品会有边缘的高光,当鼠标移出去又恢复原状。这系列的场景的存在,要求我们要有动态修改图形的能力。


Shader 编程的一些基础概念和术语

下面的术语和概念在之后的教程中会反复提及,这里先预习一下,看不懂也没关系,后面会再提。

概念:

  • 图形学渲染流水线:一个图形需要经过多道工艺渲染出来,每项工艺接受上一条工艺的产物进行进一步的加功,这样每道工艺互不相干形成了流水线,我们关注的 shader 就是其中的一道工艺。详细介绍:https://zhuanlan.zhihu.com/p/79183044

术语:

  • fragment shader:处理每个像素的绘制颜色、方式的 shader
  • vetex shader:处理像素要在哪里绘制的 shader
  • uv:画布所有的像素点,整体表现为一个 0 到 1 分布的矩阵
  • 零点:坐标系的 (0,0) 点,本系列教程采用 webgl,默认零点在左下角


教程的一些参考和说明

这系列教程大部分思路和实现均来自 https://www.youtube.com/watch?v=u5HAYVHsasc&list=PLGmrMu-IwbguU_nY2egTFmlg691DN7uE5,教程很不错建议去看一下。 本人目前也还在根据上述的视频学习 shader,水平有限难免有错误,如果有什么说得不对的欢迎指正。

Hello World!

这次我们来初步尝试下 shader 编程,由于 shader 编程在不同平台的编写形式不同,根据语法分为 glsl 和 hlsl,根据引擎分为:opengl、webgl、directx 和其他我不知道的引擎,一些游戏引擎又会针对 shader 做一些封装:Unity、UE、Godot。

这系列的教程不会详细讲不同平台的区别,虽然不同平台的函数、变量甚至语法可能都有差异,不过思路是共通的,本系列的代码均使用 webgl,而且为了方便快速体验 shader 编程,我们会在一个封装好的平台 https://www.shadertoy.com/ 上去写代码,如果你已经有熟悉的平台,例如 Unity,可以自己根据文中代码进行实践。


先来个 Hello World?不,我直接蓝屏!

对于 shader 编程来说,画个 hello world 还挺麻烦的,所以我们直接先画个蓝屏吧!

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    fragColor = vec4(0.0, 0.0, 1.0 ,1.0);
}

一句核心代码就实现了蓝屏,我们逐步看下上面的代码写的是什么意思。

首先我们定义了一个叫 mainImage 的函数,由于 shadertoy 做了一些封装,其实这个和传统的 shader 编程稍微有点不同。传统的 shader 编程这个函数应该叫 fragment,也被叫为 fragment shader ,也没有接受入参和出参。这个函数可以认为是系统给我们的钩子函数,绘制每个像素时都会调用这个函数,具体的执行时机由系统决定。

然后就是一句核心代码 fragColor = vec4(0.0, 0.0, 1.0 ,1.0)fragColor 是 webgl 的内置变量,表示当前像素的颜色。 后面的 vec4(0.0, 0.0, 1.0, 1.0)vec4 可以先看作是一个存了四个值的数据结构,变体还有 vec3vec2,里面的值类型都是 float(浮点数),vec3 存的就是三个值,以此类推。

我们给 vec4 设置的值是 (0.0, 0.0, 1.0, 1.0),因为这个值是赋给 fragColor 的,那这个值就表示的是颜色值,颜色值用 rgba 表示,所以这个值的意思是红色、绿色的值都为 0,蓝色和 alpha 值为 1.0,所以最终展现了蓝色。


再来个黄屏!

那如果我想要表现的是黄色,那要怎么做呢?根据颜色混合的原理,黄色=红色+绿色,所以我们这样设置:

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    fragColor = vec4(1.0, 1.0, 0.0 ,1.0);
}

一个像《赛博朋克 2077》跳票通知的东西就出来啦!

这篇文章的内容目前为止没讲太多东西,主要是先让大家熟悉一下,接下来就开始正式地绘制一些常规的图案了。

本次文章的作业是,让屏幕显示出呼伦贝尔大草原的颜色。

画一个笑脸

这次我们来开始真正的绘图,我们画一个笑脸吧!


先画个马赛克圆

笑脸首先需要一张脸,我们先画个圆来表示一张脸吧。

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
  // 计算 uv
  vec2 uv = fragCoord / iResolution.xy;
  // 让绘制区域位于正中心
  uv -= 0.5;
  // 根据 uv 长度得出绘制颜色
  float c = length(uv);
  // 画!
  fragColor = vec4(vec3(c) ,1.0);
}

当然这样代码画出来看起来是一团像圆的马赛克,我先解释下代码,说下为什么然后再改好它。

vec2 uv = fragCoord / iResolution.xy 这一句其实在很多平台都没必要,大部分平台 uv 都是内置变量,就 webgl 特殊 uv 都需要计算。uv 表示的是当前像素的位置,如果放到整体来看,uv 就表示整个画布的范围,范围值是 [0,1],里面的值都是基于左下角零点的比例坐标,不是物理坐标。这里面计算 uv 的方式是拿 fragCoord 当前像素的物理坐标 / iResolution 当前画布的物理大小从而算出 uv

uv -= 0.5 这个相当于把画布往左下角偏移了,为什么要这样做,这其实要和接下来画圆的操作配合说明,我们先跳过。

float c = length(uv) 这句是画圆的关键,通过这句代码我们得到了当前像素在圆中的颜色值。length 的函数作用是得出向量的长度,而我们传的是 uv,就相当于得出了当前像素和零点的距离,范围在 [0, 1]。从整体看来,离零点越远的像素得到的值也越大,图形化出来就是以零点发散的圆。

fragColor = vec4(vec3(c) ,1.0) 最后一句用这个算出来的值来决定像素颜色,值范围处于 [0, 1],值为 0 是黑色 1 是白色。为什么我们会看到的是一个马赛克的圆?因为我们得到的值是一个范围,有很多中间值,表现起来就是不同程度的灰色。

回到 uv -= 0.5 这句,先看下如果我们去掉这行代码展示的是什么:

我们看到的是一个只有右上部分的半圆,为什么会这样?因为我们是用 length(uv) 来算出像素颜色值,而离左下角零点越近那值肯定越小,所以就会出现左下角像右边扩散的情况。所以 uv -= 0.5 的作用是让零点往左下角再移一些,这样算出来的值就相当于整个圆。不理解的话可以自己在代码上调一下,或者自己画画来模拟。


高清无码的圆

问题来了,我们怎么把这样一个马赛克画质的圆转成高清无码呢?既然我们得到的是一个范围值,那只要让它要么是 0 要么是 1 就好。

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    vec2 uv = fragCoord / iResolution.xy;
    uv -= 0.5;
    float c = length(uv);
    // 大于 0.3 就是 1,否则就是 0
    c = step(c, 0.3);
    fragColor = vec4(vec3(c) ,1.0);
}

经过处理我们得到了一个看起来不错的圆,step 函数的作用是接受两个参数,然后比较这两个参数,第一个参数大于第二个参数就返回 1.0 否则返回 0.0。所以 step 传过去的第二个参数相当于圆的半径。


高清有码的圆

细心的人会发现我们上面的圆边缘看起来很锐利,因为我们的值不是 0 就是 1,没有任何过渡。我们要做的是让圆的边角部位值处于 01 之间,这样就会圆滑很多。

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    vec2 uv = fragCoord / iResolution.xy;
    uv -= 0.5;
    float c = length(uv);

    float r = 0.3;
    c = smoothstep(r, r - 0.01, c);
    fragColor = vec4(vec3(c) ,1.0);
}

我们用 smoothstep 代替了 step,区别在于 smoothstep 接受三个参数,第三个参数用来判断,第一个和第二个参数表示当判断值处于第一个参数和第二个参数之间,根据比例线性插值得到一个 0-1 的区间值,否则要么是 0 要么是 1


带点颜色

目前为止我们都是黑白,但是我们可以让圆带点颜色:

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    vec2 uv = fragCoord / iResolution.xy;
    uv -= 0.5;
    float c = length(uv);

    // 定义圆的颜色为黄色
    vec3 color = vec3(1.0, 1.0, 0.0);

    float r = 0.3;
    c = smoothstep(r, r - 0.01, c);
    // 混色
    color *= c;
    fragColor = vec4(vec3(color) ,1.0);
}

我们定义了个 color 来表示圆的颜色,然后用它来乘以我们之前算出来的值,再传到 fragColor 上。因为我们用白色来表示圆,color * c 相当于在圆的区域上混上黄色,而白色混色等于对应的颜色。


用 shader 带来笑容

经过一番波折我们画了一个脸,接下来就是眼睛和嘴巴了,这些我们都用圆实现。因为要画多次圆,我们先封装下画圆的函数。

float circle(vec2 uv, float r, vec2 p) {
    uv -= p;
    float c = length(uv);
    return smoothstep(r, r - 0.01, c);
}
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    vec2 uv = fragCoord / iResolution.xy;
    uv -= 0.5;

    vec3 color = vec3(1.0, 1.0, 0.0);
    float fare = circle(uv, 0.3, vec2(0.0, 0.0));
    color *= face;
    fragColor = vec4(vec3(color) ,1.0);
}

画面效果没什么变化,不过我们封装了个 circle 函数,接受 uv、圆的半径和圆心坐标,用 circle 函数就能很容易得到一个圆。

有了这样一个函数后,我们接下来画眼睛和嘴巴:

float circle(vec2 uv, float r, vec2 p) {
    uv -= p;
    float c = length(uv);
    return smoothstep(r, r - 0.01, c);
}
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    vec2 uv = fragCoord / iResolution.xy;
    uv -= 0.5;

    vec3 color = vec3(1.0, 1.0, 0.0);
    float face = circle(uv, 0.3, vec2(0.0, 0.0));
    float eye1 = circle(uv, 0.05, vec2(-0.1, 0.10));
    float eye2 = circle(uv, 0.05, vec2(0.1, 0.10));

    float mouth_1 = circle(uv, 0.15, vec2(0.0, -0.1));
    float mouth_2 = circle(uv, 0.15, vec2(0.0, 0.0));
    float mouth = max(mouth_1 - mouth_2, 0.0);

    color *= face;
    color -= eye1;
    color -= eye2;

    color -= mouth;
    fragColor = vec4(vec3(color) ,1.0);
}

眼睛和嘴巴的画法都是先得出区域,然后减去这部分区域的颜色,不同的是嘴巴是用两个圆相减得出一个半圆。 好了本文就到这里,这次的作业是画一个哭脸。

本文为用户投稿,不代表 indienova 观点。

近期点赞的会员

 分享这篇文章

您可能还会对这些文章感兴趣

参与此文章的讨论

  1. yellow 2022-05-20

    学一把。

  2. 粥赛 2022-05-27

    smoothstep 的图是不是画错了?

您需要登录或者注册后才能发表评论

登录/注册