Skip to content

Instantly share code, notes, and snippets.

@loopspace
Created July 5, 2013 21:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save loopspace/5937499 to your computer and use it in GitHub Desktop.
Save loopspace/5937499 to your computer and use it in GitHub Desktop.
3D Demonstration Release v1.4 -A demo of how to manipulate 3D objects using a 2D screen.
3D Demonstration Tab Order Version: 1.4
------------------------------
This file should not be included in the Codea project.
#Main
--Project: 3D Demonstration
--Version: 1.4
--Comments:
--Dependencies:
-- 3D Demonstration
VERSION = 1.4
--[[
A demonstration of how to reliably transform 2D touch data into
useful information for 3D objects.
--]]
-- Make better use of the available screen
supportedOrientations(LANDSCAPE_ANY)
function setup()
--displayMode(FULLSCREEN_NO_BUTTONS)
if AutoGist then
autoGist = AutoGist("3D Demonstration", "A demo of how to manipulate 3D objects using a 2D screen.", VERSION)
autoGist:backup(true)
end
-- Table for our objects
objs={}
-- Number of objects to create
nobjs = 9
-- A marker for which cube was last selected
lastobj = nobjs
-- Create the objects at random points in a [-2,2] region
local types = {Sphere, Cube, Picture}
for i=1,nobjs do
local th = 2*math.pi*math.random()
local z = 2*math.random() - 1
local r = math.sqrt(1 - z*z)
objs[i]=types[math.ceil(i/3)](4*vec3(math.random()-.5,
math.random()-.5,
math.random()-.5),
vec3(math.random()+.5,
math.random()+.5,
math.random()+.5),
vec4(r*math.cos(th),
r*math.sin(th),
z,
360*math.random()
),
"Cargo Bot:Codea Icon"
)
end
-- Make the initial order vaguely sensible
table.sort(objs,function(a,b) return a.pos.z>b.pos.z end)
-- Allow the user to rotate the viewport
parameter.integer("azimuth",-180,180,0)
parameter.integer("zenith",-90,90,0)
-- Allow the user to rotate the light direction
parameter.integer("light_azimuth",-180,180,0)
parameter.integer("light_zenith",0,180,0)
-- level of ambient light
parameter.number("ambient",0,1,.5)
end
function draw()
background(ambient*40,ambient*40,ambient*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)
-- Set the light direction
light = vec3(math.cos(math.rad(light_azimuth)) * math.sin(math.rad(light_zenith)),
math.cos(math.rad(light_zenith)),
math.sin(math.rad(light_azimuth)) * math.sin(math.rad(light_zenith)))
-- Draw each shape
for i,c in pairs(objs) do
c:draw()
end
end
function touched(touch)
if touch.state == BEGAN then
-- New touch, our goal is to see if it touched anything
obj = 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,nobjs do
j = ((i+lastobj-1)%nobjs)+1
if objs[j]:isTouchedBy(t) then
obj = objs[j]
lastobj = j
break
end
end
else
-- Old touch, so hand it off to the selected object for
-- processing (assuming one was selected).
if obj then
obj:processTouch(touch)
end
end
end
-- Return a basic lighting shader
function lightingShader()
return shader([[
//
// A vertex shader with normals
//
//This is the current model * view * projection matrix
// Codea sets it automatically
uniform mat4 modelViewProjection;
uniform mat4 invModel;
//This is the current mesh vertex position, color and tex coord
// Set automatically
attribute vec4 position;
attribute vec4 color;
attribute vec2 texCoord;
attribute vec3 normal;
//This is an output variable that will be passed to the fragment shader
varying lowp vec4 vColor;
varying highp vec2 vTexCoord;
varying highp vec3 vNormal;
void main()
{
//Pass the mesh color to the fragment shader
vColor = color;
vTexCoord = texCoord;
highp vec4 n = invModel * vec4(normal,0.);
vNormal = n.xyz;
//Multiply the vertex position by our combined transform
gl_Position = modelViewProjection * position;
}
]],[[
//
// A basic fragment shader
//
//Default precision qualifier
precision highp float;
//This represents the current texture on the mesh
uniform lowp sampler2D texture;
uniform highp vec3 light;
uniform lowp float ambient;
uniform lowp float useTexture;
//The interpolated vertex color for this fragment
varying lowp vec4 vColor;
//The interpolated texture coordinate for this fragment
varying highp vec2 vTexCoord;
varying highp vec3 vNormal;
void main()
{
//Sample the texture at the interpolated coordinate
lowp vec4 col = vColor;//vec4(1.,0.,0.,1.);
if (useTexture == 1.) {
col *= texture2D( texture, vTexCoord );
}
lowp float c = ambient + (1.-ambient) * max(0.,dot(light, normalize(vNormal)));
col.xyz *= c;
//Set the output color to the texture color
gl_FragColor = col;
}
]])
end
Shape = class()
-- All a shape needs to know at the start is its position, size,
-- rotation, and mesh
-- We actually want to apply the position first, but the user
-- will think of the position as applying after the scale and
-- rotation, so we adjust the initial position accordingly
function Shape:init(v,s,r,m)
v = v:rotate(-r.w,r.x,r.y,r.z)
self.pos = vec3(v.x/s.x,v.y/s.y,v.z/s.z)
self.size = s
self.rotation = r
self.mesh = m
self:cache()
end
function Shape:cache()
-- Let's cache the transformation matrix
local mm = matrix()
mm = mm:rotate(
self.rotation.w,
self.rotation.x,
self.rotation.y,
self.rotation.z
)
mm = mm:scale(self.size.x,self.size.y,self.size.z)
mm = mm:translate(self.pos.x,self.pos.y,self.pos.z)
self.trmatrix = mm
-- And its inverse for lighting purposes
mm = matrix()
mm = mm:scale(1/self.size.x,1/self.size.y,1/self.size.z)
mm = mm:rotate(
-self.rotation.w,
self.rotation.x,
self.rotation.y,
self.rotation.z
)
mm = mm:transpose()
self.invmatrix = mm
end
-- To draw, we move to the position and draw ourselves
function Shape:draw()
pushMatrix()
applyMatrix(self.trmatrix)
-- We save the matrix 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()
-- We pass the necessary details to the shader for the lighting
-- we need to do this every draw because we're being lazy and
-- reusing the mesh for each copy of each object so some of th
-- data changes between objects
self.mesh.shader.invModel = self.invmatrix
self.mesh.shader.light = light
self.mesh.shader.ambient = ambient
self.mesh:draw()
popMatrix()
end
-- Default function, to be overwritten by child classes
function Shape:isTouchedBy(t)
return false
end
-- This computes our displacement relative to the initial touch
-- and sets our position accordingly.
function Shape:processTouch(t)
local tc = screentoplane(t,
self.plane[1],
self.plane[2],
self.plane[3],
self.smatrix)
self.pos = tc - self.starttouch
self:cache()
end
-- This is a class for defining and handling a cube
Cube = class(Shape)
-- 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 nrm = {vec3(1,0,0),vec3(0,1,0),vec3(0,0,1)}
local vertices = {}
local colours = {}
local normals = {}
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])
table.insert(normals,(i*2-1)*nrm[l+1])
end
end
end
end
__cube.vertices = vertices
__cube.colors = colours
__cube.normals = normals
__cube.shader = lightingShader()
-- We're done with the temporary variables now
vertices = nil
colours = nil
corners = nil
nrm = nil
normals = nil
function Cube:init(v,s,r)
Shape.init(self,v,s,r,__cube)
end
-- This returns "true" if we claim the touch
function Cube:isTouchedBy(t)
-- Compute the vector along the ray defined by the touch
local n = screenframe(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
self.smatrix = self.matrix
return true
end
return false
end
Sphere = class(Shape)
local __sphere = mesh()
local vertices = {}
local colours = {}
local normals = {}
local step = 20
local addVertex = function(i,j)
local u,c
u = vec3(math.cos(2*math.pi*i/step)*math.sin(math.pi*j/step),
-math.cos(math.pi*j/step),
math.sin(2*math.pi*i/step)*math.sin(math.pi*j/step))
table.insert(vertices,u)
c = 255*(u + vec3(1,1,1))/2
table.insert(colours,color(c.x,c.y,c.z,255))
table.insert(normals,u)
end
for i=1,step do
addVertex(0,0)
addVertex(i,1)
addVertex(i+1,1)
for j=1,step-2 do
addVertex(i,j)
addVertex(i,j+1)
addVertex(i+1,j+1)
addVertex(i,j)
addVertex(i+1,j)
addVertex(i+1,j+1)
end
addVertex(i,step)
addVertex(i,step-1)
addVertex(i+1,step-1)
end
__sphere.vertices = vertices
__sphere.colors = colours
__sphere.normals = normals
__sphere.shader = lightingShader()
vertices = nil
colours = nil
light = nil
normals = nil
c = nil
step = nil
addVertex = nil
function Sphere:init(v,s,r)
Shape.init(self,v,s,r,__sphere)
end
-- This returns "true" if we claim the touch
function Sphere:isTouchedBy(t)
-- Store the matrix in effect at the start of the touch
self.smatrix = self.matrix
-- Compute the plane orthogonal to the ray defined by the touch
local n,u,v = screenframe(t,self.matrix)
-- Compute the touch point on that plane
local tc = screentoplane(t,
vec3(0,0,0),
u,
v,
self.matrix)
-- Was it inside the sphere?
if tc:lenSqr() <= 1 then
self.plane = {vec3(0,0,0),u,v}
self.smatrix = self.matrix
self.starttouch = tc - self.pos
return true
end
return false
end
-- A flat 2D image in space
Picture = class(Shape)
-- create a new mesh with the given image.
function Picture:init(v,s,r,i)
local m = mesh()
m:addRect(.5,.5,1,1)
m.texture = i
m.shader = lightingShader()
m.shader.useTexture = 1
m.shader.normal = {
vec3(0,0,1),vec3(0,0,1),vec3(0,0,1),
vec3(0,0,1),vec3(0,0,1),vec3(0,0,1)
}
Shape.init(self,v,s,r,m)
end
function Picture:isTouchedBy(t)
local tc = screentoplane(t,
vec3(0,0,0),
vec3(1,0,0),
vec3(0,1,0),
self.matrix)
-- Was the picture touched?
if tc.x < 0 or tc.x > 1 or tc.y < 0 or tc.y > 1 then
return false
end
-- The picture was touched, just need to decide our plane
-- of movement
self.smatrix = self.matrix
local n = screenframe(t,self.matrix)
if n.z^2 > n.x^2 + n.y^2 then
-- More "face on" so move in the plane containing the picture
self.plane = {vec3(0,0,0),
vec3(1,0,0),
vec3(0,1,0)}
else
-- More "side on" so move in the plane with the line
-- orthogonal to the picture and the line in the picture
-- which is orthogonal to the viewer
self.plane = {vec3(0,0,0),
vec3(0,0,1),
n:cross(vec3(0,0,1))}
end
tc = screentoplane(t,
self.plane[1],
self.plane[2],
self.plane[3],
self.matrix)
self.starttouch = tc - self.pos
return true
end
-- These are all auxiliary functions, we start with some basic
-- maths relating to vectors and matrices.
-- As there isn't a 3-matrix type we simulate it by a triple of
-- 3-vectors
-- Apply a 4-matrix to a 4-vector
function applymatrix4(v,m)
return vec4(
m[1]*v[1] + m[5]*v[2] + m[09]*v[3] + m[13]*v[4],
m[2]*v[1] + m[6]*v[2] + m[10]*v[3] + m[14]*v[4],
m[3]*v[1] + m[7]*v[2] + m[11]*v[3] + m[15]*v[4],
m[4]*v[1] + m[8]*v[2] + m[12]*v[3] + m[16]*v[4]
)
end
-- Apply a 3-matrix to a 3-vector
function applymatrix3(v,m)
return v.x * m[1] + v.y * m[2] + v.z * m[3]
end
-- Compute the cofactor matrix of a 3x3 matrix, entries
-- hard-coded for efficiency.
-- The cofactor differs from the inverse by a scale factor, but
-- as our matrices are only well-defined up to scale, this
-- doen't matter.
function cofactor3(m)
return {
vec3(
m[2].y * m[3].z - m[3].y * m[2].z,
m[3].y * m[1].z - m[1].y * m[3].z,
m[1].y * m[2].z - m[2].y * m[1].z
),
vec3(
m[2].z * m[3].x - m[3].z * m[2].x,
m[3].z * m[1].x - m[1].z * m[3].x,
m[1].z * m[2].x - m[2].z * m[1].x
),
vec3(
m[2].x * m[3].y - m[3].x * m[2].y,
m[3].x * m[1].y - m[1].x * m[3].y,
m[1].x * m[2].y - m[2].x * m[1].y
)
}
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 { vec3(uA[1], uA[2], uA[4]),
vec3(vA[1], vA[2], vA[4]),
vec3(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 = vec3((t.x/WIDTH - .5)*2,(t.y/HEIGHT - .5)*2,1)
local a
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 a frame in which the first vector is along
-- the "touch ray" and the other two are in the orthogonal plane
-- (but the whole frame is not guaranteed to be orthogonal)
function screenframe(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),u,v
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment