开发模式
creator是组件式开发
显示节点与渲染组件
对于所有可以显示的节点即Node,它也有一个父类BaseNode,他们的脚本均继承一个类RenderComponent
Node:包含的功能主要是记录位置,大小,缩放,旋转,尺寸,倾斜,透明度,颜色,渲染层级,遮罩层级,欧拉角,锚点等渲染的基础数据。
BaseNode:主要是管理孩子的继承,组件的继承,名字,uuid,预制体,节点的激活状态。
RenderComponent:主要是管理材质,持有一个是否为脏数据的flag,持有一个操作基础渲染数据的类assember,每一个渲染脚本都会有一个特定的数据管理类assember
小结一下
我们在代码中,对节点CCNode的渲染信息做一些改变,诸如旋转,缩放,平移,这些改变不会立刻触发渲染,CCNode的内部会有一个数组变量(this._trs),会把新的信息存到这个数组里,并且标记当前的本地矩阵和世界矩阵都是脏的,意味着要更新,等到下一帧要收集渲染信息的时候,遍历渲染树,找到这个节点,根据这棵树上的标志,更新本地矩阵和世界矩阵,最后渲染,利用矩阵去重新计算顶点的位置,并且将顶点的数据放到顶点缓冲中
数据批处理ModelBatcher
该类是个单例,负责批量处理drawcall数据
成员变量:_quadBuffer,_meshBuffer,_quadBuffer3D,_meshBuffer3D:这几种buffer都是预先创建的
成员变量_renderScene:这个是给外部调用的,这里面存储了一帧需要的所有drawcall数据,可以把他理解为一个drawcall数组,这个数组有多少元素,那就有多少drawcall
成员变量_batchedModels:这个和_renderScene一样的,只是给内部使用
成员函数_flushMaterial(),_flush(),_flushIA(),这三个函数都是负责往_renderScene里压入不同类型drawcall数据的,外界一旦调用该函数表明需要重新生成一次drawcall
成员函数terminate():此函数一旦调用,就说明数据的填充工作buffer结束了,开始了buffer上传
**成员函数getBuffer (type, vertextFormat):**在外部可以通过此函数来获取buffer,进而向buffer中填充数据
渲染车间RenderFlow
/*
此处完成一帧的渲染
scene:显示节点的根节点
*/
RenderFlow.render = function (scene, dt) {
//数据批处理类重置
_batcher.reset();
//数据批处理类开始工作
_batcher.walking = true;
//访问场景树
RenderFlow.visitRootNode(scene);
//数据批处理类处理完数据,开始上传数据
_batcher.terminate();
//数据批处理类工作完成
_batcher.walking = false;
_forward.render(_batcher._renderScene, dt);
};
检查是否可以合批
//特别注意,每一个纹理都会为它生成一个特有的hash值,这个hash值的生成主要依靠下面这些数据
var hash = Number(${minFilter}${magFilter}${pixelFormat}${wrapS}${wrapT}${genMipmaps}${premultiplyAlpha}${flipY}
)
哈希的主要组成部分是:纹理缩小,纹理放大,像素格式,纹理水平,纹理竖直,mipmap,预乘alpha,y轴倾斜
//此处是render_flow的_render函数
//参数node:渲染节点
_proto._render = function (node) {
let comp = node._renderComponent;
comp._checkBacth(_batcher, node._cullingMask);
comp._assembler.fillBuffers(comp, _batcher);
this._next._func(node);
};
//此处是renderComponent的_checkBacth函数
//参数:renderer是modelBatcher
//参数:cullingMask是当前访问的渲染节点的遮罩层级
//如果当前节点的材质的哈希值不等于上一个节点的材质的哈希值
//或者当前节点的遮罩层级不等于上一个节点的遮罩层级
//那么就不可以合并drawcall,否则可以
_checkBacth (renderer, cullingMask) {
let material = this._materials[0];
if ((material && material.getHash() !== renderer.material.getHash()) ||
renderer.cullingMask !== cullingMask) {
renderer._flush();
renderer.node = material.getDefine('CC_USE_MODEL') ? this.node : renderer._dummyNode;
renderer.material = material;
renderer.cullingMask = cullingMask;
}
}
三种普通buffer和两种特别buffer
一meshBuffer:
内部持有vertexBuffer和indexBuffer
成员函数uploadData():这个函数主要的功能的是buffer上传,即把组织好的渲染顶点数据和索引数据,上传到GPU中,该函数每一帧都会被调用一次
二quadBuffer:
quadBuffer继承meshBuffer
三spineBuffer:
spineBuffer继承meshBuffer
1vertexBuffer:
持有一个_glID = device._gl.createBuffer(),这个是要绑定到GPU缓冲类型的缓冲对象
通过调用它的update()函数:完成缓冲类型和对象的绑定,缓冲对象和缓冲数据的绑定
gl.bindBuffer(gl.ARRAY_BUFFER, this._glID);
gl.bufferData(gl.ARRAY_BUFFER, data, glUsage);
gl.bufferSubData(gl.ARRAY_BUFFER, byteOffset, data);
2indexBuffer:
持有一个_glID = device._gl.createBuffer(),这个是要绑定到GPU缓冲类型的缓冲对象
通过调用它的update()函数:完成缓冲类型和对象的绑定,缓冲对象和缓冲数据的绑定
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this._glID);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, data, glUsage);
gl.bufferSubData(gl.ELEMENT_ARRAY_BUFFER, byteOffset, data);
图片纹理的来龙去脉
Sprite:它包含的逻辑可以让外界操作图片,其内部持有一个spriteFrame
spriteFrame:
Texture2D:
Texture:
creator 2.3.2 代码webgl流程
//CCGame.js:
_initRenderer()://初始化渲染
const renderer = require('./renderer/index.js');
renderer.initWebGL(localCanvas, opts);//初始化webgl
cc.renderer://这个是渲染器对象,单例
cc.renderer.initWebGL (canvas, opts);//初始化webgl的入口函数
//这个函数主要做了以下几件非常重要的事情
let Scene = require('../../renderer/scene/scene');
let ForwardRenderer = require('../../renderer/renderers/forward-renderer');
this.device = new gfx.Device(canvas, opts);//设备上下文,用于和opengl打交道
this.scene = new Scene();//生成一个场景数据,可以说这个scene里面存储了所有渲染数据
let builtins = _initBuiltins(this.device);
this._forward = new ForwardRenderer(this.device, builtins);//前向渲染
this._handle = new ModelBatcher(this.device, this.scene);//数据批处理
this._flow.init(this._handle, this._forward);//渲染车间
//每一帧都会调用这个函数,开始渲染
cc.renderer.render (ecScene, dt) {
this.device.resetDrawCalls();
if (ecScene) {
// walk entity component scene to generate models
this._flow.render(ecScene, dt);
this.drawCalls = this.device.getDrawCalls();
}
}
camera 摄像机
CCCamera.js:它本身也是一个脚本,挂在到一个可视化节点上,内部维护一个camera的数据处理类对象,我们可以在外部操作这个相机,比如讲一个相机节点加到场景中,然后在界面操作它的相关数据,最终这个数据会有它自己的数据类来处理
…/…/renderer/scene/camera.js:这个是相机的数据处理类,它的功能具体如下
1:计算矩阵:视口矩阵,投影矩阵,投影x视口矩阵,视口逆矩阵,投影逆矩阵,投影x视口逆矩阵
2:决定投影的类型:是正交投影还是透视投影
3:设置帧缓冲;
//CCNode.js
//获取世界矩阵
//节点的旋转,平移,缩放来生成
getWorldRT (out) {
let opos = _gwrtVec3a;
let orot = _gwrtQuata;
let ltrs = this._trs;
Trs.toPosition(opos, ltrs);
Trs.toRotation(orot, ltrs);
let curr = this._parent;
while (curr) {
ltrs = curr._trs;
// opos = parent_lscale * lpos
Trs.toScale(_gwrtVec3b, ltrs);
Vec3.mul(opos, opos, _gwrtVec3b);
// opos = parent_lrot * opos
Trs.toRotation(_gwrtQuatb, ltrs);
Vec3.transformQuat(opos, opos, _gwrtQuatb);
// opos = opos + lpos
Trs.toPosition(_gwrtVec3b, ltrs);
Vec3.add(opos, opos, _gwrtVec3b);
// orot = lrot * orot
Quat.mul(orot, _gwrtQuatb, orot);
curr = curr._parent;
}
Mat4.fromRT(out, orot, opos);
return out;
}
//camera.js
_calcMatrices (width, height) {
// view matrix
//通过node节点获取它的世界矩阵,然后翻转获取世界逆矩阵
//这里有点猫腻,明明获取的是世界逆矩阵,怎么就成了视口矩阵呢
//哈哈,其实这个节点就是摄像机啊
//可以这么理解,相机也是作为一个普通的显示节点放到世界中的,把相机想象成一个局部坐标系,相机中的点,可 //以通过乘世界矩阵来转换到世界坐标系中,反过来,世界中的点可以通过乘世界的逆矩阵放到节点中,即相机节点中,//那么这个世界逆矩阵不就是视口矩阵吗
this._node.getWorldRT(_matViewInv);
Mat4.invert(_matView, _matViewInv);
// projection matrix
let aspect = width / height;
//透视投影
if (this._projection === enums.PROJ_PERSPECTIVE) {
Mat4.perspective(_matProj,
this._fov,
aspect,
this._near,
this._far
);
}
//正交投影
else
{
let x = this._orthoHeight * aspect;
let y = this._orthoHeight;
Mat4.ortho(_matProj,
-x, x, -y, y, this._near, this._far
);
}
// view-projection
//投影矩阵*视口矩阵
Mat4.mul(_matViewProj, _matProj, _matView);
// inv view-projection
//逆矩阵
Mat4.invert(_matInvViewProj, _matViewProj);
}
scene:场景数据
…/…/renderer/scene/scene/scene.js
渲染数据管理类:
渲染树上所有节点的渲染信息
摄像机信息
灯光信息
绘制顺序
1、先渲染所有不透明物体,并开启它们的深度测试和写入。
2、把半透明物体按它们离摄像机远近排序,然后按照从后往前的顺序渲染,开启深度测试,关闭深度写入。
gl.depthMask(mask):这个函数可以实现深度缓冲区的写入控制,如果mask为true,则允许写入,如果mask为false,则禁止写入,试想一下,深度缓冲区不过是一个以屏幕宽高为大小的二位数组,每一个元素都代表着屏幕上一个具体坐标的像素点的深度值,我们之所以要先绘制不透明物体,那是因为不透明物体确实存在遮挡问题,且可以粗暴的去掉,比如在世界坐标系里有一个深度值为5和一个深度值为10的两个不透明的点,那我们会根据遮挡原则,将毫不犹豫的丢弃那个深度值为10的点,而向深度缓冲中写入一个深度值为5的点,所谓的深度测试,就是去比较当前要处理的点的深度值和深度缓冲区那个对应点的深度值,一般是深度越小就会保留,即写入深度缓冲中,但是这个只能针对不透明物体,对于透明物体,就不可以这么搞了,透明物体会有一个混合效果,就是说对于不透明的物体,即使深度信息靠前,也不能说去丢掉深度缓冲区里的深度值,所以是要把深度写入禁掉的,当然如果靠后那是可以丢掉的,所以说深度测试还是要开启的,你想啊,你有一个不透明的点放在了一个透明的点的前面,那我们还能看到那个透明的点吗??,所以混合只考虑靠前的不透明的点,然后不改变深度信息,只是去把颜色信息写入颜色缓冲中,当然关于如何混合,这个有具体的方案
启动设备(device.js)draw()
该函数调用一次,说明就要产生一次drawcall,cpu的数据准备工作已经OK了,即将启动顶点着色器
1 CPU准备数据-》
顶点信息:坐标,颜色,透明度,法线,uv坐标,纹理信息,shader程序
顶点的法线:是包含该顶点的所有三角形的法线的均值。这带来了不少便利–因为在顶点着色器中,我 们处理顶点,而不是三角形;所以最好把信息放在顶点上,物体的法线向量定义了它的表面在空间中的朝向,即,定义了表面相对于光源的方向。因为OpenGL是使用法线向量来确定一个物体表面的某个顶点所接受的光照的,从数学的角度而言,三维空间中的两个向量定义了一个平面。我们对这两个向量求叉积(V1×V2),其结果所产生的向量与这个平面垂直,也就是我们所需的法线向量
一般情况下:cpu还会进行粗粒度的剔除操作,即把不在显示范围里的对象删掉比如在2d的一块显示区域里,一些显示物体不在显示区域里,可以不发送给GPU渲染
unity中 mvp矩阵的计算方法:
// Projection matrix : 45° Field of View, 4: ratio, display range : 0.1 unit 100 units
glm::mat4 Projection = glm::perspective(glm::radians(45.0f), 4.0f / 3.0f, 0.1f, 100.0f);
// Camera matrix
glm::mat4 View = glm::lookAt(
glm::vec3(4,3,3), // Camera is at (4,3,3), in World Space
glm::vec3(0,0,0), // and looks at the origin
glm::vec3(0,1,0) // Head is up (set to 0,-1,0 to look upside-down)
);
// Model matrix : an identity matrix (model will be at the origin)
glm::mat4 Model = glm::mat4(1.0f); // Changes for each model !
// Our ModelViewProjection : multiplication of our 3 matrices
glm::mat4 MVP = Projection * View * Model; // Remember, matrix multiplication is the other way around
2 顶点着色器-》坐标变换
顶点着色器的主要工作是进行坐标变换,将顶点坐标从模型坐标变换到齐次裁切坐标系下
即 point = P(投影矩阵) * V(视图矩阵) * M(模型矩阵) * point;
此时顶点是处于其次裁切坐标系下,要变到屏幕坐标系下,还需要透视除法和视口裁切,只不过这两步是GPU产商自己做的,我们进行到上一步就好了
透视除法:把坐标的每一个分量除以w,你可以理解为这个w相当于屏幕距离投影仪的距离,一般情况w为1.0,这是一个标准的距离,经过透视除法就得到了NDC归一化坐标
视口裁切:我们拿到了归一化坐标,需要进行视口变换,才可以转换到屏幕坐标系下,一般通过下面这个函数来设置视口之间的转换关系,NDC其实是我们自己构造的屏幕坐标系,它需要和真实的屏幕坐标系来一个映射,可以1:1转换,也可自己设定任意比例
this.gl.viewport(x,y,w,h);
3 曲面着色器-》
4 几何着色器-》产生更多图元
5 光栅化-》产生更多的像素点
光栅化的目的,是找出一个几何单元(比如三角形)所覆盖的像素,就是计算出1920×1080这么长的RGB数组中,每一个RGB的值,粗略地讲:你模型的那些顶点在经过各种矩阵变换后也仅仅是顶点。而由顶点构成的三角形要在屏幕上显示出来,除了需要三个顶点的信息以外,还需要确定构成这个三角形的所有像素的信息。光栅化就是干这个的
光栅化会根据三角形顶点的位置,来确定需要多少个像素点才能构成这个三角形,以及每个像素点都应该得到哪些信息,比如uv坐标该是什么…等。这是通过对顶点数据进行插值来完成的
假定屏幕分辨率为1920×1080,在二维屏幕渲染(光栅化)时,内存中frame buffer只保存着1920×1080个屏幕点的颜色,然后一个一个的画到屏幕上。(它的实现方式是以一个1920×1080长的一维数组储存每个顶点的RGB颜色,然后遍历数组画出来)
什么X, Y, Z,什么alpha之类的frame buffer都没有的,在frame buffer里只有3个值:R, G, B。
X, Y, Z, alpha等等属性要在另外的地方存储。
处理一个几何图元的过程:假设设个图元是三角形,拿到三个顶点颜色,取平均值,算出它包围的所有像素点的颜色和uv坐标,注意这只是根据顶点颜色算出来的,最后的颜色还需要在片段着色器中加上纹理颜色和我们自己改写的颜色,
6 深度测试-》提前进行深度测试,不合格的就直接丢弃掉
开启深度测试:gl.enable(gl.DEPTH_TEST);
关闭深度测试:gl.disable(gl.DEPTH_TEST);
测试结果如何处理:
gl.depthFunc(gl.LESS);深度值较小通过测试,否则丢弃
可选值如下:
gl.NEVER 绝不通过
gl.LESS 小于通过
gl.EQUAL 等于通过
gl.LEQUAL 小于等于通过
gl.GREATER 大于通过
gl.NOTEQUAL 不等于通过
gl.GEQUAL 大于等于通过
gl.ALWAYS 总是通过
写入:gl.depthMask(true)
只读:gl.depthMask(false)
GPU会在显存中开辟一块内存来存储深度信息,其本质上以屏幕宽高为下表的二维数组在显示中会存在遮挡关系,所以这就需要一个值来记录屏幕中每一个可以被绘制的点的深度值,对于最终的显示结果而言,其实就是从颜色缓冲中取值,把每一个点的rgb值计算出来然后给屏幕的像素格子涂色,颜色缓冲和深度缓冲是相互对应的,他们分别记录当前像素点的颜色信息和深度信息,深度信息主要用于深度测试用的,对于不透明的物体,他们如果靠前,他们的深度值是不会被写入到深度缓冲区的,只是根据混合方案刷新了颜色缓冲
7 片元着色器-》着色
当经历过片元着色器以后就进入到逐片元操作了,这里必须要重点理解以下,通常我们启动顶点着色器以后,GPU就开始绘制工作,这个绘制是并发进行的,也就是说我们一次性给顶点着色器发多少个顶点,都是同时进行的,我们称这个过程为一次drawcall,那么这么多顶点经过顶点着色器以后,完成了坐标变换,就是从3d空间转变为屏幕坐标系,注意坐标变化的只是x和y,z坐标没有发生变化,接着进行光栅化,产生更多的片元,片元着色器给这些片元上色,这些片元能不能显示出来,还有下面这个事情要做,就是逐片元,假设我们一次drawcall给顶点着色器发100个点,光栅化以后产生10000个片元,那本次逐片元面对的将是这10000个点,不过不用担心,片元都有一个固定位置,这个位置就是屏幕上像素的坑,一个坑可能对应若干个片元,经过测试会决定让那个片元入坑,当然也有混合,你可以这么理解,逐片元就是屏幕横扫,扫描到一个像素,就会处理这个像素位置对应的所有片元,然后对他们进行测试,比如深度测试,z值小的留下,z值大的丢弃,
那么问题来了,如果你有两次drawcall,要完成一个模板测试,咋办?其实这的丢弃也不是那么简单,所以渲染最终像素的时候,也不是说就直接把颜色缓冲的数据直接拿去渲染,这里面还是有判断的
一次drawcall
阶段一: 一堆顶点数据 经过顶点着色器 光栅化 片段着色器-----》 一大堆片元----------》开始逐片元操作
我猜测:存在一个特殊的数组,以像素点为下表,数组中含有的元素称为一个data,这个data的内容是含有相同屏幕坐标不同z值的,当然z值也可以相同,遍历屏幕,其实就是遍历这个数组,取出数组中的每一个元素开始测试,通过测试的片元,将被写入帧缓冲区,这里的缓冲区包括颜色缓冲,深度缓冲,模板缓冲
模板测试:比较模板缓冲的值和片元的模板值
8 透明度测试-》
9 模板测试-》
主要用途:比如遮罩,多pass的模型边缘选中效果等
在相同的深度下,对于这些模型进行模板值比较
遮罩算法:用一个平面来作为一个mask,先渲染,设置遮罩值,接下来渲染要显示的模型,将其模板值设置一下,最后比对一下,至于要舍弃和留下,自行决定
边缘选中算法:绘制一个模型,shader用两个pass,第一个pass采用放大和纯色,第二个pass正常渲染,这样选中效果就出来了
10 深度测试-》
拿当前深度值,与深度缓存区的深度值比较,如果小于就写入,然后更新帧缓冲区
11 混合-》
12 剔除无需渲染的对象-》
13 颜色缓冲-》交换缓冲 显示图片
一般情况,这个缓冲区的大小就是设备分辨率,比如你的屏幕是1920X720,那么这个缓冲区的大小就是1920X720;屏幕的分辨率决定了有多少个方格,而在屏幕进行绘制的时候,即就是从这个颜色缓冲区中对应位置取像素点的颜色放到方格中显示。有的时候为了一些效果,比如抗锯齿,需要边缘模糊,也可能这个缓冲区的大小是设备分辨率的n倍,这个时候通过指定位置来进行比对采样就可以了
GPU和CPU交互的缓冲区:索引缓冲区,顶点缓冲区,纹理缓冲区,
GPU帧缓冲区:几个以屏幕长宽为下标存储一些数据的二维数组,Array[width][height],精准的标记了屏幕中每一个方格对应的像素信息(rgb,z,ref),其实绘制的信息只有rgb,至于深度值和模板值,这个是作为测试条件存在的,每一个像素点经过片元着色器以后就开始各种测试,在同一个屏幕的方格中,一次渲染可能又n个片元重合,他们需要取舍或者混合,最终一个方格里只能有一个片元留下,用当前片元来更新下面的缓冲区
1颜色缓冲区:
其实就是一个二位数组,里面放一个rgb值 2深度缓冲区:
其实就是一个二位数组,里面放一个z值 3模板缓冲区:
其实就是一个二位数组,里面放一个模板值 4累积缓冲区: 未知
背面剔除操作:,发生在光栅化阶段,在对一个三角形进行光栅化的时候,首先要判断是否要剔除
在WebGL中,三角形的一个重要属性是顶点绘制顺序。三角形的顶点绘制顺序是逆时针(CounterClockWise,CCW)或者顺时针(ClockWise,CW)。当三角形按顶点的逆时针顺序构建时,我们称它的绘制顺序为逆时针,当三角形按顶点的顺时针顺序构建时,我们称它的组绕顺序为顺时针,绘制顺序之所以重要,是因为它决定了三角形的面是否朝向观察者。朝向观察者的三角形为正面三角形,否则为背面三角形。在许多情形中,WebGL不需要对背面三角形进行光栅化处理。例如,如果在某个场景中观察者看不见一些对象的背面,则可以指示WebGL剔除这些无法看到的面。很容易调用下面的3个方法来实现这一点:
gl.frontFace(gl.CCW);
gl.enable(gl.CULL_FACE);// CULL意为“剔除”
gl.cullFace(gl.BACK);
第一个方法告诉WebGL采用逆时针顺序绘制的三角形是正面三角形。即使不调用gl.frontFace()方法,WebGL默认也会采用这种处理方式。第二个方法是激活面剔除功能,默认情况下这个功能处于禁用状态,因此需要调用gl.enable(gl.CULL_FACE)方法启动它。第三个方法告诉WebGL剔除背面三角形。即使没有调用gl.cullFace()方法,背面剔除也是WebGL的默认处理方式。
如果一个场景由许多对象组成,它们的背面对用户是不可见的,则最好激活背面剔除功能。这可以改善WebGL应用程序的性能,因为GPU不需要对不可见的三角形进行光栅化处理
draw(base, count) {
const gl = this._gl;
let cur = this._current;
let next = this._next;
// commit blend
_commitBlendStates(gl, cur, next);
// commit depth
_commitDepthStates(gl, cur, next);
// commit stencil
_commitStencilStates(gl, cur, next);
// commit cull
_commitCullMode(gl, cur, next);
// commit vertex-buffer
_commitVertexBuffers(this, gl, cur, next);
// commit index-buffer
if (cur.indexBuffer !== next.indexBuffer) {
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, next.indexBuffer && next.indexBuffer._glID !== -1 ? next.indexBuffer._glID : null);
}
// commit program
let programDirty = false;
if (cur.program !== next.program) {
if (next.program._linked) {
gl.useProgram(next.program._glID);
} else {
console.warn('Failed to use program: has not linked yet.');
}
programDirty = true;
}
// commit texture/sampler
_commitTextures(gl, cur, next);
// commit uniforms
for (let i = 0; i < next.program._uniforms.length; ++i) {
let uniformInfo = next.program._uniforms[i];
let uniform = this._uniforms[uniformInfo.name];
if (!uniform) {
// console.warn(`Can not find uniform ${uniformInfo.name}`);
continue;
}
if (!programDirty && !uniform.dirty) {
continue;
}
uniform.dirty = false;
// TODO: please consider array uniform: uniformInfo.size > 0
let commitFunc = (uniformInfo.size === undefined) ? _type2uniformCommit[uniformInfo.type] : _type2uniformArrayCommit[uniformInfo.type];
if (!commitFunc) {
console.warn(`Can not find commit function for uniform ${uniformInfo.name}`);
continue;
}
commitFunc(gl, uniformInfo.location, uniform.value);
}
if (count) {
// drawPrimitives
if (next.indexBuffer) {
gl.drawElements(
this._next.primitiveType,
count,
next.indexBuffer._format,
base * next.indexBuffer._bytesPerIndex
);
} else {
gl.drawArrays(
this._next.primitiveType,
base,
count
);
}
// update stats
this._stats.drawcalls++;
}
// TODO: autogen mipmap for color buffer
// if (this._framebuffer && this._framebuffer.colors[0].mipmap) {
// gl.bindTexture(this._framebuffer.colors[i]._target, colors[i]._glID);
// gl.generateMipmap(this._framebuffer.colors[i]._target);
// }
// reset states
cur.set(next);
next.reset();
}