WebGL 中批处理调用的最快方法

Posted

技术标签:

【中文标题】WebGL 中批处理调用的最快方法【英文标题】:The Fastest Way to Batch Calls in WebGL 【发布时间】:2013-03-11 19:25:50 【问题描述】:

我正在尝试为我的 2d 游戏引擎重写基于画布的渲染。我已经取得了很好的进展,可以很好地将纹理渲染到 webgl 上下文,完成缩放、旋转和混合。但是我的表现很糟糕。在我的测试笔记本电脑上,我可以在原版 2d 画布中获得 30 fps,同时屏幕上显示 1,000 个实体;在 WebGL 中,我得到 30 fps,屏幕上有 500 个实体。我希望情况会发生逆转!

我暗自怀疑罪魁祸首就是我乱扔的Float32Array 缓冲区垃圾。这是我的渲染代码:

// boilerplate code and obj coordinates

// grab gl context
var canvas = sys.canvas;
var gl = sys.webgl;
var program = sys.glProgram;

// width and height
var scale = sys.scale;
var tileWidthScaled = Math.floor(tileWidth * scale);
var tileHeightScaled = Math.floor(tileHeight * scale);
var normalizedWidth = tileWidthScaled / this.width;
var normalizedHeight = tileHeightScaled / this.height;

var worldX = targetX * scale;
var worldY = targetY * scale;

this.bindGLBuffer(gl, this.vertexBuffer, sys.glWorldLocation);
this.bufferGLRectangle(gl, worldX, worldY, tileWidthScaled, tileHeightScaled);

gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, this.texture);

var frameX = (Math.floor(tile * tileWidth) % this.width) * scale;
var frameY = (Math.floor(tile * tileWidth / this.width) * tileHeight) * scale;

// fragment (texture) shader
this.bindGLBuffer(gl, this.textureBuffer, sys.glTextureLocation);
this.bufferGLRectangle(gl, frameX, frameY, normalizedWidth, normalizedHeight);

gl.drawArrays(gl.TRIANGLES, 0, 6);

bufferGLRectangle: function (gl, x, y, width, height) 
    var left = x;
    var right = left + width;
    var top = y;
    var bottom = top + height;
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
        left, top,
        right, top,
        left, bottom,
        left, bottom,
        right, top,
        right, bottom
    ]), gl.STATIC_DRAW);
,

bindGLBuffer: function (gl, buffer, location) 
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
    gl.vertexAttribPointer(location, 2, gl.FLOAT, false, 0, 0);
,

这是我的简单测试着色器(缺少混合、缩放和旋转):

// fragment (texture) shader
precision mediump float;
uniform sampler2D image;
varying vec2 texturePosition;

void main() 
    gl_FragColor = texture2D(image, texturePosition);


// vertex shader
attribute vec2 worldPosition;
attribute vec2 vertexPosition;

uniform vec2 canvasResolution;
varying vec2 texturePosition;

void main() 
    vec2 zeroToOne = worldPosition / canvasResolution;
    vec2 zeroToTwo = zeroToOne * 2.0;
    vec2 clipSpace = zeroToTwo - 1.0;

    gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);
    texturePosition = vertexPosition;

关于如何获得更好性能的任何想法?有没有办法批量处理我的 drawArrays?有没有办法减少缓冲区垃圾?

谢谢!

【问题讨论】:

为了吸引更多感兴趣的人,我建议您将问题命名为“在 WebGL 上批处理 drawArrays 的最快方法” @AbrahamWalters:您是否正在为每个帧的每个实体运行精确的渲染代码? (又名 500 * 30 = 每秒 1500 次)如果是这样,我认为如果你让它坐在那里,标签/浏览器将在一小时内(如果不是十分钟)耗尽内存。 youtube.com/watch?v=rfQ8rKGTVlg 【参考方案1】:

我可以看到这里有两个大问题会对您的表现产生不利影响。

您正在创建许多临时的 Float32Arrays,目前构建起来很昂贵(将来应该会变得更好)。在这种情况下,创建一个数组并每次设置顶点会更好:

verts[0] = left; verts[1] = top;
verts[2] = right; verts[3] = top;
// etc... 
gl.bufferData(gl.ARRAY_BUFFER, verts, gl.STATIC_DRAW);

然而,到目前为止,更大的问题是您一次只能绘制一个四边形。 3D API 根本就不是为了有效地做到这一点而设计的。您想要做的是尝试将尽可能多的三角形压缩到您调用的每个 drawArrays/drawElements 中。

有几种方法可以做到这一点,最直接的方法是用尽可能多的共享相同纹理的四边形填充缓冲区,然后一次性将它们全部绘制出来。在伪代码中:

var MAX_QUADS_PER_BATCH = 100;
var VERTS_PER_QUAD = 6;
var FLOATS_PER_VERT = 2;
var verts = new Float32Array(MAX_QUADS_PER_BATCH * VERTS_PER_QUAD * FLOATS_PER_VERT);

var quadCount = 0;
function addQuad(left, top, bottom, right) 
    var offset = quadCount * VERTS_PER_QUAD * FLOATS_PER_VERT;

    verts[offset] = left; verts[offset+1] = top;
    verts[offset+2] = right; verts[offset+3] = top;
    // etc...

    quadCount++;

    if(quadCount == MAX_QUADS_PER_BATCH) 
        flushQuads();
    


function flushQuads() 
    gl.bindBuffer(gl.ARRAY_BUFFER, vertsBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, verts, gl.STATIC_DRAW); // Copy the buffer we've been building to the GPU.

    // Make sure vertexAttribPointers are set, etc...

    gl.drawArrays(gl.TRIANGLES, 0, quadCount + VERTS_PER_QUAD);


// In your render loop

for(sprite in spriteTypes) 
    gl.bindTexture(gl.TEXTURE_2D, sprite.texture);

    for(instance in sprite.instances) 
        addQuad(instance.left, instance.top, instance.right, instance.bottom);  
    

    flushQuads();

这过于简单化了,还有更多批处理的方法,但希望这能让您了解如何开始批处理调用以获得更好的性能。

【讨论】:

这很有意义。我当然可以批量处理地图图块和粒子之类的东西,但归根结底,我会在每一帧中绑定很多纹理。没有纹理图集有什么办法吗?【参考方案2】:

如果您使用 WebGL Inspector,您将在跟踪中看到您是否执行了任何不必要的 GL 指令(它们被标记为亮黄色背景)。这可能会让您了解如何优化渲染。

一般来说,对你的绘制调用进行排序,以便所有使用相同的程序,然后是属性,然后是纹理,最后是制服按顺序完成。这样,您将拥有尽可能少的 GL 指令(和 JS 指令)。

【讨论】:

以上是关于WebGL 中批处理调用的最快方法的主要内容,如果未能解决你的问题,请参考以下文章

WebGL递归处理和移动?旋转?缩放

从类中分解所有依赖项的最简单、最快的方法

在函数中执行循环多处理的最快方法?

处理大文件的最快方法?

在Python中处理大型文件的最快方法

创建批处理并将函数应用于python列表的内存高效且最快的方法