RubyGem 'OpenGL' のラッパー。いちいち書くのが面倒なところを処理します。Opal を使わない例と比べてみて下さい。(2016/5/10)
RubyGem 'OpenGL', 'Glu', 'Glut' のインストールが必要です。opal.rbと描画スクリプトを同じディレクトリに置いて下さい。
Opal.app のブロック変数(例えば g)が Toolクラスのオブジェクトになっています。Toolクラスのメソッドはだからすべて、g のインスタンス・メソッドというわけです。 これらは(そうしたければ)一切使わなくてもOKです(意味ないですけれど)。このモジュールを使う際は、関数はクロージャである proc(あるいは lambda でも可)を使うのがミソです。
なお、こちらにも Opal を用いたコード例があります。
opal.rb
require 'bundler/setup' #Bundlerを使っていない人は必要ない require 'opengl' require 'glu' require 'glut' include Gl, Glu, Glut, Math module Opal class Tool def initialize @rotate_v = [1, 0, 0] @clear_color = [0, 0, 0] @clear_color_alpha = 0 @color_alpha = 1 @depth_buffer_bit = 0 @look_to = [0, 0, 0] @up = [0, 1, 0] @font = GLUT_BITMAP_TIMES_ROMAN_24 end attr_accessor :clear_color, :color_alpha, :clear_color_alpha, :buffering, :depth, :depth_buffer_bit, :look_to, :up, :font def display=(prc) disp = proc do clear prc.call glutSwapBuffers unless @buffering.zero? end glutDisplayFunc(disp) end def reshape=(prc) prc1 = proc do |w, h| glViewport(0, 0, w, h) glMatrixMode(GL_PROJECTION) clr_matrix prc.call(w, h) glMatrixMode(GL_MODELVIEW) end glutReshapeFunc(prc1) end def mouse=(prc) glutMouseFunc(prc) end def passive_motion=(prc) glutPassiveMotionFunc(prc) end def motion=(prc) glutMotionFunc(prc) end def keyboard=(prc) glutKeyboardFunc(prc) end def idle=(prc) glutIdleFunc(prc) end def add_menu(str, value) glutAddMenuEntry(str, value) end def font=(a) @font = a end def print(str) str.bytes.each {|ch| glutBitmapCharacter(@font, ch)} end def rotate=(ar) @rotate_v = ar end def rotate(ang) glRotated(ang, @rotate_v[0], @rotate_v[1], @rotate_v[2]) end def look_at(x, y, z) l = sqrt(@up[0] ** 2 + @up[1] ** 2 + @up[2] ** 2) gluLookAt(x, y, z, @look_to[0], @look_to[1], @look_to[2], @up[0] / l, @up[1] / l, @up[2] / l) end def draw(c) glBegin(c) yield glEnd end def draw_vertex(c, dim, vtx) glEnableClientState(GL_VERTEX_ARRAY) glVertexPointer(dim, GL_FLOAT, 0, vtx) glBegin(c) (vtx.size / dim).times {|i| glArrayElement(i)} glEnd end def draw_elements(c, dim, vtx, id) glEnableClientState(GL_VERTEX_ARRAY) glVertexPointer(dim, GL_FLOAT, 0, vtx) glDrawElements(c, id.size, GL_UNSIGNED_BYTE, id) end def perspective(fovy = 60, aspect = 1, znear = 0.1, zfar = 20) glMatrixMode(GL_PROJECTION) clr_matrix gluPerspective(fovy, aspect, znear, zfar) glMatrixMode(GL_MODELVIEW) end def block glPushMatrix yield glPopMatrix end def timer(time, prc, value = 0) glutTimerFunc(time, prc, value) end def repeat(time, value = 0, &bk) a = proc do yield timer(time, a, value) end glutTimerFunc(time, a, value) end def redraw glutPostRedisplay end def color(r, g, b) glColor4f(r, g, b, @color_alpha) end def clear(a = GL_COLOR_BUFFER_BIT) glClear(a | @depth_buffer_bit) end def clr_matrix glLoadIdentity end def vtx2(x, y) glVertex2d(x, y) end def vtx3(x, y, z) glVertex3d(x, y, z) end def light(num , *args) glLightfv(eval("GL_LIGHT#{num}"), *args) end def light_enable(num = 0) glEnable(eval("GL_LIGHT#{num}")) end def line(x1, y1, z1, x2, y2, z2) draw(GL_LINES) do vtx3(x1, y1, z1) vtx3(x2, y2, z2) end end def polarView(distance, twist, elevation, azimuth) #極座標 #distance: 軌道半径(カメラと原点の距離) #twist: 照準線を中心とした回転角 #elevation: yz平面でのカメラの回転角を、正のz軸から測ったもの(y軸を上として物体を見下ろす角度) #azimuth: y軸周りの回転角 glTranslated(0, 0, -distance) glRotated(-twist, 0, 0, 1) glRotated(-elevation, 1, 0, 0) glRotated(-azimuth, 0, 1, 0) end def xyzAxis(colorx: [1, 0, 0], colory: [0, 1, 0], colorz: [0, 0, 1], length: 10, color: false) color(color[0], color[1], color[2]) if color #(指定された)単一色 draw(GL_LINES) do color(colorx[0], colorx[1], colorx[2]) unless color vtx3(0, 0, 0); vtx3(length, 0, 0) color(colory[0], colory[1], colory[2]) unless color vtx3(0, 0, 0); vtx3(0, length, 0) color(colorz[0], colorz[1], colorz[2]) unless color vtx3(0, 0, 0); vtx3(0, 0, length) end end end def self.app(title: "OpenGL", left: 200, top: 50, width: 300, height: 300, rgbm: GLUT_RGBA, buffering: 0, depth: 0, culling: false) i = Tool.new i.buffering = buffering #0ならばダブルバッファリングしない、GLUT_DOUBLEならする i.depth = depth #0ならば隠面消去しない、GLUT_DEPTHならする i.depth_buffer_bit = GL_DEPTH_BUFFER_BIT unless depth.zero? glutInit glutInitDisplayMode(rgbm | i.buffering | i.depth) glutInitWindowPosition(left, top) glutInitWindowSize(width, height) glutCreateWindow(title) if culling #trueならばカリングをする glEnable(GL_CULL_FACE) glCullFace(GL_BACK) end yield(i) glClearColor(i.clear_color[0], i.clear_color[1], i.clear_color[2], i.clear_color_alpha) glutMainLoop end end #ver 0.4.1
ウィンドウの表示。たったこれだけ。
require './opal' Opal.app do |g| g.display = proc {glFlush} end
白地に黄色い正方形。以下、require './opal' は書きません。
Opal.app do |g| display = proc do g.color(1, 1, 0) #黄色 g.draw(GL_POLYGON) do glVertex2d(-0.8, -0.8) glVertex2d(0.8, -0.8) glVertex2d(0.8, 0.8) glVertex2d(-0.8, 0.8) end glFlush end g.clear_color = [1, 1, 1] #白 g.display = display end
同じのをあるいはこう。
Opal.app do |g| g.clear_color = [1, 1, 1] g.display = proc do g.color(1, 1, 0) glRectf(-0.8, 0.8, 0.8, -0.8) #長方形 glFlush end end
破線の三角形。
Opal.app title: "Triangle", width: 500, height: 500 do |g| g.display = proc do glEnable(GL_LINE_STIPPLE) #破線を可能にする glLineStipple(1, 0xF0F0) #破線のパターンの指定(0xF0F0の部分がそれ) glLineWidth(3) #線の太さ g.draw(GL_LINE_LOOP) do g.color(0, 1, 0) #緑 glVertex2f(0 , -0.9) glVertex2f(-0.9 , 0.9) glVertex2f(0.9 , 0.9) end glFlush end end
頂点を配列を使って一気に指定します。
Opal.app title: "Polygon", width: 500, height: 500 do |g| g.display = proc do vertex = [-0.9, 0.9, 0.9, 0.9, 0, -0.9] g.color(0, 1, 0) glLineWidth(3) g.draw_vertex(GL_LINE_LOOP, 2, vertex) glFlush end end
Opal.app title: "Teapot", width: 400, height: 400 do |g| g.display = proc do g.color(0, 1, 0) glutWireTeapot(1) glFlush end g.reshape = proc {|w, h| gluPerspective(30, w.to_f / h, 1, 100)} gluLookAt(3, 4, 5, 0, 0, 0, 0, 1, 0) end
ティーポットが回転します。先の例とは少し変えてあるので、おもしろいですよ。
Opal.app title: "Teapot", width: 400, height: 400, buffering: GLUT_DOUBLE do |g| timer = proc do g.rotate(1) g.redraw g.timer(10, timer) end g.rotate = [0, 1, 1] g.display = proc do g.color(1, 0, 1) glutWireTeapot(0.5) end g.timer(10, timer) end
マウスに四角がついていきます。
Opal.app title: "Mouse Move", width: 640, height: 480, buffering: GLUT_DOUBLE do |g| mouseX = mouseY = 0 g.display = proc do g.color(1, 1, 0) glPushMatrix glTranslatef(mouseX, mouseY, 0) glRectf(0, 0, 20, 20) glPopMatrix end g.reshape = proc {|w, h| glOrtho(0, w, h, 0, -1, 1)} g.passive_motion = proc do |x, y| mouseX = x mouseY = y g.redraw end end
ぐるぐる回転させてみる。だいぶコードが短くなったと思います。
gl = nil vertex = [[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0], [1, 0, 1], [0, 0, 1], [0, 1, 1], [1, 1, 1]] edge = [[0, 1], [1, 2], [2, 3], [3, 0], [4, 5], [5, 6], [6, 7], [7, 4], [0, 5], [1, 4], [2, 7], [3, 6]].flatten vertex = vertex.map {|v| [v[0] - 0.5, v[1] - 0.5, v[2] - 0.5]}.flatten timer = proc do gl.rotate(10) gl.redraw gl.timer(100, timer) end init = proc do glEnable(GL_BLEND) glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) gl.clr_matrix gluLookAt(3, 4, 5, 0, 0, 0, 0, 1, 0) end Opal.app title: "3d" do |g| gl = g init.call g.rotate = [0, 1, 0] g.display = proc do g.color(0, 1, 0) g.draw_elements(GL_LINES, 3, vertex, edge) glFlush end g.reshape = proc {|w, h| gluPerspective(30, w / h.to_f, 3, 10)} g.timer(100, timer) end
正四面体の面に色をつけて回転。隠面消去とダブルバッファリングとカリングを行っています。
gl = nil A = Math.sqrt(3) / 2 vertex = [[0, A / 2, 0], [-0.5, -A / 2, 0], [0.5, -A / 2, 0], [0, 0, A]] face = [[0, 1, 2], [0, 3, 2], [0, 1, 3], [1, 2, 3]] color = [[1, 1, 1], [1, 0, 0], [0, 1, 0], [0, 0, 1]] display = proc do gl.draw(GL_TRIANGLES) do face.each_with_index do |ar, j| gl.color(color[j][0], color[j][1], color[j][2]) ar.each {|i| glVertex3dv(vertex[i])} end end end timer = proc do gl.rotate(15) gl.redraw gl.timer(100, timer) end init = proc do glEnable(GL_DEPTH_TEST) #深度テストは有効にする(隠面消去を有効にする) gl.clr_matrix gluLookAt(4, 0, 1, 0, 0, A / 2, 0, 0, 1) end Opal.app title: "3d", buffering: GLUT_DOUBLE, depth: GLUT_DEPTH, culling: true do |g| gl = g init.call g.rotate = [0, 0, 1] g.display = display g.reshape = proc {|w, h| gluPerspective(30, w / h.to_f, 3, 10)} g.timer(100, timer) end
init = proc do .. の部分だけ下のように変えると、(色の)混合処理ができます。つまり、後ろが透けて見えます。
init = proc do glEnable(GL_BLEND) #混合処理を可能にします glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) gl.color_alpha = 0.8 #アルファ値(透け具合) glEnable(GL_DEPTH_TEST) gl.clr_matrix gluLookAt(4, 0, 1, 0, 0, A / 2, 0, 0, 1) end
全般的に下を参考にしました。
http://www.wakayama-u.ac.jp/~wuhy/GSS/index.html
キー入力によって空間内を移動します。常に原点方向を向きます。f で前進、b で後退、l で(原点を中心に)左回転、r で右回転です。
gl = nil MM = 3; MS = 0.25 r = 3; theta = 0; step = 0.05 x = z = 0 vertex = [[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0], [1, 0, 1], [0, 0, 1], [0, 1, 1], [1, 1, 1]] edge = [[0, 1], [1, 2], [2, 3], [3, 0], [4, 5], [5, 6], [6, 7], [7, 4], [0, 5], [1, 4], [2, 7], [3, 6]].flatten vertex = vertex.map {|v| [v[0] - 0.5, v[1] - 0.5, v[2] - 0.5]}.flatten display = proc do gl.color(1, 0, 0) (-MM).step(MM, MS) do |i| gl.line(i, -0.5, MM, i, -0.5, -MM) gl.line(MM, -0.5, i, -MM, -0.5, i) end gl.color(0, 1, 0) gl.draw_elements(GL_LINES, 3, vertex, edge) end pers = proc do gluLookAt(r * cos(theta), 0, r * sin(theta), 0, 0, 0, 0, 1, 0) end keyboard = proc do |key| case key when "f" r -= step r += step if r < 0.5 when "b" r += step when "l" theta += step when "r" theta -= step when " " exit end gl.clr_matrix pers.call gl.redraw end Opal.app width: 500, height: 500, buffering: GLUT_DOUBLE do |g| gl = g g.perspective g.clr_matrix pers.call g.display = display g.keyboard = keyboard end