How To Cut Texture Memory Usage In Half
ATF textures are great at reducing texture memory usage but sometimes you can’t use them. If you’re dynamically generating the texture (e.g. from a snapshot of a DisplayObject
) or you’re loading the texture from a third party then you won’t have the (realistic) option of using compressed ATF textures. Today’s article shows how you can still save a bunch of memory, even without ATF.
First, make sure your app is running under Flash Player 11.7 or AIR 3.7 with a quick check of Capabilities.version. You’ll also need to target Flash Player 11.7 when you compile with the -target-player=11.7.0
and -swf-version=20
arguments. Doing that will unlock two new types of texture, both of which use only 16 bits for each pixel rather than the usual 24 (for opaque images like JPEG) or 32 (for images with alpha like PNG).
All you have to do is pass Context3DTextureFormat.BGR_PACKED
or Context3DTextureFormat.BGRA_PACKED
to Context3D.createTexture
and you’ll get a 16 bit-per-pixel texture.
In the Context3DTextureFormat.BGR_PACKED
case the texture will have 5 bits for the red channel, 6 bits for the green, and 5 bits for the blue. This is because the human eye can see green better than blue or red. In shorthand, this texture format is called RGB565. If you need alpha you’ll use Context3DTextureFormat.BGRA_PACKED
and get 4 bits each for red, green, blue, and alpha. It’s less precision for the colors, but that’s the price you pay for decent alpha support.
After you’ve set up the texture, just call Texture.uploadFromBitmapData
like you normally would and the BitmapData
will be automatically downsampled to 16 bits-per-pixel. This yields a 33% savings on texture memory usage for opaque images and a 50% savings for images with alpha.
To try this out, I’ve made a little (< 250 lines) app that shows a 512x512 opaque earth texture and a 128x128 texture of the Flash logo with quite a bit of alpha. Click the buttons in the bottom left to change between different texture types.
package { import com.adobe.utils.AGALMiniAssembler; import flash.display.Bitmap; import flash.display.BitmapData; import flash.display.Sprite; import flash.display.Stage3D; import flash.display.StageAlign; import flash.display.StageScaleMode; import flash.display3D.Context3D; import flash.display3D.Context3DBlendFactor; import flash.display3D.Context3DProgramType; import flash.display3D.Context3DRenderMode; import flash.display3D.Context3DTextureFormat; import flash.display3D.Context3DVertexBufferFormat; import flash.display3D.IndexBuffer3D; import flash.display3D.Program3D; import flash.display3D.VertexBuffer3D; import flash.display3D.textures.Texture; import flash.events.Event; import flash.events.MouseEvent; import flash.text.TextField; import flash.text.TextFieldAutoSize; import flash.text.TextFormat; import flash.utils.ByteArray; public class Texture16BPP extends Sprite { private static const PAD:Number = 5; [Embed(source="earth.jpg")] private static const TEXTURE_RGB:Class; [Embed(source="flash_logo_alpha.png")] private static const TEXTURE_RGBA:Class; private var program:Program3D; private var posUV:VertexBuffer3D; private var tris:IndexBuffer3D; private var textureRGB888:Texture; private var textureRGB565:Texture; private var textureRGBA8888:Texture; private var textureRGBA4444:Texture; private var curTexture:Texture; private var context3D:Context3D; private var modeDisplay:TextField; public function Texture16BPP() { stage.align = StageAlign.TOP_LEFT; stage.scaleMode = StageScaleMode.NO_SCALE; stage.frameRate = 60; var stage3D:Stage3D = stage.stage3Ds[0]; stage3D.addEventListener(Event.CONTEXT3D_CREATE, onContextCreated); stage3D.requestContext3D(Context3DRenderMode.AUTO); } protected function onContextCreated(ev:Event): void { var stage3D:Stage3D = stage.stage3Ds[0]; stage3D.removeEventListener(Event.CONTEXT3D_CREATE, onContextCreated); context3D = stage3D.context3D; context3D.configureBackBuffer(stage.stageWidth, stage.stageHeight, 0); context3D.enableErrorChecking = true; makeButtons("RGB888", "RGB565", "RGBA8888", "RGBA4444"); var assembler:AGALMiniAssembler = new AGALMiniAssembler(); assembler.assemble( Context3DProgramType.VERTEX, "mov op, va0\n" + "mov v0, va1" ); var vertexProgram:ByteArray = assembler.agalcode; assembler.assemble( Context3DProgramType.FRAGMENT, "tex oc, v0, fs0 <2d,linear,mipnone,clamp,dxt1>" ); var fragmentProgram:ByteArray = assembler.agalcode; program = context3D.createProgram(); program.upload(vertexProgram, fragmentProgram); posUV = context3D.createVertexBuffer(4, 5); posUV.uploadFromVector( new <Number>[ // X, Y, Z, U, V -1, -1, 0, 0, 1, -1, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, -1, 0, 1, 1 ], 0, 4 ); // Create the triangles index buffer tris = context3D.createIndexBuffer(6); tris.uploadFromVector( new <uint>[ 0, 1, 2, 2, 3, 0 ], 0, 6 ); var opaque:BitmapData = (new TEXTURE_RGB() as Bitmap).bitmapData; var alpha:BitmapData = (new TEXTURE_RGBA() as Bitmap).bitmapData; textureRGB888 = context3D.createTexture( opaque.width, opaque.height, Context3DTextureFormat.BGRA, false ); textureRGB888.uploadFromBitmapData(opaque); textureRGB565 = context3D.createTexture( opaque.width, opaque.height, Context3DTextureFormat.BGR_PACKED, false ); textureRGB565.uploadFromBitmapData(opaque); textureRGBA8888 = context3D.createTexture( alpha.width, alpha.height, Context3DTextureFormat.BGRA, false ); textureRGBA8888.uploadFromBitmapData(alpha); textureRGBA4444 = context3D.createTexture( alpha.width, alpha.height, Context3DTextureFormat.BGRA_PACKED, false ); textureRGBA4444.uploadFromBitmapData(alpha); curTexture = textureRGB888; modeDisplay = new TextField(); modeDisplay.autoSize = TextFieldAutoSize.LEFT; modeDisplay.defaultTextFormat = new TextFormat("_sans", 36); modeDisplay.text = "RGB888"; addChild(modeDisplay); addEventListener(Event.ENTER_FRAME, onEnterFrame); } private function makeButtons(...labels): Number { var curX:Number = PAD; var curY:Number = stage.stageHeight - PAD; for each (var label:String in labels) { var tf:TextField = new TextField(); tf.mouseEnabled = false; tf.selectable = false; tf.defaultTextFormat = new TextFormat("_sans"); tf.autoSize = TextFieldAutoSize.LEFT; tf.text = label; tf.name = "lbl"; var button:Sprite = new Sprite(); button.buttonMode = true; button.graphics.beginFill(0xF5F5F5); button.graphics.drawRect(0, 0, tf.width+PAD, tf.height+PAD); button.graphics.endFill(); button.graphics.lineStyle(1); button.graphics.drawRect(0, 0, tf.width+PAD, tf.height+PAD); button.addChild(tf); button.addEventListener(MouseEvent.CLICK, onButton); if (curX + button.width > stage.stageWidth - PAD) { curX = PAD; curY -= button.height + PAD; } button.x = curX; button.y = curY - button.height; addChild(button); curX += button.width + PAD; } return curY - button.height; } private function onButton(ev:MouseEvent): void { var mode:String = ev.target.getChildByName("lbl").text; switch (mode) { case "RGB888": curTexture = textureRGB888; modeDisplay.text = "RGB888"; break; case "RGB565": curTexture = textureRGB565; modeDisplay.text = "RGB565"; break; case "RGBA8888": curTexture = textureRGBA8888; modeDisplay.text = "RGBA8888"; break; case "RGBA4444": curTexture = textureRGBA4444; modeDisplay.text = "RGBA4444"; break; } } private function onEnterFrame(ev:Event): void { context3D.clear(0.5, 0.5, 0.5); context3D.setProgram(program); context3D.setBlendFactors( Context3DBlendFactor.SOURCE_ALPHA, Context3DBlendFactor.ONE_MINUS_SOURCE_ALPHA ); // Draw selected texture context3D.setTextureAt(0, curTexture); context3D.setVertexBufferAt(0, posUV, 0, Context3DVertexBufferFormat.FLOAT_3); context3D.setVertexBufferAt(1, posUV, 3, Context3DVertexBufferFormat.FLOAT_2); context3D.drawTriangles(tris); context3D.present(); } } }
Your results will vary quite a bit depending on the texture images you choose. The opaque earth texture used here looks very good in 16 bits-per-pixel compared to the original 24 bits-per-pixel texture so it’s an easy memory savings. The Flash logo looks quite a bit worse, though. The image’s gradients are dithered quite badly, even in the alpha “shadow”. It’s probably not a very good candidate for downsampling, but others certainly are and they’ll yield the ultimate prize here: 50% texture memory usage savings.
Spot a bug? Have a suggestion or question? Post a comment!
#1 by Jeff on May 28th, 2013 ·
“The image’s gradients are dithered quite badly,”
If you know your source image is destined for 565 or 4444 you can also pre-dither your source PNG using error diffusion dither to reduce the automatic downsampling negative effects that happens.
#2 by jackson on May 28th, 2013 ·
That’s true. A lot of graphics packages (e.g. Photoshop, GIMP) will produce a lot higher quality dithered image than the simple, automatic dithering you get by
Texture.uploadFromBitmapData
.