Source/WRay_Pick.c

/* 
 *  WRay_Pick.c
 *
 *  QuickDraw 3D 1.6 Sample
 *  Robert Dierkes
 *
 *   07/28/98   RDD     Created.
 */
 
/*------------------*/
/*  Include Files   */
/*------------------*/
#include "QD3D.h"
#include "QD3DMath.h"
#include "QD3DGroup.h"
#include "QD3DPick.h"
#include "QD3DTransform.h"
#include "QD3DView.h"
 
#include "WRay_Error.h"
#include "WRay_Document.h"
#include "WRay_Memory.h"
#include "WRay_Scene.h"
#include "WRay_System.h"
#include "WRay_Pick.h"
 
#include <math.h>
 
 
/*------------------*/
/*    Constants     */
/*------------------*/
 
/*------------------*/
/*    Macros        */
/*------------------*/
#define kFirstHit               0
#define kHitDistance            (kCylRadius + 0.01f)
#define HAS_Object(validMask)   ((validMask & kQ3PickDetailMaskObject)  !=0)
#define HAS_Distance(validMask) ((validMask & kQ3PickDetailMaskDistance)!=0)
 
 
/*----------------------*/
/*  Global Declarations */
/*----------------------*/
static  TQ3PickObject       gPick = NULL;
static  TQ3Ray3D            gWorldRay;
static  TQ3Vector3D         gWorldDelta;
 
static  TQ3GroupPosition    gBallGroupPosn  = NULL;
static  TQ3GeometryObject   gBallGeo = NULL;
static  TQ3GeometryObject   gCurGeoHit  = NULL;
 
 
/*----------------------*/
/*  Local Prototypes    */
/*----------------------*/
static
TQ3PickObject Pick_New(
            TQ3Ray3D            *pWorldRay);
static
TQ3Status Pick_Document(
            TDocumentPtr        pDoc,
            TQ3PickObject       pickObj,
            unsigned long       *pNumHits);
static
TQ3Status Pick_InitializeWorldRay(
            TQ3PickObject       pickObj,
            TQ3Ray3D            *pWorldRay,
            TQ3Vector3D         *pWorldDelta);
static
TQ3Status Pick_ComputeReflectedWorldRay(
            TQ3PickObject       pickObj,
            TQ3Ray3D            *pWorldRay,
            TQ3Vector3D         *pWorldDelta);
static
TQ3GeometryObject Pick_CreateMovingObject(
            TDocumentPtr        pDoc,
            TQ3Ray3D            *pRay,
            TQ3GroupPosition    *pGroupPosn);
static
TQ3Status Pick_GetNearestObject(
            TQ3PickObject       pickObj,
            float               minDistance,
            TQ3GeometryObject   *pGeoHit,
            TQ3GeometryObject   *pPrevGeoHit);
static
TQ3Status Pick_HighlightObject(
            TDocumentPtr        pDoc,
            TQ3GeometryObject   geoObj,
            TQ3Boolean          doHighlight);
 
 
/*
 *  Pick_Initialize
 */
TQ3Boolean Pick_Initialize(
    void)
{
    TQ3Status   status = kQ3Failure;
 
    gPick           = NULL;
    gBallGroupPosn  = NULL;
    gBallGeo        = NULL;
    gCurGeoHit      = NULL;
 
    Q3Point3D_Set(&gWorldRay.origin,     0.0, 0.0, 0.0);
    Q3Vector3D_Set(&gWorldRay.direction, 0.0, 0.0, 1.0);
 
    gPick = Pick_New(&gWorldRay);
 
    return (gPick != NULL) ? kQ3True : kQ3False;
}
 
 
/*
 *  Pick_Exit
 */
TQ3Boolean Pick_Exit(
    void)
{
    Object_Dispose_NULL(&gPick);
    Object_Dispose_NULL(&gBallGeo);
    Object_Dispose_NULL(&gCurGeoHit);
 
    return kQ3True;
}
 
 
#pragma mark -
 
/*
 *  Pick_New
 */
TQ3PickObject Pick_New(
    TQ3Ray3D            *pWorldRay)
{
    TQ3WorldRayPickData rayPickData;
    TQ3PickObject       pickObject = NULL;
 
    /*
     * Tolerance values are only used for Point, Line, Ellipse,
     * NURB Curve, PolyLine, Mesh geometries and are measured
     * in world space.
     */
    #define kVertexTolerance    0.01
    #define kEdgeTolerance      0.005
 
    rayPickData.data.sort = kQ3PickSortNearToFar;
 
    rayPickData.data.mask = kQ3PickDetailMaskObject     |
                            kQ3PickDetailMaskXYZ        |
                            kQ3PickDetailMaskDistance   |
                            kQ3PickDetailMaskNormal;
    rayPickData.data.numHitsToReturn = 1;
 
    /* Make sure ray is normalized */
    Q3Vector3D_Normalize(&pWorldRay->direction, &pWorldRay->direction);
    rayPickData.ray = *pWorldRay;
 
    rayPickData.vertexTolerance = kVertexTolerance;
    rayPickData.edgeTolerance   = kEdgeTolerance;
 
    /* Create the new world ray pick object */
    pickObject = Q3WorldRayPick_New(&rayPickData);
    DEBUG_ASSERT(pickObject != NULL, Q3WorldRayPick_New);
 
    return pickObject;
}
 
 
/*
 *  Pick_Document
 */
static
TQ3Status Pick_Document(
    TDocumentPtr        pDoc,
    TQ3PickObject       pickObj,
    unsigned long       *pNumHits)
{
    TQ3Status       status = kQ3Success;
    TQ3ViewStatus   viewStatus = kQ3ViewStatusError;
 
    if (Q3View_StartPicking(pDoc->fView, pickObj) == kQ3Failure) {
        ERROR_DEBUG_STR("Pick_Document: Q3View_StartPicking failed.");
        return kQ3Failure;
    }
 
    do {
        Document_Submit_Objects(pDoc, pDoc->fView);
 
        viewStatus = Q3View_EndPicking(pDoc->fView);
    }
    while (viewStatus == kQ3ViewStatusRetraverse);
 
    DEBUG_ASSERT(viewStatus == kQ3ViewStatusDone, Pick_Document);
 
    status = Q3Pick_GetNumHits(pickObj, pNumHits);
 
    return status;
}
 
 
#pragma mark -
 
/*
 *  Pick_IsAnimating
 */
TQ3Boolean Pick_IsAnimating(
    void)
{
    return (gBallGeo != NULL) ? kQ3True : kQ3False;
}
 
 
/*
 *  Pick_BeginAnimation
 */
TQ3Status Pick_BeginAnimation(
    TDocumentPtr        pDoc)
{
    TQ3Status           status = kQ3Failure;
 
    if (Pick_IsAnimating() == kQ3True) {
        /* Animation already taking place */
        return status;
    }
 
    /* Setup initial world ray origin and direction */
    status = Pick_InitializeWorldRay(gPick, &gWorldRay, &gWorldDelta);
    if (status == kQ3Failure) {
        return status;
    }
 
    /* Create ball */
    gBallGeo = Pick_CreateMovingObject(pDoc, &gWorldRay, &gBallGroupPosn);
    DEBUG_ASSERT(gBallGeo != NULL, Pick_CreateMovingObject);
    if (gBallGeo == NULL) {
        return kQ3Failure;
    }
 
    return status;
}
 
 
/*
 *  Pick_Animate
 *
 *  Move ball through the scene avoiding objects using
 *  ray picking to check for geometries in its path.
 */
TQ3Status Pick_Animate(
    TDocumentPtr        pDoc)
{
    TQ3Status           status  = kQ3Failure;
    unsigned long       numHits;
    TQ3GeometryObject   prevGeoHit  = NULL;
 
    if (Pick_IsAnimating() == kQ3False) {
        /* If animation hasn't started then do nothing */
        return kQ3Failure;
    }
 
    /* Submit scene for picking */
    status = Pick_Document(pDoc, gPick, &numHits);
    DEBUG_ASSERT(status == kQ3Success, Pick_Document);
 
    if (numHits > 0) {
        status = Pick_GetNearestObject(gPick, kHitDistance, &gCurGeoHit, &prevGeoHit);
 
        if (gCurGeoHit != prevGeoHit) {
            if (prevGeoHit != NULL) {
                /* Unhighlight the ball & previous geometry */
                Pick_HighlightObject(pDoc, gBallGeo, kQ3False);
                Pick_HighlightObject(pDoc, prevGeoHit, kQ3False);
                Object_Dispose_NULL(&prevGeoHit);
            }
 
            if (gCurGeoHit != NULL) {
                /* Highlight the ball & the new closest geometry*/
                Pick_HighlightObject(pDoc, gBallGeo, kQ3True);
                Pick_HighlightObject(pDoc, gCurGeoHit, kQ3True);
 
                /* status = */ Pick_ComputeReflectedWorldRay(gPick, &gWorldRay, &gWorldDelta);
 
                System_Sound();
            }
        }
    }
    else {
        /* Nothing hit */
        if (gCurGeoHit != NULL) {
            /* Unhighlight the ball & current geometry */
            Pick_HighlightObject(pDoc, gBallGeo, kQ3False);
            Pick_HighlightObject(pDoc, gCurGeoHit, kQ3False);
            Object_Dispose_NULL(&gCurGeoHit);
        }
    }
 
    Q3Pick_EmptyHitList(gPick);
 
    /* Advance ball by delta amount */
    Q3Point3D_Vector3D_Add(&gWorldRay.origin, &gWorldDelta, &gWorldRay.origin);
    Q3Ellipsoid_SetOrigin(gBallGeo, &gWorldRay.origin);
 
    /* Set ray again */
    status = Q3WorldRayPick_SetRay(gPick, &gWorldRay);
    DEBUG_ASSERT(status == kQ3Success, Q3WorldRayPick_SetRay);
 
    return status;
}
 
 
/*
 *  Pick_EndAnimation
 */
TQ3Status Pick_EndAnimation(
    TDocumentPtr        pDoc)
{
    if (Pick_IsAnimating() == kQ3False) {
        /* Animation not taking place */
        return kQ3Failure;
    }
 
    /* Unhighlight anything that's still highlighted */
    if (gCurGeoHit != NULL) {
        /* Unhighlight the ball & current geometry */
        Pick_HighlightObject(pDoc, gBallGeo, kQ3False);
        Pick_HighlightObject(pDoc, gCurGeoHit, kQ3False);
        Object_Dispose_NULL(&gCurGeoHit);
    }
    Object_Dispose_NULL(&gBallGeo);
 
    /* Remove temporary ball group from main group */
    if (gBallGroupPosn != NULL) {
        DEBUG_ASSERT(gCurGeoHit == NULL, Pick_EndAnimation);
        gCurGeoHit = Q3Group_RemovePosition (pDoc->fModel, gBallGroupPosn);
        Object_Dispose_NULL(&gCurGeoHit);
        gBallGroupPosn = NULL;
    }
 
    Document_Draw(pDoc);
 
    return kQ3Success;
}
 
 
#pragma mark -
 
/*
 *  Pick_InitializeWorldRay
 *
 *  Create an world delta vector with a random direction with a
 *  magnitude that controls the rate.  Normalize this vector to
 *  make the actual world ray. The world ray is initially positioned
 *  at the origin.
 */
static
TQ3Status Pick_InitializeWorldRay(
    TQ3PickObject       pickObj,
    TQ3Ray3D            *pWorldRay,
    TQ3Vector3D         *pWorldDelta)
{
    TQ3Status           status = kQ3Failure;
 
    Q3Point3D_Set(&pWorldRay->origin, 0.0, 0.0, 0.0);
 
    /* Generate a random vector in XY plane */
    Q3Vector3D_Set(pWorldDelta, System_RandomFloat(), System_RandomFloat(), 0.0);
    Q3Vector3D_Normalize(pWorldDelta, &pWorldRay->direction);
 
    /* Set world ray's origin and direction */
    status = Q3WorldRayPick_SetRay(pickObj, pWorldRay);
    DEBUG_ASSERT(status == kQ3Success, Q3WorldRayPick_SetRay);
 
    return status;
}
 
 
/*
 *  Pick_ComputeReflectedWorldRay
 *
 *  Given:  pDelta, pRay->direction (a normalized pDelta), normal
 *  Find:   new pDelta with a negative angle relative to the angle between delta and normal
 *
 *          ++
 *         /!|\
 *        / !| \
 *       / N!|  \
 *    D /   !|   \ D'
 *     /    V|    \
 *    /      |     \
 *   /     D1|      \
 *  v<-----  V ----->v
 *      D2     D2'
 *
 *  !   N  = Normal
 *  /   D  = Delta
 *  |   D1 = Delta component
 *  -   D2 = Delta component
 *  \   D' = Reflection of D
 *      c  = scalar for D projection onto N
 *
 *          D ¥ N
 *      c = -----   but since N ¥ N = 1 then    c = D ¥ N
 *          N ¥ N
 *
 *      D  = D1 + D2
 *      D1 = cN
 *
 *      D  = cN + D2    therefore
 *      D2 = D - cN
 *
 *      D2' = Negate(D2)
 *      D' = D1 + D2'
 */
static
TQ3Status Pick_ComputeReflectedWorldRay(
    TQ3PickObject       pickObj,
    TQ3Ray3D            *pWorldRay,
    TQ3Vector3D         *pWorldDelta)
{
    TQ3Status           status;
    TQ3Vector3D         normal,
                        newDelta,
                        delta1,
                        delta2;
    float               c;
 
    status = Q3Pick_GetPickDetailData (pickObj, kFirstHit, kQ3PickDetailMaskNormal, &normal);
    DEBUG_ASSERT(status == kQ3Success, Q3Pick_GetPickDetailData);
    if (status == kQ3Failure) {
        return status;
    }
 
    /* We negate pWorldDelta to reverse it's direction giving us the newDelta */
    Q3Vector3D_Negate(pWorldDelta, &newDelta);
 
    /* Compute delta1 by projecting delta onto normal */
    c = Q3Vector3D_Dot(&newDelta, &normal);
    Q3Vector3D_Scale(&normal, c, &delta1);
    Q3Vector3D_Subtract(&newDelta, &delta1, &delta2);
 
    /* Compute deltaPrime from delta1 and negated delta2. This is delta reflected through the normal */
    Q3Vector3D_Negate(&delta2, &delta2);
    Q3Vector3D_Add(&delta1, &delta2, pWorldDelta);
 
    /* Make sure pick ray is normalized */
    Q3Vector3D_Normalize(pWorldDelta, &pWorldRay->direction);
 
    return status;
}
 
 
#pragma mark -
 
/*
 *  Pick_CreateMovingObject
 */
static
TQ3GeometryObject Pick_CreateMovingObject(
            TDocumentPtr        pDoc,
            TQ3Ray3D            *pRay,
            TQ3GroupPosition    *pGroupPosn)
{
    TQ3EllipsoidData    data;
    TQ3GeometryObject   geometry = NULL;
    TQ3AttributeSet     attr     = NULL;
    TQ3GroupObject      group    = NULL;
 
    #define kObjectRadius       (kCylRadius * 0.95f)
    #define kDefaultObjectColor 0.8, 0.8, 0.0
 
    data.origin = pRay->origin;
 
    data.orientation.x = 0.0;
    data.orientation.y = kObjectRadius;
    data.orientation.z = 0.0;
 
    data.majorRadius.x = 0.0;
    data.majorRadius.y = 0.0;
    data.majorRadius.z = kObjectRadius;
 
    data.minorRadius.x = kObjectRadius;
    data.minorRadius.y = 0.0;
    data.minorRadius.z = 0.0;
 
    data.uMin = data.vMin = 0.0f;
    data.uMax = data.vMax = 1.0f;
    data.caps = kQ3EndCapNone;
 
    data.interiorAttributeSet   = NULL;
    data.ellipsoidAttributeSet  = NULL;
 
    attr = Q3AttributeSet_New ();
    if (attr != NULL) {
        TQ3ColorRGB color = { kDefaultObjectColor };
 
        data.ellipsoidAttributeSet = attr;
        Q3AttributeSet_Add(attr, kQ3AttributeTypeDiffuseColor, &color);
 
        geometry = Q3Ellipsoid_New(&data);
        DEBUG_ASSERT(geometry != NULL, Q3Ellipsoid_New);
        Object_Dispose_NULL(&attr);
    }
 
    /* Put geometry in an unpickable group */
    group = Q3OrderedDisplayGroup_New();
    DEBUG_ASSERT(group != NULL, Q3OrderedDisplayGroup_New);
 
    if (group != NULL) {
        TQ3DisplayGroupState    state;
 
        if (Q3DisplayGroup_GetState(group, &state) == kQ3Success) {
            state &= ~kQ3DisplayGroupStateMaskIsPicked;
            Q3DisplayGroup_SetState(group, state);
        }
 
        /* Add geometry to group but don't decrement reference */
        Q3Group_AddObject(group, geometry);
 
        /* Add group to main group */
        *pGroupPosn = Q3Group_AddObject(pDoc->fModel, group);
        Object_Dispose_NULL(&group);
    }
    else {
        /* Error */
        Object_Dispose_NULL(&geometry);
    }
 
    return geometry;
}
 
 
/*
 *  Pick_GetNearestObject
 *
 *  Find and return object hit if it's within minDistance.
 *
 *  In this sample code since we're picking with a single ray
 *  and our moving object is several units wide portions of
 *  this object may pass through other objects because we're
 *  testing with a ray cast from the center of our moving object
 *  rather than near it's outer extents.
 */
static
TQ3Status Pick_GetNearestObject(
    TQ3PickObject       pickObj,
    float               minDistance,
    TQ3GeometryObject   *pGeoHit,
    TQ3GeometryObject   *pPrevGeoHit)
{
    TQ3Status           status  = kQ3Failure;
    TQ3PickDetail       pickDetailValidMask;
    float               distance;
    TQ3GeometryObject   objectHit = NULL;
 
    #define kDistanceTolerance  0.025       /* TODO This may need refinement */
 
    status = Q3Pick_GetPickDetailValidMask(pickObj, kFirstHit, &pickDetailValidMask);
 
    /* Get distance from ray to intersected geometry */
    if (HAS_Distance(pickDetailValidMask)) {
 
        status = Q3Pick_GetPickDetailData (pickObj, kFirstHit, kQ3PickDetailMaskDistance, &distance);
        DEBUG_ASSERT(status == kQ3Success, Q3Pick_GetPickDetailData);
 
        /* Is this geometry within "minDistance"? */
        if ((distance <= (minDistance - kDistanceTolerance))    ||
            (distance <= (minDistance + kDistanceTolerance)))
        {
            if (HAS_Object(pickDetailValidMask)) {
                /* Get the object */
                status = Q3Pick_GetPickDetailData (pickObj, kFirstHit, kQ3PickDetailMaskObject, &objectHit);
 
                if (objectHit == *pGeoHit) {
                    /* We've hit the same geometry */
                    Object_Dispose_NULL(&objectHit);
                }
                else {
                    /* Return new geometry hit */
                    *pPrevGeoHit = *pGeoHit;    /* Need to unhighlight this if non-NULL */
                    *pGeoHit     = objectHit;
                }
            }
        }
        else {
            *pPrevGeoHit = *pGeoHit;    /* Need to unhighlight this if non-NULL */
            *pGeoHit     = NULL;
        }
    }
 
    return status;
}
 
 
/*
 *  Pick_HighlightObject
 */
static
TQ3Status Pick_HighlightObject(
    TDocumentPtr        pDoc,
    TQ3GeometryObject   geoObj,
    TQ3Boolean          doHighlight)
{
    TQ3Status           status  = kQ3Failure;
    TQ3AttributeSet     attr    = NULL;
    TQ3Switch           isHighlighted = kQ3On;
    TQ3Boolean          doesContain,
                        doUpdate = kQ3False;
 
    DEBUG_ASSERT(pDoc != NULL, Pick_HighlightObject);
    DEBUG_ASSERT(geoObj != NULL, Pick_HighlightObject);
 
    status = Q3Geometry_GetAttributeSet(geoObj, &attr);
    DEBUG_ASSERT(status == kQ3Success, Q3Geometry_GetAttributeSet);
    DEBUG_ASSERT(attr != NULL, Q3Geometry_GetAttributeSet__attr);
 
    if (status == kQ3Success) {
        if (attr != NULL) {
            doesContain = Q3AttributeSet_Contains(
                            attr, kQ3AttributeTypeHighlightState);
 
            /* Add highlight if geometry doesn't contain one */
            if (doHighlight == kQ3True) {
                if (doesContain == kQ3False) {
                    Q3AttributeSet_Add(
                                attr, kQ3AttributeTypeHighlightState, &isHighlighted);
                    doUpdate = kQ3True;
                }
            }
            else {
                /* Remove highlight if there is one */
                if (doesContain == kQ3True) {
                    Q3AttributeSet_Clear(
                                attr, kQ3AttributeTypeHighlightState);
                    doUpdate = kQ3True;
                }
            }
 
            if (doUpdate == kQ3True) {
                status = Q3Geometry_SetAttributeSet(geoObj, attr);
                DEBUG_ASSERT(status == kQ3Success, Q3Geometry_SetAttributeSet);
 
                Document_Draw(pDoc);
            }
        }
    }
    Object_Dispose_NULL(&attr);
 
    return status;
}