HaxePunk/HaxePunk

View on GitHub
haxepunk/graphics/hardware/DrawCommand.hx

Summary

Maintainability
Test Coverage
package haxepunk.graphics.hardware;

import haxepunk.graphics.shader.Shader;
import haxepunk.math.MathUtil;
import haxepunk.math.Rectangle;
import haxepunk.utils.BlendMode;
import haxepunk.utils.Color;

@:allow(haxepunk.graphics.hardware)
private class DrawTriangle
{
    public function new() {}

    public var tx1:Float = 0;
    public var ty1:Float = 0;
    public var uvx1:Float = 0;
    public var uvy1:Float = 0;
    public var tx2:Float = 0;
    public var ty2:Float = 0;
    public var uvx2:Float = 0;
    public var uvy2:Float = 0;
    public var tx3:Float = 0;
    public var ty3:Float = 0;
    public var uvx3:Float = 0;
    public var uvy3:Float = 0;
    public var color:Color = 0;
    public var alpha:Float = 0;

    public var x1(get, never):Float;
    public inline function get_x1() return MathUtil.minOf3(tx1, tx2, tx3);
    public var x2(get, never):Float;
    public inline function get_x2() return MathUtil.maxOf3(tx1, tx2, tx3);
    public var y1(get, never):Float;
    public inline function get_y1() return MathUtil.minOf3(ty1, ty2, ty3);
    public var y2(get, never):Float;
    public inline function get_y2() return MathUtil.maxOf3(ty1, ty2, ty3);

    public inline function intersectsTriangle(x1:Float, y1:Float, x2:Float, y2:Float, x3:Float, y3:Float):Bool
    {
        return (
            linesIntersect(x1, y1, x2, y2, tx1, ty1, tx2, ty2) ||
            linesIntersect(x2, y2, x3, y3, tx1, ty1, tx2, ty2) ||
            linesIntersect(x1, y1, x3, y3, tx1, ty1, tx2, ty2) ||
            linesIntersect(x1, y1, x2, y2, tx2, ty2, tx3, ty3) ||
            linesIntersect(x2, y2, x3, y3, tx2, ty2, tx3, ty3) ||
            linesIntersect(x1, y1, x3, y3, tx2, ty2, tx3, ty3) ||
            linesIntersect(x1, y1, x2, y2, tx1, ty1, tx3, ty3) ||
            linesIntersect(x2, y2, x3, y3, tx1, ty1, tx3, ty3) ||
            linesIntersect(x1, y1, x3, y3, tx1, ty1, tx3, ty3) ||
            triangleContains(x1, y1, x2, y2, x3, y3, tx1, ty1) ||
            triangleContains(x1, y1, x2, y2, x3, y3, tx2, ty2) ||
            triangleContains(x1, y1, x2, y2, x3, y3, tx3, ty3) ||
            triangleContains(tx1, ty1, tx2, ty2, tx3, ty3, x1, y1) ||
            triangleContains(tx1, ty1, tx2, ty2, tx3, ty3, x2, y2) ||
            triangleContains(tx1, ty1, tx2, ty2, tx3, ty3, x3, y3)
        );
    }

    static inline function linesIntersect(x11:Float, y11:Float, x12:Float, y12:Float, x21:Float, y21:Float, x22:Float, y22:Float):Bool
    {
        var d = ((y22 - y21) * (x12 - x11)) - ((x22 - x21) * (y12 - y11));
        if (d != 0)
        {
            var ua = (((x22 - x21) * (y11 - y21)) - ((y22 - y21) * (x11 - x21))) / d,
                ub = (((x12 - x11) * (y11 - y21)) - ((y12 - y11) * (x11 - x21))) / d;
            if (ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1) d = 0;
        }
        return d == 0;
    }

    static inline function triangleContains(x1:Float, y1:Float, x2:Float, y2:Float, x3:Float, y3:Float, px:Float, py:Float)
    {
        var v0x = x3 - x1,
            v0y = y3 - y1,
            v1x = x2 - x1,
            v1y = y2 - y1,
            v2x = px - x1,
            v2y = py - y1;
        var u = cross(v2x, v2y, v0x, v0y),
            v = cross(v1x, v1y, v2x, v2y),
            d = cross(v1x, v1y, v0x, v0y);
        if (d < 0)
        {
            u = -u;
            v = -v;
            d = -d;
        }
        return u >= 0 && v >= 0 && (u + v) <= d;
    }

    static inline function cross(ux:Float, uy:Float, vx:Float, vy:Float):Float return ux * vy - uy * vx;

    var _next:DrawTriangle;
}

class TriangleIterator
{
    var triangle:DrawTriangle = null;

    public function new() {}

    public inline function reset(triangle:DrawTriangle)
    {
        this.triangle = triangle;
    }

    public inline function hasNext():Bool
    {
        return triangle != null;
    }

    public inline function next():DrawTriangle
    {
        var current = triangle;
        triangle = triangle._next;
        return current;
    }
}

/**
 * Represents a pending hardware draw call. A single DrawCommand batches render
 * calls for the same texture, target and parameters. Based on work by
 * @Beeblerox.
 */
@:dox(hide)
@:allow(haxepunk.graphics.hardware.DrawCommandBatch)
class DrawCommand
{
    public static function create(texture:Texture, shader:Shader, smooth:Bool, blend:BlendMode, ?clipRect:Rectangle)
    {
        var command:DrawCommand;
        if (_pool != null)
        {
            command = _pool;
            _pool = _pool._next;
            command._prev = command._next = null;
        }
        else
        {
            command = new DrawCommand();
        }
        command.shader = shader;
        command.texture = texture;
        command.smooth = smooth;
        command.blend = blend;
        command.clipRect = clipRect;
        return command;
    }

    static function _prePopulatePool(n:Int, m:Int)
    {
        for (i in 0 ... n)
        {
            var cmd = new DrawCommand();
            for (i in 0 ... m)
            {
                cmd.addData(new DrawTriangle());
            }
            cmd.recycle();
        }
        return _pool;
    }

    static var _pool:DrawCommand = _prePopulatePool(32, 4);
    static var _dataPool:DrawTriangle;

    public var shader:Shader;
    public var texture:Texture;
    public var smooth:Bool = false;
    public var blend:BlendMode = BlendMode.Alpha;
    public var clipRect:Rectangle = null;
    #if !hxp_no_render_batch
    public var bounds:Rectangle = new Rectangle();
    #end
    public var triangleCount(default, null):Int = 0;
    public var visibleArea:Rectangle;

    function new() {}

    /**
     * Compares values to this draw command to see if they all match. This is used by the batcher to reuse the previous draw command.
     */
    public inline function match(texture:Texture, shader:Shader, smooth:Bool, blend:BlendMode, clipRect:Rectangle):Bool
    {
        // These conditions are checked as individual if statements
        // to reduce the number of temporary variables created in hxcpp.
        if (this.smooth != smooth) return false;
        else if (this.texture != texture) return false;
        else if (this.shader.id != shader.id) return false;
        else if (this.blend != blend) return false;
        else
        {
            // It is faster to do a null check once and compare the results.
            var aRectIsNull = this.clipRect == null;
            var bRectIsNull = clipRect == null;
            if (aRectIsNull != bRectIsNull) return false; // one rect is null the other is not
            if (aRectIsNull) return true; // both are null, return true
            else return Std.int(this.clipRect.x) == Std.int(clipRect.x) &&
                    Std.int(this.clipRect.y) == Std.int(clipRect.y) &&
                    Std.int(this.clipRect.width) == Std.int(clipRect.width) &&
                    Std.int(this.clipRect.height) == Std.int(clipRect.height);
        }
    }

    /**
     * Add a triangle vertices to render.
     * @param tx[1-3]  Vrtex x coord
     * @param ty[1-3]  Vertex y coord
     * @param uvx[1-3] Texture x coord [0-1]
     * @param uvy[1-3] Texture y coord [0-1]
     * @param color    Vertex color tint
     * @param alpha    Vertex alpha value
     */
    public inline function addTriangle(tx1:Float, ty1:Float, uvx1:Float, uvy1:Float, tx2:Float, ty2:Float, uvx2:Float, uvy2:Float, tx3:Float, ty3:Float, uvx3:Float, uvy3:Float, color:Color, alpha:Float):Void
    {
        if (alpha > 0)
        {
            var onScreen = shader.hasAttributes || (
                MathUtil.minOf3(tx1, tx2, tx3) <= visibleArea.right &&
                MathUtil.maxOf3(tx1, tx2, tx3) >= visibleArea.left &&
                MathUtil.minOf3(ty1, ty2, ty3) <= visibleArea.bottom &&
                MathUtil.maxOf3(ty1, ty2, ty3) >= visibleArea.top
            );
            if (onScreen)
            {
                var data:DrawTriangle = getData();
                data.tx1 = tx1;
                data.ty1 = ty1;
                data.uvx1 = uvx1;
                data.uvy1 = uvy1;
                data.tx2 = tx2;
                data.ty2 = ty2;
                data.uvx2 = uvx2;
                data.uvy2 = uvy2;
                data.tx3 = tx3;
                data.ty3 = ty3;
                data.uvx3 = uvx3;
                data.uvy3 = uvy3;
                data.color = color;
                data.alpha = alpha;
                addData(data);
            }
        }
    }

    /**
     * Recycles the data for all draw commands following this one.
     */
    public function recycle()
    {
        recycleData();
        var command = this;
        while (command._next != null)
        {
            command = command._next;
            command.recycleData();
        }
        command._next = _pool;
        _pool = this;
    }

    public var triangles(get, never):TriangleIterator;
    inline function get_triangles():TriangleIterator
    {
        _iterator.reset(data);
        return _iterator;
    }

    inline function getData():DrawTriangle
    {
        var data:DrawTriangle;
        if (_dataPool != null)
        {
            data = _dataPool;
            _dataPool = _dataPool._next;
            data._next = null;
        }
        else
        {
            data = new DrawTriangle();
        }
        return data;
    }

    inline function addData(data:DrawTriangle):Void
    {
        if (this.data == null)
        {
            this.data = data;
        }
        else
        {
            _lastData._next = data;
        }
        _lastData = data;

        ++triangleCount;

        #if !hxp_no_render_batch
        // update bounds
        var x1 = data.x1, x2 = data.x2, y1 = data.y1, y2 = data.y2;
        if (bounds.width == 0)
        {
            bounds.x = x1;
            bounds.width = x2 - x1;
        }
        else
        {
            if (x1 < bounds.left)
            {
                bounds.width += bounds.left - x1;
                bounds.left = x1;
            }
            if (x2 > bounds.right) bounds.width = x2 - bounds.left;
        }
        if (bounds.height == 0)
        {
            bounds.y = y1;
            bounds.height = y2 - y1;
        }
        else
        {
            if (y1 < bounds.top)
            {
                bounds.height += bounds.top - y1;
                bounds.top = y1;
            }
            if (y2 > bounds.bottom) bounds.height = y2 - bounds.top;
        }
        #end
    }

    inline function recycleData()
    {
        triangleCount = 0;
        texture = null;
        if (data != null)
        {
            _lastData._next = _dataPool;
            _dataPool = data;
        }
        data = _lastData = null;
        #if !hxp_no_render_batch
        bounds.setTo(0, 0, 0, 0);
        #end
    }

    var _iterator = new TriangleIterator();
    var data:DrawTriangle;
    var _lastData:DrawTriangle;
    var _prev:DrawCommand;
    var _next:DrawCommand;
}