Animated Voxel Copter

https://youtu.be/u6TmLp7qX6k

The frames were created in the Dual-Joystick Voxel Editor.

Project attached.

The up and down bounce is animated using 8 separate voxel model “frames” created in the Joystick Voxel Editor using the nudge feature. It would probably be simpler to use tweening, but this has a blocky charm to it I think.

updated

  • VoxelCopter is now a stand-alone class that can be used as a dependency, or just copy-pasted, if anyone wants to use the cute little bugger in their own projects.
  • In addition to the frame-based up and down animation, there is now a tween-based animation of slight turning to the left and then to the right in a smooth loop.

updated

  • all frames now have a single parent entity so the whole copter can be moved at once
  • the code’s just better. It’s just… better.

Very nice @UberGoober, you’re doing some great things!

Thanks Ron!

It’s very creative. It looks really good?

https://youtu.be/h2bjMNMCNmA

Updated to include @Bri_G ’s excellent background.

Thanks for the kind words @binaryblues!

Also available using WebRepo now!

@UberGoober I liked the code so much that I made a few changes to your code. I hope you don’t mind. I rewrote the animation in a different way. I divided the model into two parts, the propeller and the fuselage, and then let the propeller rotation around the axis, in this way to achieve the helicopter flight animation.

https://youtu.be/3ak4peIaERQ

The code is below:

Copter = class()

function Copter:init(pos)
    self.camera = scene.camera:get(craft.camera)
    self.state = true
    self.root = scene:entity()
    self.root.position = pos or vec3(0,0,0)
    
    self.topRoot = scene:entity()
    self.topRoot.parent = self.root  
    self.topRoot.position = vec3(0,0,0)
    
    self.top = scene:entity()
    self.top.parent = self.topRoot
    self.top.position = vec3(-8.5,-8.5,-12.5)
    self.vm1 = self.top:add(craft.volume, 1,1,1)
    self.vm1:load(asset.Propellers)
    self.top.active = true
    
    self.body = scene:entity()
    self.body.parent = self.root
    self.body.position = vec3(-8.5,-8.5,-12.5)
    self.vm2 = self.body:add(craft.volume, 1,1,1)
    self.vm2:load(asset.Airframe)
    self.body.active = true
    
    self.angle = 0
    self.zoom = 1
    self.y = 0
    
    touches.addHandler(self, -1, false)
end

function Copter:update(dt)
    self.topRoot.rotation = quat.eulerAngles(0, self.angle*100,0)
    self.root.rotation = quat.eulerAngles(0,self.angle,0)
    self.root.scale = vec3(self.zoom, self.zoom, self.zoom)
    self.root.y = self.y
    -- print(self.root.worldPosition)
    local a = self.topRoot.worldPosition + vec3(0,0,-1)
    local b = self.topRoot.worldPosition + vec3(0,0,1)
    local c = self.topRoot.worldPosition + vec3(0,-1,0)
    local d = self.topRoot.worldPosition + vec3(0,1,0)    
    scene.debug:line(a, b,color(16, 239, 5))    
    scene.debug:line(c, d,color(239, 19, 5))    
end

function Copter:interact()
    if not self.open then
        self.open = true
        tween(2.5, self, {angle = 61.8, zoom=1/2}, {easing=tween.easing.backOut,loop = tween.loop.once})
    else
        self.open = false
        tween(2.5, self, {angle = 0, zoom=1}, {easing=tween.easing.backOut,loop = tween.loop.once})
    end
    local t1 = tween(0.5,self,{y=2})
    local t2 = tween(0.5,self,{y=-1})
    local t3 = tween(0.5,self,{y=1})
    local t4 = tween(0.5,self,{y=-2})
    local t5 = tween(0.5,self,{y=3})
    tween.sequence(t1,t2,t3,t4,t5)
end


function Copter:touched(touch)
    if touch.state == BEGAN then
        self:interact()                
        
        -- Returning true will capture this touch and prevent other handlers from getting it
        local origin, dir = self.camera:screenToRay(vec2(touch.x, touch.y))
        -- print(self.root.worldPosition)
        -- Do a raycast to check if touch is hitting the bulb
        local hit = scene.physics:raycast(origin, dir, 300)
        -- print(self.top.position)
        if hit and hit.entity == self.body then
            
            -- Turn up light intensity
            -- self.light.intensity = 3
            -- self.entity.material.diffuse = self.color
            self.state = not self.state
            print("Touch Began (Captured - "..touch.id..")",origin)
            return true
        end
    elseif touch.state == ENDED and self.state then
        
        -- Turn down intensity when touch ends
        -- self.light.intensity = 0.2
        -- self.entity.material.diffuse = self.color * 0.2
        print("Touch Ended (Captured - "..touch.id..")")
    end
end

function setup()
    -- Create a new craft scene
    scene = craft.scene()
    scene.ambientColor = color(218, 158, 79)
    scene.sky.active = false
    
    -- Setup camera and lighting
    scene.sun.rotation = quat.eulerAngles(125, 125, 0)
    
    -- Helper class for interactive camera  
    myViewer = scene.camera:add(OrbitViewer, vec3( 0,  0,  0), 80, 1, 400)
    myViewer.rx = 0
    myViewer.ry = 0 
    
    myCopter1 = Copter(vec3(0,0,0))   
    myCopter2 = Copter(vec3(-30,0,60))   
end

function update(dt)
    scene:update(dt)
    myCopter1:update()
    myCopter2:update()
end

-- Called automatically by codea 
function draw()
    update(DeltaTime)
    scene:draw()	
    sprite(asset.builtin.UI.Blue_Circle,CurrentTouch.x,CurrentTouch.y)
end

@binaryblues that is seriously cool! I don’t mind at all, in fact, please, anybody who wants to do anything at all with this little bugger, have at it!

binaryblues: I assume it’s reciprocally fine if I use your version in the Voxel Editor (and elsewhere if needed)?

I’m not super sure what all the controls do, though, and I’m confused why the copter doesn’t stay in motion—it seems to reverse itself halfway through animating and then come to a stop.

@UberGoober Glad to hear that. The reason the helicopter is moving like this, is because I’ve added some settings, like let the fuselage rotate and so on, you can start it up by clicking on the screen, if you want to keep it moving, you can change it like this:

function Copter:update(dt)
    self.topRoot.rotation = quat.eulerAngles(0, self.angle*110.31, 0)
    -- self.root.rotation = quat.eulerAngles(0,self.angle,0)
end

function Copter:touched(touch)
    if touch.state == BEGAN then
        -- self:interact()                
        tween(0.5, self, {angle = 90.3, zoom=1}, {easing=tween.easing.backInOut,loop = tween.loop.forever})
        -- Returning true will capture this touch and prevent other handlers from getting it

Short Film

https://youtu.be/JFLm_5vgIAI

@binaryblues What tool did you use to separate the voxel model into two pieces?

And how do I recapture that bounce? It had a nice bounce in your original version, but once I did the code changes you suggested, it just stays in one place as the propeller spins.

@UberGoober Using CODEA’s built-in VoxelEditor:

  1. load your full helicopter model, then remove the propeller section, save only the fuselage, save as the fuselage,;
  2. load your full helicopter model again, and this time delete the fuselage, just keep the propeller, and then save it as a propeller, so you have two models.
    And you can just rotate the propeller in the code. The slightly tricky part here is determining the offset coordinates of the center of the helicopter model in local space, which I counted manually.
    There was some animation of the y coordinate in the original code, and I commented it out, so you can turn it back on, and here it is:
function Copter:interact()
    if not self.open then
        self.open = true
        tween(2.5, self, {angle = 61.8, y=0,zoom=1}, {easing=tween.easing.backInOut,loop = tween.loop.once})
    else
        self.open = false
        tween(2.5, self, {angle = 0,y=0, zoom=1}, {easing=tween.easing.backOut,loop = tween.loop.once})
    end
    
    —-[[
    local t1 = tween(0.5,self,{y=2})
    local t2 = tween(0.5,self,{y=-1})
    local t3 = tween(0.5,self,{y=1})
    local t4 = tween(0.5,self,{y=-2})
    local t5 = tween(0.5,self,{y=3})
    tween.sequence(t1,t2,t3,t4,t5)
    --]]
end