Using ruby Matrix in JRubyArt
Here is vanilla processing sketch by Ira Greenberg translated for JRubyArt. This sketch features the use of ruby Matrix to do 3D transforms it is quite complicated, but is a bit easier on the eye in ruby cf vanilla processing. Also featured are Vec3D (replaces PVector), ArcBall (for intuitive manipulation) and AppRender for efficient Vec3D to vertex translation.
frame_of_reference.rb
###############
# Frame of Reference example by Ira Greenberg
# https://github.com/irajgreenberg/ProcessingTips
# Translated to JRubyArt by Martin Prout December 2015
###############
load_library :geometry
FACE_COUNT = 50
attr_reader :c, :p, :renderer
def settings
size(800, 800, P3D)
end
def setup
sketch_title 'Frame Of Reference'
ArcBall.init(self) # so we use mouse to rotate sketch and mouse wheel to zoom
@renderer = AppRender.new(self) # so we can render Vec3D as vertices
@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 respecivley.)
# 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.
# see mat4.rb where we use ruby Matrix to handle the multiplication for us
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)
# build matrix with frame and translation (to centroid of each triangle)
m4 = Mat4.new(xaxis: nn, yaxis: tt, zaxis: bb, translate: 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
FACE_COUNT.times do |i|
p[i].display(renderer)
c[i].display(renderer)
end
end
cylinder.rb
class Cylinder
include Processing::Proxy
attr_accessor :vecs
attr_reader :detail, :dim
def initialize(dim, detail)
@dim = dim
@detail = detail
init
end
def init
# created around x-axis
# y = Math.cos
# z = Math.sin
veca = []
vecb = []
(0...360).step(360 / detail) do |theta|
cost = DegLut.cos(theta)
sint = DegLut.sin(theta)
veca << Vec3D.new(0, cost * dim.y, sint * dim.z)
vecb << Vec3D.new(dim.x, cost * dim.y, sint * dim.z)
end
@vecs = veca.concat(vecb)
end
def display(renderer)
begin_shape(QUADS)
detail.times do |i|
if i < (detail - 1)
vecs[i].to_vertex(renderer)
vecs[i + 1].to_vertex(renderer)
vecs[detail + i + 1].to_vertex(renderer)
vecs[detail + i].to_vertex(renderer)
else
vecs[i].to_vertex(renderer)
vecs[0].to_vertex(renderer)
vecs[detail].to_vertex(renderer)
vecs[detail + i].to_vertex(renderer)
end
end
end_shape
end
end
plane.rb
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] - vecs[0]
v2 = vecs[2] - 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(renderer)
begin_shape(TRIANGLES)
vecs.map { |vec| vec.to_vertex(renderer) }
end_shape
# normal
stroke(200, 160, 30)
begin_shape(LINES)
c.to_vertex(renderer)
(c + n * NORM_LEN).to_vertex(renderer)
end_shape
# binormal
stroke(160, 200, 30)
begin_shape(LINES)
c.to_vertex(renderer)
# tangent
v = vecs[1].copy
v -= vecs[0]
v.normalize!
(c + v * NORM_LEN).to_vertex(renderer)
end_shape
stroke(30, 200, 160)
begin_shape(LINES)
c.to_vertex(renderer)
b = v.cross(n)
(c + b * NORM_LEN).to_vertex(renderer)
end_shape
stroke(0, 75)
end
end
mat4.rb
# uber simple Homogeneous 4 x 4 matrix
require 'matrix'
class Mat4
attr_reader :mat
def initialize(xaxis:, yaxis:, zaxis:, translate:)
@mat = Matrix[
[xaxis.x, yaxis.x, zaxis.x, translate.x],
[xaxis.y, yaxis.y, zaxis.y, translate.y],
[xaxis.z, yaxis.z, zaxis.z, translate.z],
[0, 0, 0, 1]
]
end
# Ira's processing version changes the input 'array', here we return
# a new array with transformed values (which we then assign to the input)
# see frame_of_reference.rb
def mult(array)
array.map.each do |arr|
matrix_to_vector(mat * Matrix[[arr.x], [arr.y], [arr.z], [1]])
end
end
private
# It isn't obvious but we only require first 3 elements
def matrix_to_vector(vec)
Vec3D.new(vec.column(0)[0], vec.column(0)[1], vec.column(0)[2])
end
end