3D zoom library

3D version of my zoom library in action:


--# Main
-- 3D example

-- Use this function to perform your initial setup
function setup()
    print("Hello World!")
    scn=Scene()
    scn:add(Cube(30,vec3(0,0,0),"Planet Cute:Wood Block"))
    scn:add(Cube(30,vec3(35,0,35),"Planet Cute:Dirt Block",color(255, 255, 255, 120)))
    
    parameter("FieldOfView", 10, 140, 20)

    zoom3d=Zoom3D(30,20,vec3(35,0,35))
    
    pt3D=vec3(50,15,50)
    lkat=vec3(35,0,35)
end

function touched(touch)
    zoom3d:touched(touch)
end

-- This function gets called once every frame
function draw()
    zoom3d:draw()

    -- This sets a dark background color 
    background(0, 0, 0, 255)

    -- Do your 3D drawing here
    scn:draw()
    
    local pt=zoom3d:getWorldPoint(pt3D)
    local pto=zoom3d:getWorldPoint(zoom3d.lookat)
    -- Restore orthographic projection
    ortho()
    
    -- Restore the view matrix to the identity
    viewMatrix(matrix())
    pushStyle()
    ellipseMode=CENTER
    ellipse(pt.x,pt.y,10)
    -- draw center of rotation in red
    fill(255, 0, 0, 255)
    ellipse(pto.x, pto.y,10)
    popStyle()
    -- Draw a label at the top of the screen
    fill(255)
    font("MyriadPro-Bold")
    fontSize(30)
    
    text("3D example", WIDTH/2, HEIGHT - 30)
end


--# Cube
Cube = class()

function Cube:init(size,pos,texture,colour)
    -- you can accept and set parameters here
    size = size or 1
    pos = pos or vec3(0,0,0)
    colour = colour or color(255, 255, 255, 255)
    
    -- all the unique vertices that make up a cube
    local vertices = {
      vec3(-0.5*size, -0.5*size,  0.5*size)+pos, -- Left  bottom front
      vec3( 0.5*size, -0.5*size,  0.5*size)+pos, -- Right bottom front
      vec3( 0.5*size,  0.5*size,  0.5*size)+pos, -- Right top    front
      vec3(-0.5*size,  0.5*size,  0.5*size)+pos, -- Left  top    front
      vec3(-0.5*size, -0.5*size, -0.5*size)+pos, -- Left  bottom back
      vec3( 0.5*size, -0.5*size, -0.5*size)+pos, -- Right bottom back
      vec3( 0.5*size,  0.5*size, -0.5*size)+pos, -- Right top    back
      vec3(-0.5*size,  0.5*size, -0.5*size)+pos, -- Left  top    back
    }


    -- now construct a cube out of the vertices above
    local cubeverts = {
      -- Front
      vertices[1], vertices[2], vertices[3],
      vertices[1], vertices[3], vertices[4],
      -- Right
      vertices[2], vertices[6], vertices[7],
      vertices[2], vertices[7], vertices[3],
      -- Back
      vertices[6], vertices[5], vertices[8],
      vertices[6], vertices[8], vertices[7],
      -- Left
      vertices[5], vertices[1], vertices[4],
      vertices[5], vertices[4], vertices[8],
      -- Top
      vertices[4], vertices[3], vertices[7],
      vertices[4], vertices[7], vertices[8],
      -- Bottom
      vertices[5], vertices[6], vertices[2],
      vertices[5], vertices[2], vertices[1],
    }

    -- all the unique texture positions needed
    local texvertices = { vec2(0.03,0.24),
                          vec2(0.97,0.24),
                          vec2(0.03,0.69),
                          vec2(0.97,0.69) }
                
    -- apply the texture coordinates to each triangle
    local cubetexCoords = {
      -- Front
      texvertices[1], texvertices[2], texvertices[4],
      texvertices[1], texvertices[4], texvertices[3],
      -- Right
      texvertices[1], texvertices[2], texvertices[4],
      texvertices[1], texvertices[4], texvertices[3],
      -- Back
      texvertices[1], texvertices[2], texvertices[4],
      texvertices[1], texvertices[4], texvertices[3],
      -- Left
      texvertices[1], texvertices[2], texvertices[4],
      texvertices[1], texvertices[4], texvertices[3],
      -- Top
      texvertices[1], texvertices[2], texvertices[4],
      texvertices[1], texvertices[4], texvertices[3],
      -- Bottom
      texvertices[1], texvertices[2], texvertices[4],
      texvertices[1], texvertices[4], texvertices[3],
    }
    
    -- now we make our 3 different block types
    self.mesh = mesh()
    self.mesh.vertices = cubeverts
    --sprite("Planet Cute:Wood Block")
    self.mesh.texture = texture
    self.mesh.texCoords = cubetexCoords
    self.mesh:setColors(colour.r,colour.g,colour.b,colour.a)
end

function Cube:draw()
    self.mesh:draw()
end

function Cube:touched(touch)
    -- Codea does not automatically call this method
end

Scene = class()

function Scene:init()
    self.objs = {}
end

function Scene:add(obj)
    self.objs[obj]=1
end

function Scene:remove(obj)
    self.objs[obj]=nil
end

function Scene:draw()
    for obj,_ in pairs(self.objs) do
        obj:draw()
    end
end

Plus the neede matrix inversion (should really be part of Codea, as well as vec4 matrix multiplication)

-- Matrix invert
function invert(m)
    local inv=matrix()
    local invOut=matrix()
    
    
    inv[1] =   m[6]  *   m[11] *   m[16] - 
         m[6]  *   m[12] *   m[15] - 
         m[10]  *   m[7]  *   m[16] + 
         m[10]  *   m[8]  *   m[15] +
         m[14] *   m[7]  *   m[12] - 
         m[14] *   m[8]  *   m[11]

  inv[5] = -  m[5]  *   m[11] *   m[16] + 
         m[5]  *   m[12] *   m[15] + 
         m[9]  *   m[7]  *   m[16] - 
         m[9]  *   m[8]  *   m[15] - 
         m[13] *   m[7]  *   m[12] + 
         m[13] *   m[8]  *   m[11]

  inv[9] =   m[5]  *   m[10] *   m[16] - 
         m[5]  *   m[12] *   m[14] - 
         m[9]  *   m[6] *   m[16] + 
         m[9]  *   m[8] *   m[14] + 
         m[13] *   m[6] *   m[12] - 
         m[13] *   m[8] *   m[10]

  inv[13] = -  m[5]  *   m[10] *   m[15] + 
          m[5]  *   m[11] *   m[14] +
          m[9]  *   m[6] *   m[15] - 
          m[9]  *   m[7] *   m[14] - 
          m[13] *   m[6] *   m[11] + 
          m[13] *   m[7] *   m[10]

  inv[2] = -  m[2]  *   m[11] *   m[16] + 
         m[2]  *   m[12] *   m[15] + 
         m[10]  *   m[3] *   m[16] - 
         m[10]  *   m[4] *   m[15] - 
         m[14] *   m[3] *   m[12] + 
         m[14] *   m[4] *   m[11]

  inv[6] =   m[1]  *   m[11] *   m[16] - 
         m[1]  *   m[12] *   m[15] - 
         m[9]  *   m[3] *   m[16] + 
         m[9]  *   m[4] *   m[15] + 
         m[13] *   m[3] *   m[12] - 
         m[13] *   m[4] *   m[11]

  inv[10] = -  m[1]  *   m[10] *   m[16] + 
         m[1]  *   m[12] *   m[14] + 
         m[9]  *   m[2] *   m[16] - 
         m[9]  *   m[4] *   m[14] - 
         m[13] *   m[2] *   m[12] + 
         m[13] *   m[4] *   m[10]

  inv[14] =   m[1]  *   m[10] *   m[15] - 
         m[1]  *   m[11] *   m[14] - 
         m[9]  *   m[2] *   m[15] + 
         m[9]  *   m[3] *   m[14] + 
         m[13] *   m[2] *   m[11] - 
         m[13] *   m[3] *   m[10]

  inv[3] =   m[2]  *   m[7] *   m[16] - 
         m[2]  *   m[8] *   m[15] - 
         m[6]  *   m[3] *   m[16] + 
         m[6]  *   m[4] *   m[15] + 
         m[14] *   m[3] *   m[8] - 
         m[14] *   m[4] *   m[7]

  inv[7] = -  m[1]  *   m[7] *   m[16] + 
         m[1]  *   m[8] *   m[15] + 
         m[5]  *   m[3] *   m[16] - 
         m[5]  *   m[4] *   m[15] - 
         m[13] *   m[3] *   m[8] + 
         m[13] *   m[4] *   m[7]

  inv[11] =   m[1]  *   m[6] *   m[16] - 
         m[1]  *   m[8] *   m[14] - 
         m[5]  *   m[2] *   m[16] + 
         m[5]  *   m[4] *   m[14] + 
         m[13] *   m[2] *   m[8] - 
         m[13] *   m[4] *   m[6]

  inv[15] = -  m[1]  *   m[6] *   m[15] + 
          m[1]  *   m[7] *   m[14] + 
          m[5]  *   m[2] *   m[15] - 
          m[5]  *   m[3] *   m[14] - 
          m[13] *   m[2] *   m[7] + 
          m[13] *   m[3] *   m[6]

  inv[4] = -  m[2] *   m[7] *   m[12] + 
         m[2] *   m[8] *   m[11] + 
         m[6] *   m[3] *   m[12] - 
         m[6] *   m[4] *   m[11] - 
         m[10] *   m[3] *   m[8] + 
         m[10] *   m[4] *   m[7]

  inv[8] =   m[1] *   m[7] *   m[12] - 
         m[1] *   m[8] *   m[11] - 
         m[5] *   m[3] *   m[12] + 
         m[5] *   m[4] *   m[11] + 
         m[9] *   m[3] *   m[8] - 
         m[9] *   m[4] *   m[7]

  inv[12] = -  m[1] *   m[6] *   m[12] + 
          m[1] *   m[8] *   m[10] + 
          m[5] *   m[2] *   m[12] - 
          m[5] *   m[4] *   m[10] - 
          m[9] *   m[2] *   m[8] + 
          m[9] *   m[4] *   m[6]

  inv[16] =   m[1] *   m[6] *   m[11] - 
         m[1] *   m[7] *   m[10] - 
         m[5] *   m[2] *   m[11] + 
         m[5] *   m[3] *   m[10] + 
         m[9] *   m[2] *   m[7] - 
         m[9] *   m[3] *   m[6]

  local det =   m[1] * inv[1] +   m[2] * inv[5] +   m[3] * inv[9] +   m[4] * inv[13]

    if (det == 0) then
        return nil
    end
    
    --det = 1.0 / det

    for i = 1,16 do
        invOut[i] = inv[i] / det
    end
    return invOut
end

…and finally the library itself. In this example it also uses the FieldOfView parameter.

-- Zoom3D library v1.0
-- Herwig Van Marck
-- usage:
--[[
function setup()
    -- 
    zoom3d=Zoom3D(20,30,vec3(0,0,0))
end
function touched(touch)
    zoom3d:touched(touch)
end
function draw()
    zoom3d:draw()
end
]]--

Zoom3D = class()

function Zoom3D:init(theta,phi,lookat)
    -- you can accept and set parameters here
    self.touches = {}
    self.initx=theta*WIDTH/360 or 0;
    self.inity=(phi+90)*HEIGHT/180 or 0;
    self.initl=lookat or vec3(0,0,0)
    self:clear()
    print("Tap and drag to rotate around center\
Double tap and drag to move center\
Pinch to zoom\
Tripple tap to reset zoom")
end

function Zoom3D:clear()
    self.lastPinchDist = 0
    self.pinchDelta = 1.0
    self.center = vec2(self.initx,self.inity)
    self.lcenter = vec2(WIDTH/2,HEIGHT/2)
    self.lookat = self.initl
    self.offset = vec2(0,0)
    --self.loffset = vec2(0,0)
    self.zoom = 1
    self.started = false
    self.started2 = false
    self.dbl = false
    pushMatrix()
    resetMatrix()
    self:setCamera()
    self.matrix=modelMatrix()*viewMatrix()*projectionMatrix()
    self.invMatrix=invert(self.matrix)
    popMatrix()
    self.reset=false
end

function Zoom3D:touched(touch)
    -- Codea does not automatically call this method
    if touch.state == ENDED then
        self.touches[touch.id] = nil
        if self.dbl then
            self.lcenter = vec2(WIDTH/2,HEIGHT/2)
            self.reset=true
            self.dbl=false
        end
    else
        self.touches[touch.id] = touch
        if (touch.tapCount==2) then
            self.dbl=true
        elseif (touch.tapCount==3) then
            self:clear()
        end
    end
end

function Zoom3D:processTouches()
    local touchArr = {}
    for k,touch in pairs(self.touches) do
        -- push touches into array
        table.insert(touchArr,touch)
    end

    if #touchArr == 2 then
        if self.dbl then
            self.lcenter = vec2(WIDTH/2,HEIGHT/2)
            self.reset=true
            self.dbl=false
        end
        self.started = false
        local t1 = vec2(touchArr[1].x,touchArr[1].y)
        local t2 = vec2(touchArr[2].x,touchArr[2].y)

        local dist = t1:dist(t2)
        if self.started2 then
        --if self.lastPinchDist > 0 then 
            self.pinchDelta = dist/self.lastPinchDist          
        else
            self.offset= self.offset + ((t1 + t2)/2-self.center)
            self.started2 = true
        end
        self.center = (t1 + t2)/2
        if self.center.y- self.offset.y>= HEIGHT -1 then
            self.offset.y=self.center.y- HEIGHT +1
        elseif self.center.y- self.offset.y <1 then
            self.offset.y=self.center.y +1
        end
        self.lastPinchDist = dist
        --self.reset=true
    elseif (#touchArr == 1) then
        self.started2 = false
        self.pinchDelta = 1.0
        self.lastPinchDist = 0
        local t1 = vec2(touchArr[1].x,touchArr[1].y)
        if self.dbl then
            if not(self.started) then
                self.loffset = (t1-self.lcenter)
                self.started = true
            end
            self.lcenter=t1
        else
            if not(self.started) then
                self.offset = self.offset + (t1-self.center)
                self.started = true
            end
            self.center=t1
            if self.center.y- self.offset.y>= HEIGHT -1 then
                self.offset.y=self.center.y- HEIGHT +1
            elseif self.center.y- self.offset.y <1 then
                self.offset.y=self.center.y -1
            end
        end
        --self.reset=true
    else
        self.pinchDelta = 1.0
        self.lastPinchDist = 0
        self.started = false
        self.started2 = false
        self.reset=true
    end
end

function Zoom3D:setCamera()
    -- scale by pinch delta
    self.zoom = math.max( self.zoom*self.pinchDelta, 0.2 )
    self.pinchDelta = 1.0
    local CamDistance = 300/self.zoom
    local Theta=math.fmod((self.center.x- self.offset.x)*360/WIDTH,360)
    local Phi=(self.center.y- self.offset.y)*180/HEIGHT -90
    if Phi>90 then
        Phi=90
    elseif Phi<-90 then
        Phi=-90
    end
    if self.dbl then
        self.lookat= self:getLocalPoint2(vec2(WIDTH,HEIGHT)-self.lcenter +self.loffset,
            self:getWorldPointZ2(self.lookat))
    end
    -- First arg is FOV, second is aspect
    perspective(FieldOfView, WIDTH/HEIGHT)
    camera(CamDistance*math.cos(Theta*math.pi/180)*math.cos(Phi*math.pi/180)+self.lookat.x,
        CamDistance*math.sin(Phi*math.pi/180)+self.lookat.y,
        CamDistance*math.sin(Theta*math.pi/180)*math.cos(Phi*math.pi/180)+self.lookat.z,
        self.lookat.x,self.lookat.y,self.lookat.z, 0,1,0)
    if self.reset then
        self.matrix=modelMatrix()*viewMatrix()*projectionMatrix()
        self.invMatrix=invert(self.matrix)
        self.reset=false
    end
end

function Zoom3D:draw()
    -- compute pinch delta
    self:processTouches()
    self:setCamera()
end

function Zoom3D:getWorldPoint2(pt)
    local m=self.matrix
    local sc=pt.x*m[4]+pt.y*m[8]+pt.z*m[12]+m[16]
    return vec2(((pt.x*m[1]+pt.y*m[5]+pt.z*m[9]+m[13])/sc+1)*WIDTH/2,
        ((pt.x*m[2]+pt.y*m[6]+pt.z*m[10]+m[14])/sc+1)*HEIGHT/2)
end

function Zoom3D:getWorldPoint(pt)
    m=modelMatrix()*viewMatrix()*projectionMatrix()
    local sc=pt.x*m[4]+pt.y*m[8]+pt.z*m[12]+m[16]
    return vec2(((pt.x*m[1]+pt.y*m[5]+pt.z*m[9]+m[13])/sc+1)*WIDTH/2,
        ((pt.x*m[2]+pt.y*m[6]+pt.z*m[10]+m[14])/sc+1)*HEIGHT/2)
end

function Zoom3D:getWorldPointZ2(pt)
    local m=self.matrix
    local sc=pt.x*m[4]+pt.y*m[8]+pt.z*m[12]+m[16]
    return (pt.x*m[3]+pt.y*m[7]+pt.z*m[11]+m[15])/sc
end

function Zoom3D:getLocalPoint2(pt2,ptz)
    local pt=vec3(pt2.x*2/WIDTH -1,pt2.y*2/HEIGHT -1,ptz)
    local m=self.invMatrix

    local sc=pt.x*m[4]+pt.y*m[8]+pt.z*m[12]+m[16]
    return vec3((pt.x*m[1]+pt.y*m[5]+pt.z*m[9]+m[13])/sc,
        (pt.x*m[2]+pt.y*m[6]+pt.z*m[10]+m[14])/sc,
        (pt.x*m[3]+pt.y*m[7]+pt.z*m[11]+m[15])/sc)
end


You’ve done it yet again. Thanks!

@Herwig I thought we had matrix inversion in there. I added this feature, does it not work?

invertedMatrix = m:inverse()

It also suppose methods such as m:determinant() and m:transpose(), as well as standard transformations like translate, scale, and rotate.

@Zoyt you are welcome!
@Simeon thanks! (I overlooked that in the documentation) Did I also miss the product with a vec4 object?

You didn’t miss those — you’ve reminded me to add the matrix * vec4 and vec4 * matrix operators, however. These will come in an update.

@Simeon thanks!