Sunday, May 29, 2011

Reloaded

Yes, it has been quite a while since the last post, but Sgine is anything but inactive. Pretty soon after that last post a decision was made to add support for Android to the core architecture of Sgine. As you might imagine this was no small undertaking and required some massive changes. So here we are several months later and Sgine has been re-designed from the ground up and I wanted to take a minute to discuss some of those changes.

First, there is a new OpenGL abstraction layer. No, this isn't abstracting you from the details of OpenGL, but rather the OpenGL implementation. Obviously there's a great reason for this decision, and here's where we come back to our inclusion of Android. Android uses OpenGL ES and LWJGL (our desktop rendering platform) uses standard OpenGL. Now, in order to write applications that will run on the desktop and on Android we need a point of convergence to build from and that point is OpenGL. Unfortunately though they are incredibly similar, they are still very different in many ways. This required a unification layer and ultimately a code generator to dynamically link between Android GLES and LWJGL. I'd love to say at this point it's 100% finished, but honestly I expect there's still going to be modifications happening to this layer for a while.

The result of this work is the ability to import org.sgine.opengl.GL._ and get access to a consistent OpenGL layer that will work on desktop and handheld alike. What does this look like? Well, quite simple really. The following is a recreation of Lesson 2 of the NeHe tutorial in Sgine:
class Lesson02 extends GLDisplay {
  private val triangleVertices = Array(0.0f, 1.0f, 0.0f,
                                       -1.0f, -1.0f, 0.0f,
                                       1.0f, -1.0f, 0.0f)
  private var triangleVertexBuffer: FloatBuffer = _

  private val quadVertices = Array(-1.0f, -1.0f, 0.0f,
                                   1.0f, -1.0f, 0.0f,
                                   -1.0f, 1.0f, 0.0f,
                                   1.0f, 1.0f, 0.0f)
  private var quadVertexBuffer: FloatBuffer = _

  def create() = {
    glShadeModel(GL_SMOOTH)
    glClearColor(0.0f, 0.0f, 0.0f, 0.0f)
    glClearDepth(1.0f)
    glEnable(GL_DEPTH_TEST)
    glDepthFunc(GL_LEQUAL)

    glMatrixMode(GL_PROJECTION)
    glLoadIdentity()

    // Create Triangle
    var bb = ByteBuffer.allocateDirect(triangleVertices.length * 4)
    bb.order(ByteOrder.nativeOrder())
    triangleVertexBuffer = bb.asFloatBuffer()
    triangleVertexBuffer.put(triangleVertices)
    triangleVertexBuffer.position(0)

    // Create Quad
    bb = ByteBuffer.allocateDirect(quadVertices.length * 4)
    bb.order(ByteOrder.nativeOrder())
    quadVertexBuffer = bb.asFloatBuffer()
    quadVertexBuffer.put(quadVertices)
    quadVertexBuffer.position(0)
  }

  def resize(width: Int, height: Int) = {
    glViewport(0, 0, width, height)

    gluPerspective(45.0f, width.toFloat / height.toFloat, 0.1f, 100.0f)

    glMatrixMode(GL_MODELVIEW)
    glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST)
  }

  def render() = {
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
    glLoadIdentity()
    glTranslatef(-1.5f, 0.0f, -6.0f)

    glVertexPointer(3, GL_FLOAT, 0, triangleVertexBuffer)
    glEnableClientState(GL_VERTEX_ARRAY)
    glDrawArrays(GL_TRIANGLE_STRIP, 0, triangleVertices.length / 3)
    glDisableClientState(GL_VERTEX_ARRAY)

    glTranslatef(3.0f, 0.0f, 0.0f)
    glVertexPointer(3, GL_FLOAT, 0, quadVertexBuffer)
    glEnableClientState(GL_VERTEX_ARRAY)
    glDrawArrays(GL_TRIANGLE_STRIP, 0, quadVertices.length / 3)
    glDisableClientState(GL_VERTEX_ARRAY)
  }
}

object Lesson02 {
  def main(args: Array[String]): Unit = {
    val lesson02 = new Lesson02()
    val controller = LWJGLController(lesson02, 1024, 768, "NeHe Lesson 02")
  }
}

As you can see from the original tutorial the OpenGL code is surprisingly similar. It would have been almost identical if GLES supported fixed function pipeline (old-school GL).

Hopefully in the near future many more of the NeHe tutorials will be ported to Sgine as examples of how to do OpenGL coding using Sgine, but this is a good start. If you want to see the current status check out the org.sgine.opengl.nehe project.

This leads to the next major change in Sgine. The framework / engine was becoming too large for a single project and thanks to the awesomeness that is SBT (Simple Build Tool) the project has now been split into a multi-project that is built and published individually and with better dependency separation. This will allow projects to take advantages of parts of Sgine without having to utilize the entire framework.

Now, as might be obvious from previous posts I've made, I'm a big fan of simplicity and smaller more concise blocks of code. The above snippet for Lesson 2 had two major flaws:
  1. The number of lines of code is incredibly high to draw a triangle and quad to the screen.
  2. Having to directly use OpenGL seems incredibly tedious when you need to do even something as simple as drawing a triangle and quad to the screen.

This leads to the next layer of abstraction in Sgine. We now have a render abstraction that hides the details of the rendering platform to the developer. This means we can both plug in a different rendering platform later (can you say ray-tracing?) as well as reduce the complexity of standard rendering tasks in a well defined object oriented structure.

The following code is Lesson 2 using the new rendering framework:
object RendererTest extends RenderApplication {
  private val triangleMatrix = Matrix4.Identity.translate(x = -1.5, z = -6.0)
  private val triangleShape = Shape(Vertices.triangle())
  private val quadMatrix = Matrix4.Identity.translate(x = 1.5, z = -6.0)
  private val quadShape = Shape(Vertices.quad())

  def update() = {
  }

  def render() = {
    renderer.loadMatrix(triangleMatrix)
    triangleShape.render()
    renderer.loadMatrix(quadMatrix)
    quadShape.render()
  }

  def dispose() = {
    triangleShape.dispose()
    quadShape.dispose()
  }
}

I presume it's pretty obvious what this code is doing apart from the call to the Vertices methods. The Vertices triangle and quad methods have default args that define the size of the shape to be -1 to 1 both vertically and horizontally and generates the the triangle vertices to display.

For those of you who like pictures, it's not much, but here's a screenshot:


One final thing to note is that Sgine is going back to providing its own math library. Simplex3d has been great, but the needs of Sgine are moderately different from the goals of Simplex3d and for both performance and simplicity we are rolling our own.

The new rendering framework and math library are still in early development, but more is coming fast. Once the rendering framework is complete there will be one last layer of abstraction above that: the scenegraph. The scenegraph envisioned will be extremely similar to the scenegraph that Sgine 0.1 provided (pre-refactor), the major difference being performance improvements and relying the rendering framework relying on the OpenGL abstraction. The end goal is still to provide an extremely performant, yet easy to use 3d engine in Scala.

Stay tuned, more will be coming soon.

5 comments:

  1. Nice work, glad to hear development is still very much alive!

    Perhaps you could add a print-screen of the rendering output for the easily bored ;)

    ReplyDelete
  2. @eelco, updated to include a screenshot, I hope that helps. :)

    ReplyDelete
  3. hey!

    i'm also very happy to see that the project is progressing so fast.
    because of my overal busy i hvan't had the time to actually try the library yet (also because i've become so used to the processing api), but i'm quite excited to play with this in the near future!


    best,
    hansi.

    ReplyDelete
    Replies
    1. How is this Android support coming along? At this point in time, getting something out on Android that supports Scala, works well, and is easy to use, would be a big win.

      Delete
    2. Though technically Android support should just work (since I'm now building from libgdx) it's not currently the primary focus of my development work so it will likely take someone else that actually needs the functionality to verify it works.

      Delete

Scala Engine for high-performance interactive applications.