TexturedPolygon

A function to create an Image from a polygon texturised by a texture.

function TexturedPolygon(polygon,texture, xx,yy,rr,sx,sy,ox,oy, smooth)

Parameters

polygon a polygon to texturize in a form of table of coordinates of points: {x1,y1, x2,y2, ...}

texture may be an ImageData object or a filename to picture. Used to fill the entire polygon, wrapping vertically and horizontally.

All other parameters are optional:

xx,yy,r,... are used to shift, rotate and scale texture before applying to polygon. Think of it as about a sheet of paper, filled with repeated images of transformed texture, and you use polygon points coordinates to cut out some figure using scissors. For instance, increasing an xx value will move texture right while the polygon itself will stay at place.

xx,yy coordinates of a (origin of) a texture.

rr rotation angle of a texture (related to origin), in degrees.

sx,xy scale factors of a texture along x and y axes (related to origin).

ox,oy position of an origin on a texture. It affects how rotation and scale are applied.

On default, no some transformation is applied. It means: xx,yy,rr,sx,sy,ox,oy=0,0,0,1,1,0,0

smooth whether to smooth image (true or false). Smoothed image looks better, but calculates slower. smooth is set to true by default.

Return value

Function return an object which is very simple to draw:

{img, x,y, w,h, imgData}

Here:

img an Image object, may be drawn by love.graphics.draw function.

x,y where to draw image. Use it like this: love.graphics.draw(obj.img, obj.x,obj.y)

w,h width and height of an image.

imgData an ImageData object. If you want to use it, you must to know how to do it.

Implementation

Function uses ImageData:mapPixel function to draw resulting image pixel-by-pixel. Area which is not within polygon is left fully transparent.

As soon as we need to calculate every pixel, function gets much time. Hence you'd better use it as rare as possible, for example on loading the game or new level. Don't try to use it inside of love.update function. If you still need it, try to set smooth to false and see what happens.


See text itself for more details. It also contains several auxiliary functions which you can use as well. (Scroll bravely, example is right below).

function TexturedPolygon(polygon,texture, xx,yy,rr,sx,sy,ox,oy, smooth) --return array of images. If drawn all, they represent textured polygon
    if type(polygon)~="table" or #polygon%2~=0 then return nil end  --polygon must be a table of x1,y1,x2,y2...
    if #polygon<6 then return nil end --for line return nil: nothing to draw here

    --load texture if needed
    if type(texture)=="string" then
        texture=love.image.newImageData(texture)
    end

    --default values for texture position parameters
    if xx==nil then xx=0 end
    if yy==nil then yy=0 end
    if rr==nil then rr=0 end
    if sx==nil then sx=1 end
    if sy==nil then sy=1 end
    if ox==nil then ox=0 end
    if oy==nil then oy=0 end

    if sx==0 then sx=1 end --avoid bad scenario
    if sy==0 then sy=1 end

    rr=rr/180*3.14159 --convert degrees to radians

    if smooth==nil then smooth=true end --we draw beautiful things on default

    local bb=PolygonBoundingBox(polygon) --bounding box of polygon

    --simple==0 means no simplicity, 1 means we don't transform texture, 2 means we don't even need to round x,y values
    local simple=0
    if xx==0 and yy==0 and rr==0 and sx==1 and sy==1 and ox==0 and oy==0 then
        simple=1
        if bb.x==math.floor(bb.x) and bb.y==math.floor(bb.y) then
            simple=2
        end
    end
    --additionally, simple==3 means we need to transform our texture but user don't want to smooth the image
    if simple==0 and not smooth then 
        simple=3
    end

    --calculate some oftenly used numbers once to optimize our lovely code
    local cr,sr=math.cos(rr), math.sin(rr)
    local tw,th=texture:getWidth(), texture:getHeight()

    --triangles is array of triangles (what a concidence!)
    local triangles=nil
    if #polygon==6 then 
        triangles={polygon}
    else
        triangles=love.math.triangulate(polygon)
    end
    
    local imageData=love.image.newImageData(math.ceil(bb.w),math.ceil(bb.h)) --empty ImageData for drawing. "empty" means every pixel has value 0,0,0,0

    for i,triangle in ipairs(triangles) do --for every tirangle...

        --we need three criteria to check whether pixel is within our triangle
        --we define three linear functions y=a#+b#*(x-c#) or x=a#+b#*(y-c#) (depend on side's inclination)
        --m# is mode: above (1), below (2), right (3) or left (4)
        --e#: whether this line is the edge of original polygon: nil(false) or range of x or y(true)
        local a0={}
        local b0={}
        local c0={}
        local m0={}
        local e0={}

        --I'd love to make a cycle over pair of points of triangle, but 3rd pair is (1,3) points, not (3,4)

        --for 1st pair of points    --we keep in mind that triangle[1]->x1,[2]->y1, [3]->x2,[4]->y2, [5]->x3,[6]->y3
        if triangle[1]==triangle[3] or math.abs( (triangle[4]-triangle[2])/(triangle[3]-triangle[1]) ) > 1 then --inclination is close to vertical
            m0[1]=2
        else
            m0[1]=1
        end
        if m0[1]==1 then --horizontal criterion
            a0[1],b0[1],c0[1] = triangle[2], (triangle[4]-triangle[2])/(triangle[3]-triangle[1]), triangle[1]
            if triangle[6] > a0[1] + b0[1]*(triangle[5]-c0[1]) then --check how another point of triangle pass this criteron
                m0[1]=1 --above criterion
            else
                m0[1]=2 --under criterion
            end
        else --vertial criterion
            a0[1],b0[1],c0[1] = triangle[1], (triangle[3]-triangle[1])/(triangle[4]-triangle[2]), triangle[2]
            if triangle[5] > a0[1] + b0[1]*(triangle[6]-c0[1]) then --check how another point of triangle pass this criteron
                m0[1]=3 --right criterion
            else
                m0[1]=4 --left  criterion
            end
        end
        if PolygonEdgeLine(polygon, triangle[1],triangle[2],triangle[3],triangle[4]) then --check whether this line is the edge of polygon
            if m0[1]==1 or m0[1]==2 or m0[1]==5 or m0[1]==6 then
                e0[1]={math.min(triangle[1],triangle[3]), math.max(triangle[1],triangle[3])} --range of x where it is true
            else
                e0[1]={math.min(triangle[2],triangle[4]), math.max(triangle[2],triangle[4])} --range of y where it is true
            end
        else
            e0[1]=nil --no, it's internal splitting line
        end
        --and so on...

        --for 2nd pair of points
        if triangle[3]==triangle[5] or math.abs( (triangle[6]-triangle[4])/(triangle[5]-triangle[3]) ) > 1 then --inclination is close to vertical
            m0[2]=2
        else
            m0[2]=1
        end
        if m0[2]==1 then --horizontal criterion
            a0[2],b0[2],c0[2] = triangle[4], (triangle[6]-triangle[4])/(triangle[5]-triangle[3]), triangle[3]
            if triangle[2] > a0[2] + b0[2]*(triangle[1]-c0[2]) then
                m0[2]=1 --above criterion
            else
                m0[2]=2
            end
        else --vertial criterion
            a0[2],b0[2],c0[2] = triangle[3], (triangle[5]-triangle[3])/(triangle[6]-triangle[4]), triangle[4]
            if triangle[1] > a0[2] + b0[2]*(triangle[2]-c0[2]) then
                m0[2]=3
            else
                m0[2]=4
            end
        end
        if PolygonEdgeLine(polygon, triangle[3],triangle[4],triangle[5],triangle[6]) then
            if m0[2]==1 or m0[2]==2 or m0[2]==5 or m0[2]==6 then
                e0[2]={math.min(triangle[3],triangle[5]), math.max(triangle[3],triangle[5])}
            else
                e0[2]={math.min(triangle[4],triangle[6]), math.max(triangle[4],triangle[6])}
            end
        else
            e0[2]=nil
        end

        --for 3rd pair of points
        if triangle[1]==triangle[5] or math.abs( (triangle[6]-triangle[2])/(triangle[5]-triangle[1]) ) > 1 then --inclination is close to vertical
            m0[3]=2
        else
            m0[3]=1
        end
        if m0[3]==1 then --horizontal criterion
            a0[3],b0[3],c0[3] = triangle[2], (triangle[6]-triangle[2])/(triangle[5]-triangle[1]), triangle[1]
            if triangle[4] > a0[3] + b0[3]*(triangle[3]-c0[3]) then
                m0[3]=1
            else
                m0[3]=2
            end
        else --vertial criterion
            a0[3],b0[3],c0[3] = triangle[1], (triangle[5]-triangle[1])/(triangle[6]-triangle[2]), triangle[2]
            if triangle[3] > a0[3] + b0[3]*(triangle[4]-c0[3]) then
                m0[3]=3
            else
                m0[3]=4
            end
        end
        if PolygonEdgeLine(polygon, triangle[1],triangle[2],triangle[5],triangle[6]) then
            if m0[3]==1 or m0[3]==2 or m0[3]==5 or m0[3]==6 then
                e0[3]={math.min(triangle[1],triangle[5]), math.max(triangle[1],triangle[5])}
            else
                e0[3]={math.min(triangle[2],triangle[6]), math.max(triangle[2],triangle[6])}
            end
        else
            e0[3]=nil
        end

        local function func(x,y,r,g,b,a) --function to apply to mapPixel method of ImageData polygon
            if r~=0 or g~=0 or b~=0 or a~=0 then return r,g,b,a end --pixel was already drawn, skip

            x=x+bb.x --go from resulting image coordinates to internal texture coordinates
            y=y+bb.y

            local alpha=1 --transparency to smooth edge (only used if smooth==true)

            --check all three criteria
            for j=1,3 do

                if m0[j]==1 then
                    local y0=a0[j]+b0[j]*(x-c0[j]) --what to compare with
                    if smooth and e0[j]~=nil and x>=e0[j][1] and x<e0[j][2] then --whether we need to smooth edge
                        local z=y0-y --how far we are from ideal position of edge
                        if z>1 then --we are too far
                            return 0,0,0,0
                        elseif z>0 then --we are within one-pixel edge area of smoothing
                            alpha=alpha*(1-z)
                        end
                    elseif y<y0 then --we are outside of the triangle
                        return 0,0,0,0
                    end
                    --and so on...

                elseif m0[j]==2 then
                    local y0=a0[j]+b0[j]*(x-c0[j])
                    if smooth and e0[j]~=nil and x>=e0[j][1] and x<e0[j][2] then
                        local z=y0-y
                        if z<0 then
                            return 0,0,0,0
                        elseif z<1 then
                            alpha=alpha*z
                        end
                    elseif y>=y0 then
                        return 0,0,0,0
                    end

                elseif m0[j]==3 then
                    local x0=a0[j]+b0[j]*(y-c0[j])
                    if smooth and e0[j]~=nil and y>=e0[j][1] and y<e0[j][2] then
                        local z=x0-x
                        if z>1 then
                            return 0,0,0,0
                        elseif z>0 then
                            alpha=alpha*(1-z)
                        end
                    elseif x<x0 then
                        return 0,0,0,0
                    end

                elseif m0[j]==4 then
                    local x0=a0[j]+b0[j]*(y-c0[j])
                    if smooth and e0[j]~=nil and y>=e0[j][1] and y<e0[j][2] then
                        local z=x0-x
                        if z<0 then
                            return 0,0,0,0
                        elseif z<1 then
                            alpha=alpha*z
                        end
                    elseif x>=x0 then
                        return 0,0,0,0
                    end

                end
            end

            if simple==0 then --complicated case
                r,g,b,a=getAveragePixel(texture, ((x+0.5-xx)*cr+(y+0.5-yy)*sr)/sx+ox, ((y+0.5-yy)*cr-(x+0.5-xx)*sr)/sy+oy, tw,th)
            elseif simple==1 then --simpler case
                r,g,b,a=texture:getPixel( math.floor(x)%tw, math.floor(y)%th)
            elseif simple==2 then --simplest case
                r,g,b,a=texture:getPixel( x%tw, y%th)
            else--if simple==3 then --ugly but fast case
                r,g,b,a=texture:getPixel( math.floor(((x-xx)*cr+(y-yy)*sr)/sx+ox)%tw, math.floor(((y-yy)*cr-(x-xx)*sr)/sy+oy)%th)
            end

            if smooth then a=a*alpha end --make edge as beautiful as we can
            --return texture:getPixel( (x)%texture:getWidth(), (y)%texture:getHeight())
            return r,g,b,a
        end

        imageData:mapPixel(func) --applying will draw one more triangle on the imageData
    end

    local image=love.graphics.newImage(imageData) --creating drawable object

    return {img=image, x=bb.x,y=bb.y,w=bb.w,h=bb.h, imgData=imageData}
end

function PolygonBoundingBox(polygon) --simple function that find max and min x,y coordinates of a polygon and return them in a form of BoundingBox object: {x,y,w,h}
    if type(polygon)~="table" or #polygon<2 or #polygon%2~=0 then return nil end

    local minX=polygon[1]
    local maxX=polygon[1]
    local minY=polygon[2]
    local maxY=polygon[2]

    for i=1,#polygon,2 do
        if polygon[i]<minX then minX=polygon[i] end
        if polygon[i]>maxX then maxX=polygon[i] end
        if polygon[i+1]<minY then minY=polygon[i+1] end
        if polygon[i+1]>maxY then maxY=polygon[i+1] end
    end

    return {x=minX,y=minY,w=maxX-minX,h=maxY-minY}
end

function PolygonEdgeLine(polygon, x1,y1,x2,y2) --specific function that checks whether given pair of points belongs to one edge of a polygon
    local length=#polygon
    if type(polygon)~="table" or length<4 or length%2~=0 then return nil end

    local buf={} --comparison of non-integer values may be safe only if these values are rounded (to two digits, for example)
    for i,v in ipairs(polygon) do
        buf[i]=math.floor(v*100)/100
    end

    local x1=math.floor(x1*100)/100
    local y1=math.floor(y1*100)/100
    local x2=math.floor(x2*100)/100
    local y2=math.floor(y2*100)/100

    for i=1,length-2,2 do --compare all but the last line
        if buf[i]==x1 and buf[i+1]==y1 and buf[i+2]==x2 and buf[i+3]==y2 then
            return true
        elseif buf[i]==x2 and buf[i+1]==y2 and buf[i+2]==x1 and buf[i+3]==y1 then
            return true
        end
    end

    --compare last line separately
    if buf[1]==x1 and buf[2]==y1 and buf[length-1]==x2 and buf[length]==y2 then
        return true
    elseif buf[1]==x2 and buf[2]==y2 and buf[length-1]==x1 and buf[length]==y1 then
        return true
    end

    --if we have passed all previous checks, we fail:
    return false
end

function getAveragePixel(texture, x,y,w,h) --return a pixel which color is averaged over neighbour pixels of a texture around x,y coordinates. w,h calculated previously to speed up the code
    local x1,x2,y1,y2 = math.floor(x), math.ceil(x), math.floor(y), math.ceil(y) --find neighbour integer-indexed pixels
    if x2==x1 then x2=x2+1 end
    if y2==y1 then y2=y2+1 end

    local d1,d2,d3,d4 = (x2-x)*(y2-y), (x-x1)*(y2-y), (x2-x)*(y-y1), (x-x1)*(y-y1) --find fraction of each pixel in a result

    x1=x1%w --ensure we are within texture (wrapped around)
    x2=x2%w
    y1=y1%h
    y2=y2%h

    local r1,g1,b1,a1 = texture:getPixel(x1,y1) --obtain four raw colors
    local r2,g2,b2,a2 = texture:getPixel(x2,y1)
    local r3,g3,b3,a3 = texture:getPixel(x1,y2)
    local r4,g4,b4,a4 = texture:getPixel(x2,y2)

    --...and average it all
    return d1*r1+d2*r2+d3*r3+d4*r4, d1*g1+d2*g2+d3*g3+d4*g4, d1*b1+d2*b2+d3*b3+d4*b4, d1*a1+d2*a2+d3*a3+d4*a4
end

Run an example to see how it works:

require "textured_polygon" --link to a file where TexturedPolygon function is, or paste it below here

function love.load()
    texture=love.image.newImageData('texture.png')
    bkg=love.graphics.newImage(texture)
    polygon={100,100, 200,100, 300,200, 400,100, 520,100, 530,200, 400,200, 300,300, 200,200, 100,200}

    t_start=love.timer.getTime()
    
    image=TexturedPolygon(polygon,'texture.png')

    t_end=love.timer.getTime()

    love.graphics.setBackgroundColor(128,50,100)
end
 
function love.draw()
    love.graphics.draw( bkg, 0,20, 0, 10,10)
    love.graphics.draw( image.img, image.x, image.y)

    love.graphics.print("Spent "..(t_end-t_start).." seconds to create textured polygon")
    love.graphics.print("<- see theese pixels smoothed!", 530,140)
    love.graphics.print("| works with concave polygons as well!", 302,80)
    love.graphics.print("V                                     ", 300,90)
    love.graphics.print("^                           ", 420,200)
    love.graphics.print("| semi-transparent textures!", 422,205)
end

Use this texture:

texture.png


How it works:

screenshot1.png