Mandelbrot ด้วย ruby

จากโจทย์ การบ้าน Com sci

เนื่องจากปัญหา ความเร็วของ ruby
ก็เลยต้องเอา rubyinline เข้ามาช่วย
ส่วนการ paint ก็ใช้ ruby-opengl

เริ่มด้วยการสร้าง class จัดการกับการคำนวณก่อน

require 'rubygems'
require 'inline'
require 'gl'
require 'glut'
 
class MandelSet
 
  # x,y -> topleft coordinate
  # x1,y1 -> bottomright coordinate
  # w -> width (pixel)
  # h -> height (pixel)
  def initialize(x,y,x1,y1,w,h)
    @x = x.to_f
    @y = y.to_f
    @rx = (x1 - x).to_f / w
    @ry = (y - y1).to_f / h
    @w = w
    @h = h
  end
 
  ## แปลงค่าพิกัดบน screen ไปเป็น พิกัดที่จะนำไปคำนวณ mandel set
  def from_screen(x,y)
    [@x + @rx * x, @y - @ry * y]
  end
 
  def zoom(x,y)
    [x - 100*@rx, y + 100*@ry, x + 100*@rx, y - 100*@ry, @w, @h]
  end
 
  inline do |builder|
    builder.c "
   VALUE mandel(double x, double y) {
    double x0 = x;
    double y0 = y;
    double x2 = x * x;
    double y2 = y * y;
    int cnt = 0;
    int max = 200;
    while (x2 + y2 < 4 && cnt < max) {
      y = 2*x*y + y0;
      x = x2 - y2 + x0;
 
      x2 = x*x;
      y2 = y*y;
 
      cnt++;
    }
    if (cnt == max || cnt == 0) {
      return rb_float_new(0.0);
    } else {
      return rb_float_new((double)cnt/(double)max);
    }
  }
"
  end
 
  def to_a
    ret = []
    (0..@w).each do |x|
      (0..@h).each do |y|
        c = mandel(*from_screen(x,y))
        ret << [x,y,c]        
      end
    end
    ret
  end
 
end

จากนั้นก็ implement ส่วน render
โดยไม่ลืม implement function zoom
เพราะถ้า plot mandelbrot แล้ว zoom ไม่ได้
ก็เท่ากับกินกาแฟพร้อมๆกับบีบจมูก (ไม่ได้กลิ่น)

class MandelPlotter
  def initialize(x,y,x1,y1,w,h)
    @w = w
    @h = h
    @m = MandelSet.new(x,y,x1,y1,w,h)
    @result = @m.to_a
  end
 
  def run() 
    STDOUT.sync = TRUE
    Glut.glutInit
    Glut.glutInitWindowSize(@w, @h);
    a = Glut.glutCreateWindow("mandel set")
    Glut.glutDisplayFunc(disp)
    Glut.glutReshapeFunc(reshape);
    Glut.glutMouseFunc(mouseHandle);
    Glut.glutMainLoop;
  end
 
  def disp()
    Proc.new {
      Gl.glClear(Gl::GL_COLOR_BUFFER_BIT)
      Gl.glBegin(Gl::GL_POINTS)
      @result.each do |s|
        c = s[2]
        Gl.glColor(* chooseColor(c))    
        Gl.glVertex(s[0],s[1])
      end
      Gl.glEnd
      Gl.glFlush      
    }
  end
 
  def chooseColor(c) 
    if c > 0.1 && c < 0.3
	[c * 1.7 ,0,c * 1.2]
    elsif c > 0.3 && c < 0.5
        [0,c,0]
    elsif c > 0.5 && c < 0.7
        [0,0,c]
    elsif c > 0.7 && c < 0.9
	[0,c*0.3,c*1.1]
    else
    	[c,c,c]
    end
  end
 
  def reshape() 
    Proc.new {|w, h|
      Gl.glViewport(0, 0, w, h)
      Gl.glMatrixMode(Gl::GL_PROJECTION)
      Gl.glLoadIdentity
      Gl.glOrtho(0, w, 0, h, -1, 1)
      Gl.glScale(1, -1, 1)
      Gl.glTranslate(0, -h, 0)
    }
  end
 
  def mouseHandle()
    Proc.new do |b, state, x, y|
      if b == Glut::GLUT_LEFT_BUTTON && state == Glut::GLUT_UP
        nx,ny =  @m.from_screen(x,y)
        ret = @m.zoom(nx,ny)
        @m = MandelSet.new(*ret)
        @result = @m.to_a
        Glut.glutPostRedisplay
      end
    end
  end
end

เวลา run ก็

p = MandelPlotter.new(ARGV[0].to_i,ARGV[1].to_i,ARGV[2].to_i,ARGV[3].to_i,400,400)
p.run

Note: syntax ที่หลายคนอาจจะไม่เคยเห็น ก็คือ
การ return เป็น array แล้วนำไป pass เป็น parameter
โดยแตก array เป็น argument โดยใช้ operator '*'

Note: ถ้า run แล้วรู้สึกช้า ก็ปรับค่า max จาก 200 ให้เหลือ 30

sugree's picture

โอ๊ะ มี inline ด้วย ผมก็เจอปัญหาเดียวกันใน Python มีคนแก้ด้วย Numeric แต่ก็ไม่ได้ดั่งใจ Tkinter ก็ช้ายิ่งกว่า

สงสัย Proc.new นิดหน่อย ทำไมต้องใช้ครับ เหมือนว่าจะไม่จำเป็น หรือว่าปกติ Ruby แนะนำให้ใช้ซ้อนไว้ในฟังก์ชั่น

ที่ใช้ Proc.new
เพราะ open-gl มันรับ parameter เป็น Proc object (callback) ครับ

ตรงนี้ต่างกับ python ตรงที่ python
python ทำอย่างนี้ได้เลย

>>> def hi():
...   print 'hello'
... 
>>> def test(p):
...   p()
... 
>>> test(hi)
hello
>>>

แต่ ruby ทำไม่ได้

irb(main):001:0> def hi
irb(main):002:1>  puts 'hello'
irb(main):003:1> end
=> nil
irb(main):004:0> 
irb(main):005:0* def test(p)
irb(main):006:1>  p.call
irb(main):007:1> end
=> nil
irb(main):008:0> test(hi)
hello
NoMethodError: undefined method `call' for nil:NilClass
        from (irb):6:in `test'
        from (irb):8
irb(main):009:0>

จะเห็นว่า มัน evaluate hi ก่อนจะ pass เป็น parameter

Note: สงสัยอยู่เหมือนกันว่า มันอาจจะมีวิธีแปลง method ให้เป็น proc object
แต่ยัง search หาไม่เจอ

sugree's picture

อ๋อ ลืมไปนิด ruby จะ return คำสั่งสุดท้ายก่อนจบ method

ย้าย Codenone

ประกาศย้าย Codenone ไปใช้ Forum ของ Blognone แทนครับ ตามไปตั้งกระทู้ต่อได้ที่ Codenone Forum (รายละเอียดอ่านจากกระทู้ ย้าย Codenone ไปรวมกับ Blognone)

กระทู้เก่าๆ จะย้ายตามไปในภายหลัง ตอนนี้ปิดการโพสต์กระทู้ไว้ เหลือไว้เฉพาะอ้างอิงเท่านั้น