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

Wednesday, 8 September 2010

Experimental 3D Context Free DSL in ruby-processing, a bit of a monster?

This is at fairly experimental stage, I need to decide how to order rotations/translations. At the moment order is fixed by the library script, so it doesn't matter what order they are entered you get the default order!!!

This library cannot compete with StructureSynth which is a much more refined and complete program. However if you are used to ruby it may be easier to write your rules in ruby, rather than in the EisenScript syntax. The funky extra thing is, because it is based on processing you have the animation possibilities of the 'draw' loop. My monster in some directions could be seen as a beating heart.

To use the library, create a folder for you work say "work", create a sub-folder "library" and sub-sub-folder "test_free" (or whatever better name you can think of for the library). Put the library "test_free.rb" in the sub-sub-folder. Write you sketches in the "work" directory, and run them with "rp5 run my_sketch.rb". If you want to do it the really neat way use JEdit as your editor and my macro and commando tools, then you can run the sketch from the editor. The link to the tools describes live editing, which I haven't tried with this library so I would recommend just using the run mode for now.

If you are not a ruby meddler just get StructureSynth, and learn the EisenScript language. Otherwise you will need to get ruby-processing see link in my blog header to see how. My blog header also has link to the original Jeremy Ashkenas context-free.rb on github.

The test_free.rb library


# A Context-Free library for Ruby-Processing, inspired by
# contextfreeart.org and StructureSynth
# Built on Jeremy Ashkenas context free DSL script

module Processing

  class ContextFree
 

    include Processing::Proxy

    attr_accessor :rules, :app, :xr, :yr, :zr

    AVAILABLE_OPTIONS = [:x, :y, :z, :rx, :ry, :rz, :size, :color, :hue, :saturation, :brightness, :alpha]
    HSB_ORDER         = {:hue => 0, :saturation => 1, :brightness => 2, :alpha => 3}

    # Define a context-free system. Use this method to create a ContextFree
    # object. Call render() on it to make it draw.
    def self.define(&block)
      cf = ContextFree.new
      cf.instance_eval &block
      cf
    end


    # Initialize a bare ContextFree object with empty recursion stacks.
    def initialize
      @app          = $app
      @graphics     = $app.g
      @finished     = false
      @rules        = {}
      @rewind_stack = []
      @matrix_stack = []
      @xr = 0
      @yr = 0
      @zr = 0
    end


    # Create an accessor for the current value of every option. We use a values
    # object so that all the state can be saved and restored as a unit.
    AVAILABLE_OPTIONS.each do |option_name|
      define_method option_name do
        @values[option_name]
      end
    end


    # Here's the first serious method: A Rule has an
    # identifying name, a probability, and is associated with
    # a block of code. These code blocks are saved, and indexed
    # by name in a hash, to be run later, when needed.
    # The method then dynamically defines a method of the same
    # name here, in order to determine which rule to run.
    def rule(rule_name, prob=1, &proc)
      @rules[rule_name] ||= {:procs => [], :total => 0}
      total = @rules[rule_name][:total]
      @rules[rule_name][:procs] << [(total...(prob+total)), proc]
      @rules[rule_name][:total] += prob
      unless ContextFree.method_defined? rule_name
        self.class.class_eval do
          eval <<-METH
            def #{rule_name}(options)
              merge_options(@values, options)
              pick = determine_rule(#{rule_name.inspect})
              @finished = true if @values[:size] < @values[:stop_size]
              unless @finished
                get_ready_to_draw
                pick[1].call(options)
              end
            end
          METH
        end
      end
    end


    # Rule choice is random, based on the assigned probabilities.
    def determine_rule(rule_name)
      rule = @rules[rule_name]
      chance = rand * rule[:total]
      pick = @rules[rule_name][:procs].select {|the_proc| the_proc[0].include?(chance) }
      return pick.flatten
    end


    # At each step of the way, any of the options may change, slightly.
    # Many of them have different strategies for being merged.
    def merge_options(old_ops, new_ops)
      return unless new_ops
      # Do size first
      old_ops[:size] *= new_ops[:size] if new_ops[:size]
      new_ops.each do |key, value|
        case key
        when :size
        when :x, :y, :z
          old_ops[key] = value * old_ops[:size]
        when :rz, :ry, :rx
          old_ops[key] = value * (Math::PI / 180.0)
        when :hue, :saturation, :brightness, :alpha
          adjusted = old_ops[:color].dup
          adjusted[HSB_ORDER[key]] *= value
          old_ops[:color] = adjusted
        when :width, :height
          old_ops[key] *= value
        when :color
          old_ops[key] = value
        else # Used a key that we don't know about or trying to set
          merge_unknown_key(key, value, old_ops)
        end
      end
    end


    # Using an unknown key let's you set arbitrary values,
    # to keep track of for your own ends.
    def merge_unknown_key(key, value, old_ops)
      key_s = key.to_s
      if key_s.match(/^set/)
        key_sym = key_s.sub('set_', '').to_sym
        if key_s.match(/(brightness|hue|saturation|alpha)/)
          adjusted = old_ops[:color].dup
          adjusted[HSB_ORDER[key_sym]] = value
          old_ops[:color] = adjusted
        else
          old_ops[key_sym] = value
        end
      end
    end

    # Doing a 'split' saves the context, and proceeds from there,
    # allowing you to rewind to where you split from at any moment.
    def split(options=nil, &block)
      save_context
      merge_options(@values, options) if options
      yield
      restore_context
    end

    # Saving the context means the values plus the coordinate matrix.
    def save_context
      @rewind_stack.push @values.dup
      @matrix_stack << @graphics.get_matrix
    end

    # Restore the values and the coordinate matrix as the recursion unwinds.
    def restore_context
      @values = @rewind_stack.pop
      @graphics.set_matrix @matrix_stack.pop
    end

    # Rewinding goes back one step.
    def rewind
      @finished = false
      restore_context
      save_context
    end

    # Render the is method that kicks it all off, initializing the options
    # and calling the first rule.
    def render(rule_name, starting_values={})
      @values = {:x => 0, :y => 0, :z => 0,
                 :rz => 0, :ry => 0, :rx => 0,
                 :size => 1, :width => 1, :height => 1,
                 :start_x => width/2, :start_y => height/2, :start_z => 0,
                 :color => [0.5, 0.5, 0.5, 1],
                 :stop_size => 1.5}
      @values.merge!(starting_values)
      @finished = false
      @app.reset_matrix
      @app.rect_mode CENTER
      @app.ellipse_mode CENTER
      @app.no_stroke
      @app.color_mode HSB, 1.0
      @app.translate @values[:start_x], @values[:start_y], @values[:start_z]
      self.send(rule_name, {})
    end
 
    def rotate_x rt
      @xr = rt
    end
    def rotate_y rt
      @yr = rt
    end
    def rotate_z rt
      @zr = rt
    end

    # Before actually drawing the next step, we need to move to the appropriate
    # location.
    def get_ready_to_draw
      @app.translate(@values[:x], @values[:y], @values[:z])
      @app.rotate_x(@values[:rx] + xr)
      @app.rotate_y(@values[:ry] + yr)
      @app.rotate_z(@values[:rz] + zr)
    end


    # Compute the rendering parameters for drawing a shape.
    def get_shape_values(some_options)
      old_ops = @values.dup
      merge_options(old_ops, some_options) if some_options
      @app.fill *old_ops[:color]
      return old_ops[:size], old_ops
    end


    # The shape primitives are sphere and cube
    def cube(some_options=nil)
      size, options = *get_shape_values(some_options)
      rotz = options[:rz]
      roty = options[:ry]
      rotx = options[:rx]
      @app.rotate_x rotx unless rotx.nil?
      @app.rotate_y roty unless roty.nil?
      @app.rotate_z rotz unless rotz.nil?
      @app.translate(options[:x]  * size, options[:y] * size , options[:z]  * size)
      @app.box(size)
      @app.rotate_z(-1 * rotz) unless rotz.nil?  # unwind rotations in an ordered way
      @app.rotate_y(-1 * roty) unless roty.nil?
      @app.rotate_x(-1 * rotx) unless rotx.nil?    
   
    end


    def sphere(some_options=nil)
      size, options = *get_shape_values(some_options)
      rotz = options[:rz]
      roty = options[:ry]
      rotx = options[:rx]
      @app.rotate_x rotx unless rotx.nil?
      @app.rotate_y roty unless roty.nil?
      @app.rotate_z rotz unless rotz.nil?

       @app.translate(options[:x]  * size, options[:y] * size , options[:z]  * size)

      @app.sphere_detail 10
      @app.sphere(size)
      @app.rotate_z(-1 * rotz) unless rotz.nil?  # unwind rotations in an ordered way
      @app.rotate_y(-1 * roty) unless roty.nil?
      @app.rotate_x(-1 * rotx) unless rotx.nil?    
    end
  end
end




My Test Script monster.rb



load_libraries :test_free, :control_panel

attr_reader :xrot, :yrot, :zrot

def setup_the_spiral
  @spiral = ContextFree.define do
    rule :monster do
      5.times do |i|
        split do
        cone :ry => 72 * i
        rewind
        end
      end
    end

    rule :cone do                       # two equally weighted cone rules creates the beat
      sphere :brightness => 1
      cone :size => 0.96, :y => -0.24
    end
  
    rule :cone do
      sphere :brightness => 1
      cone :size => 0.97, :y => -0.28
    end

  end
end

def setup
  size 800, 800, P3D
  configure_control
  smooth
  setup_the_spiral
end

def configure_control
  control_panel do |c|
    c.title = "Attitude Control"
    c.slider :xrot, -3.1..3.1, 0.5
    c.slider :yrot, -3.1..3.1, 0.5
    c.slider :zrot, -3.1..3.1, 1.0
  end
end
def draw
  background 0.8
  lights
  @spiral.render :monster, :start_x => 0, :start_y => 10, :start_z => -30, :size => height/200, :stop_size => 1, :color => [0, 0.8, 0.8]
  @spiral.rotate_x xrot
  @spiral.rotate_y yrot
  @spiral.rotate_z zrot    
end



 


No comments:

Post a Comment

Followers

Blog Archive

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