############### # Frame of Reference example by Ira Greenberg # https://github.com/irajgreenberg/ProcessingTips # Translated to ruby-processing by Martin Prout January 2014 # Now use mouse drag for ArcBall manipulation, and +/- keys for zoom ############### load_library :vecmath load_library :geometry FACE_COUNT = 50 attr_reader :arcball, :c, :p, :zoom def setup size(800, 800, P3D) @zoom = 1.0 camera(0, 0, (height/2.0) / tan(PI*30.0 / 180.0), 0, 0, 0, 0, -1, 0) # point camera at origin # create an Arcball at centre that pretty much fills the screen @arcball = ArcBall.new(0, 0, min(width - 20, height - 20) / 2.0) @c = [] @p = [] FACE_COUNT.times do |i| # calc some random triangles in 3 space val = Vec3D.new(rand(-width/2 .. width/2), rand(-width/2 .. width/2), rand(-width/2 .. width/2)) v0 = Vec3D.new(rand(-val.x .. -val.x + 100), rand(-val.y .. -val.y + 100), rand(-val.z .. -val.z + 100)) v1 = Vec3D.new(rand(-val.x .. -val.x + 100), rand(-val.y .. -val.y + 100), rand(-val.z .. -val.z + 100)) v2 = Vec3D.new(rand(-val.x .. -val.x + 100), rand(-val.y .. -val.y + 100), rand(-val.z .. -val.z + 100)) p << Plane.new([v0, v1, v2]) # build some cute little cylinders c << Cylinder.new(Vec3D.new(150, 5, 5), 12) # Using each Triangle normal (N), # One of the Triangle's edges as a tangent (T) # Calculate a bi-normal (B) using the cross-product between each N and T # Note caps represent constants in ruby so we used N = nn, T = tt and B = bb in the ruby code below # # A picture helps # nice, sweet orthogonal axes # N B # | / # | / # |/____T # # N, T, B together give you a Frame of Reference (cute little local coordinate system), based on each triangle. # You can then take the cylinder (or any vertices) and transform them using a 3 x 3 matrix to this coordinate system. # (In the matrix each column is based on N, T and B respectivley.) # The transform will handle any rotations and scaling, but not the translation, # but we can add another dimenson to the matrix to hold the translation values. # Here's what all this confusing description looks like: # # Matrix : Vector : # | N.x T.x B.x translation.x | | x | # | N.y T.y B.y translation.y | | y | # | N.z T.z B.z translation.z | | z | # | 0 0 0 1 | | 1 | # We add the extra row in the matrix and the 1 to each vector # so the math works. We describe the Matrix as 4 rows by 4 columns # and the vector now as a Matrix with 4 rows and 1 column. # When you multiply matrices the inner numbers MUST match, so: # [4 x 4] [4 x 1] is OK, but [4 x 4] [1 x 4] is NOT COOL. # (Please note there is also row vector approach that you can use, # Google about; it simply puts the vector on left side of matrix and treats # it as a 1 row and 4 column matrix. However, you'll also need to shift # the translation terms to the bottom of the matrix for the math to grock.) # The Matrix multiplication looks like this (sorry it's a little tedious looking.) # n.x * x + t.x *y + B.x * z + translation.x * 1 = new transformed x # n.y * x + t.y *y + B.y * z + translation.y * 1 = new transformed y # n.z * x + t.z *y + B.z * z + translation.z * 1 = new transformed z # 0 * x + 0 *y + 0 * z + 1 * 1 = disregard this crap. # nn = p[i].n tt = Vec3D.new(p[i].vecs[1].x - p[i].vecs[0].x, p[i].vecs[1].y - p[i].vecs[0].y, p[i].vecs[1].z - p[i].vecs[0].z) nn.normalize! tt.normalize! bb = nn.cross(tt) bb.normalize! # not really needed # build matrix with frame and translation (to centroid of each triangle) m4 = Mat4.new(nn, tt, bb, p[i].c) # transform each cylinder to align with each triangle c[i].vecs = m4.mult(c[i].vecs) end fill(187) stroke(50, 20) end def draw background(0) lights # update the arcball rotation update FACE_COUNT.times do |i| p[i].display c[i].display end end def update theta, x, y, z = arcball.update rotate(theta, x, y, z) end def mouse_pressed arcball.mouse_pressed(mouse_x, mouse_y) end def mouse_dragged arcball.mouse_dragged(mouse_x, mouse_y) end def key_pressed case key when '+' @zoom -= 0.1 # closer is bigger when '-' @zoom += 0.1 end camera(0, 0, (height * zoom / 2.0) / tan(PI*30.0 / 180.0), 0, 0, 0, 0, -1, 0) end

Here is the Plane class

NORM_LEN = 225.0 class Plane include Processing::Proxy attr_reader :vecs, :c, :n def initialize(vecs) @vecs = vecs init end def init v1 = vecs[1].dup v2 = vecs[2].dup v1 -= vecs[0] v2 -= vecs[0] @c = Vec3D.new( (vecs[0].x+vecs[1].x+vecs[2].x) / 3, (vecs[0].y+vecs[1].y+vecs[2].y) / 3, (vecs[0].z+vecs[1].z+vecs[2].z) / 3 ) @n = v1.cross(v2) n.normalize! end def display begin_shape(TRIANGLES) vecs.each do |vec| vertex(vec.x, vec.y, vec.z) end end_shape #normal stroke(200, 160, 30) begin_shape(LINES) vertex(c.x, c.y, c.z) vertex(c.x + n.x * NORM_LEN, c.y + n.y * NORM_LEN, c.z + n.z * NORM_LEN) end_shape #binormal stroke(160, 200, 30) begin_shape(LINES) vertex(c.x, c.y, c.z) # tangent v = vecs[1].dup #v.set(vecs[1]) v -= vecs[0] v.normalize! vertex(c.x + v.x * NORM_LEN, c.y + v.y * NORM_LEN, c.z + v.z * NORM_LEN) end_shape stroke(30, 200, 160) begin_shape(LINES) vertex(c.x, c.y, c.z) b = v.cross(n) vertex(c.x + b.x * NORM_LEN, c.y + b.y * NORM_LEN, c.z + b.z * NORM_LEN) end_shape stroke(0, 75) end end

Here is the Mat4 class

# uber simple Homogeneous 4 x 4 matrix class Mat4 attr_reader :mat def initialize(axisX, axisY, axisZ, trans) @mat = [ [axisX.x, axisY.x, axisZ.x, trans.x], [axisX.y, axisY.y, axisZ.y, trans.y], [axisX.z, axisY.z, axisZ.z, trans.z], [0, 0, 0, 1] ] end # The processing version changes the input 'array', here we return # a new array with transformed values (which we then assign to the input) # see line 91 Frame_of_Reference.rb def mult(array) temp = [] array.each do |arr| xt = mat[0][0] * arr.x + mat[0][1] * arr.y + mat[0][2] * arr.z + mat[0][3] * 1 yt = mat[1][0] * arr.x + mat[1][1] * arr.y + mat[1][2] * arr.z + mat[1][3] * 1 zt = mat[2][0] * arr.x + mat[2][1] * arr.y + mat[2][2] * arr.z + mat[2][3] * 1 temp << Vec3D.new(xt, yt, zt) end return temp end end

Here is the Cylinder class

class Cylinder include Processing::Proxy attr_accessor :vecs attr_reader :detail, :dim def initialize(dim, detail) @dim = dim @detail = detail init end def init theta = 0.0 # created around x-axis # y = Math.cos # z = Math.sin veca = [] vecb = [] detail.times do veca << Vec3D.new(0, Math.cos(theta)*dim.y, Math.sin(theta)*dim.z) vecb << Vec3D.new(dim.x, Math.cos(theta)*dim.y, Math.sin(theta)*dim.z) theta += Math::PI * 2/detail end @vecs = veca.concat(vecb) end def display begin_shape(QUADS) detail.times do |i| if (i<detail-1) vertex(vecs[i].x, vecs[i].y, vecs[i].z) vertex(vecs[i+1].x, vecs[i+1].y, vecs[i+1].z) vertex(vecs[detail+i+1].x, vecs[detail+i+1].y, vecs[detail+i+1].z) vertex(vecs[detail+i].x, vecs[detail+i].y, vecs[detail+i].z) else vertex(vecs[i].x, vecs[i].y, vecs[i].z) vertex(vecs[0].x, vecs[0].y, vecs[0].z) vertex(vecs[detail].x, vecs[detail].y, vecs[detail].z) vertex(vecs[detail+i].x, vecs[detail+i].y, vecs[detail+i].z) end end end_shape end end