总目录
- 入门
- 环境配置
- 第一个可运行的Shader
- 绘制图形——长方形和圆形
- 认识SmoothStep
- 初探GLSL
- 向量与矩阵
- 浮点数精度
- uniform
- 颜色与形状
- 颜色基本知识
- 生成渐变色
- 圆角矩形渲染
- 多变形的渲染
- 数学与图形
- 极坐标系
- 向量几何
- 三角函数
- 分型
- 生成艺术
- 噪声
- 噪声场
- 叠加
- 模糊
环境配置
step1. 首先当然是安装nodejs,我们可以选择从nodejs.org下载对应的操作系统和CPU指令集的安装包,也可以用homebrew、apt等工具安装,多数前端工程师都已经有nodejs环境,此处不详细展开了。
step2. (可选)全局安装vite,为了比较方便地使用vite,建议全局安装vite。如果不全局安装vite,我们必需利用npx执行本项目的vite。使用npm install -g vite
命令即可。
step3. 初始化项目,在一个喜欢的路径创建一个新的目录,比如这里我创建了一个element3-demo
mkdir element3-demo
cd element3-demo
复制代码
进入目录后,执行npm init
,并填写必要信息。之后,我们得到了一个基础的package.json文件。
step4. 接下来,我们为项目添加依赖,并安装相关包
首先我们用自己喜欢的文本编辑工具打开package.json,并且为它添加dependencies和devDependencies:
{
"dependencies": {
"element3-core": "0.0.7",
"vue": "^3.0.5"
},
"devDependencies": {
"@vitejs/plugin-vue": "^1.2.2",
"@vue/compiler-sfc": "^3.0.5",
"rollup-plugin-element3-webgl": "0.0.5",
"typescript": "^4.1.3",
"vite": "^2.3.0",
"vue-tsc": "^0.0.24"
}
}
复制代码
之后我们回到终端,使用npm install命令。
step5. 创建文件和基本目录结构。
编写index.html
文件:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
复制代码
编写src/main.ts
文件:
import { createApp } from "vue";
import App from "./App.vue";
createApp(App).mount("#app");
复制代码
编写src/app.vue
文件:
<template>
<div>
Hello
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
name: "App",
components: {
},
setup(){
return {
}
}
});
</script>
复制代码
编写vite.config.js文件:
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import element3Webgl from "rollup-plugin-element3-webgl";
// https://vitejs.dev/config/
export default defineConfig({
base: "/", // TODO 开发环境是 / 生产环境是 /webgl
plugins: [vue(), element3Webgl()],
});
复制代码
编写完成后,我们在命令行使用npx vite
,打开网页看到Hello表明环境已经配置好。
第一个可运行的 Shader
接下来我们创建一个 src/pure.frag
文件。
Fragment Shader 使用的语言并非 JavaScript,而是一种叫做 GLSL 的专用语言,在后面的教程中,我会逐渐为大家介绍这门语言特性,这里我们先尝试写出第一个可运行的Fragment Shader。
我们首先要理解 Fragment Shader 的概念,一段 Fragment Shader 是绘制屏幕上一个点的过程。它的执行频率非常高,绘制一个100×100区域的图像,需要执行10000次 Shader 中的代码,Shader通常是由GPU承担的。
接下来我们编写一段代码,把画布区域涂上纯色:
precision mediump float;
void main(){
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
复制代码
element3的rollup插件能够直接把这个Shader代码加载成一个vue组件,这能够帮助我们忽略掉调用 WebGL API的冗繁过程。
接下来我们更改App.vue的代码,展示这个绘制的效果:
<template>
<div>
<DrawBlock width=100 height=100></DrawBlock>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import DrawBlock from "./pure.frag";
export default defineComponent({
name: "App",
components: {
DrawBlock
},
setup(){
return {
}
}
});
</script>
复制代码
我们可以看到一个纯红色的方块区域。
接下来我们来稍微解释一下这段 GLSL 代码。
我们首先来看第一句:precision mediump float;
。这一句是必要的,他规定了程序的全局浮点数精度,此处使用了中等精度,几乎每一个Fragment Shader代码都会包含这一句,我们可以暂且认为它是固定的。
一切GLSL代码都是从main
函数开始执行的。在GLSL中,main
函数可以不返回值,这种函数我们用void来代替类型的部分。
接下来我们来看main
函数的函数体,函数体中只有一个语句。这里我们使用了一个 gl_fragColor
变量,这个名字是GLSL语言规定的名字,并不是可以随意命名的变量,我们前面讲过,Fragment Shader 是绘制一个点的代码,这个 gl_fragColor
就是我们最后要输出的点的颜色。
接下来我们看等号的另一端,这里的vec4
表示一个长度为4的浮点数向量类型,它里面可以存储4个浮点数。大家还记得线性代数里学习的向量吧?这里的vec4
就是来自数学中的向量概念。使用起来它有点像JavaScript中的数组,不同的是,它是固定长度的,这样的数据结构对图形算法非常的有用,我们将会在未来与它打很多交道。
最后提醒一下,GLSL语言不允许省略分号,忘记的话会导致整个程序无法编译,一定要注意哦。
进行到这一步,我们已经学会了如何使用element3的rollup插件来加载一段Fragment Shader,获得了一个基本的代码调试和运行的环境。
接下来,我们学习一下如何控制Shader绘制一些想要的东西。
绘制图形——长方形和圆形
首先,我们尝试缩小一下绘制的范围,要想控制范围,我们必须要知道当前所绘制的点的坐标,这时候,我们就要介绍GLSL中另一个重要的变量了: gl_fragCoord
。
如果说gl_fragColor
是Fragment Shader的输出的话,gl_fragCoord
就是Fragment Shader的输入了,它表示的是当前绘制点的坐标,它是一个vec4
类型,但这里我们只需要用到它的前两项。
我们可以分别使用 gl_fragCoord.x
和 gl_fragCoord.y
来访问它的坐标,也可以使用 gl_fragCoord.xy
来把它变为2维向量。
那么,回到我们的问题,如何绘制一个长方形呢?我们只需要判断一下它的坐标范围就可以了,请看示例代码:
precision mediump float;
void main(){
if(gl_FragCoord.x > 25.0 && gl_FragCoord.x < 75.0 &&
gl_FragCoord.y > 25.0 && gl_FragCoord.y < 75.0)
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
else
gl_FragColor = vec4(0.0, 0.0, 1.0, 1.0);
}
复制代码
这里我们要注意,很多同学从JS带来的习惯是整数类型和浮点数类型不区分,而在GLSL中,整数和浮点数是完全两种类型,不进行强制转换的话,没法混合运算。当我们写直接量时,也要非常明确地带上小数点,表示这是一个浮点数。
我们也可以用类似float(25)
这样的代码来强制转换整数到浮点数类型,但是这里无论从可读性的角度,还是执行效率的角度,我都不推荐这种写法。
画完了方形,我们来尝试一下更复杂的圆形,根据初中解析几何知识,我们可以知道圆形就是到圆心距离小于半径的点的集合,于是我们可以根据公式x²+y²<r²
来绘制圆形。
我们固然可以用乘法来实现平方,不过,根据DRY原则,我们最好还是使用系统内置函数来实现平方,在GLSL中,多数数学函数都可以直接使用,不用像JS一样加Math.
。
最后实现代码如下:
precision mediump float;
void main(){
if(pow(gl_FragCoord.x - 50.0, 2.0) + pow(gl_FragCoord.y - 50.0, 2.0) < pow(25.0, 2.0))
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
else
gl_FragColor = vec4(0.0, 0.0, 1.0, 1.0);
}
复制代码
这样我们的圆形就画好了。但是,如果我们把这个圆形放大一些来看,你会发现,它有严重的锯齿感,接下来我们将会介绍一个GLSL中重要的函数,用于解决此问题。
认识smoothstep
我们试着分析一下圆形看起来锯齿感明显的原因,我们在Shader代码中,采取了一种非黑即白的策略,而受限于显示设备,我们没法让像素小到肉眼无法分辨,因此产生了锯齿感。
那么,计算机中一般的图形显示方案是怎么处理的呢?方法很简单,就是我们在这个圆形的边缘,产生一个细微的渐变,这样,颜色过渡就没那么生硬了。
我们首先整理下Shader的代码,把点到圆心的距离单独设为一个变量。这里我们使用了一个新的函数,开平方函数sqrt
:
float l = sqrt(pow(gl_FragCoord.x - 50.0, 2.0) + pow(gl_FragCoord.y - 50.0, 2.0));
复制代码
接下来,我们尝试根据变量l来混合两种颜色,这里我们介绍一个新的函数mix
,它能够根据比例混合两种颜色(其实还有别的用途,暂且不表)。mix
有三个参数,前两个是待混合的值,最后一个参数是混合的比例。
我们尝试根据点到圆心的距离来l来混合两种颜色,最终代码如下:
precision mediump float;
void main(){
float l = sqrt(pow(gl_FragCoord.x - 50.0, 2.0) + pow(gl_FragCoord.y - 50.0, 2.0));
gl_FragColor = mix(vec4(1.0, 0.0, 0.0, 1.0), vec4(0.0, 0.0, 1.0, 1.0), l / 25.0);
}
复制代码
执行后,我们可以看到明显的渐变,但是这并不是我们最终想要的效果,我们并不希望整个圆变成渐变的,我们只希望圆形靠近边缘有几个像素宽的渐变,虽然我们可以用四则运算和if组合出这个效果,但是GLSL中提供了更优雅的解决方案,那就是smoothstep
函数。
smoothstep接受三个参数min, max和x,它的功能是,当x小于min时,返回0.0,当x大于max时,返回1.0,而x介于min和max之间时,返回一个0.0到1.0之间的值,表示x在这个区间内与min距离的占比。
接下来,我们来修改GLSL代码,利用smoothstep来绘制一个柔边的圆形。为了效果明显,这里故意设置的smoothstep范围较大,实际使用中,只做1-2像素模糊是比较合适的。
precision mediump float;
void main(){
float l = sqrt(pow(gl_FragCoord.x - 50.0, 2.0) + pow(gl_FragCoord.y - 50.0, 2.0));
gl_FragColor = mix(vec4(1.0, 0.0, 0.0, 1.0), vec4(0.0, 0.0, 1.0, 1.0), smoothstep(20.5, 25.5, l));
}
复制代码
到这里,相信你已经了解了smoothstep的基本知识。接下来,我们就来活学活用一番。我们的下一个任务是绘制直线。
说是绘制直线,其实直线还是有宽度的,这就要求我们能够计算出点到直线的距离。这里我们直接使用向量几何中的结论:
定理:给定直线 l , 其方向向量为 m . A 为 l 外一点, 若要求 A 到直线 l 的距离 d , 可任取 l 上一点 B, 点 A 到点 B 的向量记作 n , 则
根据此公式,这里我们需要用到向量点乘运算dot
,和向量长度函数length
,最后写出的GLSL代码如下:
precision mediump float;
void main(){
vec2 m = vec2(1., -1.);
vec2 n = vec2(25., 0.) - gl_FragCoord.xy;
float d = length(dot(m, n)) / length(m);
gl_FragColor = mix(vec4(1.0, 0.0, 0.0, 1.0), vec4(0.0, 0.0, 1.0, 1.0), smoothstep(0.0, 1.0, d));
}
复制代码
从这里我们可以看出向量运算的强大,结合解析几何和线性代数知识,我们可以用简洁的代码来处理各种图形图像问题。
练习题
看完了以上内容,你是否跃跃欲试了呢?这里留一个小练习给大家:
用Fragment Shader绘制一个Vue的Logo。
欢迎贴出Shader代码大家一起讨论。