# 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(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)
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

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!