What's the low-level code required to render a texture (BitmapData) to a Stage3D in 2D space?

1.7k views Asked by At

I've looked at the following website for some information on writing AGAL to render Textures / Bitmaps to the Stage3D object:

http://iflash3d.com/shaders/my-name-is-agal-i-come-from-adobe-1/

But it seems to cover more about how to create objects in 3D space. Maybe it can't be avoided?

But I'll ask this anyways:

What would be the bare-bone necessary AGAL code & AS3 code to write to render multiple sprites to the Stage3D?

Note: No Filters, Effects or Shaders required - just position, scale and rotation transformations. Also, I'm not looking for existing 3rd party APIs that do this under the hood. I would like to experiment this low-level language myself.

1

There are 1 answers

0
Casper Beyer On BEST ANSWER

It is all described in the articles you linked, the only thing missing to make it '2d' is to create an orthogonal projection matrix.

var projection:Matrix3D = new Matrix3D(Vector.<Number>([
    2 / (viewPort.right - viewPort.left), 0, 0,  0,
    0,  2 / (viewPort.top - viewPort.bottom), 0, 0,
    0,  0, 1 / (far - near), 0,
    0, 0, near / (near - far), 1
]));

A common abstraction and optimization is sprite batching. The following example is not feature complete, color tint, sorting, rotation and offsets are not implemented at the moment but it's trivial to do so.

package
{
    import com.adobe.utils.*;

    import flash.display3D.*;
    import flash.display3D.textures.*;
    import flash.geom.*;
    import flash.system.System;
    import flash.utils.*;

    public class SpriteBatch3D
    {
        private static const SPRITES_PER_BATCH:int = 2048;
        private static const DATA_PER_VERTEX:int = 4;

        private var _context3D:Context3D;

        private var _vertexBuffer:VertexBuffer3D;
        private var _indexBuffer:IndexBuffer3D;
        private var _program:Program3D;
        private var _matrix3D:Matrix3D;
        private var _numSprites:Number;
        private var _sprites:Vector.<SpriteBatch3DSprite>;

        public function SpriteBatch3D(context3D:Context3D)
        { 
            if (context3D == null) {
                throw new ArgumentError("context3D cannot be null.");
            }

            _context3D = context3D;
            allocateBuffers();
            allocateShaders();
            allocateSprites();
        }

        private function allocateSprites():void {
            _sprites = createSprites();
            _numSprites = 0;
        }

        private function createSprites():Vector.<SpriteBatch3DSprite> {
            var sprites:Vector.<SpriteBatch3DSprite> = new Vector.<SpriteBatch3DSprite>(SPRITES_PER_BATCH);

            for (var i:int = 0; i < sprites.length; i++) 
            {
                 sprites[i] = new SpriteBatch3DSprite;
            }

            return sprites;
        }

        private function allocateShaders():void
        {
            var vertexAssembler:AGALMiniAssembler = new AGALMiniAssembler();
            vertexAssembler.assemble(flash.display3D.Context3DProgramType.VERTEX,
                "m44 op, va0, vc0   \n" +
                "mov v0, va1        \n"
            );

            var fragmentAssembler:AGALMiniAssembler = new AGALMiniAssembler();
            fragmentAssembler.assemble(flash.display3D.Context3DProgramType.FRAGMENT, 
                "tex ft1, v0, fs0 <2d,linear> \n" +
                "mov oc, ft1"
            );

            _program = _context3D.createProgram();
            _program.upload(vertexAssembler.agalcode, fragmentAssembler.agalcode);
        }

        private function allocateBuffers():void
        {
            _vertexBuffer = _context3D.createVertexBuffer(SPRITES_PER_BATCH * 4, DATA_PER_VERTEX);
            _vertexBuffer.uploadFromByteArray(createVertexData(), 0, 0, SPRITES_PER_BATCH * 4);

            _indexBuffer = _context3D.createIndexBuffer(SPRITES_PER_BATCH * 6);
            _indexBuffer.uploadFromByteArray(createIndexData(), 0, 0, SPRITES_PER_BATCH * 6);
        }

        private function createVertexData():ByteArray
        {
            var data:ByteArray = new ByteArray;
            data.length = SPRITES_PER_BATCH * 4 * (4 * DATA_PER_VERTEX);
            data.endian = Endian.LITTLE_ENDIAN;

            return data;

        }
        private function createIndexData():ByteArray {

            var data:ByteArray = new ByteArray();
            data.endian = Endian.LITTLE_ENDIAN;
            data.length = (SPRITES_PER_BATCH * 6) * 2;

            for (var i:int = 0; i < SPRITES_PER_BATCH * 6; i++)
            {
                data.writeShort(i * 4);
                data.writeShort(i * 4 + 1);
                data.writeShort(i * 4 + 2);

                data.writeShort(i * 4);
                data.writeShort(i * 4 + 2);
                data.writeShort(i * 4 + 3);
            }

            return data;
        }

        public function dispose():void {
            if (_vertexBuffer) {
                _vertexBuffer.dispose();
                _vertexBuffer = null;
            }

            if (_indexBuffer) {
                _indexBuffer.dispose();
                _indexBuffer = null;
            }
        }

        public function begin(matrix3D:Matrix3D):void {
            _matrix3D = matrix3D;
        }

        public function draw(texture:Texture, destination:Rectangle,  source:Rectangle=null, origin:Point=null, color:int=0xFFFFFF, rotation:Number=0, depth:Number=0):void {
            if (source == null) {
                source = new Rectangle(0, 0, 1, 1);
            }

            if (origin == null) {
                origin = new Point(0, 0);
            }

            if (_sprites.length == _numSprites) {
                _sprites = _sprites.concat(createSprites());                
            }

            _sprites[_numSprites].texture       = texture;
            _sprites[_numSprites].destination   = destination;
            _sprites[_numSprites].source        = source;
            _sprites[_numSprites].origin        = origin;
            _sprites[_numSprites].rotation      = rotation;
            _sprites[_numSprites].depth         = depth;

            _numSprites++;          
        }

        public function end():void {    
            _context3D.setProgram(_program);
            _context3D.setProgramConstantsFromMatrix(Context3DProgramType.VERTEX, 0, _matrix3D, true);

            _context3D.setVertexBufferAt(0, _vertexBuffer, 0, Context3DVertexBufferFormat.FLOAT_2); // x, y
            _context3D.setVertexBufferAt(1, _vertexBuffer, 2, Context3DVertexBufferFormat.FLOAT_2); // u, v

            _context3D.setCulling(Context3DTriangleFace.FRONT);
            _context3D.setDepthTest(false, Context3DCompareMode.ALWAYS);

            // TODO sorting.
            var sprites:Vector.<SpriteBatch3DSprite> = _sprites;
            var texture:Texture = null;
            var firstIndex:int = 0;

            for (var index:int = 0; index < _numSprites; index++) {
                if (texture != sprites[index].texture) {
                    if (index > firstIndex) {
                        renderBatch(texture, sprites, firstIndex, index - firstIndex);
                        firstIndex = index;
                    }

                    texture = sprites[index].texture;
                }
            }

            renderBatch(texture, sprites, firstIndex, _numSprites - firstIndex);
            _numSprites = 0;
        }

        private function renderBatch(texture:Texture, sprites:Vector.<SpriteBatch3DSprite>, offset:int, count:int):void {
            if (_numSprites == 0) {
                return;
            }

            while(count > 0) {
                var data:ByteArray = new ByteArray;
                data.endian = Endian.LITTLE_ENDIAN;

                var size:int = count;
                if (size > SPRITES_PER_BATCH) {
                    size = Math.min(sprites.length, SPRITES_PER_BATCH); 
                }

                for (var i:int = offset; i < (offset + size); i++) {
                    data.writeFloat(sprites[i].destination.left);
                    data.writeFloat(sprites[i].destination.top);
                    data.writeFloat(sprites[i].source.left);
                    data.writeFloat(sprites[i].source.top);

                    data.writeFloat(sprites[i].destination.left);
                    data.writeFloat(sprites[i].destination.bottom);
                    data.writeFloat(sprites[i].source.left);
                    data.writeFloat(sprites[i].source.bottom);

                    data.writeFloat(sprites[i].destination.right);
                    data.writeFloat(sprites[i].destination.bottom);
                    data.writeFloat(sprites[i].source.right);
                    data.writeFloat(sprites[i].source.bottom);

                    data.writeFloat(sprites[i].destination.right);
                    data.writeFloat(sprites[i].destination.top);
                    data.writeFloat(sprites[i].source.right);
                    data.writeFloat(sprites[i].source.top);
                }

                _vertexBuffer.uploadFromByteArray(data, 0, 0, size * 4);            
                _context3D.setTextureAt(0, texture);
                _context3D.drawTriangles(_indexBuffer, 0, size * 2);

                offset += size;
                count -= size;
            }
        }
    }
}

import flash.display3D.textures.*;
import flash.geom.*;

internal class SpriteBatch3DSprite {
    public var texture:Texture;
    public var source:Rectangle;
    public var destination:Rectangle;
    public var origin:Point;
    public var rotation:Number;
    public var depth:Number;
}

Usage

batch.begin(matrix);
batch.draw(texture1, new Rectangle(100, 0, 100, 100));
batch.draw(texture2, new Rectangle(0, 200, 100, 100));
batch.draw(texture3, new Rectangle(0, 500, 100, 100));
batch.draw(texture4, new Rectangle(900, 0, 100, 100));
batch.end();

There are some trivial caveats however, since texture objects do not carry width or height getters you are dealing with a source rectangle in texel space (0...1)

Additionally, Context3D does not carry any information about the viewport, so the matrix cannot be constructed internally and has to be resolved as a dependency.