计算一张图片在内存中的大小
var bytes = (width * height * channels *8)/8
每一个像素点需要几个通道来存,对于png而言他自己有rgba四个通道,每一个通道需要多少个二进制,
所以在内存中需要(width * height * channels *8)个二进制
如果转换为字节数,还需要除以8 8个二进制表示一个字节
第一步加载到一张图片到内存
var img = new Image();
img.onload = function(img){
//加载完成回调
//此处的img就是你加载到内存的图像数据HTMLImageElement
//使用这个img可以发到GPU,
}.bind(this,img)
//设置你的图片路径
img.src = arr[j];
第二步从内存中将图像数据发往GPU,这个时候会在显存中重新生成一份GPU可以识别的纹理信息,注意我这里说的是重新生成,考虑到cpu这边的内存成本,你还必须选择性的干掉加载到在内存中的无用的图像数据,下面会有提到如何删除
应用1:市面上经常说要加快图片的加载速度,使用压缩的纹理,其实就是不使用texImage2D这个函数来加载,因为这个函数会对内存中的纹理数据再按照传来的参数重新生成GPU的纹理数据,而且从本地加载到内存的数据还需要展开,这两个过程比较耗时,如果是压缩过数据,那么就会直接调用glCompressedTexImage2D,不会有前面说的那两个过程,所以加载的速度比较块,而且纹理经过压缩,体积变得更小了,也降低了内存,当然我说的是加载到内存中的纹理的大小,不是指图片文件的大小,我们使用压缩工具(package或者android sdk里的工具)对图片进行纹理压缩,通常纹理文件的大小是大于图片文件的大小的,但是纹理文件加载到内存中,不需要展开,不需要转换为GPU识别的纹理,直接发给GPU,也就是说你的纹理文件大小就是内存中纹理文件的大小,最后,还可以对纹理文件进行ccz压缩,这又可以大幅度降低纹理文件的大小,降低包的体积,加载到内存,解压缩即可,ios一般是pvr,android是etc1和etc2,etc1不支持透明度,可以通过写个简单shader来解决
应用2:显存中的纹理数据就是我们在cpu端对她的控制其实就是通过(this._glID = gl.createTexture())这里面glID,利用这个变量可以实现对显存中的纹理绑定和删除
private onLoadImageFinish(image:HTMLImageElement):void{
// this._gl.activeTexture(this._gl.TEXTURE0);
console.log("纹理信息-------",image.width,image.height,image);
// 指定当前操作的贴图
this._gl.bindTexture(this._gl.TEXTURE_2D,this._glID);
// Y 轴取反
this._gl.pixelStorei(this._gl.UNPACK_FLIP_Y_WEBGL, true);
// 创建贴图, 绑定对应的图像并设置数据格式
// this._gl.texImage2D(
// this._gl.TEXTURE_2D,
// 0, // 就是这个参数指定几级 Mipmap
// this._gl.RGBA,
// this._gl.RGBA,
// this._gl.UNSIGNED_BYTE,
// image);
//256*256 p(gpu内存) = width * height * 4 /1024 = 256k
this._gl.texImage2D(this._gl.TEXTURE_2D,0, this._gl.RGBA, this._gl.RGBA, this._gl.UNSIGNED_BYTE, image);
//256*256 p(gpu内存) = width * height * 3 /1024 =342 - 342/4 = 192k 相当于内存减少1/4
// this._gl.texImage2D(this._gl.TEXTURE_2D, 0, this._gl.RGB,this._gl.RGB, this._gl.UNSIGNED_BYTE, image);
// 生成 MipMap 映射
var isOpenMipMap = true;
// 首先要调用此方法
// 要在texImage2D 后调用,否则会报错error:GL_INVALID_OPERATION gl.generateMipmap(this._gl.TEXTURE_2D)
//如果开启此技术对于256*256这个贴图 它的内存占用会比原来多出三分之一
//256*256 p(gpu内存) = (width * height * 4 /1024)*(4/3) =342
//能够使用这个技术的图片的宽高必须是2的幂
//此技术开启以后,会生成以下级别的图片,256*256这个是0级
//级别:128*128(1),64*64(1),32*32(1),16*16(1),8*8(1),4*4(1),2*2(1),1*1(1)
//实时渲染时,根据采样密度选择其中的某一级纹理,以此避免运行时的大量计算
if(isOpenMipMap&&isPow2(image.width)&&isPow2(image.height))
{
// this._gl.hint(this._gl.GENERATE_MIPMAP_HINT, this._gl.NICEST);
this._gl.generateMipmap(this._gl.TEXTURE_2D);
}
else if(isOpenMipMap)
{
console.warn('NPOT textures do not support mipmap filter');
isOpenMipMap = false;
}
//特别注意
if(isPow2(image.width)==false||isPow2(image.height)==false)
{
console.warn('WebGL1 doesn\'t support all wrap modes with NPOT textures');
}
/**
* MIN_FILTER 和 MAG_FILTER
* -------------对于纹理的放大
* 一个纹理是由离散的数据组成的,比如一个 2x2 的纹理是由 4 个像素组成的,使用 (0,0)、(0, 1) 等四个坐标去纹理上取样,自然可以取到对应的像素颜色;
* 但是,如果使用非整数坐标到这个纹理上去取色。比如,当这个纹理被「拉近」之后,在屏幕上占据了 4x4 一共 16 个像素,
* 那么就会使用 (0.33,0) 之类的坐标去取值,如何根据离散的 4 个像素颜色去计算 (0.33,0) 处的颜色,就取决于参数 MAG_FILTER
* MAG_FILTER(放大) 有两个可选项,NEAREST 和 LINEAR。
* 顾名思义,NEAREST 就是去取距离当前坐标最近的那个像素的颜色,而 LINEAR 则会根据距离当前坐标最近的 4 个点去内插计算出一个数值
* NEAREST:速度快,但图片被放的比较大的时候,图片的颗粒感会比较明显
* LINEAR: 速度慢点,但图片会显示的更顺滑一点
* -------------对于纹理的缩小
* MIN_FILTER(缩小) 有以下 6 个可选配置项:
* NEAREST
* LINEAR
* NEAREST_MIPMAP_NEAREST
* NEAREST_MIPMAP_LINEAR
* LINEAR_MIPMAP_NEAREST
* LINEAR_MIPMAP_LINEAR
* 前两个配置项和 MAG_FILTER 的含义和作用是完全一样的。
* 但问题是,当纹理被缩小时,原纹理中并不是每一个像素周围都会落上采样点,这就导致了某些像素,完全没有参与纹理的计算,新纹理丢失了一些信息。
* 假设一种极端的情况,就是一个纹理彻底缩小为了一个点,那么这个点的值应当是纹理上所有像素颜色的平均值,这才比较合理。
* 但是 NEAREST 只会从纹理中取一个点,而 LINEAR 也只是从纹理中取了四个点计算了一下而已。这时候,就该用上 MIPMAP 了
*
* 为了在纹理缩小也获得比较好的效果,需要按照采样密度,选择一定数量(通常大于 LINEAR 的 4 个,极端情况下为原纹理上所有像素)的像素进行计算。
* 实时进行计算的开销是很大的,所有有一种称为 MIPMAP(金字塔)的技术。
* 在纹理创建之初,就为纹理创建好 MIPMAP,比如对 512x512 的纹理,依次建立 256x256(称为 1 级 Mipmap)、128x128(称为 2 级 Mipmap) 乃至 2x2、1x1 的纹理。
* 实时渲染时,根据采样密度选择其中的某一级纹理,以此避免运行时的大量计算
*/
// 设定参数, 放大滤镜和缩小滤镜的采样方式
//放大
this._gl.texParameteri(this._gl.TEXTURE_2D, this._gl.TEXTURE_MAG_FILTER, this._gl.LINEAR);
//缩小
//一旦使用(NEAREST_MIPMAP_NEAREST,NEAREST_MIPMAP_LINEAR,LINEAR_MIPMAP_NEAREST,LINEAR_MIPMAP_LINEAR)
//说明就要使用mipmap了啊
this._gl.texParameteri(this._gl.TEXTURE_2D, this._gl.TEXTURE_MIN_FILTER, this._gl.LINEAR_MIPMAP_LINEAR);
// 设定参数, x 轴和 y 轴为镜面重复绘制
//纹理的填充模式
/**
* gl.REPEAT
* gl.CLAMP_TO_EDGE
* gl.MIRRORED_REPEAT
*/
//水平方向
this._gl.texParameteri(this._gl.TEXTURE_2D, this._gl.TEXTURE_WRAP_S, this._gl.MIRRORED_REPEAT);
//垂直方向
this._gl.texParameteri(this._gl.TEXTURE_2D, this._gl.TEXTURE_WRAP_T, this._gl.MIRRORED_REPEAT);
// 清除当前操作的贴图
this._gl.bindTexture(this._gl.TEXTURE_2D, null);
}
第三步在内存中干掉图像缓存数据img
public releaseCPUMemoryForImageCache(img:HTMLImageElement):void{
img.src = "";
img = null;
}
第四步在显存中干掉纹理数据
public destroy() {
if (this._glID === _nullWebGLTexture) {
console.error('The texture already destroyed');
return;
}
this._gl.deleteTexture(this._glID);
this._glID = _nullWebGLTexture;
}
查看工具
打开chrome浏览器:shift+esc,打开任务管理器,选中图片缓存和GPU内存,图片缓存指的是cpu内存这边从本地将图片加载到内存的纹理数据,GPU内存指的是将内存中的纹理数据发送到GPU显存重新生成的纹理数据,
题外话—上传到GPU的数据
顶点信息:顶点的位置,顶点的颜色,顶点的法线,顶点的uv坐标, 顶点的切线,模型对应的纹理,模型的pvm矩阵,光照
通常调用gl.createBuffer(),创建一个glID,这个是一个显存内存的标识id,我们可以把这个id绑定到GPU的操作缓冲上,然后再给这个glID绑定一个真实的数据,下次我们如果想使用这个数据的时候,只需要用这个glID来绑定这个缓冲,这个时候进行一些取值操作就可以了,下面对于GPU的读写我会做一个更为详细的说明
one:我们上传给GPU的数据,GPU那边都会创建一个buffer来存储,返回一个glID表示,注意这个glID是灵魂
this._glID = gl.createBuffer();//用于纹理
this._glID = gl.createTexture();//用于立方体纹理
two:GPU是一个强大的状态机,我们外界GPU进行读写数据的时候都会有一个骚操作,bindbuffer
//顶点 法线 uv 切线
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this._glID);
//索引
this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, this._glID);
three:当绑定完换冲以后,就可以开始写入操作了
//顶点 法线 uv 切线等
this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(this.sourceData), this.gl.STATIC_DRAW);
//索引
this.gl.bufferData(this.gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(this.sourceData), this.gl.STATIC_DRAW);
four:当渲染的时候,就会开始读取操作了,其实读取就是往shader中的变量赋值
//将buffer中数据和shader中变量建立读取联系
this._gl.bindBuffer(this._gl.ARRAY_BUFFER, glID);
this._gl.vertexAttribPointer(this.a_position_loc, itemSize, this._gl.FLOAT, false, 0, 0);
//下面是纹理的赋值
/**
* activeTexture必须在bindTexture之前。如果没activeTexture就bindTexture,会默认绑定到0号纹理单元
*/
// 激活 0 号纹理单元
this._gl.activeTexture(this._gl[glTEXTURE_UNIT_VALID[pos]]);
// 指定当前操作的贴图
this._gl.bindTexture(this._gl.TEXTURE_2D, glID);
if (this.checklocValid(this.u_texCoord_loc)) {
this._gl.uniform1i(this.u_texCoord_loc, pos);
}
five:开始渲染
我们可以利用事先传来的索引数据来绘制,对于绘制的类型也有很大种
其实所谓索引绘制,就是把顶点抽象出来,为每一个顶点设置一个id,然后用这个id组成一个数组,GPU会根据这个id数组按照顺序和绘制的一些参数,来绘制这些顶点
/*
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 扇形绘制方式
*/
this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, this.getGLID(SY.GLID_TYPE.INDEX));
this.gl.drawElements(this._glPrimitiveType, this.getBuffer(SY.GLID_TYPE.INDEX).itemNums, this.gl.UNSIGNED_SHORT, 0);
six:小结
我们上传到GPU的数据,GPU会生成一个glID来保存这些数据,所以如果我们没有改变数据的话,那数据是不需要重新上传到GPU的,渲染的时候,通过glID,我们就可以操作老的数据