Created
July 5, 2013 21:55
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
3D Demonstration Tab Order Version: 1.4 | |
------------------------------ | |
This file should not be included in the Codea project. | |
#Main |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--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