Skip to content

二、着色器

1.基本概念

使用 GLSL 的着色器(shader),GLSL 是一门特殊的有着类似于 C 语言的语法,在图形管道 (graphic pipeline) 中直接可执行的 OpenGL 着色语言。着色器有两种类型:

  • 顶点着色器(Vertex Shader) :描述顶点的特征,如位置、颜色等。
  • 片段着色器(Fragment Shader):进行逐片元处理,如光照。

GLSL 不同于 JavaScript, 它是强类型语言,并且内置很多数学公式用于计算向量和矩阵。快速编写着色器非常复杂,但创建一个简单的着色器并不难。在这篇文章我们将介绍使用着色器的基础知识,并且构建一个使用 Three.js 的例子来加速代码编写。

你可能记得基本原理那篇文章,一个顶点 (vertex) 是在空间中有自己 3D 坐标的点,并且通常包含些被定义的其他信息。空间本身会被坐标系统定义。在那个 3D 空间中一切都是关于形状的呈现。

注意:GLSL语法,每一句代码后都要加上;,不然会报错

1.1 顶点着色器

在script中建立顶点着色器

  • 顶点着色程序,要写在type=“x-shader/x-vertex”script中。
  • void main() {…… } 是主体函数。
  • gl_Position 是顶点的位置,gl_PointSize 是顶点的尺寸,这种名称都是固定的,不能写成别的。
  • vec4() 是一个4维矢量对象。
  • 将vec4() 赋值给顶点点位gl_Position 的时候,其中的前三个参数是x、y、z,第4个参数默认1.0,其含义我们后面会详解;
html
<script id="vertexShader" type="x-shader/x-vertex">
    void main() {
        gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
        gl_PointSize = 100.0;
        // ...
    }
</script>

在js中获取顶点着色器中的文本

js
const VERTEX_SHADER_SOURCE = document.getElementById('vertexShader').innerText;

用字符串的形式也行,不过推荐用script

js
const VERTEX_SHADER_SOURCE =`
    void main() {
        gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
        gl_PointSize = 100.0;
        // ...
	}
`

1.2 片段着色器

片段 (或者纹理) 着色器 在计算时定义了每像素的 RGBA 颜色 — 每个像素只调用一次片段着色器。这个着色器的作用是设置 gl_FragColor 变量,也是一个 GLSL 内置变量:

在script中建立片元着色器

  • 片元着色程序,要写在type=“x-shader/x-fragment”script中。
  • gl_FragColor 是片元的颜色。
  • 将vec4() 赋值给片元颜色gl_FragColor 的时候,其中的参数是r,g,b,a。
html
<script id="fragmentShader" type="x-shader/x-fragment">
    void main() {
        gl_FragColor = vec4(1.0, 1.0, 0.0, 1.0);
    }
</script>

在js中获取片元着色器的文本

js
const FRAGMENT_SHADER_SOURCE = document.getElementById('fragmentShader').innerText;

字符串形式

js
const FRAGMENT_SHADER_SOURCE = `
    void main() {
      gl_FragColor = vec4(1.0, 1.0, 0.0, 1.0);
      // ... 
    }
`

1.3 绘制流程

image-20230924105823778

1.4 着色器初始化

1.4.1 创建着色器对象

js
const shader = gl.createShader(shader);
  • shader:着色器类型
    • gl.VERTEX_SHADER:表示该着色器程序编译后被顶点着色器执行
    • gl.FRAGMENT_SHADER:表示该着色器程序编译后被片元着色器执行

1.4.2 指定着色器的源码

js
gl.shaderSource(shaderObject, shaderSource);
  • haderObject: 着色器对象
  • shaderSource:着色器源码,字符串格式着色器程序

1.4.3 编译着色器

js
gl.compileShader(shaderObject);
  • shaderObject:着色器对象

1.4.4 创建程序对象

创建程序对象,程序对象存在的意义是为了实现CPU和GPU的通信,控制GPU着色器的工作状态,切换不同的着色器程序。

js
const program = gl.createProgram();

可用deleteProgram(program)删除程序对象

1.4.5 绑定着色器至程序对象

js
gl.attachShader(program, shader);
  • program:程序对象
  • shader:着色器对象

1.4.6 连接webgl上下文对象和程序对象

在执行useprogram方法之前,要先连接程序对象program的顶点和片元着色器程序,检查着色程序的错误。 通过连接测试后,才能通过useprogram方法把着色器程序传递给GPU,否则报错。

测试项

  1. 检查顶点、片元着色器程序中同名varying变量是否一一对应

  2. 检查顶点着色器程序中是否给varying变量赋值顶点数据

  3. 硬件资源有限,要检测attribute、uniform、varying变量的数量是否超出限制范围

js
gl.linkProgram(program);
  • program:程序对象

1.4.7 判断程序是否连接成功

通过该方法可以判断linkProgram()方法是否连接成功,program参数指定要连接的程序对象,参数value定义了该方法执行后,需要反馈的数据,

js
getProgramParameter(program, value)
  • program:程序对象
  • value
    • gl.DELETE_STATUS:是否执行deleteProgram删除程序对象program,返回结果true或false
    • gl.LINK_STATUS:程序对象program是否通过linkProgram()方法连接验证,返回结果true或false
    • gl.VALIDATE_STATUS:程序对象program是否通过验证,返回结果true或false
    • gl.ATTACHED_SHADERS:分配给程序的着色器对象数量
    • gl.ACTIVE_ATTRIBUTES:attribute变量的数量
    • gl.ACTIVE_UNIFORMS:uniform变量的数量

1.4.8 启动程序对象

js
gl.useProgram(program);

program:程序对象

1.5 封装初始化函数

以下代码以后是不变的,所以需要封装,在script标签中引入使用

js
/**
 * 初始化着色器
 * @param {any} gl webgl绘制对象
 * @param {any} VERTEX_SHADER_SOURCE  顶点着色器源码
 * @param {any} FRAGMENT_SHADER_SOURCE 片段着色器源码
 * @returns {any}
 */
const initShader = (gl, VERTEX_SHADER_SOURCE, FRAGMENT_SHADER_SOURCE) => {
    // 1.创建着色器对象
    const vertexShader = gl.createShader(gl.VERTEX_SHADER);
    const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
    // 2.指定着色器的源码
    gl.shaderSource(vertexShader, VERTEX_SHADER_SOURCE); 
    gl.shaderSource(fragmentShader, FRAGMENT_SHADER_SOURCE);

    // 3.编译着色器
    gl.compileShader(vertexShader);
    gl.compileShader(fragmentShader);
    // 4.创建一个程序对象
    const program = gl.createProgram();
    // 5.把着色器对象装进程序对象中
    gl.attachShader(program, vertexShader);
    gl.attachShader(program, fragmentShader);
    // 6.连接webgl上下文对象和程序对象
    gl.linkProgram(program);
    // 7.启动程序对象
    gl.useProgram(program);
	
    // 8.返回程序对象,方便以后使用
    return program;
};

2.webgl坐标系

与canvas坐标系不同,webgl坐标系的原点在canvas元素的中间,x轴向右,y轴向上,并且多了一个z轴,方向往我们的视线方向

webgl坐标系中每个正半轴的范围都是0~1,负半轴范围是-1~ 0

image-20230924111707710

2.1 绘制一个点

首先要用script标签引入我们封装的初始化着色器initShader

js
const canvas = document.querySelector("#canvas");

const gl = canvas.getContext("webgl");

// 顶点着色器
const VERTEX_SHADER_SOURCE = `
      void main() {
        gl_Position = vec4(0.0,0.0,0.0,1.0);		// 点位置在坐标原点
        gl_PointSize = 30.0;						// 点的大小为30像素
      }
      `;

const FRAGMENT_SHADER_SOURCE = `
        void main() {
          gl_FragColor = vec4(1.0,0.0,0.0,1.0);		// 点的颜色为红色,rgba,r为1
        }
      `;

const program = initShader(
    gl,
    VERTEX_SHADER_SOURCE,
    FRAGMENT_SHADER_SOURCE
);

// 绘制点
gl.drawArrays(gl.POINTS, 0, 1);

可以看到绘制的点在坐标原点,也就是canvas元素的中心

image-20230924112112982

3.着色器变量

我们通过上面的方式绘制一个点,只能通过vec4()的方式,写入一个固定的数据

但是我们想通过js的方式,灵活的控制变量,于是我们就可以通过使用变量的方式

3.1 attribute变量

3.1.1 作用

js 可以通过attribute 变量向顶点着色器传递与顶点相关的数据

  • attribute 变量是只有顶点着色器才能使用它的。
  • attribute类型只能指定为float、vec2、vec3、vec4、mat2、mat3和mat4。

3.1.2 js控制顶点位置

1.声明attribute变量
  • 声明attribute变量要在glsl语言中,main函数外定义
  • attribute变量只能在顶点着色器中使用
  • 需要将定义的变量赋值给gl_Position
  • 注意要加;
html
<script id="vertexShader" type="x-shader/x-vertex">
    attribute vec4 a_Position;
    void main(){
        gl_Position = a_Position;
        gl_PointSize = 50.0;
    }
</script>

image-20230924114556936

  • attribute 是存储限定符,是专门用于向外部导出与点位相关的对象的,这类似于es6模板语法中export 。
  • vec4 是变量类型,vec4是4维矢量对象。
  • a_Position 是变量名,之后在js中会根据这个变量名导入变量。这个变量名是一个指针,指向实际数据的存储位置。也是说,我们如果在着色器外部改变了a_Position所指向的实际数据,那么在着色器中a_Position 所对应的数据也会修改。
2.获取attribute变量
  • 通过gl.getAttribLocation()
  • 获取 attribute 变量需要在 initShader 函数之后,因为会用到 program 这个程序对象。
  • 返回变量的存储地址
js
const location=gl.getAttribLocation(program,'a_Position');

参数

  • gl 是webgl 的上下文对象。
  • getAttribLocation() 方法的参数中:
    • program 是初始化着色器时,在上下文对象上挂载的程序对象。
    • 'a_Position' 是着色器暴露出的变量名。
3.修改attribute 变量
js
gl.vertexAttrib1f(location,v0) 
gl.vertexAttrib2f(location,v0,v1)
gl.vertexAttrib3f(location,v0,v1,v2)
gl.vertexAttrib4f(location,v0,v1,v2,v3)

参数

  • location:通过gl.getAttribLocation()获取的着色器变量
  • v0、v1、v2:顶点的x、y、z位置,浮点数,范围在0.0-1.0,默认值为0.0
  • v3:默认为1.0,其数值类型为float 浮点型。
4.webgl函数命名规律

GLSL ES里函数的命名结构是:<基础函数名><参数个数><参数类型>

以vertexAttrib3f(location,v0,v1,v2,v3) 为例:

  • vertexAttrib:基础函数名
  • 3:参数个数,这里的参数个数是要传给变量的参数个数,而不是当前函数的参数个数
  • f:参数类型,f 代表float 浮点类型,除此之外还有i 代表整型,v代表数字……

3.1.3 用鼠标绘制点

1.获取鼠标在canvas元素中的位置
js
canvas.onclick = (e) => {
    // 获取鼠标相对文档左上角的位置
    const x = e.clientX;
    const y = e.clientY;
    // 获取canvas元素盒子信息
    const rect = e.target.getBoundingClientRect();
    // 相减得到鼠标相对canvas左上角的坐标
    const domX = x - rect.x;
    const domY = y - rect.y;
}
2.canvas 坐标系转webgl 坐标系

由于webgl坐标系相对于canvas坐标系是向右下方移动了一半canvas高和一半canvas宽,并且需要将距离转化为0.0-1.0的小数从而修改attribute变量

js
// 获取canvas元素一半的宽高
const harfWidth = canvas.offsetWidth / 2;
const harfHeight = canvas.offsetHeight / 2;

const changePoint = (x, y) => {
    // 左边向右下平移一半宽高,并转化为0.0-1.0的浮点数
    x = (x - harfWidth) / harfWidth;
    y = (harfHeight - y) / harfHeight;
    // 返回最终浮点数
    return {
        x,
        y,
    };
};
3.最后修改attribute变量
js
canvas.onclick = (e) => {
    const x = e.clientX;
    const y = e.clientY;
    const rect = e.target.getBoundingClientRect();
    const domX = x - rect.x;
    const domY = y - rect.y;

    const newPoint = changePoint(domX, domY);
	// 修改attribute变量
    gl.vertexAttrib2f(aPosition, newPoint.x, newPoint.y);
    // 绘制
    gl.drawArrays(gl.POINTS, 0, 1);
};

4.整体代码
js
const canvas = document.querySelector("#canvas");
const gl = canvas.getContext("webgl");

const VERTEX_SHADER_SOURCE = `
      attribute vec4 aPosition;
      void main() {
        gl_Position = aPosition;
        gl_PointSize = 30.0;
      }
      `;
const FRAGMENT_SHADER_SOURCE = `
        void main() {
          gl_FragColor = vec4(1.0,0.0,0.0,1.0);
        }
      `;
const program = initShader(
    gl,
    VERTEX_SHADER_SOURCE,
    FRAGMENT_SHADER_SOURCE
);

const aPosition = gl.getAttribLocation(program, "aPosition");

const harfWidth = canvas.offsetWidth / 2;
const harfHeight = canvas.offsetHeight / 2;
const changePoint = (x, y) => {
    x = (x - harfWidth) / harfWidth;
    y = (harfHeight - y) / harfHeight;
    return {
        x,
        y,
    };
};

canvas.onclick = (e) => {
    const x = e.clientX;
    const y = e.clientY;
    const rect = e.target.getBoundingClientRect();
    const domX = x - rect.x;
    const domY = y - rect.y;

    const newPoint = changePoint(domX, domY);

    gl.vertexAttrib2f(aPosition, newPoint.x, newPoint.y);
	// 绘制点的方法
    gl.drawArrays(gl.POINTS, 0, 1);
};
5.绘制多个点
js
const canvas = document.querySelector("#canvas");

const gl = canvas.getContext("webgl");

const VERTEX_SHADER_SOURCE = `
      attribute vec4 aPosition;
      attribute float aPointSize;
      void main() {
        gl_Position = aPosition;
        gl_PointSize = aPointSize;
      }
      `;

const FRAGMENT_SHADER_SOURCE = `
        void main() {
          gl_FragColor = vec4(1.0,0.0,0.0,1.0);
        }
      `;
const program = initShader(
    gl,
    VERTEX_SHADER_SOURCE,
    FRAGMENT_SHADER_SOURCE
);

const aPosition = gl.getAttribLocation(program, "aPosition");

const harfWidth = canvas.offsetWidth / 2;
const harfHeight = canvas.offsetHeight / 2;

const changePoint = (x, y) => {
    x = (x - harfWidth) / harfWidth;
    y = (harfHeight - y) / harfHeight;
    return {
        x,
        y,
    };
};
// 创建数组用于存放点
const points = [];
canvas.onclick = (e) => {
    const x = e.clientX;
    const y = e.clientY;
    const rect = e.target.getBoundingClientRect();
    const domX = x - rect.x;
    const domY = y - rect.y;

    const newPoint = changePoint(domX, domY);
	// 数组中心增点击的坐标
    points.push({
        x: newPoint.x,
        y: newPoint.y,
    });
	// 遍历数组绘制每一个点
    points.forEach((item) => {
        gl.vertexAttrib2f(aPosition, item.x, item.y);
        gl.drawArrays(gl.POINTS, 0, 1);
    });
};

3.1.4 js控制顶点尺寸

1.定义attribute变量

尺寸是浮点类型的,所以用float

html
<script id="vertexShader" type="x-shader/x-vertex">
    attribute vec4 a_Position;
    attribute float a_PointSize;
    void main(){
        gl_Position = a_Position;
        gl_PointSize = a_PointSize;
    }
</script>
2.获取attribute 变量
js
const a_PointSize=gl.getAttribLocation(gl.program,'a_PointSize');
3.修改attribute 变量
js
gl.vertexAttrib1f(a_PointSize,100.0);
4.点击绘制不同尺寸点
js
const canvas = document.querySelector("#canvas");

const gl = canvas.getContext("webgl");

const VERTEX_SHADER_SOURCE = `
      attribute vec4 aPosition;
      attribute float aPointSize;
      void main() {
        gl_Position = aPosition;
        gl_PointSize = aPointSize;
      }
      `;

const FRAGMENT_SHADER_SOURCE = `
        void main() {
          gl_FragColor = vec4(1.0,0.0,0.0,1.0);
        }
      `;
const program = initShader(
    gl,
    VERTEX_SHADER_SOURCE,
    FRAGMENT_SHADER_SOURCE
);

const aPosition = gl.getAttribLocation(program, "aPosition");
const aPointSize = gl.getAttribLocation(program, "aPointSize");	//获取尺寸attribute变量

const harfWidth = canvas.offsetWidth / 2;
const harfHeight = canvas.offsetHeight / 2;

const changePoint = (x, y) => {
    x = (x - harfWidth) / harfWidth;
    y = (harfHeight - y) / harfHeight;
    return {
        x,
        y,
    };
};

const points = [];
canvas.onclick = (e) => {
    const x = e.clientX;
    const y = e.clientY;
    const rect = e.target.getBoundingClientRect();
    const domX = x - rect.x;
    const domY = y - rect.y;

    const newPoint = changePoint(domX, domY);

    points.push({
        x: newPoint.x,
        y: newPoint.y,
        size: Math.random() * 50,		// 生成随尺寸
    });

    points.forEach((item) => {
        gl.vertexAttrib2f(aPosition, item.x, item.y);
        gl.vertexAttrib1f(aPointSize, item.size);		// 传入尺寸信息
        gl.drawArrays(gl.POINTS, 0, 1);
    });
};

3.2 uniform变量

3.2.1 作用

attribute变量只有顶点着色器才能使用,使用片元着色器,你就需要使用uniform变量

uniform变量可以指定为除数组和结构体之外的任意类型。

uniform变量可以在顶点着色器和片元着色器中使用,且必须是全局变量

3.2.2 语法

1.定义uniform变量

在片元着色器里把控制顶点颜色的变量暴露出来。

html
<script id="fragmentShader" type="x-shader/x-fragment">
    uniform vec4 u_FragColor;
    void main() {
        gl_FragColor = u_FragColor;
    }
</script>
  • uniform:限定符
  • vec4:4维的变量类型
  • u_FragColor:变量名。
2.设置精度

通过precision <精度> float来设置精度,这个必须要有,不然画不出东西来。

片元着色器中一定要设置精度

  • highp:高精度
  • mediump:中精度
  • lowp:低精度
html
<script id="fragmentShader" type="x-shader/x-fragment">
    precision mediump float;
    uniform vec4 u_FragColor;
    void main() {
        gl_FragColor = u_FragColor;
    }
</script>
3.获取uniform变量
js
const location=gl.getUniformLocation(gl.program,'u_FragColor');
4.修改uniform变量
glsl
gl.uniform1f(location,v0);
gl.uniform2f(location,v0,v1);
gl.uniform3f(location,v0,v1,v2);
gl.uniform4f(location,v0,v1,v2,v3);

参数:

  • location:指定uniform变量的存储位置

  • v0,v1,v2:颜色的r、g、b色值,浮点数,范围在0.0-1.0

  • v3:透明度,浮点数,范围在0.0-1.0,默认值1.0

5.点击生成不同颜色点
js
const canvas = document.querySelector("#canvas");

const gl = canvas.getContext("webgl");

const VERTEX_SHADER_SOURCE = `
      attribute vec4 aPosition;
      attribute float aPointSize;
      void main() {
        gl_Position = aPosition;
        gl_PointSize = aPointSize;
      }
      `;

const FRAGMENT_SHADER_SOURCE = `
        precision mediump float;
        uniform vec4 u_FragColor;
        void main() {
          gl_FragColor = u_FragColor;
        }
      `;
const program = initShader(
    gl,
    VERTEX_SHADER_SOURCE,
    FRAGMENT_SHADER_SOURCE
);

const aPosition = gl.getAttribLocation(program, "aPosition");
const aPointSize = gl.getAttribLocation(program, "aPointSize");
const aFragColor = gl.getUniformLocation(program, "u_FragColor");	// 获取颜色uniform变量

const harfWidth = canvas.offsetWidth / 2;
const harfHeight = canvas.offsetHeight / 2;

const changePoint = (x, y) => {
    x = (x - harfWidth) / harfWidth;
    y = (harfHeight - y) / harfHeight;
    return {
        x,
        y,
    };
};

const points = [];
canvas.onclick = (e) => {
    const x = e.clientX;
    const y = e.clientY;
    const rect = e.target.getBoundingClientRect();
    const domX = x - rect.x;
    const domY = y - rect.y;

    const newPoint = changePoint(domX, domY);

    points.push({
        x: newPoint.x,
        y: newPoint.y,
        size: Math.random() * 50,
        color: {		// 生成随机颜色
            r: Math.random(),
            g: Math.random(),
            b: Math.random(),
        },
    });

    points.forEach((item) => {
        gl.vertexAttrib2f(aPosition, item.x, item.y);
        gl.vertexAttrib1f(aPointSize, item.size);
        //  传入颜色信息
        gl.uniform4f(
            aFragColor,
            item.color.r,
            item.color.g,
            item.color.b,
            1.0
        );
        gl.drawArrays(gl.POINTS, 0, 1);
    });
};
6.uniform4vf()

我们在改变uniform 变量的时候,既可以用uniform4f() 方法一个个的写参数,也可以用uniform4fv() 方法传递类型数组。

  • uniform4f 中,4 是有4个数据,f 是float 浮点类型,在我们上面的例子里就是r、g、b、a 这四个颜色数据。
  • uniform4fv 中,4f 的意思和上面一样,v 是vector 矢量的意思,这在数学里就是向量的意思。由之前的4f 可知,这个向量由4个浮点类型的分量构成。

在上面呢的案例中,我们可以知道,在修改uniform变量的时候,这两种写法是一样的:

js
gl.uniform4f(u_FragColor,1.0,1.0,0.0,1.0);
//等同于
const color=new Float32Array([1.0,1.0,0.0,1.0]);
gl.uniform4fv(u_FragColor,color);

uniform4f() 和uniform4fv() 也有着自己的同族方法,其中的4 可以变成1|2|3。

uniform4fv() 方法的第二个参数必须是Float32Array 数组,不要使用普通的Array 对象。

Float32Array 是一种32 位的浮点型数组,它在浏览器中的运行效率要比普通的Array 高很多。

3.3 varying变量

3.3.1 作用

用作顶点着色器向片元着色器传递数据

varying变量只能是以下类型:float、vec2、vec3、vec4、mat2、mat3和mat4。

3.3.2 语法

varying变量必须同时在两个着色器中声明

顶点着色器

  • 声明后在main()函数中给变量赋值
glsl
varying vec4 vColor;
void main() {
    vColor = vec4(1.0,0.0,0.0,1.0);
}

片元着色器

glsl
precision lowp float;
varying vec4 vColor;
void main() {
    gl_FragColor = vColor;
}

MIT Licensed