Opengl Tk widget for Ruby?

I'm would like to create a unified Ruby on Windows XP that will combine
user interface elements of Ruby/Tk and OpenGL. Using Google I've seen
several year old references to a Togl Tk widget that seemed to wrap
OpenGL as a Tk widget, but all the links to it appear to be dead and I
can't find any references that it ever worked on Windows.

I'm have looked into FxRuby as it has OpenGL support built in, but
unfortunately aspects of my user interface are tied to the Tk Canvas
object which provides needed 2D editing functionality for my
application. Is it possible to wrap the Tk canvase within FxRuby? I
assume not.

I would appreciate it if someone provide insight into if it is indeed
possible to wrap an OpenGL view as a Tk widget in Ruby.

I guess my fallback is to create a single application that manages two
windows, one being the OpenGL and the other being Ruby/Tk.

--Mike

Message-ID: <1158021828.394689.117290@h48g2000cwc.googlegroups.com>

I'm would like to create a unified Ruby on Windows XP that will combine
user interface elements of Ruby/Tk and OpenGL. Using Google I've seen
several year old references to a Togl Tk widget that seemed to wrap
OpenGL as a Tk widget, but all the links to it appear to be dead and I
can't find any references that it ever worked on Windows.

There is a section "Windows 95/NT/2000/XP usage" on
<http://togl.sourceforge.net/&gt;\.
However, Togl seems to be integrated into Tcl3D <http://www.tcl3d.org/&gt;\.

BTW, Ruby/Tk can handle almost all of Tcl/Tk extensions.
If there is no Ruby/Tk's wrapper for a Tcl/Tk extension,
you can send Tcl commands to handle the extension with
Tk.tk_call(...tokens_of_Tcl_command_line...), and so on.

I think that it is not difficult to write your own wrapper.
For example, simple Togl wrapper is

···

From: "mpthompson at gmail.com" <mpthompson@gmail.com>
Subject: Opengl Tk widget for Ruby?
Date: Tue, 12 Sep 2006 09:45:34 +0900
-----------------------------------------------------------
#
# IMPORTANT: NOT TESTED. So, this may not work properly.
#
TkPackage.require('Togl')

class Tk::Togl < TkWindow
  # By default, the constracter of a widget class (subclass of TkWindow)
  # uses TkCommandNames[0] as a command to create a widget object.
  TkCommandNames = ['togl'.freeze].freeze

  # The following definitions are used to create a widget object
  # from a widget path string.
  WidgetClassName = 'Togl'.freeze
  WidgetClassNames[WidgetClassName] = self

  def render
    tk_send_without_enc('render')
    self
  end

  def swapbuffers
    tk_send_without_enc('swapbuffers')
    self
  end

  def make_current
    tk_send_without_enc('makecurrent')
    self
  end
end

# f = TkFrame.new
# togl_obj1 = Tk::Togl.new(f, :width=>200, :height=>200, :ident=>'Single',
# :rgba=>true, :double=>false, :depth=>true).pack
# togl_obj2 = Tk::Togl.new(f, :width=>200, :height=>200, :ident=>'Double',
# :sharelist=>'Single',
# :rgba=>true, :double=>true, :depth=>true).pack
# togl_obj1.bind('B1-Motion', proc{....})
-----------------------------------------------------------
--
Hidetoshi NAGAI (nagai@ai.kyutech.ac.jp)

Hidetoshi NAGAI wrote:

There is a section "Windows 95/NT/2000/XP usage" on
<http://togl.sourceforge.net/&gt;\.
However, Togl seems to be integrated into Tcl3D <http://www.tcl3d.org/&gt;\.

BTW, Ruby/Tk can handle almost all of Tcl/Tk extensions.
If there is no Ruby/Tk's wrapper for a Tcl/Tk extension,
you can send Tcl commands to handle the extension with
Tk.tk_call(...tokens_of_Tcl_command_line...), and so on.

Thank you for the information. I'm actually pretty new to Ruby and
know very little about the how it interacts with OpenGL or Tk under the
hood. Writing my own wrappers looks to be a bit daunting, but I guess
I may have to go in that direction.

I did try and use Ruby SDL to accomplish my goal as I saw in a very old
example in this news group from 2001 that showed affixing SDL to a
TkFrame and the SDL displaying OpenGL graphics. However, I ran into an
issue with SDL_WINDOWID not working correctly because of underlying
issues with putenv/getenv with Windows when SDL is run from a DLL.

--Mike

mpthompson at gmail.com wrote:

Hidetoshi NAGAI wrote:
> There is a section "Windows 95/NT/2000/XP usage" on
> <http://togl.sourceforge.net/&gt;\.
> However, Togl seems to be integrated into Tcl3D <http://www.tcl3d.org/&gt;\.

I think I'm getting close to getting OpenGL and Tk playing nice in
Ruby, but I'm not quite there yet. Any help would be appreciated.

Based on the suggestion above I was able to get the Tcl3D installed
with ActiveTcl 8.4 and get the Tcl based OpenGL demos distributed with
Tcl3D working fine on my system. Next, I used Hidetoshi's code as a
guide to create a simple wrapper, after a bit of fussing to determine
the right way to get the tcl3d dll loaded, I created wrapper for togl
within Tcl3D as shown below which includes an addition for
postredisplay:

···

---------------------------------------------------------
#
# Togl wrapper for tcl3d Tk extension.
#
TkPackage.require('tcl3d')

class Tk::Togl < TkWindow
  # By default, the constracter of a widget class (subclass of
TkWindow)
  # uses TkCommandNames[0] as a command to create a widget object.
  TkCommandNames = ['togl'.freeze].freeze

  # The following definitions are used to create a widget object
  # from a widget path string.
  WidgetClassName = 'Togl'.freeze
  WidgetClassNames[WidgetClassName] = self

  def postredisplay
    tk_send_without_enc('postredisplay')
    self
  end

  def render
    tk_send_without_enc('render')
    self
  end

  def swapbuffers
    tk_send_without_enc('swapbuffers')
    self
  end

  def make_current
    tk_send_without_enc('makecurrent')
    self
  end
end

# f = TkFrame.new
# togl_obj1 = Tk::Togl.new(f, :width=>200, :height=>200,
:ident=>'Single',
# :rgba=>true, :double=>false,
:depth=>true).pack
# togl_obj2 = Tk::Togl.new(f, :width=>200, :height=>200,
:ident=>'Double',
# :sharelist=>'Single',
# :rgba=>true, :double=>true,
:depth=>true).pack
# togl_obj1.bind('B1-Motion', proc{....})
---------------------------------------------------------

Now I'm trying to get the standard gears.rb OpenGL application running
within the tk managed window. This is where I'm really getting lost. I
posted below the application as it looks now, but it isn't working and
the window just comes up blank. I assume I'm missing something
fundamental here. I'm hoping someone could either provide a working
example of a Tk/OpenGL application using togl or help steer me in where
my code is failing below.

Thanks,

Mike Thompson

---------------------------------------------------------
# 2005-05-01 Ruby version by Arto Bendiken based on gears.c rev 1.8.
# 2005-01-09 Original C version (gears.c) by Brian Paul et al.
# http://cvs.freedesktop.org/mesa/Mesa/progs/demos/gears.c?rev=1.8

require 'opengl_c'
require 'glut_c'
require 'tk'
require 'tkextlib/tkHTML'
require 'togl'

include OpenGL
include Glut

class Gears

  POS = [5.0, 5.0, 10.0, 0.0]
  RED = [0.8, 0.1, 0.0, 1.0]
  GREEN = [0.0, 0.8, 0.2, 1.0]
  BLUE = [0.2, 0.2, 1.0, 1.0]

  include Math

  # Draw a gear wheel. You'll probably want to call this function when
  # building a display list since we do a lot of trig here.
  #
  # Input: inner_radius - radius of hole at center
  # outer_radius - radius at center of teeth
  # width - width of gear
  # teeth - number of teeth
  # tooth_depth - depth of tooth
  def gear(inner_radius, outer_radius, width, teeth, tooth_depth)
    r0 = inner_radius
    r1 = outer_radius - tooth_depth / 2.0
    r2 = outer_radius + tooth_depth / 2.0

    da = 2.0 * PI / teeth / 4.0

    glShadeModel(GL_FLAT)

    glNormal(0.0, 0.0, 1.0)

    # Draw front face
    glBegin(GL_QUAD_STRIP)
    for i in 0..teeth
      angle = i * 2.0 * PI / teeth
      glVertex3f(r0 * cos(angle), r0 * sin(angle), width * 0.5)
      glVertex3f(r1 * cos(angle), r1 * sin(angle), width * 0.5)
      if i < teeth
        glVertex3f(r0 * cos(angle), r0 * sin(angle), width * 0.5)
        glVertex3f(r1 * cos(angle + 3 * da),
          r1 * sin(angle + 3 * da), width * 0.5)
      end
    end
    glEnd()

    # Draw front sides of teeth
    glBegin(GL_QUADS)
    for i in 0...teeth
      angle = i * 2.0 * PI / teeth
      glVertex3f(r1 * cos(angle), r1 * sin(angle), width * 0.5)
      glVertex3f(r2 * cos(angle + da), r2 * sin(angle + da), width *
0.5)
      glVertex3f(r2 * cos(angle + 2 * da),
        r2 * sin(angle + 2 * da), width * 0.5)
      glVertex3f(r1 * cos(angle + 3 * da),
        r1 * sin(angle + 3 * da), width * 0.5)
    end
    glEnd()

    glNormal(0.0, 0.0, -1.0)

    # Draw back face
    glBegin(GL_QUAD_STRIP)
    for i in 0..teeth
      angle = i * 2.0 * PI / teeth
      glVertex3f(r1 * cos(angle), r1 * sin(angle), -width * 0.5)
      glVertex3f(r0 * cos(angle), r0 * sin(angle), -width * 0.5)
      if i < teeth
        glVertex3f(r1 * cos(angle + 3 * da),
          r1 * sin(angle + 3 * da), -width * 0.5)
        glVertex3f(r0 * cos(angle), r0 * sin(angle), -width * 0.5)
      end
    end
    glEnd()

    # Draw back sides of teeth
    glBegin(GL_QUADS)
    for i in 0...teeth
      angle = i * 2.0 * PI / teeth
      glVertex3f(r1 * cos(angle + 3 * da),
        r1 * sin(angle + 3 * da), -width * 0.5)
      glVertex3f(r2 * cos(angle + 2 * da),
        r2 * sin(angle + 2 * da), -width * 0.5)
      glVertex3f(r2 * cos(angle + da), r2 * sin(angle + da), -width *
0.5)
      glVertex3f(r1 * cos(angle), r1 * sin(angle), -width * 0.5)
    end
    glEnd()

    # Draw outward faces of teeth
    glBegin(GL_QUAD_STRIP)
    for i in 0...teeth
      angle = i * 2.0 * PI / teeth
      glVertex3f(r1 * cos(angle), r1 * sin(angle), width * 0.5)
      glVertex3f(r1 * cos(angle), r1 * sin(angle), -width * 0.5)
      u = r2 * cos(angle + da) - r1 * cos(angle)
      v = r2 * sin(angle + da) - r1 * sin(angle)
      len = sqrt(u * u + v * v)
      u /= len
      v /= len
      glNormal(v, -u, 0.0)
      glVertex3f(r2 * cos(angle + da), r2 * sin(angle + da), width *
0.5)
      glVertex3f(r2 * cos(angle + da), r2 * sin(angle + da), -width *
0.5)
      glNormal(cos(angle), sin(angle), 0.0)
      glVertex3f(r2 * cos(angle + 2 * da),
        r2 * sin(angle + 2 * da), width * 0.5)
      glVertex3f(r2 * cos(angle + 2 * da),
        r2 * sin(angle + 2 * da), -width * 0.5)
      u = r1 * cos(angle + 3 * da) - r2 * cos(angle + 2 * da)
      v = r1 * sin(angle + 3 * da) - r2 * sin(angle + 2 * da)
      glNormal(v, -u, 0.0)
      glVertex3f(r1 * cos(angle + 3 * da),
        r1 * sin(angle + 3 * da), width * 0.5)
      glVertex3f(r1 * cos(angle + 3 * da),
        r1 * sin(angle + 3 * da), -width * 0.5)
      glNormal(cos(angle), sin(angle), 0.0)
    end
    glVertex3f(r1 * cos(0), r1 * sin(0), width * 0.5)
    glVertex3f(r1 * cos(0), r1 * sin(0), -width * 0.5)
    glEnd()

    glShadeModel(GL_SMOOTH)

    # Draw inside radius cylinder
    glBegin(GL_QUAD_STRIP)
    for i in 0..teeth
      angle = i * 2.0 * PI / teeth
      glNormal(-cos(angle), -sin(angle), 0.0)
      glVertex3f(r0 * cos(angle), r0 * sin(angle), -width * 0.5)
      glVertex3f(r0 * cos(angle), r0 * sin(angle), width * 0.5)
    end
    glEnd()
  end

  def draw
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    glPushMatrix()
    glRotate(@view_rotx, 1.0, 0.0, 0.0)
    glRotate(@view_roty, 0.0, 1.0, 0.0)
    glRotate(@view_rotz, 0.0, 0.0, 1.0)

    glPushMatrix()
    glTranslate(-3.0, -2.0, 0.0)
    glRotate(@angle, 0.0, 0.0, 1.0)
    glCallList(@gear1)
    glPopMatrix()

    glPushMatrix()
    glTranslate(3.1, -2.0, 0.0)
    glRotate(-2.0 * @angle - 9.0, 0.0, 0.0, 1.0)
    glCallList(@gear2)
    glPopMatrix()

    glPushMatrix()
    glTranslate(-3.1, 4.2, 0.0)
    glRotate(-2.0 * @angle - 25.0, 0.0, 0.0, 1.0)
    glCallList(@gear3)
    glPopMatrix()

    glPopMatrix()

    @glwin.swapbuffers
  end

  def idle
    #t = glutGet(GLUT_ELAPSED_TIME) / 1000.0
    #@t0_idle = t if !defined? @t0_idle
    # 90 degrees per second
    #@angle += 70.0 * (t - @t0_idle)
    #@t0_idle = t
    #@glwin.postredisplay
  end

  # Change view angle, exit upon ESC
  def key(k, x, y)
    case k
      when ?z
        @view_rotz += 5.0
      when ?Z
        @view_rotz -= 5.0
      when 27 # Escape
        exit
    end
    @glwin.postredisplay
  end

  # Change view angle
  def special(k, x, y)
    case k
      when GLUT_KEY_UP
        @view_rotx += 5.0
      when GLUT_KEY_DOWN
        @view_rotx -= 5.0
      when GLUT_KEY_LEFT
        @view_roty += 5.0
      when GLUT_KEY_RIGHT
        @view_roty -= 5.0
    end
    @glwin.postredisplay
  end

  # New window size or exposure
  def reshape(width, height)
    h = height.to_f / width.to_f
    glViewport(0, 0, width, height)
    glMatrixMode(GL_PROJECTION)
    glLoadIdentity()
    glFrustum(-1.0, 1.0, -h, h, 5.0, 60.0)
    glMatrixMode(GL_MODELVIEW)
    glLoadIdentity()
    glTranslate(0.0, 0.0, -40.0)
  end

  def init
    @angle = 0.0
    @view_rotx, @view_roty, @view_rotz = 20.0, 30.0, 0.0

    glLightfv(GL_LIGHT0, GL_POSITION, POS)
    glEnable(GL_CULL_FACE)
    glEnable(GL_LIGHTING)
    glEnable(GL_LIGHT0)
    glEnable(GL_DEPTH_TEST)

    # Make the gears
    @gear1 = glGenLists(1)
    glNewList(@gear1, GL_COMPILE)
    glMaterial(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, RED)
    gear(1.0, 4.0, 1.0, 20, 0.7)
    glEndList()

    @gear2 = glGenLists(1)
    glNewList(@gear2, GL_COMPILE)
    glMaterial(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, GREEN)
    gear(0.5, 2.0, 2.0, 10, 0.7)
    glEndList()

    @gear3 = glGenLists(1)
    glNewList(@gear3, GL_COMPILE)
    glMaterial(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, BLUE)
    gear(1.3, 2.0, 0.5, 10, 0.7)
    glEndList()

    glEnable(GL_NORMALIZE)

    ARGV.each do |arg|
      case arg
        when '-info'
          printf("GL_RENDERER = %s\n", glGetString(GL_RENDERER))
          printf("GL_VERSION = %s\n", glGetString(GL_VERSION))
          printf("GL_VENDOR = %s\n", glGetString(GL_VENDOR))
          printf("GL_EXTENSIONS = %s\n", glGetString(GL_EXTENSIONS))
        when '-exit'
          @autoexit = 30
          printf("Auto Exit after %i seconds.\n", @autoexit);
      end
    end
  end

  def visible(vis)
    #glutIdleFunc((vis == GLUT_VISIBLE ? method(:idle).to_proc : nil))
  end

  def mouse(button, state, x, y)
    @mouse = state
    @x0, @y0 = x, y
  end

  def motion(x, y)
    if @mouse == GLUT_DOWN then
      @view_roty += @x0 - x
      @view_rotx += @y0 - y
    end
    @x0, @y0 = x, y
  end

  def initialize
    @root = TkRoot.new
    @frame = TkFrame.new(@root)
    @glwin = Tk::Togl.new(@frame, :width=>300, :height=>300,
:ident=>'Single',
                          :rgba=>true, :depth=>true,
:double=>false).pack('fill'=>'both')
    init()
  end

  def start
    Tk.mainloop
  end

end

Gears.new.start
---------------------------------------------------------

OK, I think I got getting Ruby Tk and OpenGL figured out -- at least
for the most part. For posterity and the hope it will be useful to
someone else I'll go through the steps of how I got it working below.

Step 1. I installed ActiveTcl 8.4 <http://www.activestate.com/> and
Tcl3D <http://www.tcl3d.org/> installed on my system under the
directory 'C:\Ruby\Tcl'. I then made sure the Tcl3D demos ran fine to
confirm OpenGL was properly working.

Step 2. I added the following lines to my setup.rb file in
'C:\Ruby\lib\ruby\1.8\tkextlib'

···

---------------------------
Tk::AUTO_PATH.list <<= 'C:/Ruby/Tcl/lib/tcl3d0.3.1/tcl3dCg'
Tk::AUTO_PATH.list <<= 'C:/Ruby/Tcl/lib/tcl3d0.3.1/tcl3dFTGL'
Tk::AUTO_PATH.list <<= 'C:/Ruby/Tcl/lib/tcl3d0.3.1/tcl3dGauges'
Tk::AUTO_PATH.list <<= 'C:/Ruby/Tcl/lib/tcl3d0.3.1/tcl3dGl2ps'
Tk::AUTO_PATH.list <<= 'C:/Ruby/Tcl/lib/tcl3d0.3.1/tcl3dOde'
Tk::AUTO_PATH.list <<= 'C:/Ruby/Tcl/lib/tcl3d0.3.1/tcl3dOgl'
Tk::AUTO_PATH.list <<= 'C:/Ruby/Tcl/lib/tcl3d0.3.1/tcl3dSDL'
Tk::AUTO_PATH.list <<= 'C:/Ruby/Tcl/lib/tcl3d0.3.1/tcl3dTogl'
Tk::AUTO_PATH.list <<= 'C:/Ruby/Tcl/lib/tcl3d0.3.1/tcl3dUtil'
---------------------------

Step 3. I created a togl wrapper class in 'togl.rb' as suggested by
Hidetoshi NAGAI as shown below:

---------------------------
#
# Togl wrapper for tcl3d Tk extension.
#
TkPackage.require('tcl3d')

class Tk::Togl < TkWindow
  TkCommandNames = ['togl'.freeze].freeze
  WidgetClassName = 'Togl'.freeze
  WidgetClassNames[WidgetClassName] = self

  def render
    tk_send_without_enc('render')
    self
  end

  def swapbuffers
    tk_send_without_enc('swapbuffers')
    self
  end

  def make_current
    tk_send_without_enc('makecurrent')
    self
  end

  def post_redisplay
    tk_send_without_enc('postredisplay')
    self
  end
end
---------------------------

Step 4. I create a simple 'togltest.rb' application to test OpenGL
working within Tk togl widget:

---------------------------
require 'tk'
require 'togl'
require 'opengl'

def create(toglwin)
  light_diffuse = [1.0, 0.0, 0.0, 1.0]
  light_position = [1.0, 1.0, 1.0, 0.0]

  GL::Light(GL::LIGHT0, GL::DIFFUSE, light_diffuse);
  GL::Light(GL::LIGHT0, GL::POSITION, light_position);
  GL::Enable(GL::LIGHT0);
  GL::Enable(GL::LIGHTING);
  GL::Enable(GL::DEPTH_TEST);

  GL::MatrixMode(GL::PROJECTION);
  GLU::Perspective(40.0, 1.0, 1.0, 10.0);
  GL::MatrixMode(GL::MODELVIEW);
  GLU::LookAt(0.0, 0.0, 5.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0)

  GL::Translate(0.0, 0.0, -1.0);
  GL::Rotate(60, 1.0, 0.0, 0.0);
  GL::Rotate(-20, 0.0, 0.0, 1.0);
end

def reshape(toglwin, width, height)
  width = width.to_i
  height = height.to_i
  if $glwin
    $glwin.post_redisplay
  end
end

def display(toglwin)
  n = [
    [-1.0, 0.0, 0.0], [0.0, 1.0, 0.0],
    [1.0, 0.0, 0.0], [0.0, -1.0, 0.0],
    [0.0, 0.0, 1.0], [0.0, 0.0, -1.0]
  ]
  faces = [
    [0, 1, 2, 3], [3, 2, 6, 7], [7, 6, 5, 4],
    [4, 5, 1, 0], [5, 6, 2, 1], [7, 4, 0, 3]
  ]
  v = [
    [-1, -1,1], [-1, -1,-1],
    [-1,1,-1], [-1,1,1],
    [1, -1,1], [1, -1,-1],
    [1, 1,-1], [1,1,1]
  ]

  GL::Clear(GL::COLOR_BUFFER_BIT | GL::DEPTH_BUFFER_BIT);
  for i in (0..5)
    GL::Begin(GL::QUADS);
    GL::Normal(*(n[i]));
    GL::Vertex(v[faces[i][0]]);
    GL::Vertex(v[faces[i][1]]);
    GL::Vertex(v[faces[i][2]]);
    GL::Vertex(v[faces[i][3]]);
    GL::End()
  end

  $glwin.swapbuffers
end

$root = TkRoot.new
$frame = TkFrame.new($root).pack('fill'=>'both', 'padx'=>10,
'pady'=>10)
$glwin = Tk::Togl.new($frame, 'width'=>300, 'height'=>300,
'ident'=>'Single',
                      'rgba'=>true, 'depth'=>true, 'double'=>true,
                      'createproc'=>proc {|toglwin| create(toglwin)},
                      'displayproc'=>proc {|toglwin| display(toglwin)},
                      'reshapeproc'=>proc {|toglwin, width, height|
reshape(toglwin, width, height)}
                      ).pack('fill'=>'both')

Tk.mainloop
---------------------------

That's it. When you run the Ruby Tk application a window with an
OpenGL 3D red block should appear.

I hope someone else finds this information useful.

Mike Thompson