Experiments with ruby-processing (processing-2.2.1) and JRubyArt for processing-3.0

Tuesday 29 January 2013

Adding requirements rspec

In my previous post I presented an initial specification for my lsystems csgrammar parser. That is the most basic form, in which context is given by the preceding or following token in the axiom/production. Here is the specification of a more complicated parser that can take an ignore string, that includes those tokens that should be ignored wrt context. Typically the tokens will represent special instructions to interpreter such as [ and ] which mean push/pop matrix etc.
require 'CSLSystem'

IGNORE = 'ignore'

describe "LSystem" do
  context "given axiom 'biaaaaaa'" do
    context "with function a=b if context, ignoring IGNORE" do
      describe "#generate" do
        it "should equal 'biaaaaaa' at zero iteration" do
          LSystem.new('biaaaaaa', {'b' => 'a', 'b<a' => 'b'}, IGNORE).generate(0).should == 'biaaaaaa'
        end
        it "should equal 'aibaaaaa' after one iteration" do
          LSystem.new('biaaaaaa', {'b' => 'a', 'b<a' => 'b'}, IGNORE).generate(1).should == 'aibaaaaa'
        end
        it "should equal 'aiabaaaa' after one iteration" do
          LSystem.new('aibaaaaa', {'b' => 'a', 'b<a' => 'b'}, IGNORE).generate(1).should == 'aiabaaaa'
        end
        it "should equal 'aiabaaaa' after two iterations" do
          LSystem.new('biaaaaaa', {'b' => 'a', 'b<a' => 'b'}, IGNORE).generate(2).should == 'aiabaaaa'
        end
        it "should equal 'aiaabaaa' after three iteration" do
          LSystem.new('biaaaaaa', {'b' => 'a', 'b<a' => 'b'}, IGNORE).generate(3).should == 'aiaabaaa'
        end
        it "should equal 'aiaaaaba' after five iteration" do
          LSystem.new('biaaaaaa', {'b' => 'a', 'b<a' => 'b'}, IGNORE).generate(5).should == 'aiaaaaba'
        end
        it "should equal 'biaaaaaa' after seven iterations" do
          LSystem.new('biaaaaaa', {'b' => 'a', 'b<a' => 'b'}, IGNORE).generate(7).should == 'biaaaaaa'
        end
      end
    end
  end
end


Here is the revised grammar parser:-
######################################
# cs_grammar.rb a context sensitive
# 1-L lsystem grammar for 
# ruby/ruby-processing
# by Martin Prout (January 2013)
######################################



##################################
# The grammar class stores rules
# in two Hashes, one for cs rules,
# one for context free rules. Rules
# are filtered on input, and context
# is checked using get_rule in production
##################################

class LSystem

  attr_reader :axiom, :context, :no_context, :idx, :ignore
  def initialize(axiom, rules, ignore)
    @axiom = axiom
    @no_context = {}
    @context = {}
    @ignore = ignore
    rules.each_pair do |pair|
      add_rule pair[0], pair[1]
    end
  end

  def add_rule(pre, rule)
    if pre.length == 3
      if pre[1] == '<'
        @context[pre[2]] = pre
      elsif pre[1] == '>'
        @context[pre[0]] = pre
      end
      @no_context[pre] = rule # key length == 3
    elsif pre.length == 1
      @no_context[pre] = rule # key length == 1
    else
      print "unrecognized grammar '#{pre}'"
    end
  end

  def generate(repeat = 0) # repeat iteration grammar rules
    prod = axiom
    repeat.times do
      prod = new_production(prod)
    end
    return prod
  end


  def new_production prod  # single iteration grammar rules
    @idx = -1
    prod.gsub!(/./) do |ch|
      get_rule(prod, ch)
    end
  end

  def get_rule prod, ch
    rule = ch # default is return original character as rule (no change)
    @idx += 1 # increment the index of axiom/production as a side effect
    if (context.has_key?(ch))
      if context[ch][1] == '<'
        cs_char = context[ch][0]
        rule = no_context[context[ch]] if cs_char == get_context(prod, idx, -1) # use context sensitive rule
      elsif context[ch][1] == '>'
        cs_char = context[ch][2]
        rule = no_context[context[ch]] if cs_char == get_context(prod, idx, 1) # use context sensitive rule
      end
    else
      rule = no_context[ch] if no_context.has_key?(ch) # context free rule if it exists
    end
    return rule
  end

  def get_context prod, idx, inc
    index = idx + inc
    while (ignore.include? prod[index])
      index += inc
    end
    return prod[index]
  end
end

Monday 28 January 2013

Behaviour driven development rspec

Here is an initial specification for a context sensitive LSystem in ruby (example taken from abop, function should really be a=b if context):-
require 'CSLSystem'

describe "LSystem" do
  context "given axiom 'baaaaaa'" do
    context "with function b=a" do
      describe "#generate" do
        it "should equal 'baaaaaa' at zero iteration" do
          LSystem.new('baaaaaa', {'b' => 'a', 'b<a' => 'b'}).generate(0).should == 'baaaaaa'
        end
        it "should equal 'abaaaaa' after one iteration" do
          LSystem.new('baaaaaa', {'b' => 'a', 'b<a' => 'b'}).generate(1).should == 'abaaaaa'
        end
        it "should equal 'aabaaaa' after one iteration" do
          LSystem.new('abaaaaa', {'b' => 'a', 'b<a' => 'b'}).generate(1).should == 'aabaaaa'
        end
        it "should equal 'aabaaaa' after two iterations" do
          LSystem.new('baaaaaa', {'b' => 'a', 'b<a' => 'b'}).generate(2).should == 'aabaaaa'
        end
        it "should equal 'aaabaaa' after three iteration" do
          LSystem.new('baaaaaa', {'b' => 'a', 'b<a' => 'b'}).generate(3).should == 'aaabaaa'
        end
        it "should equal 'aaaaaba' after five iteration" do
          LSystem.new('baaaaaa', {'b' => 'a', 'b<a' => 'b'}).generate(5).should == 'aaaaaba'
        end
        it "should equal 'baaaaaa' after seven iterations" do
          LSystem.new('baaaaaa', {'b' => 'a', 'b<a' => 'b'}).generate(7).should == 'baaaaaa'
        end
      end
    end
  end
end


Here is some code designed to pass the rspec tests:-
######################################
# cs_grammar.rb a context sensitive
# 1-L lsystem grammar for 
# ruby/ruby-processing
# by Martin Prout (January 2013)
######################################



##################################
# The grammar class stores rules
# in two Hashes, one for cs rules,
# one for context free rules. Rules
# are filtered on input, and context
# is checked using get_rule in production
##################################

class LSystem

  attr_reader :axiom, :context, :no_context, :idx
  def initialize(axiom, rules)
    @axiom = axiom
    @no_context = {}
    @context = {}
    rules.each_pair do |pair|
      add_rule pair[0], pair[1]
    end
  end

  def add_rule(pre, rule)
    if pre.length == 3
      if pre[1] == '<'
        @context[pre[2]] = pre
      elsif pre[1] == '>'
        @context[pre[0]] = pre if pre[1] == '>'
      end
      @no_context[pre] = rule # key length == 3
    elsif pre.length == 1
      @no_context[pre] = rule # key length == 1
    else
      print "unrecognized grammar '#{pre}'"
    end
  end

  def generate(repeat = 0) # repeat iteration grammar rules
    prod = axiom
    repeat.times do
      prod = new_production(prod)
    end
    return prod
  end


  def new_production prod  # single iteration grammar rules
    @idx = 0
    prod.gsub!(/./) do |ch|
      get_rule(prod, ch)
    end
  end

  def get_rule prod, ch
    rule = ch # default is return original character as rule (no change)
    @idx += 1 # increment the index of axiom/production as a side effect
    if (context.has_key?(ch))
      if context[ch][1] == "<"
        cs_char = context[ch][0]
        rule = no_context[context[ch]] if cs_char == prod[idx - 2] # use context sensitive rule
      elsif context[ch][1] == ">"
        cs_char = context[ch][2]
        rule = no_context[context[ch]] if cs_char == prod[idx] # use context sensitive rule
      end
    else
      rule = no_context[ch] if no_context.has_key?(ch) # context free rule if it exists
    end
    return rule
  end
end

Result 
.......


Finished in 0.00261 seconds 7 examples, 0 failures

Some more tests...
require 'CSLSystem'

describe "LSystem" do
  context "given axiom 'aaaaaaab'" do
    context "with function b=a" do
      describe "#generate" do
        it "should equal 'aaaaaaab' at zero iteration" do
          LSystem.new('aaaaaaab', {'b' => 'a', 'a>b' => 'b'}).generate(0).should == 'aaaaaaab'
        end
        it "should equal 'aaaaaba' after one iteration" do
          LSystem.new('aaaaaaab', {'b' => 'a', 'a>b' => 'b'}).generate(1).should == 'aaaaaaba'
        end
        it "should equal 'aaaabaa' after one iteration" do
          LSystem.new('aaaaaaba', {'b' => 'a', 'a>b' => 'b'}).generate(1).should == 'aaaaabaa'
        end
        it "should equal ''aaaabaa'' after two iterations" do
          LSystem.new('aaaaaaab', {'b' => 'a', 'a>b' => 'b'}).generate(2).should == 'aaaaabaa'
        end
        it "should equal 'aaabaaa' after three iteration" do
          LSystem.new('aaaaaaab', {'b' => 'a', 'a>b' => 'b'}).generate(3).should == 'aaaabaaa'
        end
        it "should equal 'abaaaaa' after five iteration" do
          LSystem.new('aaaaaaab', {'b' => 'a', 'a>b' => 'b'}).generate(5).should == 'aabaaaaa'
        end
        it "should equal 'aaaaaaab' after seven iterations" do
          LSystem.new('aaaaaaaa', {'b' => 'a', 'a>b' => 'b'}).generate(7).should == 'aaaaaaaa'
        end
      end
    end
  end
end


Here is the revised code to pass both sets of test...
######################################
# cs_grammar.rb a context sensitive
# 1-L lsystem grammar for 
# ruby/ruby-processing
# by Martin Prout (January 2013)
######################################



##################################
# The grammar class stores rules
# in two Hashes, one for cs rules,
# one for context free rules. Rules
# are filtered on input, and context
# is checked using get_rule in production
##################################

class LSystem

  attr_reader :axiom, :context, :no_context, :idx
  def initialize(axiom, rules)
    @axiom = axiom
    @no_context = {}
    @context = {}
    rules.each_pair do |pair|
      add_rule pair[0], pair[1]
    end
  end

  def add_rule(pre, rule)
    if pre.length == 3
      if pre[1] == '<'
        @context[pre[2]] = pre
      elsif pre[1] == '>'
        @context[pre[0]] = pre 
      end
      @no_context[pre] = rule # key length == 3
    elsif pre.length == 1
      @no_context[pre] = rule # key length == 1
    else
      print "unrecognized grammar '#{pre}'"
    end
  end

  def generate(repeat = 0) # repeat iteration grammar rules
    prod = axiom
    repeat.times do
      prod = new_production(prod)
    end
    return prod
  end


  def new_production prod  # single iteration grammar rules
    @idx = -1
    prod.gsub!(/./) do |ch|
      get_rule(prod, ch)
    end
  end

  def get_rule prod, ch
    rule = ch # default is return original character as rule (no change)
    @idx += 1 # increment the index of axiom/production as a side effect
    if (context.has_key?(ch))
      if context[ch][1] == '<'
        cs_char = context[ch][0]
        rule = no_context[context[ch]] if cs_char == prod[idx - 1] # use context sensitive rule
      elsif context[ch][1] == '>'
        cs_char = context[ch][2]
        rule = no_context[context[ch]] if cs_char == prod[idx + 1] # use context sensitive rule
      end
    else
      rule = no_context[ch] if no_context.has_key?(ch) # context free rule if it exists
    end
    return rule
  end
end

Tuesday 1 January 2013

Bezier Patch for ruby processing (version 2)

The changed api for PShape, gave me cause to revisit this sketch, which I realised was not originally using the ruby-1.9 rand(range) syntax, so here is the revised sketch. Which has also been adjusted in accordance with the new PShape api.
# bezier bez_patch By Marius Watz:
# http://www.openprocessing.org/sketch/57709
# Normal calculation added by Andres Colubri
# Direct port of sample code by Paul Bourke.
# Original code: http://paulbourke.net/geometry/bezier/
#
# hit "spacebar" to generate a new shape and save current
# translated for ruby-processing by Martin Prout January 2013
#

load_library :arcball

NI=4
NJ=5
RESI=NI*10
RESJ=NJ*10

attr_accessor :outp, :inp, :normp, :auto_normals, :arcball, :bez_patch

def setup
  size(1024, 768, P3D)
  @arcball = ArcBall.new(width/2.0, height/2.0, min(width - 20, height - 20) * 0.5)
  @auto_normals = false
  build_geometry
  @bez_patch = build_shape
end

def draw
  background(255)
  translate(width/2,height/2)
  smooth(8)
  lights
  define_lights
  scale(0.9)
  update
  no_stroke
  shape(bez_patch)
end

#######
# use bez patch geometry to
# create a vbo object (PShape)
#######

def build_shape
  no_stroke
  bez = create_shape
  bez.begin_shape(QUAD_STRIP)
  bez.fill(192, 192, 192)
  bez.ambient(20, 20, 20)
  bez.specular(30)
  (0 ... RESI - 1).each do |i|
    (0 ... RESJ).each do |j|
      if (!auto_normals)
        bez.normal(*normp[i][j])
      end
      bez.vertex(*outp[i][j])
      bez.vertex(*outp[i+1][j])
    end
  end
  bez.end_shape
  return bez
end

##########
# build geometry
# for bez patch
##########

def build_geometry
  @outp = []
  @normp = []
  @inp = []
  uitang = PVector.new
  ujtang = PVector.new

  (0 ... NI).each do |i|
    row = Array.new
    (0 ... NJ).each do |j|
      row << PVector.new(i, j, rand(-3.0 .. 3.0))
    end
    inp << row
  end

  (0 ... RESI).each do |i|
    mui = i.fdiv(RESI - 1)
    row = []
    row_n = []
    (0 ... RESJ).each do |j|
      muj = j.fdiv(RESJ - 1)
      vect = PVector.new
      uitang.set(0, 0, 0)
      ujtang.set(0, 0, 0)
      (0 ... NI).each do |ki|
        bi = bezier_blend(ki, mui, NI)
        dbi = d_bezier_blend(ki, mui, NI)
        (0 ... NJ).each do |kj|
          bj = bezier_blend(kj, muj, NJ)
          dbj = d_bezier_blend(kj, muj, NJ)
          vect.x += (inp[ki][kj].x * bi * bj)
          vect.y += (inp[ki][kj].y * bi * bj)
          vect.z += (inp[ki][kj].z * bi * bj)

          uitang.x += (inp[ki][kj].x * dbi * bj)
          uitang.y += (inp[ki][kj].y * dbi * bj)
          uitang.z += (inp[ki][kj].z * dbi * bj)

          ujtang.x += (inp[ki][kj].x * bi * dbj)
          ujtang.y += (inp[ki][kj].y * bi * dbj)
          ujtang.z += (inp[ki][kj].z * bi * dbj)
        end
      end
      vect.add(PVector.new(-NI/2,-NJ/2,0))
      vect.mult(100)
      row << vect.array()
      uitang.normalize
      ujtang.normalize
      row_n << uitang.cross(ujtang).array()
    end
    @outp << row
    @normp << row_n
  end
end

def bezier_blend( k, mu,  n)
  blend = 1.0
  nn = n
  kn = k
  nkn = n - k

  while (nn >= 1)
    blend *= nn
    nn -= 1
    if (kn > 1)
      blend = blend.fdiv(kn)
      kn -= 1
    end
    if (nkn > 1)
      blend = blend.fdiv(nkn)
      nkn -= 1
    end
  end
  blend *= pow(mu, k.to_f) if (k > 0)
  blend *= pow(1-mu, (n - k).to_f) if (n - k > 0)
  return(blend)
end

def d_bezier_blend( k, mu,  n)
  dblendf = 1.0

  nn = n
  kn = k
  nkn = n - k

  while (nn >= 1)
    dblendf *= nn
    nn -= 1
    if (kn > 1)
      dblendf  = dblendf.fdiv(kn)
      kn -= 1
    end
    if (nkn > 1)
      dblendf  = dblendf.fdiv(nkn)
      nkn -= 1
    end
  end

  fk = 1
  dk = 0
  fnk = 1
  dnk = 0
  if (k > 0)
    fk = pow(mu, k.to_f)
    dk = k * pow(mu, (k - 1).to_f)
  end
  if (n - k > 0)
    fnk = pow(1 - mu, (n - k).to_f)
    dnk = (k - n)*pow(1 - mu, (n - k - 1).to_f)
  end
  dblendf *= (dk * fnk + fk * dnk)

  return(dblendf)
end

######################
# ArcBall control
# and lighting + re-run
######################

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 define_lights
  ambient_light(40, 40, 40)
  point_light(30, 30, 30, 0, 0, 0)
  directional_light(60, 60, 60, 1, 0, 0)
  spot_light(30, 30, 30, 0, 40, 200, 0, -0.5, 0.5, PI / 2, 2)
end

def key_pressed
  case(key)
  when ' '
    save_frame("bezPatch.png")
    build_geometry
    @bez_patch = build_shape
  when 'x'
    arcball.select_axis(X)
  when 'y'
    arcball.select_axis(Y)
  when 'z'
    arcball.select_axis(Z)
  end
end

def key_released
  arcball.select_axis(-1)
end

Changes to PShape api processing-2.0b8

I'm kind of assuming the recent api changes in svn version of processing-2.0 are going to stick around to at least processing-2.0b8 (I can see some sort of logic in them at any rate). Here is my retained menger sketch that uses PShape updated in line with the api changes (note to set attributes such as ambient, and specular outside of begin/end shape you now need to use the set prefix as I have done below):-
The method signature is also different, either set_attribute(col) where col is an int or processing color or set_attribute(idx, col) where idx is an index int (which I have yet to explore).
##########################
# RetainedMenger.rb
# (processing-2.0)
# author Martin Prout
##########################

load_library 'arcball'
import "arcball"

PTS = [-1, 0, 1]
MIN_SIZE = 20
X = 0
Y = 1
Z = 2

attr_reader :arcball, :menger

def setup
  size(640, 480, P3D)
  smooth(8)
  camera
  camera(width/2.0, height/2.0, (height/2.0) / tan(PI*30.0 / 180.0), 0, 0, 0, 0, 1, 0)
  @arcball = ArcBall.new(0, 0, min(width - 20, height - 20) * 0.8)
  @menger = create_shape(GROUP)
  create_menger(0, 0, 0, height * 0.8)
end

def draw
  background(20, 20, 200)
  no_stroke
  lights
  update
  define_lights
  render
end

def render
  menger.set_ambient(50)
  menger.set_specular(30)
  shape(menger)
end

def create_menger(xx, yy, zz, sz)
  u = sz / 3.0
  if (sz < MIN_SIZE) # recursion limited by minimum cube size
    no_stroke
    menger.add_child(create_cube(xx, yy, zz, sz)) # create and add a cube  
  else
    PTS.each do |i|
      PTS.each do |j|
        PTS.each do |k|
          if ((abs(i) + abs(j) + abs(k)) > 1)
            create_menger(xx + (i * u), yy + (j * u), zz + (k * u), u)
          end
        end
      end
    end
  end
end

def create_cube(xx, yy, zz, sz)
  dim = sz / 2.0
  cube = create_shape
  cube.begin_shape(QUADS)
  # Front face
  cube.fill(255)
  cube.normal(0, 0, 1)
  cube.vertex(-dim + xx, -dim + yy, -dim + zz)
  cube.vertex(+dim + xx, -dim + yy, -dim + zz)
  cube.vertex(+dim + xx, +dim + yy, -dim + zz)
  cube.vertex(-dim + xx, +dim + yy, -dim + zz)

  # Back face

  cube.normal(0, 0, -1)
  cube.vertex(-dim + xx, -dim + yy, +dim + zz)
  cube.vertex(+dim + xx, -dim + yy, +dim + zz)
  cube.vertex(+dim + xx, +dim + yy, +dim + zz)
  cube.vertex(-dim + xx, +dim + yy, +dim + zz)

  # Left face

  cube.normal(1, 0, 0)
  cube.vertex(-dim + xx, -dim + yy, -dim + zz)
  cube.vertex(-dim + xx, -dim + yy, +dim + zz)
  cube.vertex(-dim + xx, +dim + yy, +dim + zz)
  cube.vertex(-dim + xx, +dim + yy, -dim + zz)

  # Right face

  cube.normal(-1, 0, 0)
  cube.vertex(+dim + xx, -dim + yy, -dim + zz)
  cube.vertex(+dim + xx, -dim + yy, +dim + zz)
  cube.vertex(+dim + xx, +dim + yy, +dim + zz)
  cube.vertex(+dim + xx, +dim + yy, -dim + zz)

  # Top face

  cube.normal(0, 1, 0)
  cube.vertex(-dim + xx, -dim + yy, -dim + zz)
  cube.vertex(+dim + xx, -dim + yy, -dim + zz)
  cube.vertex(+dim + xx, -dim + yy, +dim + zz)
  cube.vertex(-dim + xx, -dim + yy, +dim + zz)

  # Bottom face

  cube.normal(0, -1, 0)
  cube.vertex(-dim + xx, +dim + yy, -dim + zz)
  cube.vertex(+dim + xx, +dim + yy, -dim + zz)
  cube.vertex(+dim + xx, +dim + yy, +dim + zz)
  cube.vertex(-dim + xx, +dim + yy, +dim + zz)
  cube.end_shape
  return cube
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 define_lights
  ambient(20, 20, 20)
  ambient_light(50, 50, 50)
  point_light(30, 30, 30, 200, -150, 0)
  directional_light(0, 30, 50, 1, 0, 0)
  spot_light(30, 30, 30, 0, 40, 200, 0, -0.5, -0.5, PI / 2, 2)
end

def key_pressed
  case(key)
  when 'x'
    arcball.select_axis(X)
  when 'y'
    arcball.select_axis(Y)
  when 'z'
    arcball.select_axis(Z)
  end
end

def key_released
  arcball.select_axis(-1)
end

























  • Here is another api change was shape.texture(img)
  • now shape.set_texture(img)

# Planets, by Andres Colubri
#
# Sun and mercury textures from http://planetpixelemporium.com
# Star field picture from http://www.galacticimages.com/

attr_reader :starfield, :sun, :suntex, :planet1, :surftex1, :cloudtex, :planet2, :surftex2

def setup
  size(1024, 768, P3D)
  @starfield = load_image("starfield.jpg")
  @suntex = load_image("sun.jpg")
  @surftex1 = load_image("planet.jpg")
  @surftex2 = load_image("mercury.jpg")
  no_stroke
  fill(255)
  sphere_detail(40)
  @sun = create_shape(SPHERE, 150)
  @sun.set_texture(suntex)
  @planet1 = create_shape(SPHERE, 150)
  planet1.set_texture(surftex1)
  @planet2 = create_shape(SPHERE, 50)
  planet2.set_texture(surftex2)
end

def draw
  # Even we draw a full screen image after this, it is recommended to use
  # background to clear the screen anyways, otherwise A3D will think
  # you want to keep each drawn frame in the framebuffer, which results in 
  # slower rendering.
  background(0)
  # Disabling writing to the depth mask so the 
  # background image doesn't occludes any 3D object.
  hint(DISABLE_DEPTH_MASK)
  image(starfield, 0, 0, width, height)
  hint(ENABLE_DEPTH_MASK)
  push_matrix
  translate(width/2, height/2, -300)
  push_matrix
  rotate_y(PI * frame_count / 500)
  shape(sun)
  pop_matrix
  point_light(255,  255,  255,  0,  0,  0)
  rotate_y(PI * frame_count / 300)
  translate(0, 0, 300)
  shape(planet2)
  pop_matrix
  no_lights
  point_light(255,  255,  255,  0,  0,  -150)
  translate(0.75 * width,  0.6 * height,  50)
  shape(planet1)
end

Here is yet another api chage here need to use set_fill rather fill (further takes an int or processing color as input, there is an set_fill(index, color) variant I've yet too explore)
########################
# NB: Slow to get going 
# but, performs Ok after 
########################

BOX_SIZE = 20
MARGIN = BOX_SIZE * 2
DEPTH = 400
FINT = 3

attr_reader :box_fill, :grid, :fcount, :lastm, :frate

def setup
  size(640, 360, P3D)
  frame_rate(60)
  @fcount = 0
  @lastm = 0
  no_smooth
  no_stroke
  @grid = create_shape(GROUP)
  # Build grid using multiple translations 
  (-(DEPTH/2 + MARGIN) ... (DEPTH/2 - MARGIN)).step(BOX_SIZE) do |i|
    (-(height + MARGIN) ... (height - MARGIN)).step(BOX_SIZE) do |j|
      (-(width + MARGIN) ... (width - MARGIN)).step(BOX_SIZE) do |k|
        # Base fill color on counter values, abs function 
        # ensures values stay within legal range
        @box_fill = color(i.abs.to_i, j.abs.to_i, k.abs.to_i, 50)
        cube = create_shape(BOX, BOX_SIZE.to_f, BOX_SIZE.to_f, BOX_SIZE.to_f)
        cube.set_fill(box_fill)
        cube.translate(k, j, i)
        grid.add_child(cube)
      end
    end
  end
end

def draw
  background(255)
  hint(DISABLE_DEPTH_TEST)
  # Center and spin grid
  push_matrix()
  translate(width/2, height/2, -DEPTH)
  rotate_y(frame_count * 0.01)
  rotate_x(frame_count * 0.01)
  shape(grid)
  pop_matrix()
  hint(ENABLE_DEPTH_TEST)
  @fcount += 1
  m = millis()
  if (m - lastm > 1000 * FINT)
    @frate = fcount / FINT
    @fcount = 0
    @lastm = m
    puts("fps: #{frate}")
  end
  fill(0)
  text("fps: #{frate}", 10, 20)
end

ruby-processing (pre processing-2.0b8)

Just as I was ready to get Jeremy Ashkenas to pull my updated version of ruby-processing, Andres has gone and changed the PShape api. Here is my toxiclibs volumeutils example converted for the new api (well actually just reqd mesh_to_vbo.rb library).

The change to api is that beginning a custom shape is a two stage process:-
  1. shape = create_shape (no param) 
  2. shape.begin_shape (type) 
and to be more consistent (I assume) instead of shape.end we have shape.end_shape
Here I have created a mock processing-2.0b8 version (see here how to do it) and created a local branch of my ruby-processing fork to experiemnt with. The example:-

#
# This example implements a custom VolumetricSpace using an implicit function
# to calculate each voxel. This is slower than the default array or HashMap
# based implementations, but also has much less memory requirements and so might
# be an interesting and more viable approach for very highres voxel spaces
# (e.g. >32 million voxels). This implementation here also demonstrates how to
# achieve an upper boundary on the iso value (in addition to the one given and
# acting as lower threshold when computing the iso surface)
#
# Usage:
# move mouse to rotate camera
# -/=: zoom in/out
# l: apply laplacian mesh smooth
# 
#

# 
# Copyright (c) 2010 Karsten Schmidt & ruby-processing version Martin Prout 2013
# This sketch relies on a custom ruby-processing mesh_to_vbo library
# 
# This library is free software you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation either
# version 2.1 of the License, or (at your option) any later version.
# 
# http://creativecommons.org/licenses/LGPL/2.1/
# 
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
# 
# You should have received a copy of the GNU Lesser General Public
# License along with this library if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
#

load_libraries 'toxiclibscore', 'vbo', 'volumeutils'
include_package 'toxi.geom'
include_package 'toxi.geom.mesh'
include_package 'toxi.volume'
include_package 'toxi.processing'

RES = 64
ISO = 0.2
MAX_ISO = 0.66

attr_reader :mesh, :vbo, :curr_zoom, :implicit

def setup()
  size(720,720, P3D)
  @vbo = MeshToVBO.new(self)
  @curr_zoom = 1
  vol = EvaluatingVolume.new(Vec3D.new(400,400,400), RES, RES, RES, MAX_ISO)
  surface = HashIsoSurface.new(vol)
  @mesh = WETriangleMesh.new()
  surface.compute_surface_mesh(mesh, ISO)
  @is_wire_frame = false
  no_stroke
  @implicit = vbo.meshToVBO(mesh, true)
end

def draw()
  background(0)
  lights
  define_lights
  translate(width / 2.0, height / 2.0, 0)
  rotate_x(mouse_y * -0.01)
  rotate_y(mouse_x * -0.01)
  scale(curr_zoom)
  shape(implicit)
end

def key_pressed()
  case key
  when '-'
    @curr_zoom -= 0.1
  when'='
    @curr_zoom += 0.1
  when 'l', 'L'
    LaplacianSmooth.new().filter(mesh, 1)
    @implicit = vbo.meshToVBO(mesh, true)
  when 's', 'S'
    save_frame("implicit.png")
  end
end

def define_lights
 ambient(20, 20, 20)
 ambient_light(50, 50, 50)
 point_light(30, 30, 30, 200, -150, 0)
 directional_light(0, 30, 50, 1, 0, 0)
 spot_light(30, 30, 30, 0, 40, 200, 0, -0.5, -0.5, PI / 2, 2)
end

class EvaluatingVolume < VolumetricSpace
  include Processing::Proxy
  include_package 'toxi.math'
  attr_reader :upper_bound, :lut
  FREQ = PI * 3.8

  def initialize(scal_vec, resX, resY, resZ, upper_limit)
    super(scal_vec, resX, resY, resZ)
    @upper_bound = upper_limit
    @lut=SinCosLUT.new()
  end

  def clear()
    # nothing to do here
  end

  def getVoxelAt(i)
    getVoxel(i % resX, (i % sliceRes) / resX, i / sliceRes)
  end

  def getVoxel(x, y, z)  # can't overload so we renamed
    val = 0
    if (x > 0 && x < resX1 && y > 0 && y < resY1 && z > 0 && z < resZ1)
      xx = x * 1.0 / resX - 0.5  # NB: careful about integer division !!!
      yy = y * 1.0 / resY - 0.5
      zz = z * 1.0 / resZ - 0.5
      #val = lut.sin(xx * FREQ) + lut.cos(yy * FREQ) + lut.sin(zz * FREQ)
      val = lut.cos(xx * FREQ) * lut.sin(yy* FREQ) + lut.cos(yy* FREQ) * lut.sin(zz* FREQ) + lut.cos(zz* FREQ)* lut.sin(xx* FREQ)
      if (val > upper_bound)
        val = 0
      end
    end
    return val
  end
end

The required library:-
############################################
# mesh_to_vbo.rb
# a ruby library to convert toxi.mesh object
# to vbo (PShape) written by Martin Prout
# make use of Processing::Proxy mixin
############################################
class MeshToVBO
  include Processing::Proxy

  attr_reader :parent

  def initialize(parent)
    @parent = parent
  end

  def meshToVBO(mesh, smth)
    retained = parent.createShape()
    retained.begin_shape(PShape::TRIANGLES)
    retained.enableStyle()
    retained.fill(222, 222, 222)
    retained.ambient(50)
    retained.shininess(10)
    retained.specular(50)
    if (smth)
      mesh.computeVertexNormals()
      mesh.getFaces.each do |f|
        retained.normal(f.a.normal.x, f.a.normal.y, f.a.normal.z)
        retained.vertex(f.a.x, f.a.y, f.a.z)
        retained.normal(f.b.normal.x, f.b.normal.y, f.b.normal.z)
        retained.vertex(f.b.x, f.b.y, f.b.z)
        retained.normal(f.c.normal.x, f.c.normal.y, f.c.normal.z)
        retained.vertex(f.c.x, f.c.y, f.c.z)
      end
    else
      mesh.getFaces.each do |f|
        retained.normal(f.normal.x, f.normal.y, f.normal.z)
        retained.vertex(f.a.x, f.a.y, f.a.z)
        retained.vertex(f.b.x, f.b.y, f.b.z)
        retained.vertex(f.c.x, f.c.y, f.c.z)
      end
    end
    retained.end_shape()
    return retained
  end

  # variant
  # input array of meshes, output an array of shapes
  def meshToRetained(mesh, smth)
    rshapes = []
    (0 ... mesh.length).each do |i|
      rshapes.push(meshToVBO(mesh[i], smth))
    end
    return rshapes
  end
end


Followers

About Me

My photo
I have developed JRubyArt and propane new versions of ruby-processing for JRuby-9.1.5.0 and processing-3.2.2