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

Thursday, 30 September 2010

Basic Hair Example SunflowAPIAPI just using jruby

It occurred to me that all I was getting from using ruby-processing in my previous post was the ruby processing interface to the libraries. So here is the straightforward jruby version....
Screen output very much as before (smaller) hence not included there. For your convenience I present an embedded gist (click on view raw to copy).
See previous post for how to setup java libraries.
Example translated from java version (by Christopher Warnow?) included in the subversion checkout of the sunflowAPIAPI.

Basic Hair Example SunflowAPIAPI in ruby processing

See previous post for how to setup java libraries.


# basic_hair.rb run with 'rp5 run script.rb'

class BasicHair < Processing::App
  load_libraries :sunflow_api, :sunflow
  include_package 'com.briansteen'
  include_package 'org.sunflow.math'
  include_package 'java.awt'

  API = Java::Com::briansteen::SunflowAPIAPI
  SMath = Java::Org::sunflow::math
  JColor = java.awt.Color
  WIDTH = 640
  HEIGHT = 480
  P_AMOUNT = 20
       
  def setup
    # create a new API instance
    @sunflow = API.new
    # set width and height
               
    @sunflow.set_width(WIDTH)
    @sunflow.set_height(HEIGHT)
    # set background color
    @sunflow.set_background(1, 1, 1)
    # set camera
    @sunflow.set_camera_position(0, 7, 5)
    @sunflow.set_camera_target(2, 0.5, 0)
    @sunflow.set_thinlens_camera("thinLensCamera", 50, WIDTH/HEIGHT)
    # set basic light
    @sunflow.set_point_light("myPointLight", SMath::Point3.new(0, 5, 5),
    JColor.new(255, 255, 255))
    @sunflow.set_directional_light("myDirectionalLight", SMath::Point3.new(-2, 3, 0),
    SMath::Vector3.new(0, 0, 0), 3, JColor.new(1, 1, 1))
    # @sunflow.setSphereLight("mySphereLight", SMath::Point3.new(0, 30, -5),
    # JColor.new(0, 0, 255), 32, 10)
    # draw a ground plane
    @sunflow.draw_plane("ground", SMath::Point3.new(0, 0, 0), SMath::Vector3.new(0, 1, 0))             
    # coordinates array
    hair_widths = [0.025]              
    @sunflow.draw_box("boxname", 0, 0, 0, 1)           
    # create particle coordinates
    350.times do |j|
      # particle start position
      particle_x = Math.cos(j*0.5)*j*0.0015
      particle_y = 0
      particle_z = Math.sin(j*0.5)*j*0.0015
      hair_coordinates = Array.new(P_AMOUNT*3)
      array_index = -1
      P_AMOUNT.times do |i|
        particle_x += 0.1 + Math.cos(i * 0.15 + j*0.05) * 0.13
        particle_y -= Math.sin(particle_z*0.01 + j*0.05)*0.125 +
        Math.cos(i*0.5 + particle_y)*0.125
        particle_z += Math.sin(i)*0.25 + particle_y*0.01                               
        hair_coordinates[array_index += 1] = particle_x
        hair_coordinates[array_index += 1] = particle_y
        hair_coordinates[array_index += 1] = particle_z
      end
                       
      # set ambient occlusion shader
      @sunflow.setAmbientOcclusionShader("myAmbientOcclusionShader#{j}", JColor.new(55,55,55),
      JColor.new(0,0,0), 16, 1)
      # set glass shader
      # @sunflow.setGlassShader("myGlassShader", JColor.new(1,1,1), 2.5, 3, JColor.new(1,1,1))
      # set shiny-diffuse shader
      # @sunflow.setShinyDiffuseShader("myShinyShader", JColor.new(55,55,55), 0.8)
                       
      # draw object
      @sunflow.drawHair("hair#{j}", P_AMOUNT - 2, hair_coordinates.to_java(:float),
        hair_widths.to_java(:float))
    end
               
    @sunflow.setIrradianceCacheGIEngine(32, 0.4, 1, 15, nil)           
    # render
    @sunflow.render() # display in sunflow window
    # @sunflow.render("BasicHair.png") # save as png image
  end
          
  def draw
   exit
  end 
          
end







SunflowAPIAPI in ruby processing


# sunflow_test.rb

class SunflowTest < Processing::App
  load_libraries :sunflow_api, :sunflow
  include_package 'com.briansteen'
  include_package "org.sunflow.math"
  API = Java::Com::briansteen::SunflowAPIAPI
  SMath = Java::Org::sunflow::math
  WIDTH = 500
  HEIGHT = 500

  def setup
    @sunflow = API.new
    @sunflow.set_width(WIDTH)
    @sunflow.set_height(HEIGHT)
    @sunflow.set_camera_position(0, 2.5, -5)
    @sunflow.set_camera_target(0, 0, 0)
    @sunflow.set_thinlens_camera("thinLensCamera", 50, WIDTH/HEIGHT)
    @sunflow.draw_plane("ground", SMath::Point3.new(0,-3.5,0), SMath::Vector3.new(0, 1, 0))
    @sunflow.draw_sphere_flake("mySphereFlake", 20, SMath::Vector3.new(0, 1, 0), 1)
    @sunflow.setPathTracingGIEngine 6
    @sunflow.render "SunflowTestRender.png"
  end

  def draw
    exit
  end

end

SunflowTest.new :title => "Sunflow Test"






This example was copied from amnon, following his recent posting on the processing discussion boards.

Checkout the sunflowapapi source (created by  Christopher Warnow?) as follows:-
svn checkout http://sunflowapiapi.googlecode.com/svn/trunk/ sunflowapiapi-read-only

Get the sunflow libraries here, NB must be version 0.07.3:-

http://sunflow.sourceforge.net

Or possible here for Windows users:-

http://www.polyquark.com/opensource

The usual library setup required compile and jar the sunflowapiapi code into sunflow_api.jar nest that as follows in a folder library, in a folder sunflow_api in the library folder (in the directory where the sketch you want to run is stored). Do similar for the sunflow.jar put the janino.jar in the same folder.  The janino.jar provides a fast java compiler, version is 2.~ is supplied with the sunflow download, however with a few minor modifications sunflow can be updated to use the latest version 2.6.1 (basically include the commons-compiler.jar, and remove a couple of deprecated exception types from the code and  update the CompileException to the commons compiler version org.codehaus.commons.compiler.CompileException, do this in an ide unless you are crazy).


Sunday, 26 September 2010

A Staffordshire Knot Using Ruby Processing and A Context Free Library

A Staffordshire Knot, ruby-processing 3D context-free DSL from monkstone on Vimeo.


Using a slight variation of the previous post we get something similar to the Staffordshire Knot.


segment :rz => i, :ry => Math.sin(i*Math::PI/180)*0.2, :x => Math.sin(i*Math::PI/180)*1.5


Is the main change, although to produce the video, I was saving frames and set the size to 640*480 to match vimeo preferences. Was very jumpy when I saved frames as #####.png ...

I have since uploaded an improved version, this time using the default save_frame (hence tif format). Mencoder doesn't like this format so I batch converted the tif files to png as so:-

mogrify -format png -transparent blue *.tif


See my processing blog for the video recording details.

Saturday, 25 September 2010

An Infinity Loop? With My 3D Context Free Library


# infinity.rb
# An 'infinity loop?'

load_libraries :test_free

full_screen

attr_reader :xrot, :yrot, :zrot, :wheel, :col

def setup_the_spiral
  @spiral = ContextFree.define do

    rule :infinity do
      360.times do |i|
      split do
        segment :rz => i, :ry => Math.sin(i*Math::PI/180)*0.2, :x => Math.cos(i*Math::PI/180)*3
      rewind
      end
      end
    end

    rule :segment do                      
      cube :alpha => 0.8
    end
   
    rule :segment do                      
      cube :brightness => 1.0, :saturation => 1.0
    end

  end
end



def setup
  render_mode P3D
  @wheel = JWheelListener.new(-50, -20)      # initialize listener with start_z and maximum values
  self.add_mouse_wheel_listener(wheel)       # register the mouse listener  
  smooth
  @xrot = 0.01
  @yrot = 0
  @zrot = 0
  @col = 0
  setup_the_spiral
end

def draw
  background 0
  setup_lights col
  specular col, 1, 1
  emissive 0.05
  shininess 10
  smooth_rotation(3, 3.2)
  smooth_color(6.0)
  @spiral.render :infinity, :start_x => 0, :start_y => 0, :start_z => wheel.zoom, :size => height/350,
  :stop_size => 0.2, :color => [0, 0.7, 0.7, 0.5], :rx => xrot, :ry => yrot, :rz => zrot
end

##
# Generate a gradient value that changes smoothly over time in the range [ 0, 1 ].
# The Math.sin() function is used to map the current time in milliseconds somewhere
# in the range [ 0, 1 ]. A 'speed' factor is specified by the parameters s1.
#
def smooth_gradient(s1)
  mills = millis * 0.00003   
  gradient = 0.5 * Math.sin(mills * s1) + 0.5
end
 
##
# Rotate the current coordinate system.
# Uses smooth_gradient() to smoothly animate the rotation.
#
def smooth_rotation(s1, s2)    
  @yrot = 2.0 * Math::PI * smooth_gradient(s1)
  @zrot = 2.0 * Math::PI * smooth_gradient(s2)
end

##
# Generate a 'hue' value which smoothly changes over time.
# The speed of change is controlled by the parameter s1.
#
def smooth_color(s1)
  @col = smooth_gradient(s1)
end

def setup_lights(col)   
  directional_light(col, 0.8, 0.8, -0.5, 0.5, -1)
  point_light(col, 0.8, 0.8, -0.5, 0.5, -1)
  ambient_light(col, 0.8, 0.8, 1, 1, 1)
end





Monday, 13 September 2010

Revised 3D Context Free DSL Sketch, with Mouse Wheel Zoom Utility

I demonstrate the new feature of mouse wheel zoom in this sketch. The logic for the smooth of color and rotation I copied from lazydogs classy rotating ball. PS: if you can't see the embedded code below chances are you are not using either Firefox or Google Chrome browser (doesn't always work for me with Opera, which I also find is awkward to use when copying code).







default.rb from monkstone on Vimeo.

Sunday, 12 September 2010

A Mouse Wheel Listener in Ruby Processing

For my 3D context free application/library, I would like to be able to zoom using the mouse wheel, I had vaguely thought about using peasycam or proscene to provide that, and other functionality. In the first instance I wanted to create it myself. After a bit of research, this the sort of implementation I would do using the java classes/interfaces in ruby:-

# mouse_listener.rb

class JWheelListener
  include java.awt.event.MouseWheelListener

  attr_reader :zoom

  def initialize(zoom)
    @zoom = zoom
  end

  def mouse_wheel_moved(e)
    @zoom += e.get_wheel_rotation * 10
  end

end

class Mouse < Processing::App

  attr_reader :wheel

  def setup
    size(1000, 1000)
    @wheel = JWheelListener.new(10)
    self.add_mouse_wheel_listener(@wheel)  
  end

  def draw
    background 0
    fill 255, 0, 0
    ellipse(width/2, height/2, wheel.zoom, wheel.zoom)

  end

end

Mouse.new :title => "Mouse Wheel Listener"




Saturday, 11 September 2010

3D Context Free DSL Screen Saver?

Here is an animated version of the default.es from StructureSynth in ruby-processing. Mac or windows users might want to experiment with OPENGL rendering, as this sketch exposes errors in processing P3D, I see odd missing pixels from time to time. If you don't like the holes remove the smooth, personally I prefer the holes to the glitchty non-smooth (it is a known issue with current versions of processing with both P2D and P3D renderers, something to do with tessellation algorithms).  The test_free library is as in previous post.
Nest the library as follows:-
library/test_free/test_free.rb in the folder containing the sketch
For instructions for installing ruby-processing follow the first link in the blog header.


# full_screen.rb
# An animation of default.es
# the StructureSynth default script

load_libraries :test_free
full_screen
attr_reader :xrot, :yrot, :zrot

def setup_the_spiral
  @spiral = ContextFree.define do

    rule :default do
      split do
      R1 :brightness => 1
      rewind
      R2 :brightness => 1
    end
    end

    rule :R1 do                      
      sphere :brightness => 1
      R1 :size => 0.99, :x => 0.25, :rz => 6, :ry => 6
    end

    rule :R2 do                      
      sphere :brightness => 1
      R2 :size => 0.99, :x => -0.25, :rz => 6, :ry => 6
    end

  end
end

def setup
  render_mode P3D
  smooth
  @xrot = 0.01
  @yrot = 0
  @zrot = 0
  setup_the_spiral
end

def draw
  background 0
  lights
  smooth_rotation(6.7, 7.3)
  @spiral.render :default, :start_x => 0, :start_y => 0, :start_z => -50, :size => height/350,
  :stop_size => 0.2, :color => [0, 0.8, 0.8], :rx => xrot, :ry => yrot, :rz => zrot
end

##
# Generate a vector whose components change smoothly over time in the range [ 0, 1 ].
# Each component uses a Math.sin() function to map the current time in milliseconds somewhere
# in the range [ 0, 1 ].A 'speed' factor is specified for each component.
#
def smooth_vector(s1, s2)
  mills = millis * 0.00003
  y = 0.5 * Math.sin(mills * s1) + 0.5
  z = 0.5 * Math.sin(mills * s2) + 0.5
  vec = [y, z]
end

##
# Rotate the current coordinate system.
# Uses smooth_vector() to smoothly animate the rotation.
#
def smooth_rotation(s1, s2)
  r1 = smooth_vector(s1, s2)
  @yrot = (2.0 * Math::PI * r1[0])
  @zrot = (2.0 * Math::PI * r1[1])
end






Thursday, 9 September 2010

Translating the default.es to ruby processing DSL

It was very constructive to see if I could translate the EisenScript to my ruby processing DSL, I immediately learnt something about the EisenScript (recursion seems to be implicit, something I had not understood before). What I learnt about my library, was I was losing some context by directly altering the attitude (the image expected from the EisenScript appeared briefly but quickly degraded). I have now re-factored the sketch to only adjust the initial angle, the x rotation seems to affect the amplitude of deformation from the plane of the spirals. The y and z rotation give control of the attitude, and need to be used in combination to fully explore the 3 dimensional shape, which is now somewhat like that produced by StrucureSynth. 

Here is my revised 3d context free DSL (test_free.rb):-

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


    # Sphere, cube are the primitive drawing
    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 oredered 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.sphere_detail 10
      @app.sphere(size)
      @app.rotate_z(-1 * rotz) unless rotz.nil?  # unwind rotations in an oredered way
      @app.rotate_y(-1 * roty) unless roty.nil?
      @app.rotate_x(-1 * rotx) unless rotx.nil?    
    end
  end
end





My Translation of default.es to ruby processing



# default.rb
# virtually a direct translation of default.es
# the StructureSynth default script

load_libraries :test_free, :control_panel

attr_reader :amplitude, :yrot, :zrot

def setup_the_spiral
  @spiral = ContextFree.define do

    rule :default do
      split do
      R1 :brightness => 1
      rewind
      R2 :brightness => 1
    end
    end

    rule :R1 do                      
      sphere :brightness => 1
      R1 :size => 0.99, :x => 0.25, :rz => 6, :ry => 6
    end

    rule :R2 do                      
      sphere :brightness => 1
      R2 :size => 0.99, :x => -0.25, :rz => 6, :ry => 6
    end

  end
end

def configure_control # setup control panel gui
  control_panel do |c|
    c.title = "Amplitude+Attitude"
    c.slider :amplitude, -0.025..0.015, 0.0
    c.slider :yrot, -6.3..6.3, 0.005
    c.slider :zrot, -6.3..6.3, 0.005
  end
end

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

def draw
  background 0
  lights
  @spiral.render :default, :start_x => 0, :start_y => 0, :start_z => -40, :size => height/400,
  :stop_size => 0.2, :color => [0, 0.8, 0.8], :rx => amplitude, :ry => yrot, :rz => zrot

end



Testing the cube primitive in my context free DSL

Another context free DSL example, better annotated, control panel and library as previous post (not shown):-


# cube.rb

load_libraries :test_free, :control_panel

attr_reader :xrot, :yrot, :zrot

def setup_the_spiral
  @spiral = ContextFree.define do  # define the cf rules
   
    rule :cubeform do
      5.times do |i|
        split do                   # record the context
        cuby :ry => 72 * i
        rewind                     # restore the context
        end
      end
    end

    rule :beam do                      
       6.times do |i|
        split do                    # record the context
          cube :y => 0.2 * i
        rewind                      # restore the context
        end
      end
    end
 
    rule :cuby do
      beam :brightness => 1
      cuby :size => 0.98, :y => -0.24  # recursive call ends when size < 1
    end

  end
end

def setup              # static setup
  size 800, 800, P3D   # I can only use P3D (unless full_screen) on linux opengl might work on mac/windows
  configure_control
  smooth
  setup_the_spiral
end

def configure_control # setup control panel gui
  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             # animation loop    
  background 0.8     
  lights
  @spiral.render :cubeform, :start_x => 0, :start_y => 10, :start_z => -30, :size => height/300, :stop_size => 1, :color => [0, 0.8, 0.8]
  @spiral.rotate_x xrot
  @spiral.rotate_y yrot
  @spiral.rotate_z zrot   
end


 


Within the sketch :hue, :saturation and :brightness can be individually set. If you want to use :alpha, initialize :color with a 4 dimension array. Note, the color mode in the ruleset is hsb with range 0 .. 1.0. Increment and decrement is linear cf size where it is multiplicative.

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