3D platforms

I’ve not been keeping up with the state of pseudo-3D in Corona as much as I should, so today I wanted to solve a problem for myself - that of 3D platforms using distorted images via path manipulation.

It’s kinda tricky and there are changes that I’m sure I’ve made to this code which should not have had any effect at all, yet they did and with a really nice outcome. The result is a platform (actually, a brick: https://www.dropbox.com/s/sii4aropyv9wyd2/brick.png?dl=0 ) that can be moved around (drag on the screen) and have it’s perspective point moved (drag the blue dot.)

Please let me know what you think (especially if I missed the boat!)

-- 2.5D io.output():setvbuf('no') -- Remove me for production code function dump(t) print("==========") for k,v in pairs(t) do print(k,v) end end local brick local group = display.newGroup() group.x, group.y = display.contentCenterX, display.contentCenterY local centre = display.newCircle( group, 0, 0, 25 ) centre.fill = {.3,.3,1} local back, front = display.newGroup(), display.newGroup() group:insert( back ) group:insert( front ) back.xScale, back.yScale = .85, .85 back.content, front.content = display.newGroup(), display.newGroup() back:insert( back.content ) front:insert( front.content ) local bricks = display.newGroup() group:insert( bricks ) bricks.x, bricks.y = display.contentCenterX, display.contentCenterY front:toFront() function centre:touch(e) if (e.phase == "began") then display.currentStage:setFocus( e.target ) e.target.hasFocus = true e.target.prev = e return true elseif (e.target.hasFocus) then local prev = e.target.prev local x, y = e.x-prev.x, e.y-prev.y group.x, group.y = group.x+x, group.y+y back.content.x, back.content.y = back.content.x-x, back.content.y-y bricks.x, bricks.y = bricks.x-x, bricks.y-y front.content.x, front.content.y = front.content.x-x, front.content.y-y brick:updatePathControls() if (e.phase == "moved") then e.target.prev = e else display.currentStage:setFocus( nil ) e.target.hasFocus = nil e.target.prev = nil end return true end return false end centre:addEventListener( "touch", centre ) local factors = { {x=-1,y=-1}, {x=-1,y=1}, {x=1,y=1}, {x=1,y=-1} } local function initPathControls( self, ctrls ) self.controls = ctrls end brick = display.newImage( bricks, "brick.png" ) brick.alpha = 1 local w, h = brick.width/2, brick.height/2 local function updatePathControls( self ) for i=1, 4 do local x, y = self.controls[i]:localToContent( 0, 0 ) x, y = self:contentToLocal( x, y ) x = x - (factors[i].x \* w) y = y - (factors[i].y \* h) self.path["x"..i], self.path["y"..i] = x, y end end brick.initPathControls = initPathControls brick.updatePathControls = updatePathControls local ctls = {} for i=1, 4 do local parent, colour = back.content, {1,0,0} if (i == 2 or i == 3) then parent=front.content colour = {0,1,0} end ctls[i] = display.newCircle( parent, w\*factors[i].x, 0, 15 ) ctls[i].index = i ctls[i].fill = colour end brick:initPathControls( ctls ) brick:updatePathControls() local a = display.newRect( back.content, 0, 0, 250, 100 ) a.fill = {1,0,0} local b = display.newRect( front.content, 0, 0, 250, 100 ) b.fill = {0,1,0,.5} local prev = nil local function touch(e) if (e.phase == "began") then else local x, y = e.x-prev.x, e.y-prev.y back.content.x, back.content.y = back.content.x+x, back.content.y+y front.content.x, front.content.y = front.content.x+x, front.content.y+y brick:updatePathControls() end prev = e return true end Runtime:addEventListener( "touch", touch )

Pretty cool demo.  What is your config.lua’s content area?  It seems like the more real estate the better for this demo.

Rob

I didn’t set one - just running it in iPad Air mode.

I set up the update function this way so that I could tie images to points on two layers and control the entire scene with as little hassle as possible.

sweet!

Just a small modification (though I know the code is quite untidy.) Adding child groups to the front and back layers allows you to improve the 3D effect. Here I’ve simply added rotation to them, in sync, to rotate the brick:

-- 2.5D io.output():setvbuf('no') -- Remove me for production code function dump(t) print("==========") for k,v in pairs(t) do print(k,v) end end local brick local group = display.newGroup() group.x, group.y = display.contentCenterX, display.contentCenterY local centre = display.newCircle( group, 0, 0, 25 ) centre.fill = {.3,.3,1} local back, front = display.newGroup(), display.newGroup() group:insert( back ) group:insert( front ) back.xScale, back.yScale = .85, .85 back.content, front.content = display.newGroup(), display.newGroup() back:insert( back.content ) front:insert( front.content ) local bricks = display.newGroup() group:insert( bricks ) bricks.x, bricks.y = display.contentCenterX, display.contentCenterY front:toFront() function centre:touch(e) if (e.phase == "began") then display.currentStage:setFocus( e.target ) e.target.hasFocus = true e.target.prev = e return true elseif (e.target.hasFocus) then local prev = e.target.prev local x, y = e.x-prev.x, e.y-prev.y group.x, group.y = group.x+x, group.y+y back.content.x, back.content.y = back.content.x-x, back.content.y-y bricks.x, bricks.y = bricks.x-x, bricks.y-y front.content.x, front.content.y = front.content.x-x, front.content.y-y brick:updatePathControls() if (e.phase == "moved") then e.target.prev = e else display.currentStage:setFocus( nil ) e.target.hasFocus = nil e.target.prev = nil end return true end return false end centre:addEventListener( "touch", centre ) local factors = { {x=-1,y=-1}, {x=-1,y=1}, {x=1,y=1}, {x=1,y=-1} } local function initPathControls( self, ctrls ) self.controls = ctrls end brick = display.newImage( bricks, "brick.png" ) brick.alpha = 1 local w, h = brick.width/2, brick.height/2 local function updatePathControls( self ) for i=1, 4 do local x, y = self.controls[i]:localToContent( 0, 0 ) x, y = self:contentToLocal( x, y ) x = x - (factors[i].x \* w) y = y - (factors[i].y \* h) self.path["x"..i], self.path["y"..i] = x, y end end brick.initPathControls = initPathControls brick.updatePathControls = updatePathControls --local backrotate, frontrotate = display.newGroup(), display.newGroup() Runtime:addEventListener( "enterFrame", function() back.content.rotation = back.content.rotation + 360/600 front.content.rotation = front.content.rotation + 360/600 brick:updatePathControls() end ) local ctls = {} for i=1, 4 do local parent, colour = back.content, {1,0,0} if (i == 2 or i == 3) then parent=front.content colour = {0,1,0} end ctls[i] = display.newCircle( parent, w\*factors[i].x, 0, 15 ) ctls[i].index = i ctls[i].fill = colour end brick:initPathControls( ctls ) brick:updatePathControls() local a = display.newRect( back.content, 0, 0, 250, 100 ) a.fill = {1,0,0,0} a.stroke = {1,0,0} a.strokeWidth = 3 local b = display.newRect( front.content, 0, 0, 250, 100 ) b.fill = {0,1,0,0} b.stroke = {0,1,0} b.strokeWidth = 3 local prev = nil local function touch(e) if (e.phase == "began") then else local x, y = e.x-prev.x, e.y-prev.y back.content.x, back.content.y = back.content.x+x, back.content.y+y front.content.x, front.content.y = front.content.x+x, front.content.y+y brick:updatePathControls() end prev = e return true end Runtime:addEventListener( "touch", touch )

Another small change - add this section to the very end of the code and you can control the perspective:

local widget = require( "widget" ) local sliderLabel = display.newText{ x=250, y=115, text="Slider at 85% and scale .85", fontSize=26 } sliderLabel.fill = {.97,.97,.97} -- Slider listener local function sliderListener( event ) local scale = ((1-.25)/100)\*event.value + .2 back.xScale, back.yScale = scale, scale --print( "Slider at " .. event.value .. "% and scale "..scale ) sliderLabel.text = "Slider at " .. event.value .. "% and scale "..scale end -- Create the widget local slider = widget.newSlider { top = 50, left = 50, width = 400, value = 85, listener = sliderListener }

Adding a little “backface culling” to remove the brick when it’s facing away from us, we update the enterFrame function like this:

local function isPolygonClockwise( pts ) local area = 0 local flat = {} for i=1, #pts do local x, y = pts[i]:localToContent( 0, 0 ) flat[#flat+1] = {x=x,y=y} end for i = 1, #flat-1 do local start = { x=flat[i].x - flat[1].x, y=flat[i].y - flat[1].y } local finish = { x=flat[i + 1].x - flat[1].x, y=flat[i + 1].y - flat[1].y } area = area + (start.x \* -finish.y) - (finish.x \* -start.y) end return (area \< 0) end --local backrotate, frontrotate = display.newGroup(), display.newGroup() Runtime:addEventListener( "enterFrame", function() back.content.rotation = back.content.rotation + 360/600 front.content.rotation = front.content.rotation + 360/600 brick:updatePathControls() brick.isVisible = isPolygonClockwise( ctls ) end )

With a lot of cleaning up and adjusting, we can now render a rotating pseudo-3D, texture-mapped cube:

-- 2.5D local widget = require( "widget" ) io.output():setvbuf('no') -- Remove me for production code function dump(t) print("==========") for k,v in pairs(t) do print(k,v) end end local factors = { {x=-1,y=-1}, {x=-1,y=1}, {x=1,y=1}, {x=1,y=-1} } local group = display.newGroup() group.x, group.y = display.contentCenterX, display.contentCenterY local centre = display.newCircle( group, 0, 0, 15 ) centre.fill = {.3,.3,1} local back, front = display.newGroup(), display.newGroup() group:insert( back ) group:insert( front ) back.xScale, back.yScale = .85, .85 back.content, front.content = display.newGroup(), display.newGroup() back:insert( back.content ) front:insert( front.content ) local bricks = display.newGroup() group:insert( bricks ) bricks.x, bricks.y = display.contentCenterX, display.contentCenterY front:toFront() local sliderLabel = display.newText{ x=250, y=115, text="Slider at 85% and scale .85", fontSize=26 } sliderLabel.fill = {.97,.97,.97} -- Slider listener local function sliderListener( event ) local scale = ((1-.25)/100)\*event.value + .2 back.xScale, back.yScale = scale, scale sliderLabel.text = "Slider at " .. event.value .. "% and scale "..scale end -- Create the widget local slider = widget.newSlider { top = 50, left = 50, width = 400, value = 85, listener = sliderListener } function centre:touch(e) if (e.phase == "began") then display.currentStage:setFocus( e.target ) e.target.hasFocus = true e.target.prev = e return true elseif (e.target.hasFocus) then local prev = e.target.prev local x, y = e.x-prev.x, e.y-prev.y group.x, group.y = group.x+x, group.y+y back.content.x, back.content.y = back.content.x-x, back.content.y-y bricks.x, bricks.y = bricks.x-x, bricks.y-y front.content.x, front.content.y = front.content.x-x, front.content.y-y for i=1, bricks.numChildren do bricks[i]:updatePathControls() end if (e.phase == "moved") then e.target.prev = e else display.currentStage:setFocus( nil ) e.target.hasFocus = nil e.target.prev = nil end return true end return false end centre:addEventListener( "touch", centre ) local function isPolygonClockwise( pts ) local area = 0 local flat = {} for i=1, #pts do local x, y = pts[i]:localToContent( 0, 0 ) flat[#flat+1] = {x=x,y=y} end for i = 1, #flat-1 do local start = { x=flat[i].x - flat[1].x, y=flat[i].y - flat[1].y } local finish = { x=flat[i + 1].x - flat[1].x, y=flat[i + 1].y - flat[1].y } area = area + (start.x \* -finish.y) - (finish.x \* -start.y) end return (area \< 0) end local prev = nil local function touch(e) if (e.phase == "began") then else local x, y = e.x-prev.x, e.y-prev.y back.content.x, back.content.y = back.content.x+x, back.content.y+y front.content.x, front.content.y = front.content.x+x, front.content.y+y for i=1, bricks.numChildren do bricks[i]:updatePathControls() end end prev = e return true end --local backrotate, frontrotate = display.newGroup(), display.newGroup() local function enterFrame() back.content.rotation = back.content.rotation + 360/600 front.content.rotation = front.content.rotation + 360/600 for i=1, bricks.numChildren do bricks[i]:updatePathControls() bricks[i].isVisible = isPolygonClockwise( bricks[i].controls ) end end Runtime:addEventListener( "touch", touch ) Runtime:addEventListener( "enterFrame", enterFrame ) local function updatePathControls( self ) for i=1, 4 do local x, y = self.controls[i]:localToContent( 0, 0 ) x, y = self:contentToLocal( x, y ) x = x - (factors[i].x \* self.w) y = y - (factors[i].y \* self.h) self.path["x"..i], self.path["y"..i] = x, y end end local function initPathControls( self ) local w, h = self.width/2, self.height/2 local ctls = {} for i=1, 4 do local parent, colour = back.content, {1,0,0} if (i == 2 or i == 3) then parent=front.content colour = {0,1,0} end ctls[i] = display.newCircle( parent, w\*factors[i].x, 0, 1 ) ctls[i].index = i ctls[i].fill = colour end self.controls = ctls self.w, self.h = w, h end local function newBrick( name, a, b ) local brick = display.newImage( bricks, name ) brick.initPathControls = initPathControls brick.updatePathControls = updatePathControls brick:initPathControls() brick:updatePathControls() brick.controls[1].x, brick.controls[1].y = a.x, a.y brick.controls[2].x, brick.controls[2].y = a.x, a.y brick.controls[3].x, brick.controls[3].y = b.x, b.y brick.controls[4].x, brick.controls[4].y = b.x, b.y return brick end newBrick( "brick.png", {x=-100,y=100}, {x=100,y=100} ) newBrick( "brick.png", {x=100,y=100}, {x=100,y=-100} ) newBrick( "brick.png", {x=100,y=-100}, {x=-100,y=-100} ) newBrick( "brick.png", {x=-100,y=-100}, {x=-100,y=100} ) -- not entirely sure how to composite fill with a colour and an image, so layering the two... local white = display.newPolygon( front.content, 0,0 , { -100,100, 100,100 , 100,-100 , -100,-100 } ) local cap = display.newPolygon( front.content, 0,0 , { -100,100, 100,100 , 100,-100 , -100,-100 } ) cap.fill = { type="image", filename="brick.png" } cap.alpha = .9

I should note that there is just one problem with this: Faces which are obscured by other faces when the cube is moved are overwritten by those obscuring faces. For this we need z-axis ordering.

Pretty cool demo.  What is your config.lua’s content area?  It seems like the more real estate the better for this demo.

Rob

I didn’t set one - just running it in iPad Air mode.

I set up the update function this way so that I could tie images to points on two layers and control the entire scene with as little hassle as possible.

sweet!

Just a small modification (though I know the code is quite untidy.) Adding child groups to the front and back layers allows you to improve the 3D effect. Here I’ve simply added rotation to them, in sync, to rotate the brick:

-- 2.5D io.output():setvbuf('no') -- Remove me for production code function dump(t) print("==========") for k,v in pairs(t) do print(k,v) end end local brick local group = display.newGroup() group.x, group.y = display.contentCenterX, display.contentCenterY local centre = display.newCircle( group, 0, 0, 25 ) centre.fill = {.3,.3,1} local back, front = display.newGroup(), display.newGroup() group:insert( back ) group:insert( front ) back.xScale, back.yScale = .85, .85 back.content, front.content = display.newGroup(), display.newGroup() back:insert( back.content ) front:insert( front.content ) local bricks = display.newGroup() group:insert( bricks ) bricks.x, bricks.y = display.contentCenterX, display.contentCenterY front:toFront() function centre:touch(e) if (e.phase == "began") then display.currentStage:setFocus( e.target ) e.target.hasFocus = true e.target.prev = e return true elseif (e.target.hasFocus) then local prev = e.target.prev local x, y = e.x-prev.x, e.y-prev.y group.x, group.y = group.x+x, group.y+y back.content.x, back.content.y = back.content.x-x, back.content.y-y bricks.x, bricks.y = bricks.x-x, bricks.y-y front.content.x, front.content.y = front.content.x-x, front.content.y-y brick:updatePathControls() if (e.phase == "moved") then e.target.prev = e else display.currentStage:setFocus( nil ) e.target.hasFocus = nil e.target.prev = nil end return true end return false end centre:addEventListener( "touch", centre ) local factors = { {x=-1,y=-1}, {x=-1,y=1}, {x=1,y=1}, {x=1,y=-1} } local function initPathControls( self, ctrls ) self.controls = ctrls end brick = display.newImage( bricks, "brick.png" ) brick.alpha = 1 local w, h = brick.width/2, brick.height/2 local function updatePathControls( self ) for i=1, 4 do local x, y = self.controls[i]:localToContent( 0, 0 ) x, y = self:contentToLocal( x, y ) x = x - (factors[i].x \* w) y = y - (factors[i].y \* h) self.path["x"..i], self.path["y"..i] = x, y end end brick.initPathControls = initPathControls brick.updatePathControls = updatePathControls --local backrotate, frontrotate = display.newGroup(), display.newGroup() Runtime:addEventListener( "enterFrame", function() back.content.rotation = back.content.rotation + 360/600 front.content.rotation = front.content.rotation + 360/600 brick:updatePathControls() end ) local ctls = {} for i=1, 4 do local parent, colour = back.content, {1,0,0} if (i == 2 or i == 3) then parent=front.content colour = {0,1,0} end ctls[i] = display.newCircle( parent, w\*factors[i].x, 0, 15 ) ctls[i].index = i ctls[i].fill = colour end brick:initPathControls( ctls ) brick:updatePathControls() local a = display.newRect( back.content, 0, 0, 250, 100 ) a.fill = {1,0,0,0} a.stroke = {1,0,0} a.strokeWidth = 3 local b = display.newRect( front.content, 0, 0, 250, 100 ) b.fill = {0,1,0,0} b.stroke = {0,1,0} b.strokeWidth = 3 local prev = nil local function touch(e) if (e.phase == "began") then else local x, y = e.x-prev.x, e.y-prev.y back.content.x, back.content.y = back.content.x+x, back.content.y+y front.content.x, front.content.y = front.content.x+x, front.content.y+y brick:updatePathControls() end prev = e return true end Runtime:addEventListener( "touch", touch )

Another small change - add this section to the very end of the code and you can control the perspective:

local widget = require( "widget" ) local sliderLabel = display.newText{ x=250, y=115, text="Slider at 85% and scale .85", fontSize=26 } sliderLabel.fill = {.97,.97,.97} -- Slider listener local function sliderListener( event ) local scale = ((1-.25)/100)\*event.value + .2 back.xScale, back.yScale = scale, scale --print( "Slider at " .. event.value .. "% and scale "..scale ) sliderLabel.text = "Slider at " .. event.value .. "% and scale "..scale end -- Create the widget local slider = widget.newSlider { top = 50, left = 50, width = 400, value = 85, listener = sliderListener }

Adding a little “backface culling” to remove the brick when it’s facing away from us, we update the enterFrame function like this:

local function isPolygonClockwise( pts ) local area = 0 local flat = {} for i=1, #pts do local x, y = pts[i]:localToContent( 0, 0 ) flat[#flat+1] = {x=x,y=y} end for i = 1, #flat-1 do local start = { x=flat[i].x - flat[1].x, y=flat[i].y - flat[1].y } local finish = { x=flat[i + 1].x - flat[1].x, y=flat[i + 1].y - flat[1].y } area = area + (start.x \* -finish.y) - (finish.x \* -start.y) end return (area \< 0) end --local backrotate, frontrotate = display.newGroup(), display.newGroup() Runtime:addEventListener( "enterFrame", function() back.content.rotation = back.content.rotation + 360/600 front.content.rotation = front.content.rotation + 360/600 brick:updatePathControls() brick.isVisible = isPolygonClockwise( ctls ) end )

With a lot of cleaning up and adjusting, we can now render a rotating pseudo-3D, texture-mapped cube:

-- 2.5D local widget = require( "widget" ) io.output():setvbuf('no') -- Remove me for production code function dump(t) print("==========") for k,v in pairs(t) do print(k,v) end end local factors = { {x=-1,y=-1}, {x=-1,y=1}, {x=1,y=1}, {x=1,y=-1} } local group = display.newGroup() group.x, group.y = display.contentCenterX, display.contentCenterY local centre = display.newCircle( group, 0, 0, 15 ) centre.fill = {.3,.3,1} local back, front = display.newGroup(), display.newGroup() group:insert( back ) group:insert( front ) back.xScale, back.yScale = .85, .85 back.content, front.content = display.newGroup(), display.newGroup() back:insert( back.content ) front:insert( front.content ) local bricks = display.newGroup() group:insert( bricks ) bricks.x, bricks.y = display.contentCenterX, display.contentCenterY front:toFront() local sliderLabel = display.newText{ x=250, y=115, text="Slider at 85% and scale .85", fontSize=26 } sliderLabel.fill = {.97,.97,.97} -- Slider listener local function sliderListener( event ) local scale = ((1-.25)/100)\*event.value + .2 back.xScale, back.yScale = scale, scale sliderLabel.text = "Slider at " .. event.value .. "% and scale "..scale end -- Create the widget local slider = widget.newSlider { top = 50, left = 50, width = 400, value = 85, listener = sliderListener } function centre:touch(e) if (e.phase == "began") then display.currentStage:setFocus( e.target ) e.target.hasFocus = true e.target.prev = e return true elseif (e.target.hasFocus) then local prev = e.target.prev local x, y = e.x-prev.x, e.y-prev.y group.x, group.y = group.x+x, group.y+y back.content.x, back.content.y = back.content.x-x, back.content.y-y bricks.x, bricks.y = bricks.x-x, bricks.y-y front.content.x, front.content.y = front.content.x-x, front.content.y-y for i=1, bricks.numChildren do bricks[i]:updatePathControls() end if (e.phase == "moved") then e.target.prev = e else display.currentStage:setFocus( nil ) e.target.hasFocus = nil e.target.prev = nil end return true end return false end centre:addEventListener( "touch", centre ) local function isPolygonClockwise( pts ) local area = 0 local flat = {} for i=1, #pts do local x, y = pts[i]:localToContent( 0, 0 ) flat[#flat+1] = {x=x,y=y} end for i = 1, #flat-1 do local start = { x=flat[i].x - flat[1].x, y=flat[i].y - flat[1].y } local finish = { x=flat[i + 1].x - flat[1].x, y=flat[i + 1].y - flat[1].y } area = area + (start.x \* -finish.y) - (finish.x \* -start.y) end return (area \< 0) end local prev = nil local function touch(e) if (e.phase == "began") then else local x, y = e.x-prev.x, e.y-prev.y back.content.x, back.content.y = back.content.x+x, back.content.y+y front.content.x, front.content.y = front.content.x+x, front.content.y+y for i=1, bricks.numChildren do bricks[i]:updatePathControls() end end prev = e return true end --local backrotate, frontrotate = display.newGroup(), display.newGroup() local function enterFrame() back.content.rotation = back.content.rotation + 360/600 front.content.rotation = front.content.rotation + 360/600 for i=1, bricks.numChildren do bricks[i]:updatePathControls() bricks[i].isVisible = isPolygonClockwise( bricks[i].controls ) end end Runtime:addEventListener( "touch", touch ) Runtime:addEventListener( "enterFrame", enterFrame ) local function updatePathControls( self ) for i=1, 4 do local x, y = self.controls[i]:localToContent( 0, 0 ) x, y = self:contentToLocal( x, y ) x = x - (factors[i].x \* self.w) y = y - (factors[i].y \* self.h) self.path["x"..i], self.path["y"..i] = x, y end end local function initPathControls( self ) local w, h = self.width/2, self.height/2 local ctls = {} for i=1, 4 do local parent, colour = back.content, {1,0,0} if (i == 2 or i == 3) then parent=front.content colour = {0,1,0} end ctls[i] = display.newCircle( parent, w\*factors[i].x, 0, 1 ) ctls[i].index = i ctls[i].fill = colour end self.controls = ctls self.w, self.h = w, h end local function newBrick( name, a, b ) local brick = display.newImage( bricks, name ) brick.initPathControls = initPathControls brick.updatePathControls = updatePathControls brick:initPathControls() brick:updatePathControls() brick.controls[1].x, brick.controls[1].y = a.x, a.y brick.controls[2].x, brick.controls[2].y = a.x, a.y brick.controls[3].x, brick.controls[3].y = b.x, b.y brick.controls[4].x, brick.controls[4].y = b.x, b.y return brick end newBrick( "brick.png", {x=-100,y=100}, {x=100,y=100} ) newBrick( "brick.png", {x=100,y=100}, {x=100,y=-100} ) newBrick( "brick.png", {x=100,y=-100}, {x=-100,y=-100} ) newBrick( "brick.png", {x=-100,y=-100}, {x=-100,y=100} ) -- not entirely sure how to composite fill with a colour and an image, so layering the two... local white = display.newPolygon( front.content, 0,0 , { -100,100, 100,100 , 100,-100 , -100,-100 } ) local cap = display.newPolygon( front.content, 0,0 , { -100,100, 100,100 , 100,-100 , -100,-100 } ) cap.fill = { type="image", filename="brick.png" } cap.alpha = .9

I should note that there is just one problem with this: Faces which are obscured by other faces when the cube is moved are overwritten by those obscuring faces. For this we need z-axis ordering.