基础介绍
CPU准备数据:顶点,索引,UV,法线, 切线,空间坐标系转换矩阵(pvm)等
其实这部分准备数据中,都是围绕顶点的,每一个顶点都会包含以下几个信息:
位置,索引,UV,法线,切线,当然对于空间坐标系的矩阵,这个是对于一个显示节点而言的,不能说一个顶点对应一个空间坐标系转换矩阵,应该说一个模型上的所有顶点共用同样一组空间坐标系的转换矩阵
准备完以后:会在GPU端创建一系列buffer,每一个buffer创建完以后,会返回一个GLID,你可以理解为这就是这个buffer的地址,我们可以在CPU端通过该地址来访问这个buffer,进而来操作这块buffer,比如来存储我们要传给它的数据,或者删除它
可以把GPU理解为一个状态机,GPU也有显存,我们CPU这边传来的数据会存储在显存里,在每一帧渲染前,如果数据有变化,会重新将数据上传到GPU的显存里,在每一帧渲染的时候,会将各种渲染状态传给GPU
看下面这几个绘制类型,其实就只有三大类,点,线段,三角形,而三角形是我们经常要使用的,因为我们游戏中要显示的对象几乎都是有面的,而不是一些很简单的点和线
// primitive type
export const enum glprimitive_type {
POINTS = 0, // gl.POINTS 要绘制一系列的点
LINES = 1, // gl.LINES 要绘制了一系列未连接直线段(单独行)
LINE_LOOP = 2, // gl.LINE_LOOP 要绘制一系列连接的线段
LINE_STRIP = 3, // gl.LINE_STRIP 要绘制一系列连接的线段。它还连接的第一和最后的顶点,以形成一个环
TRIANGLES = 4, // gl.TRIANGLES 一系列单独的三角形;绘制方式:(v0,v1,v2),(v1,v3,v4)
TRIANGLE_STRIP = 5, // gl.TRIANGLE_STRIP 一系列带状的三角形
TRIANGLE_FAN = 6, // gl.TRIANGLE_FAN 扇形绘制方式
}
所以接下来我们要说的就是绘制三角形,无论是直接使用顶点绘制,还是启用索引绘制,都必须要提供一个有序的三角形顶点队,三个一组,
drawArrays:假如我们要绘制一个面,我们需要四个顶点,但是你发给GPU的时候,却是6个,因为三三一组,这其中有两个顶点是重复的,形如【v1,v2,v3,v2,v3,v4】,要是一个立方体呢,有六个面,那就是多出12个重复的点,而且每个顶点都是由三个坐标组成的,至于每个坐标占多少字节,这个是我们控制的,看下面代码,每个顶点的单个坐标就要占到4个字节,如果考虑到从内存中将这些数据搬到GPU中,那么就要考虑到这些坐标的占用内存问题
//顶点buffer
class VertexsBuffer extends glBaseBuffer {
constructor(gl, vertexs: Array<number>, itemSize: number, itemNums: number) {
super(gl, vertexs, itemSize, itemNums);
}
bindBuffer(): void {
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this._glID);
}
bindData(): void {
this.itemBytes = 32 / 8;
this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(this.sourceData), this.gl.STATIC_DRAW);
}
}
//索引buffer
class IndexsBuffer extends glBaseBuffer {
constructor(gl, indexs: Array<number>, itemSize: number, itemNums: number) {
super(gl, indexs, itemSize, itemNums);
}
bindBuffer(): void {
this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, this._glID);
}
bindData(): void {
this.itemBytes = 16 / 8;
this.gl.bufferData(this.gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(this.sourceData), this.gl.STATIC_DRAW);
}
}
drawElements:这个是采用索引绘制,其实就是给每个顶点加一个id,从小到大递增,上面我们是用顶点来排序三角形队列的,我们我们用索引来排序队列,虽然一个面也有两个索引重复,但是我们存储一个顶点只需要一个值,这个值还是占2个字节,也就是说对于一个面我们就要浪费2x2个字节,相比较上面的3x4x2个字节,我们一个面就要少20个字节,还是蛮客观的,当然关于索引绘制,还有退化三角形,这个更加节省内存
空间坐标系转换矩阵
在opegl的世界里,其实她所关心的只是顶点处于啥位置和给它上什么颜色,所以我们把opegl的坐标系作为渲染坐标系,对于空间坐标系而言,它会包含三条坐标轴,互相垂直而成,在这个空间坐标系里的任何一个点的位置,都是基于这个坐标系的,这很重要!
我们在游戏的世界里,会有很多显示节点,这些显示节点,都具备自己的空间坐标系,但是这些空间坐标系,必须要从渲染坐标系孵化而来,这样你的渲染节点才能更好的管理,这个孵化无非就是通过继承关系,然后对坐标系进行旋转,缩放,平移这些变化,
四维矩阵:为此,我们造了一个这个东西来描述空间坐标系,前三列表示的对于空间坐标系的旋转和缩放,后一列表示这个空间坐标系的平移
继承:我们需要通过一组关系,来管理我们的变化和达到我们的需求,比如说一个场景里各种复杂的节点,场景空间坐标系发生变化,那么都要场景里的节点都要跟着变化,一个位置的(4x1)的向量和 4x4的空间坐标系矩阵,最后会得出一个(4x1)的向量,他就是当前这个点在这个空间坐标系的具体位置,空间坐标系没有发生任何变换的话,他就是干净的单位方阵矩阵,如果发生变化,则它的所有点都会发生变化,当然最顶层的渲染坐标系是死的,永远不变化,处于原点位置,就在屏幕的正中心,由里向外为正,从左到右为正,从下到上为正
相机:它是我们抽象出来的一个显示节点,它的主要功能就是去生成两个矩阵,一个是模型矩阵,一个是投影矩阵,可以把这个模型矩阵传递给场景,作为场景的根矩阵,继承的传下去给所有节点,关于投影矩阵,这个有两种类型,透视和正交,也就是一个由远近大小,一个没有这个功效,注意:我发现一个现象是如果我们的相机没有使用lookat函数,那么的它的投影矩阵,就是面向-z轴,你是没办法改变投影矩阵的朝向和位置的,但你如果使用了lookat函数,那么投影矩阵的朝向和位置就会随着你传入的眼睛的位置改变而改变
GPU读写数据
三部曲:GPU是一个状态机,在显存创建buffer,绑定缓冲,读写数据
1:对于shader中attribute中的变量,诸如法顶点位置,法线,uv,切线,索引
下面三步从CPU这边往GPU显存那边写入数据
第一步:在显存中创建一个buffer,并且返回这块buffer的地址
this._glID = gl.createBuffer();
第二步:和目标缓冲区绑定
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this._glID);
第三步:从内存上传数据到显存
this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(this.sourceData), this.gl.STATIC_DRAW);
下面三步是从从显存中读数据
第一步:和目标缓冲区绑定
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this._glID);
第二步:给shader程序中的变量赋值
shader程序中变量类型有:
void int bool float vec2 vec3 vec4 bvec2 bvec3 bvec4 ivec2 ivec3 ivec4 mat2 mat3 mat4
对于使用权限,shader又特别设定了三个属性:attribute,varying,uniform
对于attribute属性声明的变量,只可以在顶点着色器中定义和使用,对于uniform声明的变量在顶点着色器和片段着色器中都可以定义和使用,若是想用顶点着色器中使用attribute声明的变量在片段着色器中使用,则可以通过varying来声明一个变量,注意只要保证varying声明的变量在两个着色器中一模一样,且在顶点着色器中完成赋值,那么就可以在片段着色器中使用
对于attribute属性声明的变量,如果shader中确实使用了,那么就会给它分配一个位置,这个位置的分配也比较简单,从0开始,
从上到下,根据声明的位置进行递增,形如下边这样
'attribute vec4 a_position;' + loc:0
'attribute vec4 color;' + loc:1
'attribute mat4 matrix;' + loc:2
'attribute vec3 a_normal;' + loc:6
不过仔细看会发现从变量matrix到变量a_normal这中间的跨度是4,没错这确实是这样的,原因是matrix是一个mat4的变量,它包含一个4x4的矩阵,你可以把它看成一个包含四个vec4变量的结构体,实际上他原本就是一个结构体,根据内存对齐原则,你需要为这个结构体的每一个变量赋值,即第一行(2),第二行(3),第三行(4),第四行(5)
var matrixLoc = this._gl.getAttribLocation(this._spGLID,matrix);
//每一个矩阵是4行4列,每一个元素是一个Float32Array
const bytesPerMatrix = 4 * 16;
for (let i = 0; i < 4; ++i) {
const loc = matrixLoc + i;
gl.enableVertexAttribArray(loc);
// note the stride and offset
const offset = i * 16; // 4 floats per row, 4 bytes per float
gl.vertexAttribPointer(
loc, // location
4, // size (num values to pull from buffer per iteration)
gl.FLOAT, // type of data in buffer
false, // normalize
bytesPerMatrix, // stride, num bytes to advance to get to next set of values
offset, // offset in buffer
);
}
下面说的是一个比较简单的读数据,比如顶点数据
//找到显存中要操作的顶点数组
this._gl.bindBuffer(this._gl.ARRAY_BUFFER, glID);
//激活shader中顶点变量
this._gl.enableVertexAttribArray(this.a_position_loc);
//告诉GPU该如何赋值给顶点变量
//第一个参数index,变量地址
//第二个参数itemsize,就是从数组中一次性读取几个元素给这个变量,这个是有限制的一般是(1,2,3,4)
//第三个参数type,数组元素的类型,gl.FLOAT表示标准的32位浮点数,这里的类型和我们写数据是有关联的
//第五个参数stride参数,是描述这些顶点数据在数组中是如何分布的,如果为0则表示是紧密排列的
//第六个参数offset参数,表示第一个顶点数据要从顶点数组中哪一个位置开始读取,
this._gl.vertexAttribPointer(this.a_position_loc, itemSize, this._gl.FLOAT, false, 0, 0);
上面主要说了两个变量的读取操作,细心的人可能又有一点模糊,就是stride这个参数,对于矩阵变量mat4,这个stride的值被设置成了4416,而对于顶点变量确是0,
首先要明确一点,变量是从数组中取元素,其实也就是一次性取多少个,每个元素占多少个字节,你会发现其实顶点变量的数据是紧密排列的,顶点1取得是0-4,顶点2取得是5-8,顶点3取得是9-12,。。。依次类推
但是对于矩阵mat4来说,也可以这么取吗,理论上就应该这么取,但是GPU规定了每次只能给shader中的attribute属性定义的一个变量最多取四个数据,但是mat4是一个4x4的矩阵啊,需要16个元素,所以呢,它给mat4留了位置,即ma4(1),ma4(2),ma4(3),ma4(4),我们需要一个一个为它赋值,所以从外界开,矩阵mat4单元数据被硬性的分开了,4个位置被打包成一个变量的数据,当读取下一个变量的时候,应该偏移单个mat4的总体字节数
如果还是不理解,可以用指针来解释,用一个指针指向这个数组,指针++,读取下一个元素,当我们默认设置stride值为0时,其实就是默认步长是1,但是对于矩阵我们不能默认必须明确步长,另外数组元素类型都是基于字节的,如下图对应关系
gl.BYTE: signed 8-bit integer, with values in [-128, 127] 1个字节
有符号的8位整数,范围[-128, 127]
gl.SHORT: signed 16-bit integer, with values in [-32768, 32767] 2个字节
有符号的16位整数,范围[-32768, 32767]
gl.UNSIGNED_BYTE: unsigned 8-bit integer, with values in [0, 255] 1个字节
无符号的8位整数,范围[0, 255]
gl.UNSIGNED_SHORT: unsigned 16-bit integer, with values in [0, 65535] 2个字节
无符号的16位整数,范围[0, 65535]
gl.FLOAT: 32-bit IEEE floating point number 4个字节
32位IEEE标准的浮点数
When using a WebGL 2 context, the following values are available additionally:
使用WebGL2版本的还可以使用以下值:
gl.HALF_FLOAT: 16-bit IEEE floating point number 2个字节
16位IEEE标准的浮点数
2:对于shader程序中uniform属性定义的变量
uniform属性定义的变量在顶点着色器和片段着色器中都可以使用,常见的有空间坐标系mat4,光照vec4,纹理sampler2D等
使用纹理
第一步:上传纹理至显存
//在显存中创建一个纹理ID
this._glID = gl.createTexture();
//图片加载完以后,将图片纹理数据发往显存
//绑定缓冲
this._gl.bindTexture(this._target,this._glID)
//对图片纹理数据进行一些设置
//y轴取反
this._gl.pixelStorei(this._gl.UNPACK_FLIP_Y_WEBGL, true);
//将图片数据从内存发往显存
//这里上传的是纹理的数据类型是HTMLImageElement
this._gl.texImage2D(this._target,0, formatInfo.format,formatInfo.internalFormat,formatInfo.pixelType, image);
//放大
this._gl.texParameteri(this._target, this._gl.TEXTURE_MAG_FILTER,gltex_filter.LINEAR);
//缩小
this._gl.texParameteri(this._target, this._gl.TEXTURE_MIN_FILTER,gltex_filter.LINEAR);
//水平方向
this._gl.texParameteri(this._target, this._gl.TEXTURE_WRAP_S,this._wrapS);
//垂直方向
this._gl.texParameteri(this._target, this._gl.TEXTURE_WRAP_T,this._wrapT);
第二步:使用纹理
// 激活 0 号纹理单元
this._gl.activeTexture(this._gl[glTEXTURE_UNIT_VALID[pos]]);
// 指定当前操作的贴图
this._gl.bindTexture(this._gl.TEXTURE_2D, glID);
this._gl.uniform1i(this.u_texCoord_loc, pos);