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

Thursday 3 March 2011

The 'growing' of a context-sensitive L-system plant (ruby-processing)

LSystem rules from a paper by Tong Lin at UMBC Maryland. The following code could be simplified if you could rely on string index being a char (ruby 1.9) rather than ascii value (ruby 1.8), tripped me up at first, ruby-processing is still at 1.8 (it depends on JRuby which will start supporting ruby 1.9 from JRuby version 1.6?)

Update 10 March 2011 just tried jruby-complete-1.6-RC3 still get old behaviour ie ascii value rather than char when indexing a string, nor is ord method supported still requires Jeremys shim to string.


########################################################
# cs_test.rb
# A 3D Plant implemented using a Context Sensitive
# Lindenmayer System in ruby-processing
# by Martin Prout (3 March 2011)
########################################################

class CS_Test < Processing::App
  full_screen # NB: All distances are relative to screen height
  load_libraries 'csplant', 'PeasyCam', 'opengl'
  import 'peasy'
  import "processing.opengl" if library_loaded? "opengl"
  attr_reader :csplant, :cam

  def setup
    library_loaded?(:opengl) ? configure_opengl : render_mode(P3D)
    configure_peasycam
    @csplant = CSPlant.new(height)
    csplant.create_grammar 5
    no_stroke
  end

  def configure_peasycam
    cam = PeasyCam.new self, height / 6.5
    cam.set_minimum_distance height / 10
    cam.set_maximum_distance height
  end

  def configure_opengl
    render_mode OPENGL
    hint ENABLE_OPENGL_4X_SMOOTH     # optional
    hint DISABLE_OPENGL_ERROR_REPORT # optional
  end

  def draw
    background 0
    lights
    csplant.render
  end

end


The PeasyCam and csplant libraries need to be nested in the usual way in a library folder. This code is much more complicated than my previous post. This is mainly due to the need to ignore certain symbols when determining context, and hence need to navigate along the production string.


######################################
# csplant.rb
# A library used to implement
# a context sensitive
# 1-L lsystem grammar in
# ruby-processing
# by Martin Prout (4 March 2011)
######################################

################################################
# A helper class stores cs prefix idx, and cchar
# uses idx and char to determine context and
# pre to access context sensitive rule from rules
###############################################
class CSRule

  attr_accessor :pre

  def initialize pre
    @pre = pre
  end

  def idx
    x = 0
    if (pre[1] == 60)  # NB comparing ascii values until ruby 1.9 support
      x -= 1
    end
    if (pre[1] == 62)  # ">"[0] = 62, "<"[0] = 60
      x += 1
    end
    return x
  end

  def cchar
    return pre[0]
  end

end

##################################
# The grammar class stores lsystem rules
# in rules Hash, and context Hash if applicable
# In production context is checked (and applied)
# using get_rule method
##################################

class CSGrammar
  IGNORE = "[]+-^&3"       # characters to ignore for context as a constant string
  attr_reader :axiom, :context, :rules, :count
  def initialize(axiom)
    @axiom = axiom
    @rules = Hash.new
    @context = Hash.new
  end

  def add_rule(pre, rule)
    if pre.length == 3
      context.store(pre[2], CSRule.new(pre))
    end
    rules.store pre, rule
  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
    @count = 0       # initialise count on axiom or new prod
    prod.gsub!(/./) do |ch|
      get_rule(prod, ch)
    end
  end

  def get_rule prod, ch
    rule = ch   # default is to return original character as rule (ie no change)
    idx = count # idx is a local index, used to navigate the production string
    if (context.has_key?(ch[0]))      
      while IGNORE.include?(prod[context[ch[0]].idx + idx].chr)
        idx += context[ch[0]].idx
      end
      if prod[context[ch[0]].idx + idx] == context[ch[0]].cchar
        rule = rules[context[ch[0]].pre]
      else
        rule = rules[ch] if rules.has_key?(ch) # context free rule if it exists
      end      
    else
      rule = rules[ch] if rules.has_key?(ch) # context free rule if it exists
    end
    @count += 1 # increment the index of axiom/production as a side effect
    return rule
  end
end

############
# CSPlant
############
class CSPlant
  include Processing::Proxy

  attr_reader :grammar, :axiom, :production, :premis, :rule,
  :theta, :scale_factor, :distance, :phi, :len

  def initialize(len)
    @axiom = "F"
    @grammar = CSGrammar.new(axiom)
    @production = axiom
    @len = len
    @distance = len/4      # distance value relative to screen height
    @theta = Math::PI/180 * 25
    @phi = Math::PI/180 * 25
    grammar.add_rule("F", "F[-EF[3&A]]E[+F[3^A]]")
    grammar.add_rule("F<E", "F[&F[3+A]][^F[3-A]]")  # context sensitive rule
    no_stroke()
  end

  def render()
    fill(0, 75, 152)
    light_specular(204, 204, 204)
    specular(255, 255, 255)
    shininess(1.0)
    repeat = 1
    production.scan(/./) do |ch|
      case(ch)
      when "F"
        translate(0, distance/-2, 0)
        box(distance/9, distance, distance/9)
        translate(0, distance/-2, 0)
      when "+"
        rotateX(-theta * repeat)
        repeat = 1
      when "-"
        rotateX(theta * repeat)
        repeat = 1
      when "&"
        rotateZ(-phi * repeat)
        repeat = 1
      when "^"
        rotateZ(phi * repeat)
        repeat = 1
      when "3"
        repeat = 3
      when "["
        push_matrix
      when "]"
        pop_matrix
      when "E", "A"
      else
        puts("character '#{ch}' not in grammar")
      end
    end
  end
  ##############################
  # create grammar from axiom and
  # rules (adjust scale)
  ##############################

  def create_grammar(gen)
    @distance *= 0.5**gen
    @production = grammar.generate gen
  end
end



Tuesday 1 March 2011

Exploring Context Sensitive LSystem rules in ruby / ruby-processing

Warning the following code depends on ruby 1.9, where string index returns a char rather than an ascii value (ruby 1.8) see following post for how this tripped me up at first when exploring a proper ruby-processing example. (Update 5 March 2011)


######################################
# cs_grammar.rb a context sensitive
# 1-L lsystem grammar for
# ruby/ruby-processing
# by Martin Prout (1 March 2011)
######################################

#######################
# A helper class stores
# cs rules idx, and cchar
# methods extract cs data
#######################
class CSRule
  
  attr_accessor :pre, :crule
  
  def initialize pre, crule
    @pre, @crule = pre, crule
  end
  
  def idx
    x = 0
    if (pre[1] == "<")
      x -= 2
    end
    return x
  end
  
  def cchar
    return pre[0]
  end

end

##################################
# 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 CSGrammar

  attr_reader :axiom, :context, :no_context, :idx
  def initialize(axiom)
    @axiom = axiom
    @no_context = Hash.new
    @context = Hash.new
  end
  
  def add_rule(pre, rule)
    case pre.length
      when 3
        @context.store(pre[2], CSRule.new(pre, rule)) # index, context, rule
      when 1
        @no_context.store pre, rule
      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)) && (prod[context[ch].idx + idx] == context[ch].cchar)
      rule = context[ch].crule  # use context sensitive rule
    else
      rule = no_context[ch] if no_context.has_key?(ch) # context free rule if it exists
    end
    return rule
  end
end

# Test data taken from ABOP

7.times do |i|
   grammar = CSGrammar.new("baaaaaa")
   grammar.add_rule("b<a", "b")  # context sensitive rule replace a when preceded by b
   grammar.add_rule("b", "a")
   result = grammar.generate(i)
   print result << "\n"
end



Test Output:

baaaaaa
abaaaaa
aabaaaa
aaabaaa
aaaabaa
aaaaaba
aaaaaab

Note the way b 'travels' from left to right through the production string in the test output.

Reference:

The Algorithmic Beauty of Plants

Przemyslaw Prusinkiewicz
Aristid Lindenmayer

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