First (nearly) working batch drawing test. Instead of pushing rectangles as pairs of triangles individually, this version creates a vertex buffer object for all visible tile rectangles, with degenerate triangles separating each one from the next. The drawing is done by a single call to gl.drawArrays.

Bizarrely, this version still chokes in Firefox, implying that the previous demo's horrible performance was not down to the GPU blocking as the rectangles are sent to it.

The test is not fully working because it does not scroll the drawing position. So instead of the map moving when you hit a scroll boundary, it simply stops drawing the top or left edges as the scroll region moves away from the visible window.  It's an easy fix, but I think I'll leave it until I find out exactly why Firefox still chokes on this demo.

Progress text file updated with latest experiments and progress today.
This commit is contained in:
Pete Baron 2016-05-23 17:48:54 +12:00
parent 55870bce8a
commit 1b7eaadd06
3 changed files with 150 additions and 35 deletions

View file

@ -62,8 +62,19 @@ tile properties - works perfectly
* tilemap ray cast - draws map but not debug layer, impossible to tell if raycast is working without debug * tilemap ray cast - draws map but not debug layer, impossible to tell if raycast is working without debug
tileset from bitmapdata - works perfectly tileset from bitmapdata - works perfectly
* demos that fail for other reasons that 'tiles out by one' * demos that fail for other reasons than 'tiles out by one'
create from objects: looks like the actual 'createFromObjects' call is working perfectly (coins). The failure is due to the lack of ability to handle multiple tile source images per tilemap. Looking at features_test.json (the map source) it appears that there are multiple layers with different sources... I'll start by supporting multiple layers.
With some help from Rich, finally discovered the Tileset class and it's firstgid value. Also discovered the 'draw' function in there which is hard-wired to Canvas drawing. Options: spin off TilesetGl class, or create drawGl function and branch on every draw call, or duplicate the firstgid logic in my new GL drawing code. I prefer the third option. At some point that draw in Tileset should be moved because Tileset is a data-holder class and shouldn't really contain drawing logic (despite how convenient it is to put it there to access firstgid etc from Tileset). My rationale for this separation is that rendering for browsers is very similar to writing game for old-school consoles - we separated the code that touches hardware from the logic code, because then we could convert the game to a different platform easily. In this case the 'different platform' is webgl but the approach should be the same.
day four:
Read through the new PIXI 4 tilemap code to see another approach to this task. It looks very similar (beautiful code though, mine is still very rough). They're using TRIANGLE rather than TRIANGLE_STRIP which I believe will give this implementation a slight speed edge when I add proper batching. There's an anim x,y property which is passed to the shaders, I'll need to look at their shader code to see how that's being used... might be a clever way of allowing map tile animations and passing most of the work off to the GPU.
Ran a profiler on my code using FireFox which runs the sci-fly demo extremely slowly (6 fps). As expected 95% of the time is locked in the _renderTile function, I'm virtually certain that the GPU is blocking because I'm only sending pairs in my TRIANGLE_STRIP at the moment. I shall prioritise the batch processing then re-run those tests to make sure the problem is solved. It would be very interesting to find out why Chrome and Canary on my computer do not suffer from the same problem (after all, it's the same GPU!). But I suspect I won't ever be able to answer that question in light of Rich's results (it ran slowly on his Chrome browser with a more powerful GPU...)
The batch changes are in, in a very rough form. The entire visible screen is now drawn with a single gl.drawArrays call after the JS sets up a large VBO with degenerate tris to separate the individual tiles from each other. This approach was hugely successful at drawing in the "pbRenderer" (now called "Beam"), however it is still choking FireFox. Further investigation is needed including: test the old pbRenderer demos to make sure they don't choke FireFox, if it does then check the PIXI 4 tilemap renderer to see if that does too. If the old demos work properly, then compare them with this new implementation line-by-line to find out what's causing the problem now.

View file

@ -28,8 +28,8 @@ PIXI.Tilemap = function(texture, mapwidth, mapheight, tilewidth, tileheight, lay
this.texTilesHigh = Math.ceil(this.texture.height / this.tileHigh); this.texTilesHigh = Math.ceil(this.texture.height / this.tileHigh);
// proportion of texture used by one tile (uv coordinate scales) // proportion of texture used by one tile (uv coordinate scales)
this.sx = this.tileWide / this.texture.width; this.scalex = this.tileWide / this.texture.width;
this.sy = this.tileHigh / this.texture.height; this.scaley = this.tileHigh / this.texture.height;
// TODO: switch here to create DisplayObjectContainer at correct size for the render mode // TODO: switch here to create DisplayObjectContainer at correct size for the render mode
this.width = this.mapWide * this.tileWide; this.width = this.mapWide * this.tileWide;
@ -61,8 +61,11 @@ PIXI.Tilemap = function(texture, mapwidth, mapheight, tilewidth, tileheight, lay
*/ */
this.blendMode = PIXI.blendModes.NORMAL; this.blendMode = PIXI.blendModes.NORMAL;
// calculate size of map
var mapSize = mapwidth * mapheight * 16;
// create buffer with all data required by shader to draw this object // create buffer with all data required by shader to draw this object
this.buffer = new PIXI.Float32Array(16); this.buffer = new PIXI.Float32Array( mapSize * 16 );
/** /**
* create buffer data for the webgl rendering of this tile * create buffer data for the webgl rendering of this tile
@ -80,20 +83,18 @@ PIXI.Tilemap = function(texture, mapwidth, mapheight, tilewidth, tileheight, lay
var t = 0; var t = 0;
var b = t + this.tileHigh; var b = t + this.tileHigh;
this.buffer[ 0 ] = this.buffer[ 4 ] = l; for(var i = 0; i < mapSize; i++)
this.buffer[ 1 ] = this.buffer[ 9 ] = b; {
this.buffer[ 8 ] = this.buffer[ 12] = r; var b = i * 16;
this.buffer[ 5 ] = this.buffer[ 13] = t; this.buffer[ b + 0 ] = this.buffer[ b + 4 ] = l;
this.buffer[ b + 1 ] = this.buffer[ b + 9 ] = b;
// texture source position for the whole texture (uv coordinates adjusted for each tile) this.buffer[ b + 8 ] = this.buffer[ b + 12] = r;
// l, b, 2,3 this.buffer[ b + 5 ] = this.buffer[ b + 13] = t;
// l, t, 6,7 this.buffer[ b + 2 ] = this.buffer[ b + 6 ] = 0;
// r, b, 10,11 this.buffer[ b + 3 ] = this.buffer[ b + 11] = 1;
// r, t, 14,15 this.buffer[ b + 10] = this.buffer[ b + 14] = 1;
this.buffer[ 2 ] = this.buffer[ 6 ] = 0; this.buffer[ b + 7 ] = this.buffer[ b + 15] = 0;
this.buffer[ 3 ] = this.buffer[ 11] = 1; }
this.buffer[ 10] = this.buffer[ 14] = 1;
this.buffer[ 7 ] = this.buffer[ 15] = 0;
}; };
// constructor // constructor
@ -225,7 +226,7 @@ PIXI.Tilemap.prototype._renderVisibleTiles = function(renderSession)
// bind the source buffer // bind the source buffer
gl.bindBuffer( gl.ARRAY_BUFFER, this.positionBuffer ); gl.bindBuffer( gl.ARRAY_BUFFER, this.positionBuffer );
// draw the entire map layer // draw the visible portion of the map layer
this._renderVisibleLayer(this.layer, renderSession); this._renderVisibleLayer(this.layer, renderSession);
}; };
@ -234,24 +235,103 @@ PIXI.Tilemap.prototype._renderVisibleLayer = function( _layer, renderSession )
{ {
var gl = renderSession.gl; var gl = renderSession.gl;
var shader = renderSession.shaderManager.tilemapShader; var shader = renderSession.shaderManager.tilemapShader;
var firstX = Math.max(Math.floor(this.scrollX / this.tileWide), 0); // this.shaders.setProgram(this.shaders.blitShaderProgram, _textureNumber);
var firstY = Math.max(Math.floor(this.scrollY / this.tileHigh), 0);
var lastX = Math.min(firstX + Math.ceil(this.game.width / this.tileWide) + 1, this.mapWide);
var lastY = Math.min(firstY + Math.ceil(this.game.height / this.tileHigh) + 1, this.mapHigh);
for(var y = firstY; y < lastY; y++) var firstX = Math.max(Math.floor(this.scrollX / this.tileWide), 0);
var lastX = Math.min(firstX + Math.ceil(this.game.width / this.tileWide) + 1, this.mapWide);
var firstY = Math.max(Math.floor(this.scrollY / this.tileHigh), 0);
var lastY = Math.min(firstY + Math.ceil(this.game.height / this.tileHigh) + 1, this.mapHigh);
var len = (lastX - firstX) * (lastY - firstY);
var screenWide2 = gl.drawingBufferWidth * 0.5;
var screenHigh2 = gl.drawingBufferHeight * 0.5;
// calculate inverse to avoid division in loop
var iWide = 1.0 / screenWide2;
var iHigh = 1.0 / screenHigh2;
var scale = 1.0;
var wide = this.tileWide * scale * 0.5 / screenWide2;
var high = this.tileHigh * scale * 0.5 / screenHigh2;
var old_t;
var old_r;
var c = 0;
var buffer = this.buffer;
for(var ty = firstY; ty < lastY; ty++)
{ {
var layerRow = _layer.data[y]; var layerRow = _layer.data[ty];
for(var x = firstX; x < lastX; x++) var sy = ty * this.tileHigh;
for(var tx = firstX; tx < lastX; tx++)
{ {
var tile = layerRow[x].index - 1; var tile = layerRow[tx].index - 1;
if ( tile >= 0 ) if ( tile >= 0 )
{ {
this._renderTile(gl, shader, x * this.tileWide, y * this.tileHigh, tile); var sx = tx * this.tileWide;
var tmx = tile % this.texTilesWide;
var tmy = Math.floor(tile / this.texTilesWide);
// from blitSimpleDrawAnimImages
var uvl = tmx * this.scalex;
var uvr = uvl + this.scalex;
var uvt = tmy * this.scaley;
var uvb = uvt + this.scaley;
//this._renderTile(gl, shader, bi, x * this.tileWide, y * this.tileHigh, tile);
var x = sx * iWide - 1;
var y = 1 - sy * iHigh;
var l = x - wide;
var b = y + high;
if ( c > 0 )
{
// degenerate triangle: repeat the last vertex
buffer[ c ] = old_r;
buffer[ c + 1 ] = old_t;
// repeat the next vertex
buffer[ c + 4 ] = l;
buffer[ c + 5 ] = b;
// texture coordinates
buffer[ c + 2 ] = buffer[ c + 6 ] = uvl;
buffer[ c + 3 ] = buffer[ c + 7 ] = uvt;
c += 8;
}
// screen destination position
// l, b, 0,1
// l, t, 4,5
// r, b, 8,9
// r, t, 12,13
buffer[ c ] = buffer[ c + 4 ] = l;
buffer[ c + 1 ] = buffer[ c + 9 ] = b;
buffer[ c + 8 ] = buffer[ c + 12] = old_r = x + wide;
buffer[ c + 5 ] = buffer[ c + 13] = old_t = y - high;
// texture source position
// l, b, 2,3
// l, t, 6,7
// r, b, 10,11
// r, t, 14,15
buffer[ c + 2 ] = buffer[ c + 6 ] = uvl; // l
buffer[ c + 3 ] = buffer[ c + 11] = uvt; // t
buffer[ c + 10] = buffer[ c + 14] = uvr; // r
buffer[ c + 7 ] = buffer[ c + 15] = uvb; // b
c += 16;
} }
} }
} }
gl.bufferData( gl.ARRAY_BUFFER, buffer, gl.STATIC_DRAW );
gl.vertexAttribPointer( shader.aPosition, 4, gl.FLOAT, false, 0, 0 );
gl.drawArrays(gl.TRIANGLE_STRIP, 0, len * 6 - 2);
}; };
@ -297,6 +377,7 @@ PIXI.Tilemap.prototype._renderLayer = function( _layer, renderSession )
{ {
var gl = renderSession.gl; var gl = renderSession.gl;
var shader = renderSession.shaderManager.tilemapShader; var shader = renderSession.shaderManager.tilemapShader;
var bi = 0;
var wide = _layer.width, high = _layer.height; var wide = _layer.width, high = _layer.height;
for(var y = 0; y < high; y++) for(var y = 0; y < high; y++)
@ -307,7 +388,8 @@ PIXI.Tilemap.prototype._renderLayer = function( _layer, renderSession )
var tile = layerRow[x].index; var tile = layerRow[x].index;
if ( tile >= 0 ) if ( tile >= 0 )
{ {
this._renderTile(gl, shader, x * this.tileWide, y * this.tileHigh, tile); this._renderTile(gl, shader, bi, x * this.tileWide, y * this.tileHigh, tile);
bi += 16;
} }
} }
} }
@ -315,7 +397,7 @@ PIXI.Tilemap.prototype._renderLayer = function( _layer, renderSession )
}; };
PIXI.Tilemap.prototype._renderTile = function(gl, shader, x, y, tile) PIXI.Tilemap.prototype._renderTile = function(gl, shader, bufferIndex, x, y, tile)
{ {
// if repeating same tile, skip almost everything... // if repeating same tile, skip almost everything...
if ( tile != this.lastTile ) if ( tile != this.lastTile )

View file

@ -29,11 +29,30 @@ PIXI.TilemapShader = function(gl)
*/ */
this.program = null; this.program = null;
this.fragmentSrc = [
" precision lowp float;",
" uniform sampler2D uImageSampler;",
" varying vec2 vTexCoord;",
" void main(void) {",
" gl_FragColor = texture2D(uImageSampler, vTexCoord);",
" }"
];
this.vertexSrc = [
" precision lowp float;",
" attribute vec4 aPosition;",
" varying vec2 vTexCoord;",
" void main(void) {",
" gl_Position.zw = vec2(1, 1);",
" gl_Position.xy = aPosition.xy;",
" vTexCoord = aPosition.zw;",
" }"
];
/** /**
* The fragment shader. * The fragment shader.
* @property fragmentSrc * @property fragmentSrc
* @type Array * @type Array
*/
this.fragmentSrc = [ this.fragmentSrc = [
" precision mediump float;", " precision mediump float;",
" uniform sampler2D uImageSampler;", " uniform sampler2D uImageSampler;",
@ -42,12 +61,12 @@ PIXI.TilemapShader = function(gl)
" gl_FragColor = texture2D(uImageSampler, vTexCoord);", " gl_FragColor = texture2D(uImageSampler, vTexCoord);",
" }" " }"
]; ];
*/
/** /**
* The vertex shader. * The vertex shader.
* @property vertexSrc * @property vertexSrc
* @type Array * @type Array
*/
this.vertexSrc = [ this.vertexSrc = [
" uniform vec2 uScreenPosition;", " uniform vec2 uScreenPosition;",
" attribute vec4 aPosition;", " attribute vec4 aPosition;",
@ -59,6 +78,7 @@ PIXI.TilemapShader = function(gl)
" vTexCoord = aPosition.zw;", " vTexCoord = aPosition.zw;",
" }" " }"
]; ];
*/
/** /**
* A local texture counter for multi-texture shaders. * A local texture counter for multi-texture shaders.
@ -70,6 +90,7 @@ PIXI.TilemapShader = function(gl)
this.init(); this.init();
}; };
PIXI.TilemapShader.prototype.constructor = PIXI.TilemapShader; PIXI.TilemapShader.prototype.constructor = PIXI.TilemapShader;
/** /**
@ -86,11 +107,12 @@ PIXI.TilemapShader.prototype.init = function()
// get and store the attributes // get and store the attributes
this.aPosition = gl.getAttribLocation(program, 'aPosition'); this.aPosition = gl.getAttribLocation(program, 'aPosition');
this.uProjectionMatrix = gl.getUniformLocation(program, 'uProjectionMatrix'); // this.uProjectionMatrix = gl.getUniformLocation(program, 'uProjectionMatrix');
this.uScreenPosition = gl.getUniformLocation(program, 'uScreenPosition'); // this.uScreenPosition = gl.getUniformLocation(program, 'uScreenPosition');
this.uSampler = gl.getUniformLocation(program, 'uImageSampler'); this.uSampler = gl.getUniformLocation(program, 'uImageSampler');
this.attributes = [this.aScreenPosition, this.aPosition, this.uProjectionMatrix, this.uSampler]; // this.attributes = [this.aScreenPosition, this.aPosition, this.uProjectionMatrix, this.uSampler];
this.attributes = [this.aPosition, this.uSampler];
this.program = program; this.program = program;
}; };