An approach to manipulating 3D objects

function setup()
    --displayMode(FULLSCREEN_NO_BUTTONS)
    -- Make better use of the available screen
    supportedOrientations(LANDSCAPE_ANY)
    -- Table for our cubes
    cubez={}
    -- Number of cubes to create
    ncubes = 5
    -- Marker for which cube was last selected
    lastcube = ncubes
    -- Create the cubes at random points in a [-2,2] region
    for i=1,ncubes do
        cubez[i]=Cube(4*vec3(math.random()-.5,
                            math.random()-.5,
                            math.random()-.5),
                        vec3(math.random()+.5,
                            math.random()+.5,
                            math.random()+.5)
                            )
    end
    -- Make the initial order vaguely sensible
    table.sort(cubez,function(a,b) return a.pos.z>b.pos.z end)
    -- Allow the user to rotate he viewport
    parameter.integer("azimuth",-180,180,0)
    parameter.integer("zenith",-90,90,0)
end
 
function draw()
    background(40, 40, 50)
    --[[
    -- uncomment this section to see a small circle at the touch point
    -- useful for checking that what you think is happening actually is
    noSmooth()
    noStroke()
    fill(0, 255, 238, 255)
    ellipse(CurrentTouch.x,CurrentTouch.y,15)
    --]]
    -- Set the projection matrix
    perspective(40, WIDTH/HEIGHT)
    -- Set the view matrix
    camera(0,0,10, 0,0,0, 0,1,0)
    -- Apply the user-specified rotations
    rotate(zenith,1,0,0)
    rotate(azimuth,0,1,0)
    -- Draw each shape
    for i,c in pairs(cubez) do
        c:draw()
    end
end
 
function touched(touch)
    if touch.state == BEGAN then
        -- New touch, our goal is to see if it touched anything
        cube = nil
        local j
        -- We step through the objects asking each one if it was
        -- touched.
        -- The starting point of the array is offset so that the
        -- object that was just touched is asked last.
        -- This means that by tapping an object we move it to the
        -- end of the list and so can select objects behind it next
        -- time.
        for i=1,ncubes do
            j = ((i+lastcube-1)%ncubes)+1
            if cubez[j]:isTouchedBy(t) then
                cube = cubez[j]
                lastcube = j
                break
            end
        end
    else
        -- Old touch, so hand it off to the selected object for
        -- processing (assuming one was selected).
        if cube then
            cube:processTouch(touch)
        end
    end
end

-- This is a class for defining and handling a cube
Cube = class()

-- For simplicity, all our cubes are the same so we use the same
-- mesh to draw them.
-- The locality of these variables is not significant here, but
-- if this were on another tab they would be hidden from the
-- main code since tabs are "chunks".
local __cube = mesh()
local corners = {}
for l=0,7 do
    i,j,k=l%2,math.floor(l/2)%2,math.floor(l/4)%2
    table.insert(corners,{vec3(i,j,k),color(255*i,255*j,255*k)})
end
local vertices = {}
local colours = {}
local u
for l=0,2 do
    for i=0,1 do
        for k=0,1 do
            for j=0,2 do
                u = (i*2^l + ((j+k)%2)*2^((l+1)%3)
                    + (math.floor((j+k)/2)%2)*2^((l+2)%3)) + 1
                table.insert(vertices,corners[u][1])
                table.insert(colours,corners[u][2])
            end
        end
    end
end
__cube.vertices = vertices
__cube.colors = colours
-- We're done with the temporary variables now
vertices = nil
colours = nil
corners = nil

-- All a cube needs to know at the start is its position and size
-- We actually want to apply the position first, but the user
-- will think of the position as applying after the scale, so
-- we adjust the position accordingly
function Cube:init(v,s)
    self.pos = vec3(v.x/s.x,v.y/s.y,v.z/s.z)
    self.size = s
end

-- To draw, we move to the position and draw ourselves
function Cube:draw()
    pushMatrix()
    scale(self.size.x,self.size.y,self.size.z)
    translate(self.pos.x,self.pos.y,self.pos.z)
    -- We save the mayrix in place at time of draw for checking
    -- against touches.  This could be optimised as we only need
    -- the matrix from the time the screen was touched.
    self.matrix = modelMatrix() * viewMatrix() * projectionMatrix()
    __cube:draw()
    popMatrix()
end

-- This returns "true" if we claim the touch
function Cube:isTouchedBy(t)
    -- Store the matrix in effect at the start of the touch
    self.smatrix = self.matrix
    -- Compute the vector along the ray defined by the touch
    local n = screennormal(t,self.matrix)
    local plane
    -- The next segments of code ask if the touch fell on one of the
    -- faces of the cube.  We use the normal vector to determine
    -- which faces are towards the viewer.  Then for each face that
    -- is towards the viewer, we test if the touch point was on that
    -- face.
    if n.z > 0 then
        plane = {vec3(0,0,1),vec3(1,0,0),vec3(0,1,0)}
    else
        plane = {vec3(0,0,0),vec3(1,0,0),vec3(0,1,0)}
    end
    if self:touchFace(plane,t) then
        return true
    end
    if n.y > 0 then
        plane = {vec3(0,1,0),vec3(1,0,0),vec3(0,0,1)}
    else
        plane = {vec3(0,0,0),vec3(1,0,0),vec3(0,0,1)}
    end
    if self:touchFace(plane,t) then
        return true
    end
    if n.x > 0 then
        plane = {vec3(1,0,0),vec3(0,1,0),vec3(0,0,1)}
    else
        plane = {vec3(0,0,0),vec3(0,1,0),vec3(0,0,1)}
    end
    if self:touchFace(plane,t) then
        return true
    end
    return false
end

-- This tests if the touch point is on a particular face.
-- A face defines a plane in space and generically the touch line
-- will intersect that plane once.  We compute that point and
-- test if it is on the corresponding face.
-- If so, we save the plane as that will be our plane of movement
-- while this touch is active.
-- As the position and size are encoded in the matrix, when we test
-- coordinates we just need to test against the original cube where
-- the faces are [0,1]x[0,1]
function Cube:touchFace(plane,t)
    local tc = screentoplane(t,
                   plane[1],
                   plane[2],
                   plane[3],
                   self.matrix)
    if tc:dot(plane[2]) > 0 and tc:dot(plane[2]) < 1 and
       tc:dot(plane[3]) > 0 and tc:dot(plane[3]) < 1 then
        self.plane = plane
        self.starttouch = tc - self.pos
        return true
    end
    return false
end

-- This computes our displacement relative to the initial touch
-- and sets our position accordingly.
function Cube:processTouch(t)
    local tc = screentoplane(t,
                   self.plane[1],
                   self.plane[2],
                   self.plane[3],
                   self.smatrix)
    self.pos = tc - self.starttouch
end

-- These are all auxiliary macros, some predate matrix and
-- vector types in Codea so could now be optimised

-- Apply a 4-matrix to a 4-vector
function applymatrix4(v,m)
    local u = {}
    u[1] = m[1]*v[1] + m[5]*v[2] + m[09]*v[3] + m[13]*v[4]
    u[2] = m[2]*v[1] + m[6]*v[2] + m[10]*v[3] + m[14]*v[4]
    u[3] = m[3]*v[1] + m[7]*v[2] + m[11]*v[3] + m[15]*v[4]
    u[4] = m[4]*v[1] + m[8]*v[2] + m[12]*v[3] + m[16]*v[4]
    return u
end

-- Apply a 3-matrix to a 3-vector
function applymatrix3(v,m)
    local u = {}
    u[1] = m[1]*v[1] + m[4]*v[2] + m[7]*v[3]
    u[2] = m[2]*v[1] + m[5]*v[2] + m[8]*v[3]
    u[3] = m[3]*v[1] + m[6]*v[2] + m[9]*v[3]
    return u
end

-- Compute the cofactor matrix of a 3x3 matrix
function cofactor3(m)
    local rm = {}
    local sgn,l
    local fm = {}
    for k=1,9 do
        fm = {}
        l = math.floor((k-1)/3) + 1 + 3*((k-1)%3)
        sgn = (-1)^(math.floor((k-1)/3))*(-1)^((k-1)%3)
        for j=1,9 do
             if j%3 ~= k%3
                 and math.floor((j-1)/3) ~= math.floor((k-1)/3)
                 then
                     table.insert(fm,m[j])
             end
        end
        rm[l] = sgn*Det2(fm)
    end
    return rm
end

-- Determinant of a 2x2 matrix (needed for the cofactor of a 3x3)
function Det2(t)
    return t[1]*t[4] - t[2]*t[3]
end

-- Given a plane in space, this computes the transformation
-- matrix from that plane to the screen
function __planetoscreen(o,u,v,A)
    A = A or modelMatrix() * viewMatrix() * projectionMatrix()
    o = o or vec3(0,0,0)
    u = u or vec3(1,0,0)
    v = v or vec3(0,1,0)
    -- promote to 4-vectors
    o = vec4(o.x,o.y,o.z,1)
    u = vec4(u.x,u.y,u.z,0)
    v = vec4(v.x,v.y,v.z,0)
    local oA, uA, vA
    oA = applymatrix4(o,A)
    uA = applymatrix4(u,A)
    vA = applymatrix4(v,A)
    return { uA[1], uA[2], uA[4],
              vA[1], vA[2], vA[4],
              oA[1], oA[2], oA[4]}
end

-- Given a plane in space, this computes the transformation
-- matrix from the screen to that plane
function screentoplane(t,o,u,v,A)
    A = A or modelMatrix() * viewMatrix() * projectionMatrix()
    o = o or vec3(0,0,0)
    u = u or vec3(1,0,0)
    v = v or vec3(0,1,0)
    t = t or CurrentTouch
    local m = __planetoscreen(o,u,v,A)
    m = cofactor3(m)
    local ndc = {}
    local a
    ndc[1] = (t.x/WIDTH - .5)*2
    ndc[2] = (t.y/HEIGHT - .5)*2
    ndc[3] = 1
    a = applymatrix3(ndc,m)
    if (a[3] == 0) then return end
    a = vec2(a[1], a[2])/a[3]
    return o + a.x*u + a.y*v
end

-- This computes the vector along the "touch ray"
function screennormal(t,A)
    A = A or modelMatrix() * viewMatrix() * projectionMatrix()
    t = t or CurrentTouch
    local u,v,w,x,y
    u = vec3(A[1],A[5],A[9])
    v = vec3(A[2],A[6],A[10])
    w = vec3(A[4],A[8],A[12])
    x = (t.x/WIDTH - .5)*2
    y = (t.y/HEIGHT - .5)*2
    u = u - x*w
    v = v - y*w
    return u:cross(v)
end

Following the various discussions about touching and moving 3D objects, I thought I’d post my code. It’s an extension of the code (of mine) that Ignatz posted in another thread. The extensions are: the code is commented, and the objects are now cuboids, not cubes.

@Andrew_Stacey => That was great ! I understand until screen normal for touch, but after… I’m confused. I have made 2 classes Screen and Mesh, can you help me for the function Mesh:isTouched(touch, offset) please ?

Very nice indeed! I’m certainly going to be looking here for inspiration when I take the dreaded step into is something touched in 3D space or perhaps some extra matrix maths :slight_smile:

@Andrew_Stacey, if you can help me please ^^

Latest version includes spheres and flat rectangles to demonstrate different ways of reacting to touch information:

  1. Cubes always move parallel to one of their faces
  2. Spheres always move parallel to the screen
  3. Rectangles move in their plane of definition if they are currently “face on”, but if “side on” move in a plane that includes the vector normal to their plane of definition.

Also now includes an initial scale and initial rotation.

@HyroVitalyProtago take a look at how the Picture object reacts to touches in this code.

Available as:

  1. AutoGisted version: https://gist.github.com/loopspace/5937499
  2. Self-installer: https://gist.github.com/loopspace/5937539

(@Briarfox: first attempt at an auto install - hope I got it right!)

mmm… I’ve tried to understand the system for plane (that i need), and to implement the same thing in my project, but that doesn’t work…

This is the project in a one file : https://gist.github.com/HyroVitalyProtago/5939178

@HyroVitalyProtago

As your mesh is not of unit size, you need to check the touch against its actual size: in Mesh:isTouched you should do the following.

    if tc.x < 0 or tc.x > self.dim.w or tc.y < 0 or tc.y > self.dim.h then

Thanks god ^:)^

Great example, thanks.