【可视化学习】91-从入门到放弃WebGL(二十)
发表于:2024-09-27 |

前言

本篇文章继续跟着李伟老师学习 webgl,本篇主要内容是 webgl 代码的封装

明确封装层级

首先我们要明确我们该如何封装代码,大概思路如下
效果图

  • 场景 Scene:包含所有的三维对象,并负责绘图
  • 三维对象 Obj3D:包含几何体 Geo 和材质 Mat,对两者进行统一管理
  • 几何体 Geo:对应 attribute 顶点数据
  • 材质 Mat:包含程序对象,对应 uniform 变量

几何体 Geo

默认属性

1
2
3
4
5
6
7
8
9
10
11
12
13
const defAttr = () => ({
data: {},
count:0,
index: null,
drawType:'drawArrays',//drawElements
})
export default class Geo {
constructor(attr) {
// 合并this,默认值,传进来的参数
Object.assign(this, defAttr(), attr)
}
……
}
  • data 顶点数据
  • count 顶点总数
  • index 顶点索引数据
    • 默认为 null,用 drawArrays 的方式绘图
    • 若不为 null,用 drawElements 的方式绘图
  • drawType 绘图方式
    • drawArrays 使用顶点集合绘图,默认
    • drawElements,使用顶点索引绘图
data 的数据结构如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
a_Position: {
array:类型数组,
size:矢量长度,
buffer:缓冲对象,
location:attribute变量,
needUpdate:true
},
a_Color: {
array:类型数组,
size:矢量长度,
buffer:缓冲对象,
location:attribute变量,
needUpdate:true
},
……
}
  • array 存储所有的 attribute 数据
  • size 构成一个顶点的所有分量的数目
  • buffer 用 createBuffer() 方法建立的缓冲对象
  • location 用 getAttribLocation() 方法获取的 attribute 变量
  • needUpdate 在连续渲染时,是否更新缓冲对象
index 数据结构
1
2
3
4
5
{
array:类型数组,
buffer:缓冲对象,
needUpdate:true
}

初始化方法

1
2
3
4
5
6
7
8
init(gl,program) {
// webgl上下文对象添加程序对象
gl.useProgram(program)
// 初始化attribute变量
this.initData(gl,program)
// 初始化顶点索引
this.initIndex(gl)
}

init(gl,program) 方法会在场景 Scene 初始化时被调用

  • gl:webgl 上下文对象,会通过场景 Scene 的初始化方法传入
  • program:程序对象,会通过 Obj3D 的初始化方法传入
  • initData() 初始化 attribute 变量
  • initIndex() 初始化顶点索引

初始化顶点数量 count 和绘图方式 drawType
若顶点索引不为 null,就建立缓冲区对象,向其中写入顶点索引数据

initData() 初始化 attribute 变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
initData(gl,program) {
// 这里的key就是变量名,如位置信息a_Position,颜色a_Color,这里的value是其对应的值
for (let [key, attr] of Object.entries(this.data)) {
// 创建并初始化一个用于储存顶点数据或着色数据的缓冲区对象
attr.buffer = gl.createBuffer()
// 绑定缓冲区对象
gl.bindBuffer(gl.ARRAY_BUFFER, attr.buffer)
// 创建并初始化了 Buffer 对象的数据存储区。
gl.bufferData(gl.ARRAY_BUFFER, attr.array, gl.STATIC_DRAW)
// 获取给定程序对象中某属性的下标指向位置
const location = gl.getAttribLocation(program, key)
// 从当前绑定的缓冲区(bindBuffer() 指定的缓冲区)中读取顶点数据。
/**
* 参数1:index-指定要修改的顶点属性的索引。
* 参数2:size-指定每个顶点属性的组成数量,必须是 1,2,3 或 4。
* 参数3: type-指定数组中每个元素的数据类型可能是:这个有很多,比如BTYE,SHORT等,默认使用FLOAT (标准32位)
* 参数4:normalized 当转换为浮点数时是否应该将整数数值归一化到特定的范围。对于类型gl.FLOAT和gl.HALF_FLOAT,此参数无效
* 参数5:stride 以字节为单位指定连续顶点属性开始之间的偏移量 (即数组中一行长度)。不能大于 255。如果 stride 为 0,则假定该属性是紧密打包的,即不交错属性,每个属性在一个单独的块中,下一个顶点的属性紧跟当前顶点之后。
* 参数6 :offset 指定顶点属性数组中第一部分的字节偏移量。必须是类型的字节长度的倍数。
*/
gl.vertexAttribPointer(
location,
attr.size,
gl.FLOAT,
false,
0,
0
)
// 可以打开属性数组列表中指定索引处的通用顶点属性数组。
gl.enableVertexAttribArray(location)
attr.location=location
}
}

initIndex() 初始化顶点索引

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
initIndex(gl) {
const { index } = this
// 这里的index是 顶点索引数据
/**
* 格式如:
* {array:类型数组,
buffer:缓冲对象,
needUpdate:true}
*/
if (index) {
this.count=index.array.length
this.drawType = 'drawElements'
index.buffer = gl.createBuffer()
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, index.buffer)
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, index.array, gl.STATIC_DRAW)
}else{
const { array, size } = this.data['a_Position']
this.count = array.length / size
this.drawType='drawArrays'
}
}

drawElements 和 drawArrays 区别

这里拓展回顾一下 drawElements 和 drawArrays 的区别

drawElements
1
gl.drawElements(mode, count, type, offset);
参数 1-mode

枚举类型 指定要渲染的图元类型。可以是以下类型:

POINTS

POINTS
上面六个点的绘制顺序是:v0, v1, v2, v3, v4, v5

LINES

LINES
上面三条有向线段的绘制顺序是:
​ v0>v1
​ v2>v3
​ v4>v5

LINES_STRIP

LINES_STRIP
上面线条的绘制顺序是:v0>v1>v2>v3>v4>v5

LINE_LOOP

LINE_LOOP
上面线条的绘制顺序是:v0>v1>v2>v3>v4>v5>v0

对于面的绘制,我们首先要知道一个原理:
面有正反两面。
面向我们的面,如果是正面,那它必然是逆时针绘制的;
面向我们的面,如果是反面,那它必然是顺时针绘制的;

TRIANGLES

TRIANGLES
上面两个面的绘制顺序是:
​ v0>v1>v2
​ v3>v4>v5

TRIANGLE_STRIP

TRIANGLE_STRIP
上面四个面的绘制顺序是:

v0>v1>v2

以上一个三角形的第二条边+下一个点为基础,以和第二条边相反的方向绘制三角形

v2>v1>v3

以上一个三角形的第三条边+下一个点为基础,以和第三条边相反的方向绘制三角形

v2>v3>v4

以上一个三角形的第二条边+下一个点为基础,以和第二条边相反的方向绘制三角形

v4>v3>v5

规律:

第一个三角形:v0>v1>v2

第偶数个三角形:以上一个三角形的第二条边+下一个点为基础,以和第二条边相反的方向绘制三角形

第奇数个三角形:以上一个三角形的第三条边+下一个点为基础,以和第三条边相反的方向绘制三角形

TRIANGLE_FAN

TRIANGLE_FAN
​ 上面四个面的绘制顺序是:

​ v0>v1>v2

以上一个三角形的第三条边+下一个点为基础,按照和第三条边相反的顺序,绘制三角形

​ v0>v2>v3

以上一个三角形的第三条边+下一个点为基础,按照和第三条边相反的顺序,绘制三角形

​ v0>v3>v4

以上一个三角形的第三条边+下一个点为基础,按照和第三条边相反的顺序,绘制三角形

​ v0>v4>v5

规律:
第一个三角形:v0>v1>v2
以上一个三角形的第三条边+下一个点为基础,按照和第三条边相反的顺序,绘制三角形

参数 2-count

整数型 指定要渲染的元素数量。

参数 3-type

枚举类型 指定元素数组缓冲区中的值的类型。可能的值是:

  • gl.UNSIGNED_BYTE
  • gl.UNSIGNED_SHORT
    当使用 OES_element_index_uint 扩展时:
  • gl.UNSIGNED_INT
参数 4-offset

字节单位 指定元素数组缓冲区中的偏移量。必须是给定类型大小的有效倍数

drawArrays
1
gl.drawArrays(mode, first, count);
参数 1-mode

同上

参数 2-first

指定从哪个点开始绘制

参数 3-count

指定绘制需要使用到多少个点。

更新方法,用于连续渲染

1
2
3
4
update(gl) {
this.updateData(gl)
this.updateIndex(gl)
}

updateData(gl) 更新 attribute 变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
updateData(gl) {
for (let attr of Object.values(this.data)){
/**
* buffer 程序对象
* location 当前索引
* size 几个点为一组
* needUpdate 在连续渲染时,是否更新缓冲对象
* array 类型数组
*/
const { buffer, location, size, needUpdate,array } = attr
gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
if (needUpdate) {
attr.needUpdate = false
gl.bufferData(gl.ARRAY_BUFFER, array, gl.STATIC_DRAW)
}
gl.vertexAttribPointer(
location,
size,
gl.FLOAT,
false,
0,
0
)
}
}

updateIndex(gl) 更新顶点索引

1
2
3
4
5
6
7
8
9
10
updateIndex(gl) {
const {index} = this
if (index) {
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, index.buffer)
if (index.needUpdate) {
index.needUpdate = false
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, index.array, gl.STATIC_DRAW)
}
}
}

设置 attribute 数据和顶点索引数据的方法

setData(key,val) 设置 attribute 数据

1
2
3
4
5
6
7
setData(key,val){
const { data } = this
const obj = data[key]
if (!obj) { return }
obj.needUpdate=true
Object.assign(obj,val)
}

setIndex(val)设置顶点索引数据

1
2
3
4
5
6
7
8
9
10
11
12
13
setIndex(val) {
const {index}=this
if (val) {
index.needUpdate = true
index.array=val
this.count=index.array.length
this.drawType = 'drawElements'
}else{
const { array, size } = this.data['a_Position']
this.count = array.length / size
this.drawType='drawArrays'
}
}

材质 Mat

知道了上面的点,那么这个材质也就差不多懂了

默认值

1
2
3
4
5
6
7
8
9
10
11
12
const defAttr = () => ({
program: null,
data: {},
mode: 'TRIANGLES',
maps: {}
})
export default class Mat {
constructor(attr) {
Object.assign(this, defAttr(), attr)
}
……
}
  • program 程序对象
  • data uniform 数据
  • mode 图形的绘制方式,默认独立三角形。
    注:mode 也可以是数组,表示多种绘图方式,如[‘TRIANGLE_STRIP’, ‘POINTS’]
  • maps 集合

data 结构

1
2
3
4
5
6
7
8
9
{
u_Color: {
value:1,
type: 'uniform1f',
location:null,
needUpdate:true,
},
……
}
  • value uniform 数据值
  • type uniform 数据的写入方式
  • location 用 getUniformLocation() 方法获取的 uniform 变量
  • needUpdate 在连续渲染时,是否更新 uniform 变量

maps 数据结构:

1
2
3
4
5
6
7
8
u_Sampler:{
image,
format,
wrapS,
wrapT,
magFilter,
minFilter
},
  • image 图形源
  • format 数据类型,默认 gl.RGB
  • wrapS 对应纹理对象的 TEXTURE_WRAP_S 属性
  • wrapT 对应纹理对象的 TEXTURE_WRAP_T 属性
  • magFilter 对应纹理对象的 TEXTURE_MAG_FILTER 属性
  • minFilter 对应纹理对象的 TEXTURE_MIN_FILTER 属性

初始化方法

获取 uniform 变量,绑定到其所在的对象上。

1
2
3
4
5
6
7
init(gl) {
const {program,data,maps}=this
for (let [key, obj] of [...Object.entries(data),...Object.entries(maps)]) {
obj.location = gl.getUniformLocation(program, key)
obj.needUpdate=true
}
}

更新方法,用于连续渲染

1
2
3
4
update(gl) {
this.updateData(gl)
this.updateMaps(gl)
}

updateData(gl) 更新 uniform 变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
updateData(gl) {
for (let obj of Object.values(this.data)) {
if (!obj.needUpdate) { continue }
obj.needUpdate=false
/**
* - value uniform数据值
- type uniform数据的写入方式
- location 用getUniformLocation() 方法获取的uniform变量
- needUpdate 在连续渲染时,是否更新uniform变量
*/
const { type, value, location } = obj
if (type.includes('Matrix')) {
gl[type](location,false,value)
} else {
gl[type](location,value)
}
}
}

updateMaps(gl) 更新纹理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
updateMaps(gl) {
const { maps } = this
Object.values(maps).forEach((map, ind) => {
if (!map.needUpdate) { return }
map.needUpdate = false
const {
format = gl.RGB,
image,
wrapS,
wrapT,
magFilter,
minFilter,
location,
} = map

gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1)
gl.activeTexture(gl[`TEXTURE${ind}`])
const texture = gl.createTexture()
gl.bindTexture(gl.TEXTURE_2D, texture)
gl.texImage2D(
gl.TEXTURE_2D,
0,
format,
format,
gl.UNSIGNED_BYTE,
image
)
// 横向重复
wrapS&&gl.texParameteri(
gl.TEXTURE_2D,
gl.TEXTURE_WRAP_S,
wrapS
)
// 纵向重复
wrapT&&gl.texParameteri(
gl.TEXTURE_2D,
gl.TEXTURE_WRAP_T,
wrapT
)
// 纹理放大
magFilter&&gl.texParameteri(
gl.TEXTURE_2D,
gl.TEXTURE_MAG_FILTER,
magFilter
)
if (!minFilter || minFilter > 9729) {
gl.generateMipmap(gl.TEXTURE_2D)
}
// 纹理缩小
minFilter&&gl.texParameteri(
gl.TEXTURE_2D,
gl.TEXTURE_MIN_FILTER,
minFilter
)
gl.uniform1i(location, ind)
})
}

设置 uniform 数据和纹理的方法

setData(key,val) 设置 uniform 数据

1
2
3
4
5
6
7
setData(key,val){
const { data } = this
const obj = data[key]
if (!obj) { return }
obj.needUpdate=true
Object.assign(obj,val)
}

setMap(val)设置纹理

1
2
3
4
5
6
7
setMap(key,val) {
const { maps } = this
const obj = maps[key]
if (!obj) { return }
obj.needUpdate=true
Object.assign(obj,val)
}

三维对象Obj3D

obj3D对象比较简单,主要负责对Geo对象和Mat对象的统一初始化和更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const defAttr = () => ({
geo: null,
mat: null,
})
export default class Obj3D {
constructor(attr) {
Object.assign(this, defAttr(), attr)
}
init(gl) {
const {mat,geo}=this
mat.init(gl)
geo.init(gl,mat.program)
}
update(gl) {
const { mat, geo } = this
mat.update(gl)
geo.update(gl)
}
}

场景

Scene对象的主要功能就是收集所有的三维对象,然后画出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/*默认属性*/
const defAttr = () => ({
gl:null,
children: [],
});

export default class Scene{
constructor(attr={}){
Object.assign(this,defAttr(),attr);
}
init() {
const { children, gl} = this
children.forEach(obj => {
obj.init(gl)
})
}
add(...objs){
const {children,gl}=this
objs.forEach(obj=>{
children.push(obj)
obj.parent = this
obj.init(gl)
})
}
remove(obj){
const {children}=this
const i = children.indexOf(obj)
if (i!==-1) {
children.splice(i, 1)
}
}
setUniform(key, val) {
this.children.forEach(({ mat }) => {
mat.setData(key,val)
})
}
draw() {
const { gl,children } = this
gl.clear(gl.COLOR_BUFFER_BIT)
children.forEach(obj => {
const { geo: {drawType,count }, mat:{mode,program}}=obj
gl.useProgram(program)
obj.update(gl)
if (typeof mode==='string') {
this[drawType](gl,count, mode)
} else {
mode.forEach(m => {
this[drawType](gl,count, m)
})
}
})
}
drawArrays(gl, count, mode) {
gl.drawArrays(gl[mode], 0, count)
}
drawElements(gl,count, mode) {
gl.drawElements(gl[mode], count, gl.UNSIGNED_BYTE, 0)
}
}
  • Scene 对象的属性只有两个:
  • gl:webgl上下文对象
  • children:三维对象集合
  • init() 初始化方法
  • add() 添加三维对象
  • remove() 删除三维对象
  • setUniform() 统一设置所有对象共有的属性,比如视图投影矩阵
  • draw() 绘图方法

结语

本篇文章就到这里了,更多内容敬请期待,债见~

上一篇:
【可视化学习】92-从入门到放弃WebGL(二十一)
下一篇:
【可视化学习】90-无限管/隧道效果