m3g/m3gcore11/src/m3g_sprite.c
author Dremov Kirill (Nokia-D-MSW/Tampere) <kirill.dremov@nokia.com>
Fri, 12 Mar 2010 15:50:05 +0200
branchRCL_3
changeset 18 5e30ef2e26cb
parent 0 5d03bc08d59c
child 116 171fae344dd4
permissions -rw-r--r--
Revision: 201007 Kit: 201008

/*
* 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);
    }
#ifndef M3G_USE_NGL_API
    /* Store w after projection */
    eyeSpace->w = ot.w;
#endif
    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);
    }
#ifdef M3G_USE_NGL_API
    /* Store final Z */
    if (eyeSpace != NULL) {
        eyeSpace->w = ot.z;
    }
#endif
    /* 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 */
#ifdef M3G_USE_NGL_API
    m3gApplySpriteFog(sprite->appearance->fog, eyeSpace.z, eyeSpace.w);
#else
    m3gApplyFog(sprite->appearance->fog);
#endif
    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);

#ifndef M3G_USE_NGL_API
    /* Transform the sprite vertices (in NDC) back to eye coordinates, so that 
       the fog distance will be calculated correctly in the OpenGL pipeline. */
    {
        GLfloat transform[16];
        GLfloat scaleW[16] = { 0.f, 0.f, 0.f, 0.f,
                               0.f, 0.f, 0.f, 0.f,
                               0.f, 0.f, 0.f, 0.f,
                               0.f, 0.f, 0.f, 0.f };
        Matrix invProjMatrix;
        const Matrix *projMatrix = m3gProjectionMatrix(m3gGetCurrentCamera(ctx));

        m3gMatrixInverse(&invProjMatrix, projMatrix);
		m3gGetMatrixColumns(&invProjMatrix, transform);
        
        glMatrixMode(GL_MODELVIEW);
        glMultMatrixf(transform);
        scaleW[0] = scaleW[5] = scaleW[10] = scaleW[15] = eyeSpace.w;
        glMultMatrixf(scaleW);

        glMatrixMode(GL_PROJECTION);
        m3gGetMatrixColumns(projMatrix, transform);
        glLoadMatrixf(transform);
    }
#endif

    /* 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