OpenGL で正多面体を回転させてみる(Ruby)

Ruby 用に OpenGL を手軽に使うライブラリ「miniopengl.rb」を書いたので、その例として 5つの正多面体すべてを回転させてみました。
 

 
正多面体のデータはここより頂戴しました。ありがとうございます。polyhedrons_obj.zip をダウンロードして解凍し、フォルダを下のコードと同じディレクトリに置いて下さい。

コードは以下です。miniopengl.rb はこのコードと同じディレクトリに配置して下さい。当然ながら OpenGL が使えるようにしなければなりません。必要な準備はここを参照して下さい。また、並列処理を行う Gem 'parallel' を使っています。これはインストール($ gem install parallel)しておいて下さい。

動作は Linux Mint 18.2, Ruby 2.3.3 で確認しました。Gem 'parallel' でおそらく Kernel.#fork を使っているので、Windows では動かないと思われます。

require_relative 'miniopengl'
require 'parallel'

filenames = []
1.upto(5) {|i| filenames << "./polyhedrons_obj/r" + ("%02d" % i) + ".obj"}
data = []

filenames.each do |fn|
  open(fn, "r") do |io|
    ar = [[], []]
    io.each_line do |l|
      l.chomp!
      a = l[2..-1].split(" ")
      (l[0] == "v") ? ar[0] << a.map(&:to_f) : ar[1] << a.map {|i| i.to_i - 1}
    end
    data << ar
  end
end


Parallel.each(data, in_processes: 5) do |vertex, apexes|
  MiniOpenGL.app width: 400, height: 400 do
    clear_color(0, 0, 0.3)
    
    draw do
      clear
      color(0, 1, 0)
      glEnableClientState(GL_VERTEX_ARRAY)
      glVertexPointer(3, GL_DOUBLE, 0, vertex.flatten)
      apexes.size.times {|i| glDrawElements(GL_LINE_LOOP, apexes[i].size,
         GL_UNSIGNED_BYTE, apexes[i])}
      display
    end
  
    reshape do |w, h|
      viewport(0, 0, w, h)
      init_projection
      perspective(30, w / h.to_f, 3, 10)
      init_modelview
      look_at(3, 4, 5, 0, 0, 0, 0, 1, 0)
    end
  
    repeat(30) do
      modelview
      rotate(5, 0, 1, 0)
      redisplay
    end
  end
end

Gem 'parallel' によって 5つのウィンドウが同時に立ち上がります。


※Gem 'parallel' に関しては下を参照。
rubyで簡単並列処理(Parallel)
マルチスレッド/プロセスまとめ(Ruby編)


Ruby のサンプルコードについて


結城先生が仰っていることは大事なことだと思います。いちばん大変なのは初心者のときです。とにかく簡単なコードを動かす(つまりは 'Hello World!')のがわからない。僕はプログラミングに関してまわりに誰も聞く人がいなかったし今もいないので、(いまでもまだ)いつも簡単なことがわからない。そういうことが、初心者を卒業してしまった人にはなかなかわかりにくいのですね。

でもこれは自戒なのである。自分も説明するのが結構めんどうでサボったりするのだよなあ…。結城先生の本はいつも親切である。

 
Ruby は「A Programmer's Best Friend」ってことなのだが、言語自体は初心者にも friendly なのに、それを動かすことに関しては初心者にあまり親切でないと思う。特に Windows で使うのが大変。僕が初めて Ruby を使ったのは Windows でだったのだが、とりあえず使うエディタがなくて困った記憶がある。ぐぐって何とか「サクラエディタ」をインストールして使ったのだった。このあたり、絶対つまらぬところで損をしていると思う。ここは Python を見習った方がいいのではないか。

RubyGem 'Oekaki' ver.0.1.0 Release

RubyGem 'oekaki'のヴァージョン 0.1.0 をリリースしました。
oekaki | RubyGems.org | your community gem host
GTK+でお絵かきしてみた(Ruby) - Camera Obscura
 
新しいメソッド Tool#clear, Tool#get_window_size, Event#window_changed を追加しました。

  • Tool#clear(color = nil) は画面のクリアです。color はクリアする色です。
  • Tool#get_window_size は現在の画面の大きさを配列 [width, height] に入れて返します。
  • Event#window_changed {...} はウィンドウの状態が替わったとき(最大化など)、ブロック内が呼ばれます。
  • デフォルトでウィンドウのリサイズができないようにしました。リサイズしたい場合は、Oekaki.app(resizable: true) {..} のように指定して下さい。

また、ヴァージョン 0.0.11 ではメソッド Tool#circle が追加されています。

  • Tool#circle(fill, x, y, r, color = nil) は中心 (x, y)、半径 r の円を書きます。fill が true ならば塗りつぶし、false ならば円周のみ描画されます。

 
これらに対応した
GTK+ で落書き 11(Ruby) - Camera Obscura
を書き直してみました。ウィンドウの最大化に対応しています。コードは下です。
円が降ってくる(スクリーンセーバーもどき) · GitHub

GTK+ で落書き 11(Ruby)


スクリーンセーバーみたいなものを Ruby で描いてみました。円が大きくなったり小さくなったりしながら落下していきます。

自画自賛ですけれど、これ結構好きです。

描画には自作の Gem 'oekaki' を使っています。
oekaki | RubyGems.org | your community gem host
GTK+でお絵かきしてみた(Ruby) - Camera Obscura

require 'oekaki'

Width, Height = if ARGV.size == 2
  ARGV.map(&:to_i)
else
  [1000, 700]
end

Max_r, Min_r = 40, 10
ColorMax = 65535
MaxNum = 60

class Circle
  def initialize(ob)
    @slot = ob
    renewal
    @y = rand(Height)
  end
  
  def renewal
    @max_r = rand(Max_r - Min_r) + Min_r
    @x =  rand(Width)
    @y = -rand(@max_r)
    @color = [rand(ColorMax), rand(ColorMax), rand(ColorMax)]
    @fall_step = 1 + rand * 3
    @r = 1
    @r_step = rand * 0.2 + 0.8
  end

  def paint
    @slot.color(@color[0], @color[1], @color[2])
    @slot.circle(true, @x, @y, @r)
    @y += @fall_step
    @r += @r_step
    @r_step *= -1 if @r > @max_r or @r < 1
    renewal if @y > Height + Max_r
    true
  end
end


Oekaki.app width: Width, height: Height do
  circles = []
  MaxNum.times {circles << Circle.new(self)}

  draw do
    color(0, 0, 0)  
    rectangle(true, 0, 0, Width, Height)
  end

  timer(80) do
    color(0, 0, 0)  
    rectangle(true, 0, 0, Width, Height)
    circles.each(&:paint)
  end
end

円によって落下速度や点滅測度が微妙に変えてあります。円の総数はいつも同じで、定数 MaxNum に入っています。

Gem 'oekaki' のバージョンは最新の 0.0.11 が必要です。古いバージョンならば、上の @slot.circle(true, @x, @y, @r)@slot.arc(true, @x - @r, @y - @r, 2 * @r, 2 * @r, 0, 64 * 360) に代えて下さい。それで動きます。


追記
以上は Linux Mint 18.2, Ruby 2.3.3 で確認しましたが、Windows 8.1, Ruby 2.2.2 [i386-mingw32] でもそのまま動くことを確認しました。ただ、Windows 版ではなぜか画面がちらつきますし、Linux Mint で実行したよりも動きがなめらかでないです。

GTK+ でスクロールするテキスト・ウィンドウを作る(Ruby)

RubyGTK+ を使ってテキストを表示させるウィンドウを作ってみました。Gem 'gtk2' を使っています。
 

window = ScrolledTextWindow.open

5.times do |i|
  window.write "send #{i}"
  window.write "  ok!\n"
  sleep(rand(2) + 1) 
end
  
window.close

これでこんな風に表示されます。

write で改行のたびにリアルタイムで書き込まれます。スクロールもします。ただし、残念ながら close してもウィンドウは消えません。ウィンドウのボタンで消去して下さい。
ウィンドウの縦横幅、タイトルを指定することもできます。ScrolledTextWindow.open(width: 300, height: 200, title: "Window") みたいな感じ。

scrolled_text_window.rb

require 'gtk2'

class ScrolledTextWindow
  def self.open(width: 500, height: 200, title: "Text Window")
    #IO{read, write}, Gtk::Window{w}, Gtk::ScrolledWindow{sw}, Gtk::TextView{t}
    #Gtk::TextBuffer{b}, control
    
    read, write = IO.pipe
    control = Object.new
    
    control.define_singleton_method(:write) do |text|
      write.print text
    end
    
    control.define_singleton_method(:close) do  
      write.close
    end
    
    fork do
      write.close
      
      w = Gtk::Window.new
      w.signal_connect("destroy") {Gtk.main_quit}
      w.set_size_request(width, height)
      w.title = title
      
      sw = Gtk::ScrolledWindow.new
      sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
      
      t = Gtk::TextView.new
      t.modify_font(Pango::FontDescription.new("12"))
      sw.add(t)
      w.add(sw)
      
      b = t.buffer
      Thread.new(t, b) do |t, b|
        loop do
          b.insert_at_cursor(read.gets)
          t.scroll_mark_onscreen(b.create_mark(nil, b.end_iter, true))
        end
      end
      
      Gtk.quit_add(0) {read.close}
      w.show_all
      Gtk.main
    end
    
    read.close
    control
  end
end

 

※参考
GTK+ で簡単なテキスト入力(Ruby) - Marginalia
noanoa 日々の日記 : Ruby/GTK2,GTK3 プログラミング Tips(3)- ボタン

ファイルの単純なコピー(C言語)

C言語の stdio(標準入出力ライブラリ)のお勉強をしたので、ファイルのコピーをする関数 my_copy() を書いてみました。Linux Mint 18.2 + gcc で確認しました。

mycopy.c

#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>

int main(){
    my_copy("mycopy.c", "another_mycopy.c");
    exit(0);
}

void my_copy(char *from[], char *to[]) {
    FILE *f1, *f2;
    int c;
    
    f1 = fopen(from, "r");
    if (!f1) err(from);
    
    file_exist(to);
    f2 = fopen(to, "w");
    if (!f2) err(to);
    
    while ((c = fgetc(f1)) != EOF) {
        if (fputc(c, f2) == EOF) err(to);
    }
    
    fclose(f2);
    fclose(f1);
}

int err(const char *s) {
    perror(s);
    exit(1);
}

//コピー先のファイルが既に存在しているならエラー終了
void file_exist(char *fname[]) {
    struct stat st;    //#include <sys/stat.h>が必要

    if (!stat(fname, &st)) {
        printf("File %s already exists.\n", fname);
        exit(1);
    }
}

コピー先のファイルが既に存在するならば、コピーをせずにエラー終了します。
fgetc も fputc も一文字の入出力だが、stdio は「バッファリング」をするので大丈夫。一文字のせいで遅くなったりしません。


main() 関数を次のように変更すれば、コマンドライン引数を取ってコピーすることができます。

int main(int argc, char *argv[]){
    if (argc != 3) {
        printf("引数の数は2でなければなりません。\n");
        exit(1);
    }
    
    my_copy(argv[1], argv[2]);
    exit(0);
}

実行。

$ ./mycopy mycopy.c another_mycopy.c

こんな感じです。
 
 

フロイド−ワーシャル・アルゴリズム(Ruby)


フロイド−ワーシャル・アルゴリズムは、グラフの全点から全点へのすべての最短経路を求めます。重みは負値を許しますが、負閉路は許しません。

上のようなグラフは次のようなハッシュで表現されます。頂点を表わすオブジェクトは何でもかまいません。

g = {a: {b: 3, c: 8}, b: {d: 1}, c: {b: 4}, d: {a: 2, c: -5}}

これは Hash#to_graph によって次のような配列に変換されます。

[[0, 3, 8, Infinity],
 [Infinity, 0, Infinity, 1],
 [Infinity, 4, 0, Infinity],
 [2, Infinity, -5, 0]]

メソッドの内部では @tr = {0=>:a, 1=>:b, 2=>:c, 3=>:d} というハッシュによって、頂点と頂点をあらわす番号(頂点番号)との間が関係づけられています。配列[from][to] は、from, to の頂点番号により、経路の重みを表現しています。Infinity は経路が存在しません。

メソッド Array#floyd_warshall は、その配列から全点最短経路を求めます。返り値は [shortest, pred] で、shortest も pred も(頂点数を n とすると) (n + 1) × n × n の大きさの配列です。具体的には

shortest =
[[[0, 3, 8, Infinity],
  [Infinity, 0, Infinity, 1],
  [Infinity, 4, 0, Infinity],
  [2, Infinity, -5, 0]],
 [[0, 3, 8, Infinity],
  [Infinity, 0, Infinity, 1],
  [Infinity, 4, 0, Infinity],
  [2, 5, -5, 0]],
 [[0, 3, 8, 4],
  [Infinity, 0, Infinity, 1],
  [Infinity, 4, 0, 5],
  [2, 5, -5, 0]],
 [[0, 3, 8, 4],
  [Infinity, 0, Infinity, 1],
  [Infinity, 4, 0, 5],
  [2, -1, -5, 0]],
 [[0, 3, -1, 4], [3, 0, -4, 1], [7, 4, 0, 5], [2, -1, -5, 0]]]
pred =
[[[nil, 0, 0, nil], [nil, nil, nil, 1], [nil, 2, nil, nil], [3, nil, 3, nil]],
 [[nil, 0, 0, nil], [nil, nil, nil, 1], [nil, 2, nil, nil], [3, 0, 3, nil]],
 [[nil, 0, 0, 1], [nil, nil, nil, 1], [nil, 2, nil, 1], [3, 0, 3, nil]],
 [[nil, 0, 0, 1], [nil, nil, nil, 1], [nil, 2, nil, 1], [3, 2, 3, nil]],
 [[nil, 0, 3, 1], [3, nil, 3, 1], [3, 2, nil, 1], [3, 2, 3, nil]]]

となります。いずれも最後の 2次元行列が求めるべき回答です。shortest の最後は重みの最小値を、pred の最後はたどるべき経路を表します。例えば頂点 0 から頂点 2 の重みの最小値は shortest[4][0][2] = -1 となり、最短経路は 0→1→3→2 となります。最短経路は後ろから pred[4][0][2] = 3 で頂点 3、pred[4][0][3] = 2 で頂点 2、pred[4][0][2] = 0 で頂点 0 という具合です。

この結果、この場合での最短経路中の最大値は shortest[4][2][0] = 7 であり、つまりは頂点 2 から 0 の経路ということがわかります。

メソッド Hash#order(from, to) は Array#floyd_warshall を呼び出したあと、頂点 from から to までの最短経路(経路番号ではありません)を順に配列に入れて返します。

floyd_warshall.rb

class Array
  def floyd_warshall
    #Float or Integer or Rational{ar[][], shortest[][][], a}, Integer{pred[][][]}, size
    ar = self
    size = ar.size
    class << ar
      def include?(u, v)
        self[u][v] != Float::INFINITY and !self[u][v].zero?
      end
    end
    
    shortest = Array.new(size + 1) {Array.new(size) {Array.new(size)}}
    pred     = Array.new(size + 1) {Array.new(size) {Array.new(size)}}
    size.times do |u|
      size.times do |v|
        shortest[0][u][v] = ar[u][v]
        pred[0][u][v] = ar.include?(u, v) ? u : nil
      end
    end
    
    size.times do |x|
      size.times do |u|
        size.times do |v|
          if shortest[x][u][v] > (a = shortest[x][u][x] + shortest[x][x][v]) 
            shortest[x + 1][u][v] = a
            pred[x + 1][u][v] = pred[x][x][v]
          else
            shortest[x + 1][u][v] = shortest[x][u][v]
            pred[x + 1][u][v] = pred[x][u][v]
          end
        end
      end
    end
    
    [shortest, pred]
  end
end

class Hash
  def to_graph
    #Object{node[]}, Float or Integer or Rational{ar[][]}
    #Hash{@tr[Integer]: Object}
    
    node = keys
    @tr = node.map.with_index {|key, i| [i, key]}.to_h
    ar = Array.new(size) {Array.new(size, Float::INFINITY)}
    
    size.times do |x|
      self[@tr[x]].each do |g|
        ar[x][node.index(g[0])] = g[1] 
      end
      ar[x][x] = 0
    end
    ar
  end
  
  def order(from, to)
    #Object{from, to} ->
    #Object{ans[]}, Hash{tr[Object]: Integer}
    #Integer{pred[][][], ar[][], from1, nxt}, shortest
    
    shortest, pred = to_graph.floyd_warshall
    ar = pred[-1]
    tr = @tr.invert
    from1 = tr[from]
    ans = [to]
    nxt = ar[from1][tr[to]]
    
    while nxt != from1
      ans.unshift(@tr[nxt])
      nxt = ar[from1][nxt]
    end
    [@tr[nxt]] + ans
  end
end

if __FILE__ == $0
  g = {a: {b: 3, c: 8}, b: {d: 1}, c: {b: 4}, d: {a: 2, c: -5}}
  p g.order(:a, :c)    #=>[:a, :b, :d, :c]
end

 

アルゴリズムの基本

アルゴリズムの基本