在我们进行编码之前,我们应该弄一下准备工作

编译语言的选择

目前来说Shader的编程语言有四种

  • GLSL:主要用于OpenGL平台的图形API
  • HLSL :微软主导开发,主要用于DirectX 平台的图形API
  • CgNvidia开发,可以编译为GLSL HLSL
  • WGSL :主要用于webGPU的图形API

因为我的本业是前端开发,我们学习这个东西主要是在Web环境上进行的,而这个环境有两种图形API,分别是:WebGLWebGPUWebGL本身是基于OpenGL ES的一个JS图形API,然后OpenGL ES本身也属于OpenGL,所以我们可以在GLSLWGSL中进行选择。(不考虑Cg,因为它主要被用于Unity平台。)

WebGPU尽管较新,但浏览器的支持率并不是很高,这使得写出的效果只能在少数的浏览器下运行,用来初学不是很合适。而WebGL则是主流的浏览器几乎都支持,并且大部分网站的Shader效果也是基于这个图形平台实现的,因此我们将选择WebGL平台所对应的语言——GLSL

WebGL 渲染管线流程

由于GLSL是属于WebGL平台的,因此我们有必要了解下WebGL渲染管线的整个流程是什么样的。说的通俗点,就是一个物体是如何被渲染到屏幕上的这一过程。

下图是我用无边记 APP 画出来的示意图:

整个流程是这样的:我们在JS中提供顶点的数据(通常是 Float32Array 类型的数组,包含了顶点的位置等信息),将这些数据传递给顶点着色器,让它计算每个顶点的位置,然后WebGL将顶点装配成图元(如三角形),图元再被转换成屏幕上的空像素(光栅化),让片元着色器来计算每个像素的颜色并填充上去,最终将物体渲染到屏幕上。

其中,图元装配和光栅化是WebGL自带的操作,我们无法进行额外的定制,但顶点着色器和片元着色器则是完全可通过编程来定制化的。我记录下这次的学习记录,其实也就就是要把对它们两者的定制化给做到极致。

前面的内容,会专注于学习片元着色器,要渲染的物体仅仅是一个占满屏幕的平面。到后期,当接触到顶点着色器时,就可以来玩 3D 的物体了

Shader 开发环境搭建

Shader 的开发环境主要有 2 种:网站和编辑器。

但是基于个人习惯,我选择的是 vscode 来编写

在网站里开发 Shader

但是我们还是不能跳过网站的编写,因为这个网站可以说是 Shader 学习者一定要了解的东西:

shadertoy 是一个充满着大量优秀 Shader 作品的网站,当然,你也可以在上面直接创作你的 Shader,只需点击右上角的“新建”,就能从一个最基础的模板开始 Shader。

在编辑器内开发 Shader

以 VSCode 编辑器为例

以下列举了一些助力 Shader 开发的插件,它们是 VSCode 编辑器专用的,如果你用的是别的编辑器,请自行寻找对应的替代品。

Shader 语言支持(必须)

首先,我们的 Shader 文件的代码需要有完整的高亮支持。

在插件中搜索 Shader languages support for VS Code,安装即可。

Shader 实时预览(必须)

其次,我们希望能实时预览我们 Shader 渲染的效果。

在插件中搜索 Shader Toy,安装即可。

HTML 实时预览(必须)

在后面的某几个章节里,我们将会把 Shader 代码直接作为字符串写入 html 文件,因此需要一个能直接预览 html 文件渲染结果的插件。

在插件中搜索 Live Preview,安装即可。

JS 中的 Shader 高亮(可选)

在后面的某几个章节里,我们的 html 文件的 JS 部分会有 Shader 代码的字符串,它们也需要得到高亮提示。

在插件中搜索 es6-string-html,安装即可。

Shader 格式化(可选)

有一个比较实用的插件,它为 Shader 代码提供了格式化功能,同时也提供了取色器,方便调整 3 维变量的颜色值。

在插件中搜索 glsl-canvas,安装即可。

第一个 Shader 程序

GLSL语言的 Shader 文件后缀名就是.glsl,新建一个文件,命名为first-shader.glsl

首先我们定义 Shader 的主体:

void mainImage(out vec4 fragColor,in vec2 fragCoord){

}

我们定义了一个mainImage函数,它不返回任何值,故返回类型为void,它接受 2 个参数:一个是 4 维的fragColor,代表输出的像素颜色;另一个是 2 维的fragCoord,代表输入的像素坐标。

直到顶点着色器之前,我们都是使用的 Shadertoy 环境,主函数 main 需要携程 mainImage,输出颜色 gl_FragColor 需写成 fragColor,输入坐标 gl_FragCoord 需写成 fragCoord

暂时先不用管 fragCoord,直接向屏幕输出一个颜色吧,例如红色:

void mainImage(out vec4 fragColor,in vec2 fragCoord){
    vec3 color=vec3(1.,0.,0.);
    fragColor=vec4(color,1.);
}

我们定义了一个名为color的 3 维变量,要将它的值设置为红色,红色的 RGB 颜色值为(255,0,0),在GLSL中,我们需要先将颜色原先的值进行归一化操作(除以 255)后才能将它正确地输出,因此将红色的值归一化后我们就得到了(1,0,0)这个值,将它转换为 3 维变量vec3(1.,0.,0.)赋给 color 变量。最后我们给输出颜色fragColor赋值一个 4 维变量,前 3 维就是 color 这个颜色变量,最后一维是透明度,由于纯红色并不透明,直接将其设为 1 即可。

按下 Ctrl+Shift+P,输入Shader Toy: Show GLSL Preview,点击即可预览我们的结果,如果一切顺利的话,你应该能看到画面是一片红色。

仅仅输出一种颜色也太单调了,让我们尝试连续输出四种颜色吧!

// 定义了一个mainImage函数,这个函数接受两个参数,一个是输出的颜色,一个是当前像素的坐标
// vec4表示的是一个四维向量,分别表示红、绿、蓝、透明度
// vec2表示的是一个二维向量,分别表示x、y坐标
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    // vec3表示的是一个三维向量,分别表示红、绿、蓝
    vec3 color1 = vec3(0.15, 0.13, 0.84);
    vec3 color2 = vec3(0.84, 0.13, 0.15);
    vec3 color3 = vec3(0.13, 0.84, 0.15);
    vec3 color4 = vec3(0.84, 0.84, 0.15);

    if (fragCoord.x < iResolution.x * .25) {
        fragColor = vec4(color1, 1.0);
    } else if (fragCoord.x < iResolution.x * .5 && fragCoord.x >= iResolution.x * .25) {
        fragColor = vec4(color2, 1.0);
    } else if (fragCoord.x >= iResolution.x * .5 && fragCoord.x < iResolution.x * .75) {
        fragColor = vec4(color3, 1.0);
    } else {
        fragColor = vec4(color4, 1.0);
    }
}

这里我们就要用到fragCoord变量了,它代表了输入的像素坐标,有 2 个维度 xy,它们的大小取决于画面本身的大小。假设我们画面当前的大小为3024 × 1964,那么每一个像素的fragCoord 的 x 坐标值将会分布在(0,3024)之间,y 坐标值则分布在(0,1964)之间。

在当前的 Shader 开发环境内,还有个内置的变量iResolution,代表了画面整体的大小,使用它时一般会取它的 xy 维度。

我们取fragCoord的 x 轴维度,判断如果它小于四分之一的画面长度iResolution.x*.25,就填充第一种颜色color1

按照这类方法以此内推,四种颜色就得到了下面的效果:

这样,我们就得到了我们第一个 Shader 啦。

尽管它很简单,但也很好地说明了 Shader 的核心要点:根据像素的坐标来计算出对应的颜色。

总结一下就是:

  • Shader 有一个主体函数mainImage,接受 2 个参数:输出颜色fragColor和输入坐标fragCoord
  • 通过定义vec类型的变量赋值给fragColor,我们可以向屏幕输出颜色,颜色的值需要归一化。
  • 我们可以通过判断像素在屏幕上的哪个位置来填充不同的颜色,同时也是Shader的核心要点:根据像素的坐标来计算出对应的颜色。
最后修改:2024 年 08 月 14 日
收款不要了,给孩子补充点点赞数吧