任何复杂的场景都能用点线三角面实现,从本章开始将会从基本形状开始介绍 Shader 程序的编写。
OpenGL 可以根据顶点信息绘制三角形,如果每个顶点发送一次数据到 GPU 后再执行绘制效率会很低,因为从 CPU 发送数据到 GPU 相对较慢,使用 vertexBuffer 缓冲对象可以一次性发送一大批数据到 GPU上,而不是每个顶点发送一次。WebGL 通过顶点缓冲对象(VBO)在 GPU 上开辟内存并管理,GPU 显存中存储大量顶点数据,再结合顶点组合方式解析这些内存,供顶点着色器使用。
总结一下,使用顶点缓冲有两大作用:
(1) 能同时传入多组数据
(2) 提高存储效率
import initShaders from "./initShaders.js";
const gl = document.getElementById("webgl").getContext("webgl");
const vertexShader = `
attribute vec2 a_position;
void main() {
gl_Position = vec4(a_position, 0.0, 1.0);
}
`;
const fragmentShader = `
precision mediump float;
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
`;
initShaders(gl, vertexShader, fragmentShader);
gl.clearColor(0.5, 0.5, 0.5, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
// 定义三角形三个顶点的vec2(xy)坐标(z=0)
const positions = [
-0.5, 0.0,
0.5, 0.0,
0.0, 0.8,
];
// 【1】createBuffer 创建顶点缓冲对象
const vertexBuffer = gl.createBuffer();
// 【2】bindBuffer 将顶点缓冲对象绑定到 ARRAY_BUFFER 字段上
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
// 【3】bufferData 将顶点数据存入缓冲(vertexBuffer)中
// 需要指明数据大小用于分配GPU内存,这里的每个分量都是 32 位浮点型数据
// 最后一个参数用于提示 WebGL 数据的使用方式:
// gl.STATIC_DRAW: 数据不变或几乎不变
// gl.DYNAMIC_DRAW: 数据会较多改变
// gl.STREAM_DRAW: 数据每次绘制时都会改变
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
// 【4】把带有数据的buffer赋值给attribute
const a_position = gl.getAttribLocation(gl.program, "a_position");
gl.enableVertexAttribArray(a_position); // 确认赋值
// 【5】指定缓冲数据的解析方式,本案例为每2个float一组
/**
* @function gl.vertexAttribPointer(positionAttributeLocation, size, type, normalize, stride, offset)
* @param positionAttributeLocation 顶点着色器上的"a_position"属性的位置指针
* @param size 一个顶点数据的获取长度,本案例每个顶点包含2个位置分量(xy)
* @param type 数据缓冲类型,本案例顶点数据类型为 float32,因此使用 gl.FLOAT
* @param normalize 归一化,如 [1,2]=>[1/√5,2/√5],通常为 false
* @param stride 数据存储方式,单位是字节,0 表示连续存放,非 0 表示一个顶点数据占的字节长度(步长)
* @param offset 当前输入数据在一个顶点数据里的偏移字节数,由于本案例一组数据只有 position 的两个值,因此偏移量为0,若还包含其他值,如顶点颜色、纹理坐标等则需要设置
*/
gl.vertexAttribPointer(a_position, 2, gl.FLOAT, false, 0, 0);
// 【6】执行绘制,本案例为从0开始,每次取3个点绘制三角形
/**
* @function gl.drawArrays(primitiveType, offset, count)
* @param primitiveType 指定图元绘制形式,共七种,如 gl.POINTS / gl.LINE_STRIP / gl.TRIANGLES / ...
* @param offset 起始索引
* @param count 本次绘制使用的点的数量,也表示顶点着色器的运行次数(顶点着色器每次只处理一个顶点)
*/
gl.drawArrays(gl.TRIANGLES, 0, 3);
其中,6种变量类型对应的类型化数组如下所示:
变量类型 | 类型化数组 |
---|---|
gl.UNSIGNED_BYTE | Uint8Array |
gl.SHORT | Int16Array |
gl.UNSIGNED_SHORT | Uint16Array |
gl.INT | Int32Array |
gl.UNSIGNED_INT | Uint32Array |
gl.FLOAT | Float32Array |
在 OpenGL 中,使用不同颜色的顶点绘制三角形,在光栅化阶段会在顶点之间进行像素插值。如果需要动态顶点颜色,有两种方式:
(1) 使用 uniform
传入三个顶点相同的颜色
(2) 扩展顶点属性
为每个顶点定义不同颜色,就不能使用 uniform
方式传值,因为它作为全局变量被访问,对每个顶点和片段着色器来说是相同的,此时可以通过扩展顶点缓冲对象的属性为每个顶点传递不同的颜色信息。
import initShaders from "./initShaders.js";
const gl = document.getElementById("webgl").getContext("webgl");
const vertexShader = `
attribute vec2 a_position;
attribute vec3 a_color;
varying vec3 v_color; // 颜色传递
void main() {
v_color = a_color;
gl_Position = vec4(a_position, 0.0, 1.0);
}
`;
const fragmentShader = `
precision mediump float;
varying vec3 v_color;
void main() {
gl_FragColor = vec4(v_color, 1.0);
}
`;
initShaders(gl, vertexShader, fragmentShader);
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
// x y r g b
let vertices = [
-0.5, 0.0, 1.0, 0.0, 0.0, // node1
0.5, 0.0, 0.0, 1.0, 0.0, // node2
0.0, 0.8, 0.0, 0.0, 1.0, // node3
];
vertices = new Float32Array(vertices);
const FSIZE = vertices.BYTES_PER_ELEMENT;
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
const a_position = gl.getAttribLocation(gl.program, "a_position");
const a_color = gl.getAttribLocation(gl.program, "a_color");
gl.vertexAttribPointer(a_position, 2, gl.FLOAT, false,
5 * FSIZE, // stride 每个点的信息所占 BYTES,当前一个顶点的步长是 (2+3)*4=20
0, // offset 起始索引 BYTES
);
gl.vertexAttribPointer(a_color, 3, gl.FLOAT, false, 5 * FSIZE,
2 * FSIZE, // offset 起始索引 BYTES,当前起始索引为 2*4=8
);
gl.enableVertexAttribArray(a_position);
gl.enableVertexAttribArray(a_color);
gl.drawArrays(gl.TRIANGLES, 0, 3);
上述的顶点缓冲对象的存储方式如下图所示:
绘制一个三角形需要在数组中指定3个顶点坐标,不难联想到两个三角形提供6个顶点坐标即可,如下所示,但这样做会造成极大的资源浪费,因为有两个顶点是重复的,可以使用索引缓冲指定顶点缓冲中的顶点来高效绘制。
...
let vertices = [
-0.5, 0.0, ..., ..., ...,
0.5, 0.0, ..., ..., ...,
0.0, 0.8, ..., ..., ...,
0.5, 0.0, ..., ..., ..., // 重复
-0.5, 0.0, ..., ..., ..., // 重复
0.0, -0.8, ..., ..., ...,
];
...
gl.drawArrays(gl.TRIANGLES, 0, 6);
如下所示,顶点缓冲只需要提供4个顶点,通过索引缓冲指定顶点索引即可。需要注意,索引是非负整型,需要使用 Uint16Array 类型来存储索引值。
// x y r g b
const vertices = [
-0.5, 0.0, 1.0, 0.0, 0.0, // 0
0.5, 0.0, 0.0, 1.0, 0.0, // 1
0.0, 0.8, 0.0, 0.0, 1.0, // 2
0.0, -0.8, 0.0, 0.0, 1.0, // 3
];
const vertexBuffer = gl.createBuffer();
...
// 指定绘制顺序,即两个三角形的顶点索引顺序
const indices = [
0, 1, 2, // 三角形1
1, 0, 3 // 三角形2
];
const indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
// 合理分配内存:因为索引不会有小数点,所以取用无符号16位整型
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
// gl.drawArrays(gl.TRIANGLES, 0, 6); // 改为 gl.drawElements
/**
* @function gl.drawElements(primitiveType, count, indexType, offset)
* @param primitiveType 同gl.drawArrays
* @param indexType 指定元素数组缓冲区中的值类型,gl.UNSIGNED_BYTE / gl.UNSIGNED_SHORT / 扩展类型
* gl.UNSIGNED_BYTE 最大索引值为 255 而 gl.UNSIGNED_SHORT 最大索引值为 65535
*/
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
注意:提交的顶点索引需要按逆时针顺序提交,因为一个平面包含正反两面,逆时针为正面(右手定则),图形管线一般会在 vs 后 fs 前做一次预剔除背面以提升渲染性能。
gl.drawArrays
和 gl.drawElements
第一个参数都为 primitiveType
,它有 7 种可选值,以顶点着色器输出的所有节点作为输入进行绘制,重点需要关注点的连接方式。
沿用上面案例的 4 个点,7种参数值的绘制结果如下图所示,POINTS
、LINES
、LINE_STRIP
、LINE_LOOP
、TRIANGLES
都很好理解,最后两种的点连接方式特殊,TRIANGLE_STRIP
会循环连接点,按照 012/123/234/... 的顺序,一个点最多被三个三角形共享,相比 gl.TRIANGLES
可以用更少的信息绘制同样的效果,而 TRIANGLE_FAN
按照 012/023/034/... 的顺序,第一个点会被所有三角形共用。
// x y r g b
let vertices = [
-0.5, 0.0, 1.0, 0.0, 0.0, // 0
0.5, 0.0, 0.0, 1.0, 0.0, // 1
0.0, 0.8, 0.0, 0.0, 1.0, // 2
0.0, -0.8, 0.0, 0.0, 1.0, // 3
];
// 七种方式分别绘制
gl.drawArrays(gl.POINTS, 0, 4); // 点(顶点着色器中需要设置gl_PointSize)
gl.drawArrays(gl.LINES, 0, 4); // 线(两个点一组,多余的舍弃)
gl.drawArrays(gl.LINE_STRIP, 0, 4); // 线(开口不闭环)
gl.drawArrays(gl.LINE_LOOP, 0, 4); // 线(闭环)
gl.drawArrays(gl.TRIANGLES, 0, 4); // 三角形(三个点一组),此处最后一个点舍弃
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); // 循环连接 (012/123/234/...)
gl.drawArrays(gl.TRIANGLE_FAN, 0, 4); // 扇面 (012/023/034/...) 第一个点会被所有三角形共用
有了这七种基本形状,再复杂的图形都能由这些基础形状构成。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。