OpenGL 3D gears demo for Ruby

A

Arto Bendiken

------=_Part_967_24960416.1114927351434
Content-Type: text/plain; charset=ISO-8859-1
Content-Transfer-Encoding: quoted-printable
Content-Disposition: inline

Hi there,

I'm a recent convert from Python, PHP et al., and aside from more
serious web-related development with Rails, have been playing a bit
with Yoshi's OpenGL/GLUT bindings. I was interested to see how
ruby-opengl performance would compare with C/C++, so I wrote a Ruby
version of the (in)famous spinning 3D gear wheels demo program, which
is often used as a simple benchmark for OpenGL.

(Kudos to Yoshi for the OpenGL bindings: translating from the original
C code was straightforward and only took an hour or so to finish.)

The results were a bit surprising, in a good way. On my Linux computer
with ATI Radeon 9700, the compiled version of the original gears.c
gives an average 1271 FPS, whereas the gears.rb version achieves an
average of 1203 FPS. (Both programs were run for a couple of minutes
and results averaged.) That's only a 6% gap in performance.

Granted this is a fairly simple OpenGL app, but it's certainly easy to
imagine how productive programmers of open-source games, simulations
etc. could be writing Ruby instead of C/C++ as is the norm.

I've attached the Ruby file gears.rb. It only requires the ruby-opengl
extension to run. The code was translated from rev 1.8 of the
GLUT-based gears.c (not the same as glxgears!) included in the Mesa 3D
software package, keeping the structure close to the original, but
wrapping it in a Ruby class. I also added some simple code to do mouse
rotation. If anyone wishes to compare performance with the original
version, it can downloaded from:
http://cvs.freedesktop.org/mesa/Mesa/progs/demos/gears.c

Arto Bendiken

------=_Part_967_24960416.1114927351434
Content-Type: text/plain; name=gears.rb; charset=us-ascii
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="gears.rb"

#!/usr/bin/env ruby

# 3-D gear wheels. This program is in the public domain.
#
# Command line options:
# -info print GL implementation information
# -exit automatically exit after 30 seconds
#
# 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'
require '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

GL.ShadeModel(GL::FLAT)

GL.Normal(0.0, 0.0, 1.0)

# Draw front face
GL.Begin(GL::QUAD_STRIP)
for i in 0..teeth
angle = i * 2.0 * PI / teeth
GL.Vertex3f(r0 * cos(angle), r0 * sin(angle), width * 0.5)
GL.Vertex3f(r1 * cos(angle), r1 * sin(angle), width * 0.5)
if i < teeth
GL.Vertex3f(r0 * cos(angle), r0 * sin(angle), width * 0.5)
GL.Vertex3f(r1 * cos(angle + 3 * da),
r1 * sin(angle + 3 * da), width * 0.5)
end
end
GL.End()

# Draw front sides of teeth
GL.Begin(GL::QUADS)
for i in 0...teeth
angle = i * 2.0 * PI / teeth
GL.Vertex3f(r1 * cos(angle), r1 * sin(angle), width * 0.5)
GL.Vertex3f(r2 * cos(angle + da), r2 * sin(angle + da), width * 0.5)
GL.Vertex3f(r2 * cos(angle + 2 * da),
r2 * sin(angle + 2 * da), width * 0.5)
GL.Vertex3f(r1 * cos(angle + 3 * da),
r1 * sin(angle + 3 * da), width * 0.5)
end
GL.End()

GL.Normal(0.0, 0.0, -1.0)

# Draw back face
GL.Begin(GL::QUAD_STRIP)
for i in 0..teeth
angle = i * 2.0 * PI / teeth
GL.Vertex3f(r1 * cos(angle), r1 * sin(angle), -width * 0.5)
GL.Vertex3f(r0 * cos(angle), r0 * sin(angle), -width * 0.5)
if i < teeth
GL.Vertex3f(r1 * cos(angle + 3 * da),
r1 * sin(angle + 3 * da), -width * 0.5)
GL.Vertex3f(r0 * cos(angle), r0 * sin(angle), -width * 0.5)
end
end
GL.End()

# Draw back sides of teeth
GL.Begin(GL::QUADS)
for i in 0...teeth
angle = i * 2.0 * PI / teeth
GL.Vertex3f(r1 * cos(angle + 3 * da),
r1 * sin(angle + 3 * da), -width * 0.5)
GL.Vertex3f(r2 * cos(angle + 2 * da),
r2 * sin(angle + 2 * da), -width * 0.5)
GL.Vertex3f(r2 * cos(angle + da), r2 * sin(angle + da), -width * 0.5)
GL.Vertex3f(r1 * cos(angle), r1 * sin(angle), -width * 0.5)
end
GL.End()

# Draw outward faces of teeth
GL.Begin(GL::QUAD_STRIP)
for i in 0...teeth
angle = i * 2.0 * PI / teeth
GL.Vertex3f(r1 * cos(angle), r1 * sin(angle), width * 0.5)
GL.Vertex3f(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
GL.Normal(v, -u, 0.0)
GL.Vertex3f(r2 * cos(angle + da), r2 * sin(angle + da), width * 0.5)
GL.Vertex3f(r2 * cos(angle + da), r2 * sin(angle + da), -width * 0.5)
GL.Normal(cos(angle), sin(angle), 0.0)
GL.Vertex3f(r2 * cos(angle + 2 * da),
r2 * sin(angle + 2 * da), width * 0.5)
GL.Vertex3f(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)
GL.Normal(v, -u, 0.0)
GL.Vertex3f(r1 * cos(angle + 3 * da),
r1 * sin(angle + 3 * da), width * 0.5)
GL.Vertex3f(r1 * cos(angle + 3 * da),
r1 * sin(angle + 3 * da), -width * 0.5)
GL.Normal(cos(angle), sin(angle), 0.0)
end
GL.Vertex3f(r1 * cos(0), r1 * sin(0), width * 0.5)
GL.Vertex3f(r1 * cos(0), r1 * sin(0), -width * 0.5)
GL.End()

GL.ShadeModel(GL::SMOOTH)

# Draw inside radius cylinder
GL.Begin(GL::QUAD_STRIP)
for i in 0..teeth
angle = i * 2.0 * PI / teeth
GL.Normal(-cos(angle), -sin(angle), 0.0)
GL.Vertex3f(r0 * cos(angle), r0 * sin(angle), -width * 0.5)
GL.Vertex3f(r0 * cos(angle), r0 * sin(angle), width * 0.5)
end
GL.End()
end

def draw
GL.Clear(GL::COLOR_BUFFER_BIT | GL::DEPTH_BUFFER_BIT);

GL.PushMatrix()
GL.Rotate(@view_rotx, 1.0, 0.0, 0.0)
GL.Rotate(@view_roty, 0.0, 1.0, 0.0)
GL.Rotate(@view_rotz, 0.0, 0.0, 1.0)

GL.PushMatrix()
GL.Translate(-3.0, -2.0, 0.0)
GL.Rotate(@angle, 0.0, 0.0, 1.0)
GL.CallList(@gear1)
GL.PopMatrix()

GL.PushMatrix()
GL.Translate(3.1, -2.0, 0.0)
GL.Rotate(-2.0 * @angle - 9.0, 0.0, 0.0, 1.0)
GL.CallList(@gear2)
GL.PopMatrix()

GL.PushMatrix()
GL.Translate(-3.1, 4.2, 0.0)
GL.Rotate(-2.0 * @angle - 25.0, 0.0, 0.0, 1.0)
GL.CallList(@gear3)
GL.PopMatrix()

GL.PopMatrix()

GLUT.SwapBuffers()

@frames = 0 if not defined? @frames
@t0 = 0 if not defined? @t0

@frames += 1
t = GLUT.Get(GLUT::ELAPSED_TIME)
if t - @t0 >= 5000
seconds = (t - @t0) / 1000.0
fps = @frames / seconds
printf("%d frames in %6.3f seconds = %6.3f FPS\n",
@frames, seconds, fps)
@t0, @frames = t, 0
exit if defined? @autoexit and t >= 999.0 * @autoexit
end
end

def idle
t = GLUT.Get(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
GLUT.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
GLUT.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
GLUT.PostRedisplay()
end

# New window size or exposure
def reshape(width, height)
h = height.to_f / width.to_f
GL.Viewport(0, 0, width, height)
GL.MatrixMode(GL::pROJECTION)
GL.LoadIdentity()
GL.Frustum(-1.0, 1.0, -h, h, 5.0, 60.0)
GL.MatrixMode(GL::MODELVIEW)
GL.LoadIdentity()
GL.Translate(0.0, 0.0, -40.0)
end

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

GL.Lightfv(GL::LIGHT0, GL::pOSITION, POS)
GL.Enable(GL::CULL_FACE)
GL.Enable(GL::LIGHTING)
GL.Enable(GL::LIGHT0)
GL.Enable(GL::DEPTH_TEST)

# Make the gears
@gear1 = GL.GenLists(1)
GL.NewList(@gear1, GL::COMPILE)
GL.Material(GL::FRONT, GL::AMBIENT_AND_DIFFUSE, RED)
gear(1.0, 4.0, 1.0, 20, 0.7)
GL.EndList()

@gear2 = GL.GenLists(1)
GL.NewList(@gear2, GL::COMPILE)
GL.Material(GL::FRONT, GL::AMBIENT_AND_DIFFUSE, GREEN)
gear(0.5, 2.0, 2.0, 10, 0.7)
GL.EndList()

@gear3 = GL.GenLists(1)
GL.NewList(@gear3, GL::COMPILE)
GL.Material(GL::FRONT, GL::AMBIENT_AND_DIFFUSE, BLUE)
gear(1.3, 2.0, 0.5, 10, 0.7)
GL.EndList()

GL.Enable(GL::NORMALIZE)

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

def visible(vis)
GLUT.IdleFunc((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
GLUT.Init()
GLUT.InitDisplayMode(GLUT::RGB | GLUT::DEPTH | GLUT::DOUBLE)

GLUT.InitWindowPosition(0, 0)
GLUT.InitWindowSize(300, 300)
GLUT.CreateWindow('Gears')
init()

GLUT.DisplayFunc(method:)draw).to_proc)
GLUT.ReshapeFunc(method:)reshape).to_proc)
GLUT.KeyboardFunc(method:)key).to_proc)
GLUT.SpecialFunc(method:)special).to_proc)
GLUT.VisibilityFunc(method:)visible).to_proc)
GLUT.MouseFunc(method:)mouse).to_proc)
GLUT.MotionFunc(method:)motion).to_proc)
end

def start
GLUT.MainLoop()
end

end

Gears.new.start




------=_Part_967_24960416.1114927351434--
 
I

Ilmari Heikkinen

su, 2005-05-01 kello 09:02, Arto Bendiken kirjoitti:
Hi there,
The results were a bit surprising, in a good way. On my Linux computer
with ATI Radeon 9700, the compiled version of the original gears.c
gives an average 1271 FPS, whereas the gears.rb version achieves an
average of 1203 FPS. (Both programs were run for a couple of minutes
and results averaged.) That's only a 6% gap in performance.

Hello,
The reason the Ruby version runs fast is because the gears-program uses
display lists and hence the draw function is pretty much limited just by
the speed of OpenGL's display list drawing efficiency.

I did a couple of experiments comparing drawing the gears with and
without display lists and got the following results:

C, using display lists: 3321FPS
Ruby, using display lists: 3033FPS

C, without display lists: 2417FPS
Ruby, without display lists: 90FPS

The 27x speed difference sounds about right..

The gear function could probably be optimized by minimizing the amount
of Ruby math there and doing as much as possible with GL.Translate and
GL.Rotate, since the GPU does those (and fast!).

It's definitely possible to use Ruby for writing GL apps, though good
performance requires doing things in a way that uses GL's
performance-helping things as much as possible (HW T&L, vertex arrays,
display lists, or even writing inner loops and math-intensive parts in
C).


Anxiously waiting for the fast VM(s), :)
Ilmari
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,744
Messages
2,569,483
Members
44,901
Latest member
Noble71S45

Latest Threads

Top