Tech/LxEngine/Tutorials/Tutorial 2

From Athile

Jump to:navigation, search
This page is a work-in-progress. It is not yet complete.
It may contain inaccurate, incorrect information. Use at your own risk.

Contents

Overview

The objective of this tutorial is to introduce the LxEngine Rasterizer subsystem. The direct OpenGL calls will be replaced with use of the higher-level Rasterizer subsystem - taking less code to get some useful graphics on the screen.


Concepts Introduced

  • Rasterizer subsystem
  • lambda functions

Prerequisites

  • This tutorial assumes the reader is reasonably comfortable with the material in Tutorial 1
  • This tutorial assumes a basic familiarity with computer graphics concepts, most regarding basic geometry and lighting concepts in rasterization

Results

If all goes well, the application should produce a red, green, and blue cube spinning around the origin. The tutorial can be exited at any time using the ESC key.

 

Setup

Headers

The first item to take note of is a change in headers. The OpenGL header GL/gl.h is no longer included, as no direct OpenGL calls will be made any more. The LxEngine Rasterizer subsystem will be used to handle the low-level rendering instead.

The Rasterizer subsystem is a "low-level, retained mode" rasterizer: namely it allows you to create cached objects representing pieces of geometry, materials, lights, and other rasterization state. A list of these retained objects is then sent every frame to the rasterizer to draw that particular frame. It is still considered "low-level" in that, while it provides mechanisms for multi-pass rendering and global scene rendering algorithms, these must be set up by the caller.

//===========================================================================//
//   H E A D E R S   &   D E C L A R A T I O N S 
//===========================================================================//
 
#include <lx0/lxengine.hpp>
#include <lx0/views/canvas.hpp>
#include <lx0/subsystem/rasterizer.hpp>

As noted in the previous tutorial, lx0/lxengine.hpp only includes the core functionality of LxEngine. Any optional components which may not be used in every application are included via a separate header file. In this case, two extension components are being used. The Canvas View implementation and now the Rasterizer subsystem.

Changes to Renderer

The entirety of the other changes to the code from the previous tutorial come in the Renderer class!

No other changes have been made. Let's look at the particulars.

Renderer::initialize()

As noted in the previous tutorial, the Renderer is derived from lx0::View::Component and has its virtual method initialize() called once the view context has been set up. In this case, it means once OpenGL is full set up.

This tutorial is going to use the Rasterizer subsystem which is headed up by one primary class: RasterizerGL. This class represents the primary rasterization and object creation mechanism.

protected:
    lx0::RasterizerGLPtr mspRasterizer;

As such, we add a shared_ptr to a rasterizer object as part of the Renderer class.

However, since the RasterizerGL object is based on OpenGL, it can't be properly initialized until OpenGL itself is set up properly. Therefore, we don't actually create the object until the Renderer:initialize() call is made. Let's look at the first couple lines of the Renderer::initialize() method:

    virtual void initialize(lx0::ViewPtr spView)
    {
        //
        // Initialize the rasterizer subsystem as soon as the OpenGL context is
        // available.
        //
        mspRasterizer.reset( new lx0::RasterizerGL );
        mspRasterizer->initialize();

There's not much to it. The header include introduced the RasterizerGL class into the lx0 namespace and now we create a new object of that class and initialize it once the Renderer is told to initialize itself.

Rasterizer subsystem

Conceptually, RasterizerGL is a fairly low-level rasterizer that takes a list of "items" and draws each one in the order it is given. What's an "item"? An item is a packet of enough geometry, material, and transformation information to properly make a draw call.

In addition to a list of objects to draw, the scene also needs a camera to specify where the scene is being drawn from.

Lastly, a scene usually will also need a set of lights. In this case, however, we're using a solid color shader that does not need any lighting information (it chooses the face colors based solely on the direction of the normal - a useful testing shader).

In summary, to draw a scene, we'll need:

Since the objective of this tutorial is to draw a single spinning cube, the "list" of items is only length one; so only one Item is needed. However, since we want the cube to spin, we'll need some way to track the current transformation and update it. We'll track it using a 4x4 matrix.

To handle the above, we'll add the following member variables to Renderer. An Item has pointers to its geometry, material, and transform, so there's no need to track those in the Renderer class directly. We do choose to track the transformation matrix however, since we'll be updating that continually to make the cube spin. Note that LxEngine uses the OpenGL Mathematics Library (GLM) for its matrix classes.

    lx0::CameraPtr       mspCamera;
    lx0::ItemPtr         mspItem;
    glm::mat4            mRotation;

Creating the Camera

Creating the camera is straightforward if you're familiar with OpenGL. The GLM library is used to compute a "lookAt" view matrix (in the style of gluLookAt()). This is then passed into the RasterizerGL object along with a field-of-view and near/far planes to create a camera with both view and projection transformation information.

        //
        // Create a camera
        // 
        glm::mat4 viewMatrix = glm::lookAt(glm::vec3(1, -2, 1.5f), glm::vec3(0, 0, 0), glm::vec3(0, 0, 1));
        mspCamera = mspRasterizer->createCamera(60.0f, 0.01f, 1000.0f, viewMatrix);

Now we have a camera specified for the scene.

Building geometry

LxEngine supports loading Blender .blend files directly, but we'll use a lower-level interface in this tutorial for demonstration purposes.

The RasterizerGL interface provides numerous means of creating chunks of geometry, however in this case, we'll simply specify the cube as a list of indexed quads. RasterizerGL will take care of all the OpenGL vertex array object creation and binding internally.

        //
        // Build the cube geometry
        //
        std::vector<glgeom::point3f> positions(8);
        positions[0] = glgeom::point3f(-.5f,-.5f,-.5f);
        positions[1] = glgeom::point3f( .5f,-.5f,-.5f);
        positions[2] = glgeom::point3f(-.5f, .5f,-.5f);
        positions[3] = glgeom::point3f( .5f, .5f,-.5f);
        positions[4] = glgeom::point3f(-.5f,-.5f, .5f);
        positions[5] = glgeom::point3f( .5f,-.5f, .5f);
        positions[6] = glgeom::point3f(-.5f, .5f, .5f);
        positions[7] = glgeom::point3f( .5f, .5f, .5f);
 
        std::vector<lx0::uint16> indices;
        indices.reserve(4 * 6);
        auto push_face = [&indices](lx0::uint8 i0, lx0::uint8 i1, lx0::uint8 i2, lx0::uint8 i3) {
            indices.push_back(i0);
            indices.push_back(i1);
            indices.push_back(i2);
            indices.push_back(i3);
        };
        push_face(0, 2, 3, 1);      // -Z face
        push_face(4, 5, 7, 6);      // +Z face
        push_face(0, 4, 6, 2);      // -X face
        push_face(1, 3, 7, 5);      // +X face
        push_face(0, 1, 5, 4);      // -Y face
        push_face(2, 6, 7, 3);      // +Y face
 
        //
        // Create indexed geometry
        // Channels such as normals, per-vertex color, uv coordinates, etc. are optional
        //
        lx0::GeometryPtr spCube = mspRasterizer->createQuadList(indices, positions);

lambda functions

Note in the above example the use of a C++0x lambda function for the push_face convenience function. LxEngine makes extensive use of lambda functions so it's good to be aware of how they work.

Creating the Item

The Item is the set of geometry, material, and transform. Let's create it.

The transform is created from the rotation matrix (which GLM initializes to identity in the constructor).

The material is specified via a pre-packaged fragmented shader that comes with the LxEngine distribution. (NOTE: This likely should be updated. This is a somewhat confusing and incomplete API.)

The geometry was created in the prior step, so now it's a matter of assigning that variable to the Item. Remember that LxEngine uses shared_ptr<>'s throughout the system: therefore, there's little worry about ownership conventions on most LxEngine objects.

        //
        // Build the cube renderable
        //
        mspItem.reset(new lx0::Item);
        mspItem->spTransform = mspRasterizer->createTransform(mRotation);
        mspItem->spMaterial = mspRasterizer->createMaterial("media2/shaders/glsl/fragment/normal.frag");
        mspItem->spGeometry = spCube;

Creation Complete

That wraps up the creation of all the resources needed:

Rendering

Now we most on to the render() function...

This tutorial is a pretty simple example. RasterizerGL is designed to handle multi-pass algorithms with graphics layering, various render state changes and overrides, and other more complicated features than a simple "just draw everything in order" loop. Therefore, to do a simply draw, there are a few extra steps required - but as you'll see in the next tutorial, these objects make it easy to accomplish more advanced effects.

Let's look at the render() function and examine piece by piece:

    virtual void render (void)	
    {
        lx0::RenderAlgorithm algorithm;
        algorithm.mClearColor = glgeom::color4f(0.1f, 0.3f, 0.8f, 1.0f);
 
        lx0::GlobalPass pass;
        pass.spCamera = mspCamera;
        algorithm.mPasses.push_back(pass);
 
        lx0::RenderList items;
        items.push_back(0, mspItem);
 
        mspRasterizer->beginScene(algorithm);
        for (auto it = items.begin(); it != items.end(); ++it)
        {
            mspRasterizer->rasterizeList(algorithm, it->second.list);
        }
        mspRasterizer->endScene();
    }

Let's start with RenderAlgorithm this is an object describing how to render the entire frame. In this case, all the defaults are used with the exception that we explicitly set what color to clear the color buffer to at the start of the frame.

A single frame may be composed of multiple passes; in our case, it's only a single pass over the scene. Therefore, we create a single GlobalPass object and add it to the RenderAlgorithm list of passes. The only option we set is the Camera to use for that pass.

Next, we set the list of Items to render in that frame. The first argument of "0" is the layer number. The layers are essentially 2D overlays that are stacked up together and rendered in order - in this case it really doesn't matter since only a single object is being drawn. Using multiple layers can be useful for Heads-Up-Displays and other overlay graphics, but can also be used to partition 3D geometry to create interesting effects.

Next, we start the frame. We loop over each layer and rasterize the list of Items in that layer.

Lastly, we call endScene to mark the end of the frame.

Rotating the Cube

Rotating the cube is trivial. We set up an update() implementation and use GLM to incrementally change the rotation applied to the Item. We then send a "redraw" event to force the render() method to be re-run.

    virtual void update (lx0::ViewPtr spView) 
    {
        mRotation = glm::rotate(mRotation, 1.0f, glm::vec3(0, 0, 1));
        mspItem->spTransform = mspRasterizer->createTransform(mRotation);
 
        spView->sendEvent("redraw");
    }

Conclusion

For those familiar with the basics of OpenGL and DirectX, there shouldn't be much suprising here. The Rasterizer subsystem implements a fairly standard rasterization process. The challenge is merely to get accustomed to API names and conventions to know how to quickly do what is desired.

The next tutorial gets more interesting as the Rasterizer system is used to provide interesting effects rather than simply demonstrating the basic setup of its use.

Appendix A: Feedback, Questions, & Issues

Please provide any feedback, questions, or issues on the forums.

LxEngine is in active development and in need of improvements. Suggestions for making it simpler to use are most welcome!

Navigation
Toolbox