WebGL-实例化绘制

Posted giserYZ2SS

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了WebGL-实例化绘制相关的知识,希望对你有一定的参考价值。

今天来学习webgl一个重要功能:Instanced Drawing (实例化绘制),内容翻译自webgl学习网站webglfundamentals(由于英语水平尽量按原文翻译):https://webglfundamentals.org/webgl/lessons/webgl-instanced-drawing.html
WebGL下个版本(WebGL2.0)有一个功能叫实例化绘制。 它的基本思想是:一次绘制多个相同对象的效率要高于多次绘制同一个对象。这个功能在WebGL1.0标准可以通过扩展实现,目前大部分浏览器都支持这个功能。
首先我们来做一个示例:绘制同一个对象的多个实例。
我们以下面这两个shader代码开始:

·         <!-- vertex shader -->

·         <scriptid="3d-vertex-shader"type="x-shader/x-vertex">

·         attribute vec4 a_position;

·         uniform mat4 matrix;

·         void main(){

·           // Multiply the position by the matrix.

·           gl_Position= matrix* a_position;

·         }

·         </script>


·        <!-- fragment shader -->

·        <scriptid="3d-fragment-shader"type="x-shader/x-fragment">

·        precision mediump float;        

·        uniform vec4 color;

·        void main(){

·          gl_FragColor= color;

·        }

·        </script>

顶点着色器代码中实现每个顶点都乘以一个变换矩阵实现坐标转换,片元着色器中通过uniform变量传进一个颜色实现每个片元都是一样颜色。最终绘制需要编译和连接shader代码,关联attributeuniform变量位置:

·         const program= webglUtils.createProgramFromScripts(

·             gl,['3d-vertex-shader','3d-fragment-shader']);

·         const positionLoc= gl.getAttribLocation(program,'a_position');

·         const colorLoc= gl.getUniformLocation(program,'color');

·         const matrixLoc = gl.getUniformLocation(program,'matrix');

通过buffer关联所需顶点数据:

·        const positionBuffer= gl.createBuffer();

·        gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);

·        gl.bufferData(gl.ARRAY_BUFFER,newFloat32Array([

·            -0.1, 0.4,

·            -0.1,-0.4,

·             0.1,-0.4,

·             0.1,-0.4,

·            -0.1, 0.4,

·             0.1, 0.4,

·             0.4,-0.1,

·            -0.4,-0.1,

·            -0.4, 0.1,

·            -0.4, 0.1,

·             0.4,-0.1,

·             0.4, 0.1,

·          ]), gl.STATIC_DRAW);

·        const numVertices=12;

我们绘制5个相同(这里的相同指每个示例的形状大小一样)实例,那么就需要对应5个矩阵和5个颜色值:

·         const numInstances=5;

·         const matrices=[

·           m4.identity(),

·           m4.identity(),

·           m4.identity(),

·           m4.identity(),

·           m4.identity(),

·         ];       

·         const colors =[
·           [1,0,0,1,],  // red
·           [0,1,0,1,],  // green
·           [0,0,1,1,],  // blue
·           [1,0,1,1,],  // magenta
·           [0,1,1,1,],  // cyan
·         ];

渲染对象首先设置attribute对应的值,循环5次(这里绘制5个实例),每次设置不同的变换矩阵和颜色。

·         function render(time){

·           time *=0.001;// seconds        

·           gl.useProgram(program);      
·           // setup the position attribute
·           gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
·           gl.enableVertexAttribArray(positionLoc);
·           gl.vertexAttribPointer(
·               positionLoc,  // location
·               2,            // size (num values to pull from buffer per iteration)
·               gl.FLOAT,     // type of data in buffer
·               false,        // normalize
·               0,            // stride (0 = compute from size and type above)
·               0,            // offset in buffer
·           );
·           matrices.forEach((mat, ndx)=>{
·             m4.translation(-0.5+ ndx *0.25,0,0, mat);
·             m4.zRotate(mat, time *(0.1+0.1* ndx), mat);
·             const color = colors[ndx];         
·             gl.uniform4fv(colorLoc, color);
·             gl.uniformMatrix4fv(matrixLoc,false, mat);

·            gl.drawArrays(

·                 gl.TRIANGLES,

·                 0,            // offset

·                 numVertices,  // num vertices per instance

·             );

·           });          

·           requestAnimationFrame(render);
·         }
·         requestAnimationFrame(render);

这里我们注意到,在调用矩阵计算的数学库时,在每个方法传入已创建的矩阵对象,这样为了将计算结果保存下来以便后续使用。在大部分场景下我们不这样使用,而是直接用数学库新建一个矩阵。

这些工作完成后我们会得到5个形状大小一致、颜色和旋转角度不同的实例:

也打开以下网址可以查看效果:

http://39.106.0.97:8090/lesson/13Misc/07InstancedDrawing-01.html

我们看到上面实现过程每次调用了gl.uniform4vgl.uniformMatrix4fv、和gl.drawArrays,循环5次一共15次的WebGL调用命令。如果我们的shader程序复杂,就像 the article on spotlighting 例子中,每个实例至少调用7次:6gl.uniformXXX1gl.drawArrays。如果我们要绘制400个这样的实例那么我们就要调用2800WebGL命令。
实例化是减少这种调用命令次数的途径。它的工作可以让你告诉 WebGL你希望绘制多少相同的对象(实例化的个数)。在每次的顶点着色器shader执行时,对于每个attribute量可以通过标记的buffer数据来赋值。
之前我们使用uniform类型来给变换矩阵和颜色赋值,现在我们换成attribute类型。我们不再渲染一个实例就使用一个buffer对象传递变换矩阵和颜色的值,而是将他们一起放在buffer中使用attribute传递,并告诉WebGL,什么时候使用和切换。
让我们来实现这个流程!
第一件事就是要检测当前环境是否支持实例化。

·         const canvas = document.getElementById('canvas');

·         const gl = canvas.getContext('webgl');

·         if(!gl){

·           return;

·         }          

·         const ext = gl.getExtension('ANGLE_instanced_arrays');

·         if(!ext){

·           return alert('need ANGLE_instanced_arrays');

·         }

接下来,改变shader,用attribute代替uniform来传递变换矩阵和颜色。

·         <!-- vertex shader -->

·         <scriptid="3d-vertex-shader"type="x-shader/x-vertex">

·         attribute vec4 a_position;

·         uniform mat4 matrix;

·         attribute vec4 color;

·         attribute mat4 matrix;

·         varying vec4 v_color;
·         void main(){

·           // Multiply the position by the matrix.

·           gl_Position = matrix * a_position;

·           // Pass the vertex color to the fragment shader.

·           v_color = color;

·         }

·         </script>


·         <!-- fragment shader -->

·         <scriptid="3d-fragment-shader"type="x-shader/x-fragment">

·         precision mediump float;

·         uniform vec4 color;
·         // Passed in from the vertex shader.

·         varying vec4 v_color;

·         void main(){
·           gl_FragColor = color;
·           gl_FragColor = v_color;
·         }

·         </script>

因为attribute类型只能在顶点着色器中运行,所以我们要通过varying来将颜色值传递到片元着色器。

下一步关联attribute:

·         const program = webglUtils.createProgramFromScripts(

·             gl,['3d-vertex-shader','3d-fragment-shader']);

·         const positionLoc = gl.getAttribLocation(program,'a_position');
·         const colorLoc = gl.getUniformLocation(program,'color');
·         const matrixLoc = gl.getUniformLocation(program,'matrix');
·         const colorLoc = gl.getAttribLocation(program,'color');
·         const matrixLoc = gl.getAttribLocation(program,'matrix');


现在我们需要一个buffer对象保存所有变换矩阵,随后会通过attribute使用。

 
   
   
 
·         // setup matrixes, one per instance
 
   
   
 
·         const numInstances =5;
 
   
   
 
·         // make a typed array with one view per matrix
 
   
   
 
·         const matrixData =newFloat32Array(numInstances *16);
为每一个变换矩阵创建 Float32Array 对象

·         const matrices =[

·           m4.identity(),

·           m4.identity(),

·           m4.identity(),

·           m4.identity(),

·           m4.identity(),

·         ];

·         const matrices =[];

·         for(let i =0; i < numInstances;++i){

·           const byteOffsetToMatrix = i *16*4;

·           const numFloatsForView =16;

·           matrices.push(newFloat32Array(

·               matrixData.buffer,

·               byteOffsetToMatrix,

·               numFloatsForView));

·         }

通过这种方式,我们可以对所有转换矩阵数据做映射,我们可以通过matrices[ndx]单独使用每个转换矩阵。同时需要在GPU中为这些数据创建buffer。我们仅需要分配这些buffer:

·         const matrixBuffer = gl.createBuffer();

·         gl.bindBuffer(gl.ARRAY_BUFFER, matrixBuffer);

·         // just allocate the buffer

·         gl.bufferData(gl.ARRAY_BUFFER, matrixData.byteLength, gl.DYNAMIC_DRAW);

注意到,我们最后一个参数传递gl.DYNAMIC_DRAW,这是告诉WebGL我们要经常改变这个值。下一步,我们需要为颜色创建一个buffer,这个值不需要改变,至少这个例子中不需要改变,只需要加载这个数据即可。

·         const colors =[

·           [1,0,0,1,],  // red

·           [0,1,0,1,],  // green

·           [0,0,1,1,],  // blue

·           [1,0,1,1,],  // magenta

·           [0,1,1,1,],  // cyan

·         ];

·         // setup colors, one per instance

·         const colorBuffer = gl.createBuffer();

·         gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);

·         gl.bufferData(gl.ARRAY_BUFFER,

·             newFloat32Array([

·                 1,0,0,1,  // red

·                 0,1,0,1,  // green

·                 0,0,1,1,  // blue

·                 1,0,1,1,  // magenta

·                 0,1,1,1,  // cyan

·               ]),

·             gl.STATIC_DRAW);

在渲染时,之前做法是针对每个实例对象设置转换矩阵和颜色对应的uniform,循环渲染,在渲染前计算出每个转换矩阵。

·         // update all the matrices

·         matrices.forEach((mat, ndx)=>{

·           m4.translation(-0.5+ ndx *0.25,0,0, mat);

·           m4.zRotate(mat, time *(0.1+0.1* ndx), mat);

·           const color = colors[ndx];

·           gl.uniform4fv(colorLoc, color);
·           gl.uniformMatrix4fv(matrixLoc,false, mat);
·           gl.drawArrays(
·               gl.TRIANGLES,
·               0,             // offset
·               numVertices,   // num vertices per instance
·           );
·         });

因为matrices对象关联matrixData对象的,当所有matrix计算完成,转换矩阵数据就会加载到GPU中。

·         // upload the new matrix data

·         gl.bindBuffer(gl.ARRAY_BUFFER, matrixBuffer);

·         gl.bufferSubData(gl.ARRAY_BUFFER,0, matrixData);

现在我们需要关联转换矩阵和颜色的attribute。转换矩阵的attributemat4类型,一个mat4实际上使用4attribute量。

·         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 line says this attribute only changes for each 1 instance

·           ext.vertexAttribDivisorANGLE(loc,1);

·         }

实例化中最重要的是调用ext.vertexAttribDivisorANGLE接口。他的作用是告知该实例以后渲染的都使用这个值。即第一个实例渲染时使用第一个转换矩阵的attribute值,第二实例渲染时使用第二个转换矩阵的attribute值,以此类推。

同样的,关联颜色attribute:

·         // set attribute for color

·         gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);

·         gl.enableVertexAttribArray(colorLoc);

·         gl.vertexAttribPointer(colorLoc,4, gl.FLOAT,false,0,0);

·         // this line says this attribute only changes for each 1 instance

·         ext.vertexAttribDivisorANGLE(colorLoc,1);

需要记住的是,如果我们需要在其它地方使用这两个attribute的话就要设置divisor,此外需要重置它们到默认值。

最后,我们可以通过一个绘制命令绘制所有实例。

·         ext.drawArraysInstancedANGLE(

·           gl.TRIANGLES,

·           0,             // offset

·           numVertices,   // num vertices per instance

·           numInstances,  // num instances

·         );

打开以下链接查看效果:
http://39.106.0.97:8090/lesson/13Misc/07InstancedDrawing-02.html

在第一个示例中每个图形需要调用三次WebGL命令,5个图形就是15次。在这个例子我们仅仅调用2次命令绘制所有图形:一次是加载matrices,一次是绘制。在开始绘制前需要对转换矩阵和颜色进行一些操作。我们可以通过vertex array object将这些操作从绘制过程移到初始化的过程。

最后强调一点,也是我一直习惯的做法,虽然这不是这篇文章要介绍的内容。上面的代码没有考虑canvas的因素,没有使用projection matrix 或者 view matrix来进行从模型坐标到裁剪坐标转换,如果你需要这样的转换就要在javascript中计算。这样的话就增加了JavaScript的计算量,更高效的做法就是用uniform传入shader来计算。

·         <!-- vertex shader -->

·         <scriptid="3d-vertex-shader"type="x-shader/x-vertex">

·         attribute vec4 a_position;

·         attribute vec4 color;

·         attribute mat4 matrix;

·         uniform mat4 projection;

·         uniform mat4 view;    

·         varying vec4 v_color;         
·         void main(){
·           // Multiply the position by the matrix.
·           gl_Position = matrix * a_position;
·           gl_Position = projection * view * matrix * a_position;
·           // Pass the vertex color to the fragment shader.
·           v_color = color;
·         }

·         </script>

在初始化时关联

·         const positionLoc = gl.getAttribLocation(program,'a_position');

·         const colorLoc = gl.getAttribLocation(program,'color');

·         const matrixLoc = gl.getAttribLocation(program,'matrix');

·         const projectionLoc = gl.getUniformLocation(program,'projection');

·         const viewLoc = gl.getUniformLocation(program,'view');

渲染时在合适的地方传值

·         gl.useProgram(program);

·         // set the view and projection matrices since
·         // they are shared by all instances
·         const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
·         gl.uniformMatrix4fv(projectionLoc,false,
·             m4.orthographic(-aspect, aspect,-1,1,-1,1));
·         gl.uniformMatrix4fv(viewLoc,false, m4.zRotation(time *.1));


打开以下链接查看效果:
http://39.106.0.97:8090/lesson/13Misc/07InstancedDrawing-03.html

以上三个示例代码都上传git:
https://github.com/YanzheZhang/WebGL.Fundamentals.DEMO/blob/master/lesson/13Misc/07InstancedDrawing-01.html
https://github.com/YanzheZhang/WebGL.Fundamentals.DEMO/blob/master/lesson/13Misc/07InstancedDrawing-02.html
https://github.com/YanzheZhang/WebGL.Fundamentals.DEMO/blob/master/lesson/13Misc/07InstancedDrawing-03.html
学习交流小伙伴公众号 giserYZ2SS直接留言。

以上是关于WebGL-实例化绘制的主要内容,如果未能解决你的问题,请参考以下文章

WebGL纹理颜色原理

WebGL 纹理颜色原理

创建片段而不从 java 代码实例化它

WebGL 单通道wireframe渲染

片段事务中的实例化错误

webgl 系列 —— 渐变三角形