drawQuads and atlases

I just noticed the newish drawQuads() call and updated to the latest OpenFL hoping to improve performance in my tile-based renderer. I had a couple of questions though:

  1. Is there some method available to render sprites directly from an atlas using drawQuads? The docs say that any fill transforms are ignored, and I don’t see a way to to provide uv coords or anything - is this just not possible or am I overlooking something obvious?

  2. Is there any simple/performant way to render with an alpha channel when using drawQuads?

  3. Am I just attempting to misuse the API, and should use something else?

Thanks in advance for any insight :slight_smile:

Let’s pretend we have a BitmapData atlas with two graphics in it:

[  A  |  B  ]

Assuming each side is 100 pixels all around, you should be able to use drawQuads in order to draw both, like this:

graphics.beginBitmapFill (bitmapData);
graphics.drawQuads (Vector.ofArray ([
    0, 0, 100, 100,
    100, 0, 100, 100
]));

This draws both A and B from the source BitmapData, using the proper UV coordinates for each rectangle. You could use drawRect with the full-size of the BitmapData again and get the same effect. However, drawQuads supports the ability to transform the target draw position. The source UV remains the same, but we can override the rectangle position and define our own (x, y) pair, abcd, or full (a, b, c, d, tx, ty) transform.

graphics.beginBitmapFill (bitmapData);
graphics.drawQuads (Vector.ofArray ([
    0, 0, 100, 100,
    100, 0, 100, 100
]), null, Vector.ofArray ([
    100, 200,
    300, 100
]));

The second parameter can be used to reference rectangles (from the first parameter) by index. You can define the rect data as in the above examples, or another way is the Tileset class (for example) exposes the data for each rectangle in the tileset as an array that can be used with drawQuads.

var tileset = new Tileset (bitmapData);
tileset.addRect (new Rectangle (0, 0, 100, 100));
tileset.addRect (new Rectangle (100, 0, 100, 100));

graphics.beginBitmapFill (bitmapData);
graphics.drawQuads (tileset.rectData, Vector.ofArray ([ 0, 1 ]));

It’s not currently possible to add alpha without using a shader fill. I think a shader with alpha only would look something like this:

import openfl.display.GraphicsShader;

class AlphaGraphicsShader extends GraphicsShader {
	
	@:glVertexSource(
		"#pragma header
		attribute float alpha;
		varying float alphav;
		
		void main(void) {
			
			alphav = alpha;
			#pragma body
			
		}"
	)
	
	@:glFragmentSource(
		"#pragma header
		varying float alphav;
		
		void main(void) {
			
			#pragma body
			gl_FragColor = gl_FragColor * alphav;
			
		}"
	)
	
	public function new () {
		super ();
	}
	
}

Then rendering would look more like:

var shader = new AlphaGraphicsShader ();
shader.bitmap.input = bitmapData;
shader.alpha.value = [ 0.5 ];

graphics.beginShaderFill (shader);
graphics.drawQuads (tileset.rectData, new Vector ([ 0, 1 ]));

This would set 0.5 as a constant value for all vertices. The set the alpha for each vertex (and ultimately, quad) separately, you would need to define enough alpha values to cover the render. In the latest OpenFL release, you need 6 values per quad. In the next release, I think this will be dropping down to 4 since we’ll be using an index buffer instead.

var shader = new AlphaGraphicsShader ();
shader.alpha = [];

for (i in 0...6) shader.alpha.push (0.5);
for (i in 0...6) shader.alpha.push (0.25);

graphics.beginShaderFill (shader);
graphics.drawQuads (tileset.rectData, new Vector ([ 0, 1 ]));

You are not trying to abuse the API, though (depending on your use case) you might also consider Tilemap, which is easier to use

var tilemap = new Tilemap (stage.stageWidth, stage.stageHeight);

var tile = new Tile (0);
tile.tileset = tileset;
tile.alpha = 0.5;
tile.x = 200;
tilemap.addTile (tile);

var tile = new Tile (1);
tile.tileset = tileset;
tile.alpha = 0.25;
tile.x = 300;
tile.y = 100;
tilemap.addTile (tile);

Tilemap lets you set a default Tileset for the whole map, but you also can set/override the tileset on each Tile, making it pretty flexible.

You can do alpha, x, y, scale, color transform, custom shaders and other properties on each Tile, or group using TileContainer, so it’s sort of a mix of drawQuads and the display list rendering. Some features (like color transform or shaders) don’t work on software (cairo, canvas, DOM) targets, but otherwise Tilemap (like drawQuads) should work on these renderers

4 Likes

This is fantastic, and makes perfect sense. Thanks for the detailed response!

1 Like

Hey is there a way to make the AlphaGraphicsShader example work with the new OpenFL api? It looks like the alpha is now a ShaderParameter and the bitmap is a ShaderInput. I have tried to initialize these and then set the bitmap and push the alpha floats but I get a NPE.

openfl._internal.renderer.cairo.$CairoGraphics.createImagePattern(openfl/_internal/renderer/cairo/CairoGraphics.hx:159

Make sure you push an alpha for each vertex drawn… this means you need to add the same alpha value four times for each quad, or you can add only one value and it will be treated as a uniform constant for all quads drawn

Sorry, I am a bit confused though because I think I am initializing everything correctly.

alphaShader = new AlphaGraphicsShader();
alphaShader.alpha = new ShaderParameter();
alphaShader.alpha.value = [0.5];
alphaShader.bitmap = new ShaderInput();
alphaShader.bitmap.input = tileset.bitmapData;

I am then later calling beginShaderFill instead of beginBitmapFill and get the NPE. I looked at the line number of the NPE and it appears the bitmapData is null in CairoGraphics.createImagePattern.

Nevermind, I realized I shouldn’t be newing the ShaderParam and Input. Sorry.

1 Like