The Little Grasshopper

Archive for the ‘OpenCTM’ tag

PEZ: A Teeny Tiny GLUT Alternative

without comments

Pez enables cross-platform development of extremely simple OpenGL apps. It’s not really a library since it’s so small. Instead of linking it in, just plop one of these files into your project:

You’ll also need a single header file:

Like GLUT, Pez lets you quickly build simple OpenGL apps that will run on any platform. Unlike GLUT, it never makes any OpenGL calls on your behalf. This makes it easy to write demos that conform to the forward-compatible profile in OpenGL 3.0+.

Pez doesn’t require you to register callbacks like GLUT does. Instead, your program simply provides definitions for four functions:

// receive window size and return window title
const char* PezInitialize(int width, int height);

// draw scene (Pez swaps the backbuffer for you)
void PezRender();

// receive elapsed time (e.g., update physics)
void PezUpdate(unsigned int milliseconds);

// handle mouse action: PEZ_DOWN, PEZ_UP, or PEZ_MOVE
void PezHandleMouse(int x, int y, int action);    

That’s it! Pez defines main() (or WinMain on Microsoft platforms) and runs a game loop for you. Additionally, Pez defines a handful of utility functions that serve as alternatives to printf, exceptions, and asserts:

void PezDebugString(const char* pStr, ...);
void PezDebugStringW(const wchar_t* pStr, ...);
void PezFatalError(const char* pStr, ...);
void PezFatalErrorW(const wchar_t* pStr, ...);
#define PezCheckCondition(A, B) if (!(A)) { PezFatalError(B); }
#define PezCheckConditionW(A, B) if (!(A)) { PezFatalErrorW(B); }

Pez Uniform

On Windows, strings get sent to the debugger, so use the VS debugger window or dbgview to see them.

Pez doesn’t handle keyboard stuff and resizable windows. If you want to do anything fancy, just edit the Pez source and treat it as a starting point. Pez is written in ANSI C and released under the MIT License.

The only outside library that Pez depends on is GLEW, which is also so tiny that you can plop it down directly into your project. Usage of glew is especially nice on Windows platforms, since it lets you avoid including the huge windows.h header, and it loads up function pointers on your behalf. Pez calls glewInit() for you.

GLEW is included in the downloadable Pez package at the end of the post, along with a few other super-light libraries such as GLSW and pnglite. Together, these libraries form the “Pez Ecosystem”, which I’ll discuss later. You don’t need to use the entire ecosystem to use Pez; GLEW is the only required library.

You can configure some aspects of Pez (most importantly, window size) by editing pez.h and modifying one of these constants:

#define PEZ_VIEWPORT_WIDTH 853
#define PEZ_VIEWPORT_HEIGHT 480
#define PEZ_ENABLE_MULTISAMPLING 1
#define PEZ_VERTICAL_SYNC 1

Simple Example

Here’s a little flashlight app that oscillates the background color and becomes bright yellow when pressing a mouse button. Note that it includes glew.h rather than gl.h.

#include <pez.h>
#include <glew.h>
#include <math.h>

static float elapsed = 0;
static int pressing = 0;
static float speed = 0.01f;

const char* PezInitialize(int width, int height)
{
    return "Pez Intro";
}

void PezRender()
{
    if (pressing) {
        glClearColor(1, 1, 0.75f, 1);
    } else {
        float blue = 0.5f * (1.0f + sinf(elapsed));
        glClearColor(0, 0.25f, blue, 1);
    }
    glClear(GL_COLOR_BUFFER_BIT);
}

void PezUpdate(unsigned int elapsedMilliseconds)
{
    elapsed += elapsedMilliseconds * speed;
}

void PezHandleMouse(int x, int y, int action)
{
    if (action == PEZ_DOWN)
        pressing = 1;
    else if (action == PEZ_UP)
        pressing = 0;
}

Hello Triangle

Next let’s try something a bit more complex:

Pez Triangle

Here are the source files for this demo:

Since we’re using modern OpenGL, even a simple task like this requires the usage of shaders. First let’s write the effect file:

-- Vertex.GL3

in vec2 Position;
in vec3 InColor;
out vec3 OutColor;

void main()
{
    OutColor = InColor;
    gl_Position = vec4(Position, 0, 1);
}

-- Fragment.GL3

in vec3 OutColor;
out vec4 FragColor;

void main()
{
    FragColor = vec4(OutColor, 1);
}

The gray ‘--’ section dividers are not legal in the shading language, but they get parsed out by GLSW. Let’s move on to the C code:

#include <pez.h>
#include <glew.h>
#include <glsw.h>

static void BuildGeometry();
static void LoadEffect();

enum { PositionSlot, ColorSlot };

void PezHandleMouse(int x, int y, int action) { }

void PezUpdate(unsigned int elapsedMilliseconds) { }

void PezRender()
{
    glClear(GL_COLOR_BUFFER_BIT);
    glDrawArrays(GL_TRIANGLES, 0, 3);
}

const char* PezInitialize(int width, int height)
{
    BuildGeometry();
    LoadEffect();
    return "Pez Intro";
}

Note that I used a static qualifier on the forward declarations of BuildGeometry() and LoadEffect(); this emphasizes the fact that they’re private to this file.

Let’s go ahead and implement BuildGeometry(), which creates a VBO for the triangle and enables a couple vertex attributes (position and color):

static void BuildGeometry()
{
    float verts[] = {
        -0.5f, -0.5f,  1,1,0, // Yellow
        +0.0f, +0.5f,  1,0,1, // Magenta
        +0.5f, -0.5f,  0,1,1, // Cyan
    };

    GLuint vboHandle;
    GLsizeiptr vboSize = sizeof(verts);
    GLsizei stride = 5 * sizeof(float);
    GLenum usage = GL_STATIC_DRAW;
    GLvoid* colorOffset = (GLvoid*) (sizeof(float) * 2);

    glGenBuffers(1, &vboHandle);
    glBindBuffer(GL_ARRAY_BUFFER, vboHandle);
    glBufferData(GL_ARRAY_BUFFER, vboSize, verts, usage);
    glVertexAttribPointer(PositionSlot, 2, GL_FLOAT, GL_FALSE, stride, 0);
    glVertexAttribPointer(ColorSlot, 3, GL_FLOAT, GL_FALSE, stride, colorOffset);
    glEnableVertexAttribArray(PositionSlot);
    glEnableVertexAttribArray(ColorSlot);
}

Next we’ll write the code that fetches a pair of shaders from GLSW, compiles them, binds the color and position slots, and links them together to form a program object:

static void LoadEffect()
{
    const char* vsSource, * fsSource;
    GLuint vsHandle, fsHandle;
    GLint compileSuccess, linkSuccess;
    GLchar compilerSpew[256];
    GLuint programHandle;

    glswInit();
    glswSetPath("../demo/", ".glsl");
    glswAddDirectiveToken("GL3", "#version 130");

    vsSource = glswGetShader("Simple.Vertex." PEZ_GL_VERSION_TOKEN);
    fsSource = glswGetShader("Simple.Fragment." PEZ_GL_VERSION_TOKEN);
    PezCheckCondition(vsSource, "Can't find vertex shader.\n");
    PezCheckCondition(fsSource, "Can't find fragment shader.\n");

    vsHandle = glCreateShader(GL_VERTEX_SHADER);
    fsHandle = glCreateShader(GL_FRAGMENT_SHADER);
    
    glShaderSource(vsHandle, 1, &vsSource, 0);
    glCompileShader(vsHandle);
    glGetShaderiv(vsHandle, GL_COMPILE_STATUS, &compileSuccess);
    glGetShaderInfoLog(vsHandle, sizeof(compilerSpew), 0, compilerSpew);
    PezCheckCondition(compileSuccess, compilerSpew);

    glShaderSource(fsHandle, 1, &fsSource, 0);
    glCompileShader(fsHandle);
    glGetShaderiv(fsHandle, GL_COMPILE_STATUS, &compileSuccess);
    glGetShaderInfoLog(fsHandle, sizeof(compilerSpew), 0, compilerSpew);
    PezCheckCondition(compileSuccess, compilerSpew);

    programHandle = glCreateProgram();
    glAttachShader(programHandle, vsHandle);
    glAttachShader(programHandle, fsHandle);
    glBindAttribLocation(programHandle, PositionSlot, "Position");
    glBindAttribLocation(programHandle, ColorSlot, "Color");
    glLinkProgram(programHandle);
    glGetProgramiv(programHandle, GL_LINK_STATUS, &linkSuccess);
    glGetProgramInfoLog(programHandle, sizeof(compilerSpew), 0, compilerSpew);
    PezCheckCondition(linkSuccess, compilerSpew);

    glUseProgram(programHandle);
}

That’s it!

Ghandi Texture

Next we’ll write a tiny Pez program that loads a PNG texture and draws it to the screen:

Pez Texture

Here are the source files for this demo:

Aside from Pez itself, so far we’ve pulled in two other libraries to facilitate our sample code: GLEW and GLSW. We’re slowly forming the “Pez Ecosystem” that I’ll cover later. We now need a library to help us load textures from an external file. I like pnglite since it’s yet another “one C file, one H file” solution. Here’s how to use pnglite to load in our Gandhi texture:

static void LoadTexture()
{
    png_t tex;
    unsigned char* data;
    GLuint textureHandle;

    png_init(0, 0);
    png_open_file_read(&tex, "../demo/Gandhi.png");
    data = (unsigned char*) malloc(tex.width * tex.height * tex.bpp);
    png_get_data(&tex, data);

    glGenTextures(1, &textureHandle);
    glBindTexture(GL_TEXTURE_2D, textureHandle);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, tex.width, tex.height, 0,
                 GL_RGB, GL_UNSIGNED_BYTE, data);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

    png_close_file(&tex);
    free(data);
}

The above code assumes that the PNG image is RGB only (does not contain alpha). I don’t advise hardcoding stuff like this in production code, but we’re just listing out some simple examples. Here’s the effect file:

-- Vertex.Textured.GL3

in vec2 Position;
in vec2 InCoord;
out vec2 OutCoord;

void main()
{
    OutCoord = InCoord;
    gl_Position = vec4(Position, 0, 1);
}

-- Fragment.Textured.GL3

in vec2 OutCoord;
out vec4 FragColor;
uniform sampler2D Sampler;

void main()
{
    FragColor = texture(Sampler, OutCoord);
}

The C source looks like this:

#include <pez.h>
#include <glew.h>
#include <glsw.h>
#include <pnglite.h>
#include <stdlib.h>

static void BuildGeometry();
static void LoadEffect();
static void LoadTexture();

enum { PositionSlot, TexCoordSlot };

void PezHandleMouse(int x, int y, int action) { }

void PezUpdate(unsigned int elapsedMilliseconds) { }

void PezRender()
{
    glClear(GL_COLOR_BUFFER_BIT);
    glDrawArrays(GL_TRIANGLES, 0, 6);
}

const char* PezInitialize(int width, int height)
{
    BuildGeometry();
    LoadEffect();
    LoadTexture();
    return "Pez Intro";
}

static void LoadTexture()
{
    // ...see previous listing for implementation...
}

static void BuildGeometry()
{
    #define X (0.5f * PEZ_VIEWPORT_HEIGHT / PEZ_VIEWPORT_WIDTH)
    #define Y (0.5f)
    float verts[] = {
        -X, -Y,  0,1,
        -X, +Y,  0,0,
        +X, +Y,  1,0,
        +X, +Y,  1,0,
        +X, -Y,  1,1,
        -X, -Y,  0,1,
    };
    #undef X
    #undef Y

    GLuint vboHandle;
    GLsizeiptr vboSize = sizeof(verts);
    GLsizei stride = 4 * sizeof(float);
    GLenum usage = GL_STATIC_DRAW;
    GLvoid* texCoordOffset = (GLvoid*) (sizeof(float) * 2);

    glGenBuffers(1, &vboHandle);
    glBindBuffer(GL_ARRAY_BUFFER, vboHandle);
    glBufferData(GL_ARRAY_BUFFER, vboSize, verts, usage);
    glVertexAttribPointer(PositionSlot, 2, GL_FLOAT, GL_FALSE, stride, 0);
    glVertexAttribPointer(TexCoordSlot, 2, GL_FLOAT, GL_FALSE, stride, texCoordOffset);
    glEnableVertexAttribArray(PositionSlot);
    glEnableVertexAttribArray(TexCoordSlot);
}

static void LoadEffect()
{
    const char* vsSource, * fsSource;
    GLuint vsHandle, fsHandle;
    GLint compileSuccess, linkSuccess;
    GLchar compilerSpew[256];
    GLuint programHandle;

    glswInit();
    glswSetPath("../demo/", ".glsl");
    glswAddDirectiveToken("GL3", "#version 130");

    vsSource = glswGetShader("Simple.Vertex.Textured." PEZ_GL_VERSION_TOKEN);
    fsSource = glswGetShader("Simple.Fragment.Textured." PEZ_GL_VERSION_TOKEN);
    PezCheckCondition(vsSource, "Can't find vertex shader.\n");
    PezCheckCondition(fsSource, "Can't find fragment shader.\n");

    vsHandle = glCreateShader(GL_VERTEX_SHADER);
    fsHandle = glCreateShader(GL_FRAGMENT_SHADER);
    
    glShaderSource(vsHandle, 1, &vsSource, 0);
    glCompileShader(vsHandle);
    glGetShaderiv(vsHandle, GL_COMPILE_STATUS, &compileSuccess);
    glGetShaderInfoLog(vsHandle, sizeof(compilerSpew), 0, compilerSpew);
    PezCheckCondition(compileSuccess, compilerSpew);

    glShaderSource(fsHandle, 1, &fsSource, 0);
    glCompileShader(fsHandle);
    glGetShaderiv(fsHandle, GL_COMPILE_STATUS, &compileSuccess);
    glGetShaderInfoLog(fsHandle, sizeof(compilerSpew), 0, compilerSpew);
    PezCheckCondition(compileSuccess, compilerSpew);

    programHandle = glCreateProgram();
    glAttachShader(programHandle, vsHandle);
    glAttachShader(programHandle, fsHandle);
    glBindAttribLocation(programHandle, PositionSlot, "Position");
    glBindAttribLocation(programHandle, TexCoordSlot, "InCoord");
    glLinkProgram(programHandle);
    glGetProgramiv(programHandle, GL_LINK_STATUS, &linkSuccess);
    glGetProgramInfoLog(programHandle, sizeof(compilerSpew), 0, compilerSpew);
    PezCheckCondition(linkSuccess, compilerSpew);

    glUseProgram(programHandle);
}

That’s it for the texture viewer!

Mesh Viewer

Next let’s light up some artistic 3D content and spin it around:

Pez Mesh

For this demo, we’ll switch from ANSI C to C++. Here are the source files:

For the per-pixel lighting effect, see my other blog post. For loading the “headless giant” mesh, we’ll use OpenCTM, which supports good compression and has a nice & simple API. Here’s how we’ll use it to load the model and create a VBO:

static void LoadMesh()
{
    RenderContext& rc = GlobalRenderContext;

    // Open the CTM file:
    CTMcontext ctmContext = ctmNewContext(CTM_IMPORT);
    ctmLoad(ctmContext, "../demo/HeadlessGiant.ctm");
    PezCheckCondition(ctmGetError(ctmContext) == CTM_NONE, "OpenCTM Issue");
    CTMuint vertexCount = ctmGetInteger(ctmContext, CTM_VERTEX_COUNT);
    rc.IndexCount = 3 * ctmGetInteger(ctmContext, CTM_TRIANGLE_COUNT);

    // Create the VBO for positions:
    const CTMfloat* positions = ctmGetFloatArray(ctmContext, CTM_VERTICES);
    if (positions) {
        GLuint handle;
        GLsizeiptr size = vertexCount * sizeof(float) * 3;
        glGenBuffers(1, &handle);
        glBindBuffer(GL_ARRAY_BUFFER, handle);
        glBufferData(GL_ARRAY_BUFFER, size, positions, GL_STATIC_DRAW);
        glEnableVertexAttribArray(SlotPosition);
        glVertexAttribPointer(SlotPosition, 3, GL_FLOAT, GL_FALSE, sizeof(float) * 3, 0);
    }

    // Create the VBO for normals:
    const CTMfloat* normals = ctmGetFloatArray(ctmContext, CTM_NORMALS);
    if (normals) {
        GLuint handle;
        GLsizeiptr size = vertexCount * sizeof(float) * 3;
        glGenBuffers(1, &handle);
        glBindBuffer(GL_ARRAY_BUFFER, handle);
        glBufferData(GL_ARRAY_BUFFER, size, normals, GL_STATIC_DRAW);
        glEnableVertexAttribArray(SlotNormal);
        glVertexAttribPointer(SlotNormal, 3, GL_FLOAT, GL_FALSE, sizeof(float) * 3, 0);
    }

    // Create the VBO for indices:
    const CTMuint* indices = ctmGetIntegerArray(ctmContext, CTM_INDICES);
    if (indices) {
        GLuint handle;
        GLsizeiptr size = rc.IndexCount * sizeof(CTMuint);
        glGenBuffers(1, &handle);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, handle);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, size, indices, GL_STATIC_DRAW);
    }

    ctmFreeContext(ctmContext);
}

The other lightweight library we’ll pull in for this example is the Sony Vector Library, which has nice APIs for C and C++, and supports the various matrix operations from old-school OpenGL that are no longer in the core specification. For example, here’s how you’d use their C++ API to compute a projection matrix from a viewing frustum:

Matrix4 projection = Matrix4::frustum(left, right, bottom, top, zNear, zFar);

The C++ source code for this demo is fairly straightforward. Here it is:

#include <pez.h>
#include <vectormath_aos.h>
#include <glsw.h>
#include <glew.h>
#include <openctm.h>

using namespace Vectormath::Aos;

enum { SlotPosition, SlotNormal };

struct ShaderUniforms {
    GLuint Projection;
    GLuint Modelview;
    GLuint NormalMatrix;
};

struct RenderContext {
    GLuint EffectHandle;
    ShaderUniforms EffectUniforms;
    Matrix4 Projection;
    Matrix4 Modelview;
    Matrix3 NormalMatrix;
    float PackedNormalMatrix[9];
    float Theta;
    CTMuint IndexCount;
};

static RenderContext GlobalRenderContext;
static GLuint BuildShader(const char* source, GLenum shaderType);
static GLuint BuildProgram(const char* vsKey, const char* fsKey);
static void LoadMesh();
static void LoadEffect();

void PezHandleMouse(int x, int y, int action) { }

const char* PezInitialize(int width, int height)
{
    LoadMesh();
    LoadEffect();
    glEnable(GL_DEPTH_TEST);
    return "OpenCTM Viewer";
}

void PezRender()
{
    RenderContext& rc = GlobalRenderContext;
    glClearColor(0, 0.25f, 0.5f, 1);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glUniformMatrix4fv(rc.EffectUniforms.Projection, 1, 0, &rc.Projection[0][0]);
    glUniformMatrix4fv(rc.EffectUniforms.Modelview, 1, 0, &rc.Modelview[0][0]);
    glUniformMatrix3fv(rc.EffectUniforms.NormalMatrix, 1, 0, &rc.PackedNormalMatrix[0]);
    glDrawElements(GL_TRIANGLES, rc.IndexCount, GL_UNSIGNED_INT, 0);
}

void PezUpdate(unsigned int elapsedMilliseconds)
{
    RenderContext& rc = GlobalRenderContext;

    rc.Theta += elapsedMilliseconds * 0.05f;

    Matrix4 rotation = Matrix4::rotationY(rc.Theta * Pi / 180.0f);
    Matrix4 translation = Matrix4::translation(Vector3(0, 0, -50));
    rc.Modelview = translation * rotation;
    rc.NormalMatrix = rc.Modelview.getUpper3x3();

    for (int i = 0; i < 9; ++i)
        rc.PackedNormalMatrix[i] = rc.NormalMatrix[i / 3][i % 3];

    const float x = 0.6f;
    const float y = x * PEZ_VIEWPORT_HEIGHT / PEZ_VIEWPORT_WIDTH;
    const float left = -x, right = x;
    const float bottom = -y, top = y;
    const float zNear = 4, zFar = 100;
    rc.Projection = Matrix4::frustum(left, right, bottom, top, zNear, zFar);
}

static void LoadMesh()
{
    // ...see previous listing for implementation...
}

static GLuint BuildShader(const char* source, GLenum shaderType)
{
    // ...similar to previous listings...
}

static GLuint BuildProgram(const char* vsKey, const char* fsKey)
{
    // ...similar to previous listings...
}

static void LoadEffect()
{
    RenderContext& rc = GlobalRenderContext;

    glswInit();
    glswSetPath("../demo/", ".glsl");
    glswAddDirectiveToken("GL3", "#version 130");

    const char* vsKey = "PixelLighting.Vertex." PEZ_GL_VERSION_TOKEN;
    const char* fsKey = "PixelLighting.Fragment." PEZ_GL_VERSION_TOKEN;

    rc.EffectHandle = BuildProgram(vsKey, fsKey);
    rc.EffectUniforms.Projection = glGetUniformLocation(rc.EffectHandle, "Projection");
    rc.EffectUniforms.Modelview = glGetUniformLocation(rc.EffectHandle, "Modelview");
    rc.EffectUniforms.NormalMatrix = glGetUniformLocation(rc.EffectHandle, "NormalMatrix");
    rc.Theta = 0;

    glUseProgram(rc.EffectHandle);

    GLuint LightPosition = glGetUniformLocation(rc.EffectHandle, "LightPosition");
    GLuint AmbientMaterial = glGetUniformLocation(rc.EffectHandle, "AmbientMaterial");
    GLuint DiffuseMaterial = glGetUniformLocation(rc.EffectHandle, "DiffuseMaterial");
    GLuint SpecularMaterial = glGetUniformLocation(rc.EffectHandle, "SpecularMaterial");
    GLuint Shininess = glGetUniformLocation(rc.EffectHandle, "Shininess");

    glUniform3f(DiffuseMaterial, 0.75, 0.75, 0.5);
    glUniform3f(AmbientMaterial, 0.04f, 0.04f, 0.04f);
    glUniform3f(SpecularMaterial, 0.5, 0.5, 0.5);
    glUniform1f(Shininess, 50);
    glUniform3f(LightPosition, 0.25, 0.25, 1);
}

Pez Ecosystem

While developing these tutorials, I’ve selected a variety of libraries to help us along. OpenGL is a low-level API and it (sensibly) doesn’t attempt to do things like decode image files and whatnot.

I’ve made sure that all these libraries have fairly liberal licenses (specifically, they’re not LGPL) and that they’re all very tiny. Here’s the complete list:

  • Pez itself
  • OpenGL Extension Wrangler
  • OpenGL Shader Wrangler (uses bstrlib)
  • pnglite (uses zlib)
  • OpenCTM Mesh Format (uses lzma)
  • Sony Vector Library (extracted from Bullet)

Downloads

You can get a package that has all the demos on this page and the complete Pez ecosystem. It uses CMake for the build system. CMake is easy to use and very popular (Blender, KDE, and Boost use it). It’s similar to autoconf in that it can generate makefiles, but it can also generate actual project files which you can open with IDE’s like Visual Studio and Xcode.

Or, you can just download the Pez files individually and drop them into whatever build system you’re using:

Have fun! Remember, Pez is designed only for very, very tiny OpenGL programs. Don’t try to use it to build a CAD package or bring about world peace.

Written by Philip Rideout

May 16th, 2010 at 4:40 pm

Posted in OpenGL

Tagged with ,

X to OpenCTM Converter

without comments

CTM Ship

The OpenCTM file format for compressed triangle meshes looks very promising. To celebrate my gigasecond birthday, I wrote a .x to .ctm converter that exports positions and normals. It currently discards texture coordinates, but it shouldn’t be hard to extend it. I can’t guarantee that this tool is super robust, but it worked well enough for the test cases that I ran across.

I simply used ID3DXMesh to load in the .x file and the OpenCTM API to write out the .ctm file. Unfortunately, the usage of ID3DXMesh makes this tool Windows-only.

It’s a command line tool that works like this:

Usage: x2ctm [sourcefile] [sourcefile] ...

To form a destination filename, it simply replaces the existing file extension with .ctm and preserves the folder path.

At a high level, here’s how it works: (if you want, skip to the downloads section)

void ExportCTM(ID3DXMesh* pMesh, const char* destFile)
{
    // 1. Find where the positions, normals, and texture coordinates live.
    // 2. Check that we support the format of the data.
    // 3. Obtain vertex & index counts from the D3D mesh; allocate memory for the CTM mesh.
    // 4. Lock down the verts and pluck out the positions and normals.
    // 5. Lock down the indices and convert them to unsigned 32-bit integers.
    // 6. Instance the OpenCTM mesh and save it to disk.
    // 7. Free the OpenCTM buffers.
}

Here’s the actual code for the above summary:

void ExportCTM(ID3DXMesh* pMesh, const char* destFile)
{
    // Find where the positions, normals, and texture coordinates live.

    const WORD MISSING_ATTRIBUTE = 0xffff;
    WORD positionsOffset = MISSING_ATTRIBUTE;
    WORD normalsOffset = MISSING_ATTRIBUTE;
    WORD texCoordsOffset = MISSING_ATTRIBUTE;

    D3DVERTEXELEMENT9 vertexLayout[MAX_FVF_DECL_SIZE];
    D3DVERTEXELEMENT9 endMarker = D3DDECL_END();
    pMesh->GetDeclaration(vertexLayout);
    D3DVERTEXELEMENT9* pVertexLayout = &vertexLayout[0];
    for (int attrib = 0;  attrib < MAX_FVF_DECL_SIZE; ++attrib, pVertexLayout++)
    {
        if (0 == memcmp(&vertexLayout[attrib], &endMarker, sizeof(endMarker)))
        {
            break;
        }
        if (pVertexLayout->Stream != 0)
        {
            continue;
        }
        if (pVertexLayout->Usage == D3DDECLUSAGE_POSITION &&
            pVertexLayout->Type == D3DDECLTYPE_FLOAT3)
        {
            cout << "Contains positions." << endl;
            positionsOffset = pVertexLayout->Offset;
        }
        if (pVertexLayout->Usage == D3DDECLUSAGE_NORMAL &&
            pVertexLayout->Type == D3DDECLTYPE_FLOAT3)
        {
            cout << "Contains normals." << endl;
            normalsOffset = pVertexLayout->Offset;
        }
        if (pVertexLayout->Usage == D3DDECLUSAGE_TEXCOORD &&
            pVertexLayout->Type == D3DDECLTYPE_FLOAT2)
        {
            cout << "Contains texture coordinates." << endl;
            texCoordsOffset = pVertexLayout->Offset;
        }
    }

    // Check that we support the format of the data.

    if (positionsOffset == MISSING_ATTRIBUTE)
    {
        cerr << "Unsupported Vertex Declaration in " << destFile << endl;
        exit(1);
    }

    // Obtain vertex & index counts from the D3D mesh; allocate memory for the CTM mesh.
    DWORD dwVertexCount = pMesh->GetNumVertices();
    DWORD dwTriangleCount = pMesh->GetNumFaces();
    DWORD dwIndexCount = dwTriangleCount * 3;
    CTMfloat* pCtmVertices = new CTMfloat[3* dwVertexCount];
    CTMuint* pCtmIndices = new CTMuint[dwIndexCount];
    CTMfloat* pCtmNormals = (normalsOffset == 0xffff) ? 0 : new CTMfloat[3 * dwVertexCount];

    // Lock down the verts and pluck out the positions and normals.
    {
        void* pData = 0;
        pMesh->LockVertexBuffer(D3DLOCK_READONLY , &pData);

        if (positionsOffset != MISSING_ATTRIBUTE)
        {
            unsigned char* pSource = ((unsigned char*) pData) + positionsOffset;
            unsigned char* pDest = (unsigned char* ) pCtmVertices;
            DWORD dwSourceStride = pMesh->GetNumBytesPerVertex();
            DWORD dwDestStride = sizeof(CTMfloat) * 3;
    
            for (DWORD dwVertex = 0; dwVertex < dwVertexCount; ++dwVertex)
            {
                memcpy(pDest, pSource, dwDestStride);
                pSource += dwSourceStride;
                pDest += dwDestStride;
            }
        }

        if (normalsOffset != MISSING_ATTRIBUTE)
        {
            unsigned char* pSource = ((unsigned char*) pData) + normalsOffset;
            unsigned char* pDest = (unsigned char* ) pCtmNormals;
            DWORD dwSourceStride = pMesh->GetNumBytesPerVertex();
            DWORD dwDestStride = sizeof(CTMfloat) * 3;
    
            for (DWORD dwVertex = 0; dwVertex < dwVertexCount; ++dwVertex)
            {
                memcpy(pDest, pSource, dwDestStride);
                pSource += dwSourceStride;
                pDest += dwDestStride;
            }
        }

        pMesh->UnlockVertexBuffer();
    }

    // Lock down the indices and convert them to unsigned 32-bit integers.
    {
        void* pData = 0;
        pMesh->LockIndexBuffer(D3DLOCK_READONLY , &pData);

        DWORD dwOptions = pMesh->GetOptions();
        DWORD dwSourceStride = (dwOptions & D3DXMESH_32BIT) ? 4 : 2;

        unsigned char* pSource = (unsigned char*) pData;
        unsigned char* pDest = (unsigned char*) pCtmIndices;
        
        DWORD dwDestStride = sizeof(CTMuint);
    
        for (DWORD dwIndex = 0; dwIndex < dwIndexCount; ++dwIndex)
        {
            memset(pDest, 0, dwDestStride);
            memcpy(pDest, pSource, dwSourceStride);
            pSource += dwSourceStride;
            pDest += dwDestStride;
        }

        pMesh->UnlockIndexBuffer();
    }

    // Instance the OpenCTM mesh and save it to disk.
    CTMcontext context = ctmNewContext(CTM_EXPORT);
    ctmDefineMesh(context,
        pCtmVertices,
        dwVertexCount,
        pCtmIndices,
        dwTriangleCount,
        pCtmNormals);
    ctmSave(context, destFile);
    ctmFreeContext(context);

    // Free the OpenCTM buffers.
    delete [] pCtmVertices;
    delete [] pCtmIndices;
    delete [] pCtmNormals;
}

Downloads

I wrote this tool with Visual Studio 2010. The code is covered by the WTFPL license.

Written by Philip Rideout

May 9th, 2010 at 6:43 pm

Posted in DirectX

Tagged with , ,