m3g/m3gcore11/src/m3g_sprite.c
changeset 0 5d03bc08d59c
child 18 5e30ef2e26cb
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/m3g/m3gcore11/src/m3g_sprite.c	Tue Feb 02 01:47:50 2010 +0200
@@ -0,0 +1,1009 @@
+/*
+* Copyright (c) 2003 Nokia Corporation and/or its subsidiary(-ies).
+* All rights reserved.
+* This component and the accompanying materials are made available
+* under the terms of the License "Eclipse Public License v1.0"
+* which accompanies this distribution, and is available
+* at the URL "http://www.eclipse.org/legal/epl-v10.html".
+*
+* Initial Contributors:
+* Nokia Corporation - initial contribution.
+*
+* Contributors:
+*
+* Description: Sprite implementation
+*
+*/
+
+
+/*!
+ * \internal
+ * \file
+ * \brief Sprite implementation
+ */
+
+#ifndef M3G_CORE_INCLUDE
+#   error included by m3g_core.c; do not compile separately.
+#endif
+
+/*#include <stdio.h>*/
+
+#include "m3g_sprite.h"
+#include "m3g_appearance.h"
+#include "m3g_camera.h"
+#include "m3g_rendercontext.h"
+#include "m3g_renderqueue.h"
+
+#define FLIPX   1
+#define FLIPY   2
+
+
+/*----------------------------------------------------------------------
+ * Internal functions
+ *--------------------------------------------------------------------*/
+
+/*!
+ * \internal
+ * \brief Destroys this Sprite object.
+ *
+ * \param obj Sprite object
+ */
+static void m3gDestroySprite(Object *obj)
+{
+    Sprite *sprite = (Sprite *) obj;
+    M3G_VALIDATE_OBJECT(sprite);
+
+    M3G_ASSIGN_REF(sprite->image, NULL);
+    M3G_ASSIGN_REF(sprite->appearance, NULL);
+
+    m3gIncStat(M3G_INTERFACE(obj), M3G_STAT_RENDERABLES, -1);
+    
+    m3gDestroyNode(obj);
+}
+
+/*!
+ * \internal
+ * \brief Overloaded Object3D method.
+ *
+ * \param property      animation property
+ * \retval M3G_TRUE     property supported
+ * \retval M3G_FALSE    property not supported
+ */
+static M3Gbool m3gSpriteIsCompatible(M3Gint property)
+{
+    switch (property) {
+    case M3G_ANIM_CROP:
+        return M3G_TRUE;
+    default:
+        return m3gNodeIsCompatible(property);
+    }
+}
+
+/*!
+ * \internal
+ * \brief Overloaded Node method
+ */
+static M3Gint m3gSpriteGetBBox(Node *self, AABB *bbox)
+{
+    Sprite *sprite = (Sprite*) self;
+
+    /* Only scaled sprites can have a bounding box; non-scaled ones
+     * are marked as non-cullable in the "SetParent" function in
+     * m3g_node.c */
+    
+    if (sprite->scaled) {
+        const AABB spriteBBox = { { -.5f, -.5f,  0.f },
+                                  {  .5f,  .5f,  0.f } };
+        *bbox = spriteBBox;
+        return (4 * VFC_VERTEX_COST +
+                2 * VFC_TRIANGLE_COST +
+                VFC_NODE_OVERHEAD);
+    }
+    else {
+        return 0; /* no bounding box for non-scaled sprites */
+    }
+}
+
+/*!
+ * \internal
+ * \brief Overloaded Object3D method.
+ *
+ * \param self          Sprite object
+ * \param property      animation property
+ * \param valueSize     size of value array
+ * \param value         value array
+ */
+static void m3gSpriteUpdateProperty(Object *self,
+                                    M3Gint property,
+                                    M3Gint valueSize,
+                                    const M3Gfloat *value)
+{
+    Sprite *sprite = (Sprite *) self;
+    M3G_VALIDATE_OBJECT(sprite);
+    M3G_ASSERT_PTR(value);
+
+    switch (property) {
+    case M3G_ANIM_CROP:
+        /* Assert that the value vector is large enough */
+        if (valueSize > 2) {
+            M3G_ASSERT(valueSize >= 4);
+            m3gSetCrop(sprite,  m3gRoundToInt(value[0]),
+                       m3gRoundToInt(value[1]),
+                       m3gClampInt(m3gRoundToInt(value[2]),
+                                   -M3G_MAX_TEXTURE_DIMENSION,
+                                   M3G_MAX_TEXTURE_DIMENSION),
+                       m3gClampInt(m3gRoundToInt(value[3]),
+                                   -M3G_MAX_TEXTURE_DIMENSION,
+                                   M3G_MAX_TEXTURE_DIMENSION) );
+        }
+        else {
+            M3G_ASSERT(valueSize >= 2);
+            m3gSetCrop(sprite,  m3gRoundToInt(value[0]),
+                       m3gRoundToInt(value[1]),
+                       sprite->crop.width,
+                       sprite->crop.height );
+        }
+        break;
+    default:
+        m3gNodeUpdateProperty(self, property, valueSize, value);
+    }
+}
+
+/*!
+ * \internal
+ * \brief Overloaded Node method.
+ *
+ * \param self Sprite object
+ * \param toCamera transform to camera
+ * \param alphaFactor total alpha factor
+ * \param caller caller node
+ * \param renderQueue RenderQueue
+ *
+ * \retval M3G_TRUE continue render setup
+ * \retval M3G_FALSE abort render setup
+ */
+static M3Gbool m3gSpriteSetupRender(Node *self,
+                                    const Node *caller,
+                                    SetupRenderState *s,
+                                    RenderQueue *renderQueue)
+{
+    Sprite *sprite = (Sprite *)self;
+    Interface *m3g = M3G_INTERFACE(sprite);
+    M3G_UNREF(caller);
+    m3gIncStat(M3G_INTERFACE(self), M3G_STAT_RENDER_NODES, 1);
+
+    if ((self->enableBits & NODE_RENDER_BIT) != 0 &&
+        (self->scope & renderQueue->scope) != 0) {
+        
+        if (sprite->appearance != NULL && sprite->image != NULL &&
+            sprite->crop.width != 0 && sprite->crop.height != 0) {
+
+            /* Fetch the cumulative alpha factor for this node */
+            sprite->totalAlphaFactor =
+                (M3Gushort) m3gGetTotalAlphaFactor((Node*) sprite, renderQueue->root);
+
+            /* Touch the POT image to make sure it's allocated prior
+             * to rendering */
+            
+            if (!m3gGetPowerOfTwoImage(sprite->image) ||
+                !m3gInsertDrawable(m3g,
+                                   renderQueue,
+                                   self,
+                                   &s->toCamera,
+                                   0,
+                                   m3gGetAppearanceSortKey(sprite->appearance)))
+                return M3G_FALSE;
+        }
+    }
+
+    return M3G_TRUE;
+}
+
+/*!
+ * \internal
+ * \brief Calculates sprite vertex positions and texture coordinates.
+ *
+ * \param sprite        Sprite object
+ * \param ctx           RenderContext object (Graphics3D)
+ * \param cam           Camera object
+ * \param vert          vertex position to fill in
+ * \param texvert       texture coordinates to fill in
+ * \param eyeSpace      coordinates after modelview
+ * \param adjust        adjust for texture coorinates, render and
+ *                      pick need different adjustment
+ * \retval M3G_TRUE     crop and image intersect
+ * \retval M3G_FALSE    crop and image do not intersect
+ */
+static M3Gbool m3gGetSpriteCoordinates(Sprite *sprite,
+                                       RenderContext *ctx,
+                                       const Camera *cam,
+                                       const Matrix *toCamera,
+                                       M3Gint *vert,
+                                       M3Gshort *texvert,
+                                       Vec4 *eyeSpace,
+                                       M3Gshort adjust)
+{
+    Vec4 o = {0, 0, 0, 1};      /* Origin */
+    Vec4 x = {0.5f, 0, 0, 1};   /* Half of x unit */
+    Vec4 y = {0, 0.5f, 0, 1};   /* Half of y unit */
+    Vec4 ot;
+    Rect rIsect, rImage;
+
+    rImage.x = 0;
+    rImage.y = 0;
+    rImage.width = sprite->width;
+    rImage.height = sprite->height;
+
+    /* Intersection of image and crop*/
+    if (!m3gIntersectRectangle(&rIsect, &rImage, &sprite->crop)) {
+        /* No intersection -> nothing to render / pick */
+        return M3G_FALSE;
+    }
+
+    /* Calculate origin and vectors after modelview */
+    m3gTransformVec4(toCamera, &o);
+    m3gTransformVec4(toCamera, &x);
+    m3gTransformVec4(toCamera, &y);
+
+    ot = o;
+
+    m3gScaleVec4(&o, m3gRcp(o.w));
+    m3gScaleVec4(&x, m3gRcp(x.w));
+    m3gScaleVec4(&y, m3gRcp(y.w));
+
+    /* Store eyespace coordinates */
+    if (eyeSpace != NULL) {
+        eyeSpace->x = o.x;
+        eyeSpace->y = o.y;
+        eyeSpace->z = o.z;
+    }
+
+    m3gSubVec4(&x, &o);
+    m3gSubVec4(&y, &o);
+
+    x.x = m3gAdd(ot.x, m3gLengthVec3((const Vec3*) &x));
+    x.y = ot.y;
+    x.z = ot.z;
+    x.w = ot.w;
+
+    y.y = m3gAdd(ot.y, m3gLengthVec3((const Vec3*) &y));
+    y.x = ot.x;
+    y.z = ot.z;
+    y.w = ot.w;
+
+    /* Calculate origin and vectors after projection */
+    {
+        const Matrix *projMatrix = m3gProjectionMatrix(cam);
+        m3gTransformVec4(projMatrix, &ot);
+        m3gTransformVec4(projMatrix, &x);
+        m3gTransformVec4(projMatrix, &y);
+    }
+
+    m3gScaleVec4(&ot, m3gRcp(ot.w));
+    m3gScaleVec4(&x, m3gRcp(x.w));
+    m3gScaleVec4(&y, m3gRcp(y.w));
+
+    m3gSubVec4(&x, &ot);
+    m3gSubVec4(&y, &ot);
+
+    x.x = m3gLengthVec3((const Vec3*) &x);
+    y.y = m3gLengthVec3((const Vec3*) &y);
+
+    /* Non-scaled sprites take width from crop rectangle*/
+    if (!sprite->scaled) {
+        M3Gint viewport[4];
+        if (ctx != NULL) {
+            m3gGetViewport(ctx, viewport, viewport + 1, viewport + 2, viewport + 3);
+        }
+        else {
+            /* Use a dummy viewport, this is only when picking and
+               not rendering to anything. Values must represent a valid viewport */
+            viewport[0] = 0;
+            viewport[1] = 0;
+            viewport[2] = 256;
+            viewport[3] = 256;
+        }
+
+        x.x = m3gDivif (rIsect.width, viewport[2]);
+        y.y = m3gDivif (rIsect.height, viewport[3]);
+
+        ot.x = m3gSub(ot.x,
+                      m3gDivif (2 * sprite->crop.x + sprite->crop.width - 2 * rIsect.x - rIsect.width,
+                                viewport[2]));
+
+        ot.y = m3gAdd(ot.y,
+                      m3gDivif (2 * sprite->crop.y + sprite->crop.height - 2 * rIsect.y - rIsect.height,
+                                viewport[3]));
+    }
+    else {
+        /* Adjust width and height according to cropping rectangle */
+        x.x = m3gDiv(x.x, (M3Gfloat) sprite->crop.width);
+        y.y = m3gDiv(y.y, (M3Gfloat) sprite->crop.height);
+
+        ot.x = m3gSub(ot.x,
+                      m3gMul((M3Gfloat)(2 * sprite->crop.x + sprite->crop.width - 2 * rIsect.x - rIsect.width),
+                             x.x));
+
+        ot.y = m3gAdd(ot.y,
+                      m3gMul((M3Gfloat)(2 * sprite->crop.y + sprite->crop.height - 2 * rIsect.y - rIsect.height),
+                             y.y));
+
+        x.x = m3gMul(x.x, (M3Gfloat) rIsect.width);
+        y.y = m3gMul(y.y, (M3Gfloat) rIsect.height);
+    }
+
+    /* Store final Z */
+    if (eyeSpace != NULL) {
+        eyeSpace->w = ot.z;
+    }
+
+    /* Set up positions */
+    vert[0 * 3 + 0] = (M3Gint) m3gMul(65536, m3gSub(ot.x, x.x));
+    vert[0 * 3 + 1] = m3gRoundToInt(m3gAdd(m3gMul(65536, m3gAdd(ot.y, y.y)), 0.5f));
+    vert[0 * 3 + 2] = m3gRoundToInt(m3gMul(65536, ot.z));
+
+    vert[1 * 3 + 0] = vert[0 * 3 + 0];
+    vert[1 * 3 + 1] = (M3Gint) m3gMul(65536, m3gSub(ot.y, y.y));
+    vert[1 * 3 + 2] = vert[0 * 3 + 2];
+
+    vert[2 * 3 + 0] = m3gRoundToInt(m3gAdd(m3gMul(65536, m3gAdd(ot.x, x.x)), 0.5f));
+    vert[2 * 3 + 1] = vert[0 * 3 + 1];
+    vert[2 * 3 + 2] = vert[0 * 3 + 2];
+
+    vert[3 * 3 + 0] = vert[2 * 3 + 0];
+    vert[3 * 3 + 1] = vert[1 * 3 + 1];
+    vert[3 * 3 + 2] = vert[0 * 3 + 2];
+
+    /* Set up texture coordinates */
+    if (!(sprite->flip & FLIPX)) {
+        texvert[0 * 2 + 0] = (M3Gshort) rIsect.x;
+        texvert[1 * 2 + 0] = (M3Gshort) rIsect.x;
+        texvert[2 * 2 + 0] = (M3Gshort) (rIsect.x + rIsect.width - adjust);
+        texvert[3 * 2 + 0] = (M3Gshort) (rIsect.x + rIsect.width - adjust);
+    }
+    else {
+        texvert[0 * 2 + 0] = (M3Gshort) (rIsect.x + rIsect.width - adjust);
+        texvert[1 * 2 + 0] = (M3Gshort) (rIsect.x + rIsect.width - adjust);
+        texvert[2 * 2 + 0] = (M3Gshort) rIsect.x;
+        texvert[3 * 2 + 0] = (M3Gshort) rIsect.x;
+    }
+
+    if (!(sprite->flip & FLIPY)) {
+        texvert[0 * 2 + 1] = (M3Gshort) rIsect.y;
+        texvert[1 * 2 + 1] = (M3Gshort) (rIsect.y + rIsect.height - adjust);
+        texvert[2 * 2 + 1] = (M3Gshort) rIsect.y;
+        texvert[3 * 2 + 1] = (M3Gshort) (rIsect.y + rIsect.height - adjust);
+    }
+    else {
+        texvert[0 * 2 + 1] = (M3Gshort) (rIsect.y + rIsect.height - adjust);
+        texvert[1 * 2 + 1] = (M3Gshort) rIsect.y;
+        texvert[2 * 2 + 1] = (M3Gshort) (rIsect.y + rIsect.height - adjust);
+        texvert[3 * 2 + 1] = (M3Gshort) rIsect.y;
+    }
+
+    return M3G_TRUE;
+}
+
+/*!
+ * \internal
+ * \brief Overloaded Node method.
+ *
+ * Renders the sprite as a textured quad.
+ *
+ * \param self Mesh object
+ * \param ctx current render context
+ * \param patchIndex submesh index
+ */
+static void m3gSpriteDoRender(Node *self,
+                              RenderContext *ctx,
+                              const Matrix *toCamera,
+                              M3Gint patchIndex)
+{
+    Sprite *sprite = (Sprite *)self;
+    M3Gshort texvert[4 * 2];
+    M3Gint vert[4 * 3];
+    Vec4 eyeSpace;
+    Image *imagePow2;
+    M3G_UNREF(patchIndex);
+
+    M3G_BEGIN_PROFILE(M3G_INTERFACE(self), M3G_PROFILE_SETUP_TRANSFORMS);
+    if (!m3gGetSpriteCoordinates(sprite,
+                                 ctx,
+                                 m3gGetCurrentCamera(ctx),
+                                 toCamera,
+                                 vert,
+                                 texvert,
+                                 &eyeSpace,
+                                 0)) {
+        return;
+    }
+    M3G_END_PROFILE(M3G_INTERFACE(self), M3G_PROFILE_SETUP_TRANSFORMS);
+
+    /* Get power of two image */
+    imagePow2 = m3gGetPowerOfTwoImage(sprite->image);
+    /* If NULL -> out of memory */
+    if (imagePow2 == NULL) {
+        return;
+    }
+
+    if (m3gGetColorMaskWorkaround(M3G_INTERFACE(ctx))) {
+        m3gUpdateColorMaskStatus(ctx,
+                                 m3gColorMask(sprite->appearance),
+                                 m3gAlphaMask(sprite->appearance));
+    }
+
+    /* Disable unwanted state. Note that we do this BEFORE setting the
+     * sprite color to avoid any problems with glColorMaterial  */
+    m3gApplyDefaultMaterial();
+    m3gApplyDefaultPolygonMode();
+
+    /* Disable color array, normals and textures*/
+    glDisableClientState(GL_COLOR_ARRAY);
+    glDisableClientState(GL_NORMAL_ARRAY);
+    m3gDisableTextures();
+
+    /* Sprite image to texture unit 0 */
+    glClientActiveTexture(GL_TEXTURE0);
+    glActiveTexture(GL_TEXTURE0);
+    glEnableClientState(GL_TEXTURE_COORD_ARRAY);
+    glTexCoordPointer(2, GL_SHORT, 0, texvert);
+    glEnable(GL_TEXTURE_2D);
+    glTexEnvx(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, (GLfixed) GL_MODULATE);
+    m3gBindTextureImage(imagePow2,
+                        M3G_FILTER_BASE_LEVEL,
+                        m3gIsAccelerated(ctx) ? M3G_FILTER_LINEAR : M3G_FILTER_NEAREST);
+
+    glTexParameterx(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+    glTexParameterx(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+    
+    glMatrixMode(GL_TEXTURE);
+    glLoadIdentity();
+    glScalef(m3gRcp((M3Gfloat) m3gGetWidth(sprite->image)),
+             m3gRcp((M3Gfloat) m3gGetHeight(sprite->image)),
+             1.f);
+    glMatrixMode(GL_MODELVIEW);
+
+    /* Apply fog and compositing mode */
+    m3gApplySpriteFog(sprite->appearance->fog, eyeSpace.z, eyeSpace.w);
+    m3gApplyCompositingMode(sprite->appearance->compositingMode, ctx);
+
+    {
+        GLfixed a = (GLfixed) (0xff * sprite->totalAlphaFactor);
+        a = (a >> (NODE_ALPHA_FACTOR_BITS - 8))
+            + (a >> NODE_ALPHA_FACTOR_BITS)
+            + (a >> (NODE_ALPHA_FACTOR_BITS + 7));
+        glColor4x((GLfixed) 1 << 16, (GLfixed) 1 << 16, (GLfixed) 1 << 16, a);
+    }
+
+    /* Load vertices */
+    glEnableClientState(GL_VERTEX_ARRAY);
+    glVertexPointer(3, GL_FIXED, 0, vert);
+
+    /* Store current matrices, then set up an identity modelview and
+     * projection */
+
+    m3gPushScreenSpace(ctx, M3G_FALSE);
+
+    /* Load indices -> draws the sprite */
+    M3G_BEGIN_PROFILE(M3G_INTERFACE(ctx), M3G_PROFILE_NGL_DRAW);
+    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
+    M3G_END_PROFILE(M3G_INTERFACE(ctx), M3G_PROFILE_NGL_DRAW);
+
+    m3gReleaseTextureImage(imagePow2);
+    
+    /* Restore the previous modelview and projection */
+
+    m3gPopSpace(ctx);
+}
+
+/*!
+ * \internal
+ * \brief Overloaded Node method.
+ *
+ * Picks a scaled sprite as 2D from viewport.
+ *
+ * \param self      Mesh object
+ * \param mask      pick scope mask
+ * \param ray       pick ray
+ * \param ri        RayIntersection object
+ * \param toGroup   transform to originating group
+ * \retval          M3G_TRUE    continue pick
+ * \retval          M3G_FALSE   abort pick
+ */
+static M3Gbool m3gSpriteRayIntersect(Node *self,
+                                     M3Gint mask,
+                                     M3Gfloat *ray,
+                                     RayIntersection *ri,
+                                     Matrix *toGroup)
+{
+    Sprite *sprite = (Sprite *)self;
+    M3Gshort texvert[4 * 2];
+    M3Gint vert[4 * 3];
+    M3Gint x, y;
+    Vec4 eyeSpace;
+    M3Gfloat distance;
+    M3G_UNREF(toGroup);
+
+    /* Check that picking is possible */
+    
+    if (sprite->image == NULL ||
+        sprite->appearance == NULL ||
+        ri->camera == NULL ||
+        !sprite->scaled ||
+        sprite->crop.width == 0 ||
+        sprite->crop.height == 0 ||
+        (self->scope & mask) == 0) {
+        return M3G_TRUE;
+    }
+
+    /* Calculate modelview transform, picking is possible without rendering */
+    
+    {
+        Matrix toCamera;
+        
+        if (!m3gGetTransformTo(self, (Node *)ri->camera,
+                               &toCamera)) {
+            return M3G_FALSE;
+        }
+        if (!m3gGetSpriteCoordinates(sprite, NULL,
+                                     (const Camera *)ri->camera, &toCamera,
+                                     vert, texvert, &eyeSpace, 1)) {
+            return M3G_TRUE;
+        }
+    }
+
+    /* Do the pick in 2D, formula is from the spec and values are
+       set to 16.16 fixed point format */
+    
+    x = m3gRoundToInt(m3gMul(2 * 65536, ri->x)) - 65536;
+    y = 65536 - m3gRoundToInt(m3gMul(2 * 65536, ri->y));
+
+    if (x >= vert[0 * 3 + 0] && x <= vert[2 * 3 + 0] &&
+        y <= vert[0 * 3 + 1] && y >= vert[1 * 3 + 1] ) {
+
+        distance = m3gDiv(m3gSub(eyeSpace.z, ray[6]), m3gSub(ray[7], ray[6]));
+
+        if (distance <= 0 ||
+            distance >= ri->tMin) return M3G_TRUE;
+
+        ri->tMin = distance;
+        ri->distance = ri->tMin;
+        ri->submeshIndex = 0;
+
+        x -= vert[0 * 3 + 0];
+        y  = vert[0 * 3 + 1] - y;
+
+        if (!(sprite->flip & FLIPX)) {
+            ri->textureS[0] = m3gAdd(texvert[0 * 2 + 0],
+                                     m3gDivif ((texvert[2 * 2 + 0] - texvert[0 * 2 + 0] + 1) * x,
+                                               vert[2 * 3 + 0] - vert[0 * 3 + 0]));
+        }
+        else {
+            ri->textureS[0] = m3gSub((M3Gfloat)(texvert[0 * 2 + 0] + 1),
+                                     m3gDivif ((texvert[0 * 2 + 0] - texvert[2 * 2 + 0] + 1) * x,
+                                               vert[2 * 3 + 0] - vert[0 * 3 + 0]));
+        }
+
+        if (!(sprite->flip & FLIPY)) {
+            ri->textureT[0] = m3gAdd(texvert[0 * 2 + 1],
+                                     m3gDivif ((texvert[1 * 2 + 1] - texvert[0 * 2 + 1] + 1) * y,
+                                               vert[0 * 3 + 1] - vert[1 * 3 + 1]));
+        }
+        else {
+            ri->textureT[0] = m3gSub((M3Gfloat)(texvert[0 * 2 + 1] + 1),
+                                     m3gDivif ((texvert[0 * 2 + 1] - texvert[1 * 2 + 1] + 1) * y,
+                                               vert[0 * 3 + 1] - vert[1 * 3 + 1]));
+        }
+
+        {
+            /* Finally check against alpha */
+            M3Gint threshold = 0, alpha;
+
+            if (sprite->appearance->compositingMode) {
+                threshold = (M3Gint)m3gMul(m3gGetAlphaThreshold(sprite->appearance->compositingMode), 256);
+            }
+
+            alpha = m3gGetAlpha(sprite->image, (M3Gint)ri->textureS[0], (M3Gint)ri->textureT[0]);
+
+            if (alpha >= threshold) {
+                /* Normalize texture coordinates */
+                ri->textureS[0] = m3gDiv(ri->textureS[0], (M3Gfloat) sprite->width);
+                ri->textureT[0] = m3gDiv(ri->textureT[0], (M3Gfloat) sprite->height);
+
+                ri->textureS[1] = 0.f;
+                ri->textureT[1] = 0.f;
+
+                ri->normal[0] = 0.f;
+                ri->normal[1] = 0.f;
+                ri->normal[2] = 1.f;
+
+                ri->intersected = self;
+            }
+        }
+    }
+
+    return M3G_TRUE;
+}
+
+/*!
+ * \internal
+ * \brief Overloaded Object3D method.
+ *
+ * \param self Sprite object
+ * \param references array of reference objects
+ * \return number of references
+ */
+static M3Gint m3gSpriteDoGetReferences(Object *self, Object **references)
+{
+    Sprite *sprite = (Sprite *)self;
+    int num = m3gObjectDoGetReferences(self, references);
+    if (sprite->image != NULL) {
+        if (references != NULL)
+            references[num] = (Object *)sprite->image;
+        num++;
+    }
+    if (sprite->appearance != NULL) {
+        if (references != NULL)
+            references[num] = (Object *)sprite->appearance;
+        num++;
+    }
+    return num;
+}
+
+/*!
+ * \internal
+ * \brief Overloaded Object3D method.
+ */
+static Object *m3gSpriteFindID(Object *self, M3Gint userID)
+{
+    Sprite *sprite = (Sprite *)self;
+    Object *found = m3gObjectFindID(self, userID);
+        
+    if (!found && sprite->image != NULL) {
+        found = m3gFindID((Object*) sprite->image, userID);
+    }
+    if (!found && sprite->appearance != NULL) {
+        found = m3gFindID((Object*) sprite->appearance, userID);
+    }
+    return found;
+}
+
+/*!
+ * \internal
+ * \brief Overloaded Object3D method.
+ *
+ * \param originalObj original Sprite object
+ * \param cloneObj pointer to cloned Sprite object
+ * \param pairs array for all object-duplicate pairs
+ * \param numPairs number of pairs
+ */
+static M3Gbool m3gSpriteDuplicate(const Object *originalObj,
+                                  Object **cloneObj,
+                                  Object **pairs,
+                                  M3Gint *numPairs)
+{
+    Sprite *original = (Sprite *)originalObj;
+    Sprite *clone;
+    M3G_ASSERT(*cloneObj == NULL); /* no derived classes */
+
+    /* Create the clone object */
+    
+    clone = (Sprite *)m3gCreateSprite(originalObj->interface,
+                                      original->scaled,
+                                      original->image,
+                                      original->appearance);
+    if (!clone) {
+        return M3G_FALSE;
+    }
+    *cloneObj = (Object *)clone;
+
+    /* Duplicate our own fields */
+    
+    clone->crop = original->crop;
+    clone->flip = original->flip;
+    
+    /* Duplicate base class data */
+    
+    return m3gNodeDuplicate(originalObj, cloneObj, pairs, numPairs);
+}
+
+/*!
+ * \internal
+ * \brief Overloaded Object3D method.
+ *
+ * \param self Sprite object
+ * \param time current world time
+ * \return minimum validity
+ */
+static M3Gint m3gSpriteApplyAnimation(Object *self, M3Gint time)
+{
+    M3Gint validity, minValidity;
+    Sprite *sprite = (Sprite *)self;
+    Object *app;
+    M3G_VALIDATE_OBJECT(sprite);
+
+    minValidity = m3gObjectApplyAnimation(self, time);
+
+    if (minValidity > 0) {
+        app = (Object *) sprite->appearance;
+        
+        if (app != NULL) {
+            validity = M3G_VFUNC(Object, app, applyAnimation)(app, time);
+            minValidity = M3G_MIN(validity, minValidity);
+        }
+    }
+    return minValidity;
+}
+
+/*!
+ * \internal
+ * \brief Initializes a Sprite object. See specification
+ * for default values.
+ *
+ * \param m3g           M3G interface
+ * \param sprite        Sprite object
+ * \param scaled        scaled flag
+ * \param appearance    Appearance object
+ * \param image         Image2D object
+ * \retval M3G_TRUE     Sprite initialized
+ * \retval M3G_FALSE    initialization failed
+ */
+static M3Gbool m3gInitSprite(Interface *m3g,
+                             Sprite *sprite,
+                             M3Gbool scaled,
+                             Appearance *appearance,
+                             Image *image)
+{
+    /* Sprite is derived from node */
+    m3gInitNode(m3g, &sprite->node, M3G_CLASS_SPRITE);
+    sprite->node.hasRenderables = M3G_TRUE;
+
+    m3gIncStat(m3g, M3G_STAT_RENDERABLES, 1);
+    
+    sprite->scaled = scaled;
+    M3G_ASSIGN_REF(sprite->appearance, appearance);
+    return m3gSetSpriteImage(sprite, image);
+}
+
+/*----------------------------------------------------------------------
+ * Virtual function table
+ *--------------------------------------------------------------------*/
+
+static const NodeVFTable m3gvf_Sprite = {
+    {
+        {
+            m3gSpriteApplyAnimation,
+            m3gSpriteIsCompatible,
+            m3gSpriteUpdateProperty,
+            m3gSpriteDoGetReferences,
+            m3gSpriteFindID,
+            m3gSpriteDuplicate,
+            m3gDestroySprite
+        }
+    },
+    m3gNodeAlign,
+    m3gSpriteDoRender,
+    m3gSpriteGetBBox,
+    m3gSpriteRayIntersect,
+    m3gSpriteSetupRender,
+    m3gNodeUpdateDuplicateReferences,
+    m3gNodeValidate
+};
+
+
+/*----------------------------------------------------------------------
+ * Public API functions
+ *--------------------------------------------------------------------*/
+
+/*!
+ * \brief Creates a Sprite object.
+ *
+ * \param hInterface    M3G interface
+ * \param scaled        scaled flag
+ * \param hImage        Image2D object
+ * \param hAppearance   Appearance object
+ * \retval Sprite new Sprite object
+ * \retval NULL Sprite creating failed
+ */
+M3G_API M3GSprite m3gCreateSprite(M3GInterface hInterface,
+                                  M3Gbool scaled,
+                                  M3GImage hImage,
+                                  M3GAppearance hAppearance)
+{
+    Interface *m3g = (Interface *) hInterface;
+    M3G_VALIDATE_INTERFACE(m3g);
+
+    if (hImage == 0) {
+        m3gRaiseError(m3g, M3G_NULL_POINTER);
+        return NULL;
+    }
+
+    {
+        Sprite *sprite =  m3gAllocZ(m3g, sizeof(Sprite));
+
+        if (sprite != NULL) {
+            if (!m3gInitSprite(m3g,
+                               sprite,
+                               scaled,
+                               (Appearance *)hAppearance,
+                               (Image *)hImage)) {
+                M3G_ASSIGN_REF(sprite->image, NULL);
+                M3G_ASSIGN_REF(sprite->appearance, NULL);
+                m3gFree(m3g, sprite);
+                return NULL;
+            }
+        }
+
+        return (M3GSprite) sprite;
+    }
+}
+
+/*!
+ * \brief Get sprite scaled flag.
+ *
+ * \param handle        Sprite object
+ * \retval M3G_TRUE     sprite is scaled
+ * \retval M3G_FALSE    sprite is not scaled
+ */
+M3G_API M3Gbool m3gIsScaledSprite(M3GSprite handle)
+{
+    Sprite *sprite = (Sprite *) handle;
+    M3G_VALIDATE_OBJECT(sprite);
+
+    return sprite->scaled;
+}
+
+/*!
+ * \brief Set sprite appearance.
+ *
+ * \param handle        Sprite object
+ * \param hAppearance   Appearance object
+ */
+M3G_API void m3gSetSpriteAppearance(M3GSprite handle,
+                                    M3GAppearance hAppearance)
+{
+    Sprite *sprite = (Sprite *) handle;
+    M3G_VALIDATE_OBJECT(sprite);
+
+    M3G_ASSIGN_REF(sprite->appearance, hAppearance);
+}
+
+/*!
+ * \brief Set sprite image
+ *
+ * \param handle        Sprite object
+ * \param hImage        Image2D object
+ * \retval              M3G_TRUE image was set
+ * \retval              M3G_FALSE failed to set image
+ */
+M3G_API M3Gbool m3gSetSpriteImage(M3GSprite handle, M3GImage hImage)
+{
+    Sprite *sprite = (Sprite *) handle;
+    Image *image = (Image *)hImage;
+    M3G_VALIDATE_OBJECT(sprite);
+
+    if (image == NULL) {
+        m3gRaiseError(M3G_INTERFACE(sprite), M3G_NULL_POINTER);
+        return M3G_FALSE;
+    }
+
+    M3G_ASSIGN_REF(sprite->image, image);
+
+    sprite->width = m3gGetWidth(image);
+    sprite->height = m3gGetHeight(image);
+
+    sprite->crop.x = 0;
+    sprite->crop.y = 0;
+    sprite->crop.width  = m3gClampInt(sprite->width,  0, M3G_MAX_TEXTURE_DIMENSION);
+    sprite->crop.height = m3gClampInt(sprite->height, 0, M3G_MAX_TEXTURE_DIMENSION);
+
+    sprite->flip = 0;
+
+    return M3G_TRUE;
+}
+
+/*!
+ * \brief Set sprite image crop rectangle.
+ *
+ * \param handle        Sprite object
+ * \param cropX         crop upper left x
+ * \param cropY         crop upper left y
+ * \param width         crop width
+ * \param height        crop height
+ */
+M3G_API void m3gSetCrop(M3GSprite handle,
+                        M3Gint cropX, M3Gint cropY,
+                        M3Gint width, M3Gint height)
+{
+    Sprite *sprite = (Sprite *) handle;
+    M3G_VALIDATE_OBJECT(sprite);
+
+    /* Check for illegal crop size */
+    if (!m3gInRange(width,  -M3G_MAX_TEXTURE_DIMENSION, M3G_MAX_TEXTURE_DIMENSION) ||
+        !m3gInRange(height, -M3G_MAX_TEXTURE_DIMENSION, M3G_MAX_TEXTURE_DIMENSION) ) {
+        m3gRaiseError(M3G_INTERFACE(sprite), M3G_INVALID_VALUE);
+        return;
+    }
+
+    sprite->crop.x = cropX;
+    sprite->crop.y = cropY;
+
+    if (width < 0) {
+        sprite->crop.width = -width;
+        sprite->flip |= FLIPX;
+    }
+    else {
+        sprite->crop.width = width;
+        sprite->flip &= ~FLIPX;
+    }
+
+    if (height < 0) {
+        sprite->crop.height = -height;
+        sprite->flip |= FLIPY;
+    }
+    else {
+        sprite->crop.height = height;
+        sprite->flip &= ~FLIPY;
+    }
+}
+
+/*!
+ * \brief Get sprite image crop parameter.
+ *
+ * \param handle        Sprite object
+ * \param which         which crop parameter to return
+ *                      \arg M3G_GET_CROPX
+ *                      \arg M3G_GET_CROPY
+ *                      \arg M3G_GET_CROPWIDTH
+ *                      \arg M3G_GET_CROPHEIGHT
+ * \return              image crop parameter
+ */
+M3Gint m3gGetCrop(M3GSprite handle, M3Gint which)
+{
+    Sprite *sprite = (Sprite *) handle;
+    M3G_VALIDATE_OBJECT(sprite);
+
+    switch(which) {
+    case M3G_GET_CROPX:
+        return sprite->crop.x;
+    case M3G_GET_CROPY:
+        return sprite->crop.y;
+    case M3G_GET_CROPWIDTH:
+        return (sprite->flip & FLIPX) ? -sprite->crop.width : sprite->crop.width;
+    case M3G_GET_CROPHEIGHT:
+    default:
+        return (sprite->flip & FLIPY) ? -sprite->crop.height : sprite->crop.height;
+    }
+}
+
+/*!
+ * \brief Gets sprite appearance.
+ *
+ * \param handle        Sprite object
+ * \return              Appearance object
+ */
+M3G_API M3GAppearance m3gGetSpriteAppearance(M3GSprite handle)
+{
+    Sprite *sprite = (Sprite *) handle;
+    M3G_VALIDATE_OBJECT(sprite);
+
+    return sprite->appearance;
+}
+
+/*!
+ * \brief Gets sprite image.
+ *
+ * \param handle        Sprite object
+ * \return              Image2D object
+ */
+M3G_API M3GImage m3gGetSpriteImage(M3GSprite handle)
+{
+    Sprite *sprite = (Sprite *) handle;
+    M3G_VALIDATE_OBJECT(sprite);
+
+    return sprite->image;
+}
+
+#undef FLIPX
+#undef FLIPY
+