MakePano.c

/********************************************************************************
 *  File:       MakePano.c
 *
 *  Contains:   Code for creating a QuickTime VR panoramic movie from a panoramic image.
 *
 *  Written by: Ken Turkowski
 *              Directly derived from VTMakeOPano.c by Tim Monroe.
 *              Based largely on MakeQTVRPanorama code by Ed Harp (and others?).
 *              Cubic projection code based on SampleMakeCubic.cp code by Ken Doyle.
 *
 *  Copyright:  © 1996-1998 by Apple Computer, Inc., all rights reserved.
 *
 *  Change History (most recent first):
 *
 *     <15>     06/14/00    rtm     modified ImportVideoTrack as per Ken Doyle's changes in MakeCubicPPC
 *     <14>     03/23/00    rtm     added code to handle new cubic file format
 *     <13>     03/21/00    rtm     made changes to get things running under CarbonLib
 *     <12>     11/24/99    rtm     reworked file opening and saving to avoid using Standard File Package
 *                                  on Macintosh (for Carbonization effort)
 *     <11>     02/01/99    rtm     reworked prompt and filename handling to remove "\p" sequences
 *     <10>     10/19/98    rtm     added check in CreatePanoTrack to make sure we were passed a
 *                                  valid hot spot file spec (otherwise no panorama is created!); added
 *                                  the default movie progress procedure
 *     <9>      09/30/98    rtm     tweaked call to AddMovieResource to create single-fork movies;
 *                                  tweaked call to FlattenMovieData to enable FastStart option
 *     <8>      08/06/98    rtm     added VRPano_MakeHotSpotVers2x0 (based loosely on code from John Mott)
 *                                  to support version 2.0 hot spot atoms; sketched out preliminary
 *                                  support for 1.0 hot spots, but that's left as an exercise for the reader
 *     <7>      08/05/98    rtm     added support for hot spots image tracks (both version 1.0 and 2.0)
 *     <6>      08/26/98    rtm     added support for pan/tilt/field-of-view constraint atoms
 *     <5>      08/18/98    rtm     added support for version 1.0 panoramic movies
 *     <4>      08/16/98    rtm     final clean-up before initial release
 *     <3>      08/15/98    rtm     reworked the image file handling using QuickTime graphics importers;
 *                                  added tile-display window for debugging purposes;
 *                                  now we run fine under both MacOS and Windows. Ta da!
 *     <2>      01/14/98    rtm     further work; added Endian* macros; got working on MacOS
 *     <1>      01/12/98    rtm     first file from CMovieMaker.cp in MakeQTVRPanorama 2.0b
 *
 *  This file contains functions that convert a panoramic image into a QuickTime VR movie. The image
 *  can be a picture (of type 'PICT') or any other kind of image for which QuickTime has a graphics
 *  importer component. Here, we can create both version 1.0 and version 2.0 QTVR panoramic movies.
 *  We can also create hot spot image tracks and (for version 2.0 only) assemble the various hot spot
 *  atoms.
 *
 *  This file also contains a function that constructs a QuickTime VR movie that uses the new cubic
 *  projection. In the following notes, we'll call such movies "cubic movies" or "cubic panoramas".
 *
 ********************************************************************************
 *
 *  VERSION 2.0 FILE FORMAT
 *
 *  The definitive source of information about creating QTVR 2.0 panoramic movies is Chapter 3 of the
 *  book "Virtual Reality Programming With QuickTime VR 2.0". (This information is also available
 *  online, at <http://dev.info.apple.com/dev/techsupport/insidemac/qtvr/qtvrapi-2.html>.) Here is
 *  a condensed version of the info in that chapter, as pertains to panoramas:
 *
 *  A panoramic movie is a QuickTime movie that contains at least three tracks: a QTVR track, a panorama
 *  track, and a panorama image track. In addition, a QuickTime VR movie must contain some special user data
 *  that specifies the QuickTime VR movie controller. A QuickTime VR movie can also contain other kinds of
 *  tracks, such as hot spot image tracks and even sound tracks.
 *
 *  A QuickTime VR movie contains a single "QTVR track", which maintains a list of the nodes in the movie.
 *  Each individual sample in the QTVR track's media contains information about a single node, such as the
 *  node's type, ID, and name. Since we are creating a single-node movie here, our QTVR track will contain
 *  a single media sample. 
 *
 *  Every media sample in a QTVR track has the same sample description, whose type is QTVRSampleDescription.
 *  The data field of that sample description is a "VR world", an atom container whose child atoms specify
 *  information about the nodes in the movie, such as the default node ID and the default imaging properties.
 *  We'll spend a good bit of time putting things into the VR world.
 *
 *  A panoramic movie also contains a single "panorama track", which contains information specific to the
 *  panorama. A panorama track has a media sample for each media sample in the QTVR track. As a result,
 *  our panorama track will have one sample. The QTVRPanoSampleAtom structure defines the media sample data. 
 *
 *  The actual image data for a panoramic node is contained in a "panorama image track". The individual
 *  frames in that track are the diced (and also perhaps compressed) tiles of the original panoramic image.
 *  There may also be a "hot spot image track" that contains the diced (and also perhaps compressed) tiles
 *  of the hot spot panoramic image.
 *
 *  So, our general strategy, given a panoramic image, is as follows (though perhaps not in the order listed):
 *
 *      (1) Create a movie containing a video track whose frames are the compressed tiles of the panoramic
 *          image. Call this movie the "tile movie". Create a similar movie for the hot spot image. Call
 *          this movie the "hot spot tile movie".
 *      (2) Create a new, empty movie. Call this movie the "QTVR movie".
 *      (3) Create a QTVR track and its associated media.
 *      (4) Create a VR world atom container; this is stored in the sample description for the QTVR track.
 *      (5) Create a node information atom container for each node; this is stored as a media sample
 *          in the QTVR track.
 *      (6) Create a panorama track and add it to the movie.
 *      (7) Create a panorama image track by copying the video track from the tile movie to the QTVR movie.
 *      (8) Create a hot spot image track by copying the video track from the hot spot tile movie to the QTVR movie.
 *      (9) Set up track references from the QTVR track to the panorama track, and from the panorama track
 *          to the panorama image track and the hot spot image track.
 *      (A) Add a user data item that identifies the QTVR movie controller.
 *      (B) Flatten the QTVR movie into the final panoramic movie.
 *
 ********************************************************************************
 *
 *  VERSION 1.0 FILE FORMAT
 *
 *  The definitive source of information about creating QTVR 1.0 panoramic movies is Technote 1035,
 *  "QuickTime VR 1.0 Panorama Movie File Format" released in March 1996, available online at the address
 *  <http://devworld.apple.com/dev/technotes/tn/tn1035.html>. Here is a condensed version of the info
 *  in that technote:
 *
 *  For version 1.0 panoramic movies, the file format is somewhat simpler. A single-node panoramic movie
 *  contains a "scene track" and a "panorama track". The scene track is an inactive video track that contains
 *  the diced (and also perhaps compressed) tiles of the original panoramic image. A version 1.0 scene track
 *  is essentially a version 2.0 panorama image track.
 *
 *  A panoramic movie also contains a single "panorama track", which contains information specific to the
 *  panorama. The panorama track in version 1.0 differs significantly from the panorama track in version 2.0,
 *  so don't let the names confuse you. 
 *
 *  A "panorama track" in version 1.0 contains one media sample for each node in the movie. Every media sample
 *  in a panorama track has the same sample description, whose type is PanoramaDescription. This structure
 *  contains information about how the panoramic image was diced, along with information about any hot spot
 *  and low-resolution scene tracks that might be contained in the movie file.
 *
 *  Each individual sample in the panorama track's media contains information about a single node,
 *  such as the node's default view angles, pan and zoom constraints, and hot spot information.
 *  Since we are creating a single-node movie here, our panorama track will contain a single media sample. 
 *  The data in a panorama track sample is organized as a sequence of "atom data structures"; each such
 *  structure begins with size and type fields, so you can easily read thru the atom data structures by
 *  starting at the beginning of the sample data and hopping over each structure. The atoms can appear in
 *  any order in the panorama track sample data.
 *
 *  -> IMPORTANT: The "atom data structures" that comprise a panorama track sample are *not* QTAtoms, and the
 *  -> panorama track sample itself is *not* a QTAtomContainer. The version 1.0 panorama file format predates
 *  -> QuickTime 2.1, which introduced the atom data routines. As a result, you cannot use the atom data routines
 *  -> to read or write version 1.0 panorama track samples.
 *
 *  A panorama track sample must contain a "panorama header atom", whose structure is PanoSampleHeaderAtom.
 *  The sample may contain other atoms as well, such as a string table atom (which contains strings, such as
 *  node names) and a hot spot table atom (which lists all of the hot spots in a node). In this sample code,
 *  we add a hot spot image track to the VR movie, but we do not (yet) build a hot spot atom table; as a result,
 *  our panorama track sample will need to contain only a panorama header atom.
 *
 *  So, our general strategy, given a panoramic image, is as follows (though perhaps not in the order listed):
 *
 *      (1) Create a movie containing a video track whose frames are the compressed tiles of the panoramic
 *          image. Call this movie the "tile movie". Create a similar movie for the hot spot image. Call
 *          this movie the "hot spot tile movie".
 *      (2) Create a new, empty movie. Call this movie the "QTVR movie".
 *      (3) Create a scene track by copying the video track from the tile movie to the QTVR movie.
 *      (4) Create a hot spot image track by copying the video track from the hot spot tile movie to the QTVR movie.
 *      (5) Create a panorama track and add it to the movie.
 *      (6) Add a user data item that identifies the QTVR movie controller.
 *      (7) Flatten the QTVR movie into the final panoramic movie.
 *
 ********************************************************************************
 *
 *  CUBIC PANORAMA FILE FORMAT
 *
 *  QuickTime VR version 5.0 introduces "cubic panoramas", which store the panoramic image as 6 (or more) separate images that
 *  are (during playback) projected onto the sides of a cube (polyhedron). This allows the user to look straight up and straight
 *  down. The file format for cubic panoramas is identical with the Version 2.0 cylindrical file format, with these
 *  exceptions:
 *
 *  The image track (and any associated hot spot and preview tracks) must contain 6 samples/frames/faces
 *  (these can be subtiled, though tile based memory management may not be available initially). The first
 *  sample is the front cube face, the second is the right-hand cube face, and so on; sample 5 is the top of the cube
 *  and sample 6 is the bottom of the cube. You can change this default orientation with the 'cufa' descriptor.
 *  The frames are oriented horizontally (except for the top and bottom), not rotated as with cylindrical panoramas.
 *
 *  For cubic panoramas, some of the fields in the panorama sample atom should be assigned special values that allow
 *  the file to be displayed with the cylindrical engine if the cubic engine is not available. The cubic engine ignores
 *  those values, instead using values stored in the new "cubic view atom". See the function CreatePanoTrack for
 *  the special values you should use.
 *
 *
 *  NOTES:
 *
 *  *** (1) ***
 *  This code is based largely on the existing MakeQTVRPanorama sample code written by Ed Harp (and others?).
 *  MakeQTVRPanorama is a full-featured application written in C++ using Metrowerks' PowerPlant, a Mac-only
 *  application framework. Here I've uncoupled the central portion of that code, contained in the file CMovieMaker.cp,
 *  and converted it into straight C. I have taken the liberty of reworking that code as necessary to make it run
 *  also on Windows platforms and to bring it into line with the other QuickTime code samples. So far the biggest
 *  changes involve using graphics importers instead of the original PICT-reading code and inserting all those
 *  Endian macros.
 *
 *  *** (2) ***
 *  All data in QTAtom structures must be in big-endian format. We use macros like EndianU32_NtoB to convert
 *  values into the proper format before inserting them into atoms. See CreateVRWorld for some examples.
 *
 ********************************************************************************/
 
 
 
/********************************************************************************
 ********************************************************************************
 ********************************************************************************
 ***        compiler flags
 ********************************************************************************
 ********************************************************************************
 ********************************************************************************/
 
/* The following flag determines whether we show the uncompressed tiles in a window as they get processed;
 * I did this mainly for debugging purposes, but it's also a useful progress indicator....
 */
#define USE_TILE_DISPLAY_WINDOW         1
#define USE_TEMP_MEM_FIRST
 
 
/********************************************************************************
 ********************************************************************************
 ********************************************************************************
 ***        header files
 ********************************************************************************
 ********************************************************************************
 ********************************************************************************/
 
#include <Endian.h>
#include <QuickTimeVR.h>
#include <QuickTimeVRFormat.h>
#include <QuickTimeComponents.h>
#include <FixMath.h>
#include <Script.h>
#include <Sound.h>
#include <stdio.h>
#include <string.h>
#if TARGET_OS_MAC
#include <fp.h>
#else
#include <math.h>
#endif
#include "MakePano.h"
 
 
#pragma mark #Constants
/********************************************************************************
 ********************************************************************************
 ********************************************************************************
 ***        Constants
 ********************************************************************************
 ********************************************************************************
 ********************************************************************************/
 
enum {
    QTVRFlattenerType = FOUR_CHAR_CODE('vrwe')  /* An oversight prevented this from going into QuickTimeVRFormat.h */
};
 
#define D_PI                            3.1415926535898
#define D_2PI                           6.2831853071796
#define D_PI_2                          1.5707963267949
#define D_DEGREES_PER_RADIAN            57.295779513082
#define D_RADIANS_PER_DEGREE            0.017453292519943
 
#define kQTVRStandardTimeScale          3600                        /* time scale for QTVR panoramas */
#define kDefaultNodeID                  1                           /* default node ID */
 
#define kQTVRCylindricalVersion1        (long)1
#define kQTVRCylindricalVersion2v       (long)2
#define kQTVRCylindricalVersion2h       (long)3
#define kQTVRCubicVersion1              (long)4
 
#define kPanDescType                    FOUR_CHAR_CODE('pano')
#define kPanHeaderType                  FOUR_CHAR_CODE('pHdr')
#define kStringTableType                FOUR_CHAR_CODE('strT')
#define kHotSpotTableType               FOUR_CHAR_CODE('pHot')
 
#define kQTVRPanoTypeCube               FOUR_CHAR_CODE('cube')
#define kQTVRCubicViewAtomType          FOUR_CHAR_CODE('cuvw')
#define kQTVRCubicFaceDataAtomType      FOUR_CHAR_CODE('cufa')
 
#define kCreateMovieFlags               (createMovieFileDeleteCurFile | createMovieFileDontCreateResFile)
 
 
 
#pragma mark #Data Structures
/********************************************************************************
 ********************************************************************************
 ********************************************************************************
 ***        Data Structures
 ********************************************************************************
 ********************************************************************************
 ********************************************************************************/
 
/* version 1.0 data types */
 
#pragma options align=mac68k
 
/* panorama description:
 * this defines the structure of a sample description for a media sample in a panorama track
 */
typedef struct PanoramaDescription {
    long            size;                       /* total size of PanoramaDescription */
    long            type;                       /* must be 'pano' */
    long            reserved1;                  /* must be zero */
    long            reserved2;                  /* must be zero */
    short           majorVersion;               /* must be zero */
    short           minorVersion;               /* must be zero */
    long            sceneTrackID;               /* ID of video track that contains panoramic scene */
    long            loResSceneTrackID;          /* ID of video track that contains lo res panoramic scene */
    long            reserved3[6];               /* must be zero */
    long            hotSpotTrackID;             /* ID of video track that contains hotspot image */
    long            loResHotSpotTrackID;        /* ID of video track that contains lo res hotspot image */
    long            reserved4[8];               /* must be zero */
    Fixed           hPanStart;                  /* horizontal pan range start angle (eg. 0) */
    Fixed           hPanEnd;                    /* horizontal pan range end angle (eg. 360) */
    Fixed           vPanTop;                    /* vertical pan range top angle(eg. 42.5) */
    Fixed           vPanBottom;                 /* vertical pan range bottom angle (eg. -42.5) */
    Fixed           minimumZoom;                /* minimum zoom angle (eg. 5; use 0 for default)) */
    Fixed           maximumZoom;                /* maximum zoom angle (eg. 65; use 0 for default) */
    /* info for highest res version of scene track */
    long            sceneSizeX;                 /* pixel width of the panorama (eg. 768) */
    long            sceneSizeY;                 /* pixel height of the panorama (eg. 2496) */
    long            numFrames;                  /* number of diced frames (eg. 24) */
    short           reserved5;                  /* must be zero */
    short           sceneNumFramesX;            /* diced frames wide (eg. 1) */
    short           sceneNumFramesY;            /* diced frames high (eg. 24) */
    short           sceneColorDepth;            /* bit depth of the scene track (eg. 32) */
    /* info for highest res version of hotSpot track */
    long            hotSpotSizeX;               /* pixel width of the hot spot panorama (eg. 768) */
    long            hotSpotSizeY;               /* pixel height of the hot spot panorama (eg. 2496) */
    short           reserved6;                  /* must be zero */
    short           hotSpotNumFramesX;          /* diced frames wide (eg. 1) */
    short           hotSpotNumFramesY;          /* diced frames high (eg. 24) */
    short           hotSpotColorDepth;          /* must be 8 */
    
} PanoramaDescription, *PanoramaDescriptionPtr, **PanoramaDescriptionHandle;
 
/* panorama sample header atom:
 * this defines the structure of (part of) a media sample in a panorama track
 */
typedef struct PanoSampleHeaderAtom {
    long            size;
    OSType          type;                       /* must be 'pHdr' */
    unsigned long   nodeID;                     /* corresponds to a node ID in the idToTime table  */
    Fixed           defHPan;                    /* default horizontal pan angle when displaying this node */
    Fixed           defVPan;                    /* default vertical pan angle when displaying this node */
    Fixed           defZoom;                    /* default zoom angle when displaying this node */
    Fixed           minHPan;                    /* constraints for this node; use zero for default */
    Fixed           minVPan;
    Fixed           minZoom;
    Fixed           maxHPan;
    Fixed           maxVPan;
    Fixed           maxZoom;
    long            reserved1;                  /* must be zero */
    long            reserved2;                  /* must be zero */
    long            nameStrOffset;              /* offset into string table atom */
    long            commentStrOffset;           /* offset into string table atom */
} PanoSampleHeaderAtom, *PanoSampleHeaderAtomPtr, **PanoSampleHeaderAtomHandle;
 
/* hot spot atom */
typedef struct HotSpot {
    unsigned short  hotSpotID;                  /* the ID of this hot spot */
    short           reserved1;                  /* must be zero */
    OSType          type;                       /* the hot spot type (e.g. 'link',  'navg', etc) */
    unsigned long   typeData;                   /* for link and navg, the ID in the link  and navg table */
    Fixed           viewHPan;                   /* canonical view for this hot spot */
    Fixed           viewVPan;
    Fixed           viewZoom;
    Rect            hotSpotRect;                /* bounding rectangle of the hot spot in  the panorama */
    long            mouseOverCursorID;
    long            mouseDownCursorID;
    long            mouseUpCursorID;
    long            reserved2;                  /* must be 0 */
    long            nameStrOffset;              /* offset into string table atom */
    long            commentStrOffset;           /* offset into string table atom */
} HotSpot, *HotSpotPtr, **HotSpotHandle;
 
/* hot spot table atom */
typedef struct HotSpotTableAtom {
    long            size;
    OSType          type;                       /* must be 'pHot' */
    short           pad;                        /* must be 0 */
    short           numHotSpots;
    HotSpot         hotSpots[1];                /* list of hot spots */
} HotSpotTableAtom, *HotSpotTableAtomPtr, **HotSpotTableAtomHandle;
 
 
typedef struct StringTableAtom {
    long            size;
    OSType          type;                       /* must be 'strT' */
    char            bunchOstrings[1];           /* concatenated Pascal strings */
} StringTableAtom, *StringTableAtomPtr, **StringTableAtomHandle;
 
 
#pragma options align=reset
 
 
typedef struct MyPanoTrackDescriptor {
    long    versionToCreate;
 
    long    imageTileWidth;
    long    imageTileHeight;
    long    imageRefTrackIndex;
    
    long    hotSpotTileWidth;
    long    hotSpotTileHeight;
    long    hotSpotRefTrackIndex;
    
    long    fastStartTileWidth;
    long    fastStartTileHeight;
    long    fastStartRefTrackIndex;
} MyPanoTrackDescriptor;
 
 
typedef struct FaceOrientation {    float x, y, z, theta;   } FaceOrientation;
 
 
#pragma mark #Global Variables
/********************************************************************************
 ********************************************************************************
 ********************************************************************************
 ***        Global Variables
 ********************************************************************************
 ********************************************************************************
 ********************************************************************************/
 
 
static FaceOrientation faceOrientation[6] = {
/*  X       Y       Z       theta   */
    0.0f,   1.0f,   0.0f,      0.0f,    /* Front (no rotation) */
    0.0f,   1.0f,   0.0f,    -90.0f,    /* Right (rotate 90 degrees rightward about positive Y axis) */
    0.0f,   1.0f,   0.0f,   -180.0f,    /* Back (rotate 180 degrees rightward about positive Y axis) */
    0.0f,   1.0f,   0.0f,   -270.0f,    /* Left (rotate 90 degrees rightward about negative Y axis) */
    1.0f,   0.0f,   0.0f,     90.0f,    /* Top (rotate 90 degrees upward about positive X axis) */
    1.0f,   0.0f,   0.0f,    -90.0f     /* Bottom (rotate 90 degrees downward about positive X axis) */
};
 
 
/********************************************************************************
 ********************************************************************************
 ********************************************************************************
 ***        Support Functions
 ********************************************************************************
 ********************************************************************************
 ********************************************************************************/
 
 
 
/********************************************************************************
 * MyNewHandle
 ********************************************************************************/
 
static Handle
MyNewHandle(Size size)
{
    OSErr err;
    Handle h;
    
    #ifdef USE_TEMP_MEM_FIRST
        h = TempNewHandle(size, &err);
        if (err != noErr)
            h = NewHandle(size);
    #else /* USE_HEAP_MEM_FIRST */
        h = NewHandle(size);
        if (h == NULL)
            h = TempNewHandle(size, &err);
    #endif /* USE_HEAP_MEM_FIRST */
 
    return(h);
}
 
 
/********************************************************************************
 * MyNewHandleClear
 ********************************************************************************/
 
static Handle
MyNewHandleClear(Size size)
{
    OSErr err;
    Handle h;
    
    #ifdef USE_TEMP_MEM_FIRST
        h = TempNewHandle(size, &err);
        if (err == noErr)
            BlockZero(*h, size);
        else
            h = NewHandleClear(size);
    #else /* USE_HEAP_MEM_FIRST */
        h = NewHandleClear(size);
        if (h == NULL) {
            h = TempNewHandle(size, &err);
            if (err == noErr)
                BlockZero(*h, size);
        }
    #endif /* USE_HEAP_MEM_FIRST */
 
    return(h);
}
 
 
/*******************************************************************************
 * MakeTempFSSpec
 *
 *  Tack the given tmpSuffix onto the origSpec, resulting in the tmpSpec.
 *******************************************************************************/
 
static void
MakeTempFSSpec(const FSSpec *origSpec, const char *tmpSuffix, FSSpec *tmpSpec)
{
    long    sufSize, namSize;
    char    *t;
    
    *tmpSpec = *origSpec;
    namSize  = tmpSpec->name[0];
    sufSize  = strlen(tmpSuffix);
    if (namSize > (sizeof(StrFileName) - 1 - sufSize))
        namSize = (sizeof(StrFileName) - 1 - sufSize);
    tmpSpec->name[0] = namSize + sufSize;
    t = (char*)(&tmpSpec->name[namSize+1]);
    while (sufSize--)
        *t++ = *tmpSuffix++;
}
 
 
/*******************************************************************************
 * ClearFSSpec
 *******************************************************************************/
 
static void
ClearFSSpec(FSSpec *fsSpec)
{
    fsSpec->vRefNum = 0;
    fsSpec->parID   = 0;
    fsSpec->name[0] = 0;
}
 
 
/********************************************************************************
 * ConvertFloatToBigEndian
 * EndianF32_NtoB
 *
 *  Convert the specified floating-point number to big-endian format.
 ********************************************************************************/
 
#if TARGET_RT_BIG_ENDIAN
# define EndianF32_NtoB(f)          ((float)(f))
#else /* TARGET_RT_LITTLE_ENDIAN */
 
static float
EndianF32_NtoB(float f)
{
    unsigned long fl;
    
    *((float*)(&fl)) = f;
    fl = EndianU32_NtoB(fl);
    f = *((float*)(&fl));
    return(f);
}
 
#endif /* TARGET_RT_LITTLE_ENDIAN */
 
 
/*******************************************************************************
 * EndianF32NtoFixedB
 *******************************************************************************/
 
static Fixed
EndianF32NtoFixedB(float f)
{
    Fixed x;
    
    x = roundtol(f * 65536.0);
    x = EndianU32_NtoB(x);
    return(x);
}
 
 
/********************************************************************************
 * FocalLengthToFOV
 *      t = 2 atan((h-1)/(2f))
 ********************************************************************************/
 
static float
FocalLengthToFOV(float focalLength, long height)
{
    return(2.0F * atan((height - 1) * 0.5F / focalLength));
}
 
 
/********************************************************************************
 * PixelZoomToFOV
 ********************************************************************************/
 
static float
PixelZoomToFOV(float srcFocalLength, long windowHeight, float pixelZoom)
{
    return(FocalLengthToFOV(srcFocalLength * pixelZoom, windowHeight));
}
 
 
#pragma mark -
/*******************************************************************************
 * AddCStringToAtomContainer
 *      Add a C string to the specified atom container;
 *      return (through theID) the ID of the new string atom.
 *******************************************************************************/
 
static OSErr
AddCStringToAtomContainer(QTAtomContainer theContainer, QTAtom theParent, const char *theString, QTAtomID *theID)
{
    OSErr               err         = noErr;
    QTAtom              myStringAtom;
    UInt16              mySize;
    QTVRStringAtomPtr   myStringAtomPtr;
    unsigned short      stringLength;
 
    *theID = 0;             /* initialize the returned atom ID */
    
    if ((theContainer == NULL) || (theParent == 0))
        return(paramErr);
        
    
    stringLength = strlen(theString);
    mySize = sizeof(QTVRStringAtom) - 4 + stringLength;
    myStringAtomPtr = (QTVRStringAtomPtr)NewPtr(mySize);
    
    if (myStringAtomPtr != NULL) {
        myStringAtomPtr->stringUsage    = EndianU16_NtoB(1);
        myStringAtomPtr->stringLength   = EndianU16_NtoB(stringLength);
        BlockMoveData(theString, myStringAtomPtr->theString, stringLength);
        err = QTInsertChild(theContainer, theParent, kQTVRStringAtomType, 0, 0, mySize, (Ptr)myStringAtomPtr, &myStringAtom);
        DisposePtr((Ptr)myStringAtomPtr);
        
        if (err == noErr)
            QTGetAtomTypeAndID(theContainer, myStringAtom, NULL, theID);
    }
    
    return(err);
}
 
 
/*******************************************************************************
 * SetQTControllerType
 *
 *  Set the controller type of the specified movie.
 *
 * This function adds an item to the movie's user data;
 * the updated user data is written to the movie file when the movie is next updated
 * (by calling AddMovieResource or UpdateMovieResource).
 *
 *******************************************************************************/
 
static OSErr
SetQTControllerType(Movie theMovie, OSType theType)
{
    UserData    myUserData;
    OSErr       err     = noErr;
 
    /* make sure we've got a movie */
    if (theMovie == NULL)
        return(paramErr);
        
    /* get the movie's user data list */
    myUserData = GetMovieUserData(theMovie);
    if (myUserData == NULL)
        return(paramErr);
    
    theType = EndianU32_NtoB(theType);
    err = SetUserDataItem(myUserData, &theType, sizeof(theType), kQTControllerType, 0);
 
    return(err);
}
 
 
/*******************************************************************************
 * MakeQTVRHotSpot1x0
 *
 *  Create a hot spot table atom with atoms for a single hot spot; also, if necessary,
 *  resize the string table handle theStringTable to contain a name and comment for the
 *  hot spot.
 *
 *  NOTE: This function builds hot spots that conform to version 1.0 of the QuickTime VR file format.
 *******************************************************************************/
 
static HotSpotTableAtomHandle
MakeQTVRHotSpot1x0(StringTableAtomHandle theStringTable)
{
#pragma unused(theStringTable)
 
    HotSpotTableAtomHandle  myHandle = NULL;
    
    /* [left as an exercise for the reader] */
    
    return(myHandle);
}
 
 
/*******************************************************************************
 * MakeQTVRHotSpot2x0
 *
 *  Create the necessary atoms inside of theNodeInfo to configure the specified hot spot
 *  as a URL hot spot linked to the specified URL.
 *
 *  NOTE: This function builds hot spots that conform to version 2.0 of the QuickTime VR file format.
 *******************************************************************************/
 
static OSErr
MakeQTVRHotSpot2x0(
    QTAtomContainer theNodeInfo,
    QTAtom          theHSParent,
    char            *theURL,
    char            *theHSName,
    UInt32          theIndex
)
{
    QTAtom                  myHSAtom;
    QTVRHotSpotInfoAtom     myHSInfoAtom;
    QTAtomID                myHSNameID;
    OSErr                   err     = noErr;
 
    /*
     * add a hot spot atom to the specified node info atom
     *
     * a hot spot atom contains two children:
     * a hot spot information atom, which contains general info about the hot spot,
     * and (for URL hot spots) a URL hot spot atom
     */
    
    /* the atom ID should be the same as the hot spot ID, which is an index in a 8-bit color table   */
    err = QTInsertChild(theNodeInfo, theHSParent, kQTVRHotSpotAtomType, theIndex, 0, 0, NULL, &myHSAtom);
    if (err != noErr)
        goto bail;
    
    /*
     * add a hot spot information atom
     */
    
    /* fill in the fields of the hot spot information atom data structure */
    myHSInfoAtom.majorVersion   = EndianU16_NtoB(kQTVRMajorVersion);
    myHSInfoAtom.minorVersion   = EndianU16_NtoB(kQTVRMinorVersion);
    myHSInfoAtom.hotSpotType    = EndianU32_NtoB(kQTVRHotSpotURLType);
 
    /* the published documentation says that the hot spot name is contained in a string atom
     * that is a sibling of the hot spot atom (that is, a child of the hot spot parent atom);
     * some other documents indicate that a string atom is always a sibling of the atom that
     * contains the reference (in this case, a sibling of the hot spot information atom, and
     * hence a child of the hot spot atom); I'd recommend coding to the latter....
     */
    
    /* add the hot spot name atom */
    err = AddCStringToAtomContainer(theNodeInfo, myHSAtom, theHSName, &myHSNameID);
    if (err != noErr)
        goto bail;
    
    myHSInfoAtom.nameAtomID = EndianU32_NtoB(myHSNameID);
    
    /* add the hot spot comment atom; 0 means that no comment atom exists */
    myHSInfoAtom.commentAtomID = EndianU32_NtoB(0L);
    
    /* set the custom cursor IDs; 0 means that no custom cursors exist */
    myHSInfoAtom.cursorID[0] = 0;
    myHSInfoAtom.cursorID[1] = 0;
    myHSInfoAtom.cursorID[2] = 0;
    
    /* set viewing hints */
    myHSInfoAtom.bestPan    = EndianF32_NtoB(0.0);
    myHSInfoAtom.bestTilt   = EndianF32_NtoB(0.0);
    myHSInfoAtom.bestFOV    = EndianF32_NtoB(0.0);
    
    myHSInfoAtom.bestViewCenter.x = 0.0;
    myHSInfoAtom.bestViewCenter.y = 0.0;
 
    /* set hot spot bounding rectangle; apparently unused */
    myHSInfoAtom.hotSpotRect.top    = 0;
    myHSInfoAtom.hotSpotRect.left   = 0;
    myHSInfoAtom.hotSpotRect.bottom = 0;
    myHSInfoAtom.hotSpotRect.right  = 0;
 
    myHSInfoAtom.flags      = 0L;
    myHSInfoAtom.reserved1  = 0L;
    myHSInfoAtom.reserved2  = 0L;
    
    /* insert the hot spot information atom into the hot spot atom */
    err = QTInsertChild(theNodeInfo, myHSAtom, kQTVRHotSpotInfoAtomType, 1, 0, sizeof(myHSInfoAtom), &myHSInfoAtom, NULL);
    if (err != noErr)
        goto bail;
        
    /*
     * add a URL hot spot atom as a child of the hot spot atom
     */
    
    /* the atom data is the URL text
     * (not a Pascal or C string, but just the characters themselves)
     */
    err = QTInsertChild(theNodeInfo, myHSAtom, kQTVRHotSpotURLType, 1, 0, strlen(theURL), theURL, NULL);
 
bail:
    return(err);
}
 
 
/*******************************************************************************
 * CountTrackSamples
 *******************************************************************************/
 
static long
CountTrackSamples(Track track)
{
    long        numSamples;
    TimeValue   currentTime, interestingTime;
    OSErr       err;
 
    err = GetMoviesStickyError();
    interestingTime = 1;
    currentTime = 0;
    numSamples = -1; /* will iterate one more time than there is slices */
 
    while (interestingTime != currentTime) {
        currentTime = interestingTime;
        ++numSamples;
        GetTrackNextInterestingTime(track, nextTimeMediaSample, currentTime, 1, &interestingTime, NULL);
    }
    
    if (err == noErr)
        ClearMoviesStickyError();   /* Because the last GetTrackNextInterestingTime() goes beyond the end */
 
    return(numSamples);
}
 
 
/*******************************************************************************
 * ImportFirstVideoTrackFromMovie
 *
 *  Copy a video track from one movie (the source) to another (the destination).
 *
 *  We assume that we want the first video track in theSrcMovie.
 *******************************************************************************/
 
static OSErr
ImportFirstVideoTrackFromMovie(
    Movie       srcMovie,               /* Transfer the first video track of this movie... */
    Movie       dstMovie,               /* to this movie. */
    TimeValue   dstSequenceDuration,    /* duration of the whole sequence */
    long        numSamples,             /* the number of samples, or zero if the whole track is desired */
    long        *pDstTrackWidth,        /* returned track width,  in integral units */
    long        *pDstTrackHeight,       /* returned track height, in integral units */
    Track       *pDstTrack              /* the track added to the dstMovie */
)
{
    Track                       srcTrack        = NULL;
    Media                       srcMedia        = NULL;
    Track                       dstTrack        = NULL;
    Media                       dstMedia        = NULL;
    Fixed                       trackWidth, trackHeight;
    OSType                      mediaType;
    SampleDescriptionHandle     sampleDesc      = (SampleDescriptionHandle)MyNewHandle(0);
    Handle                      sampleData      = MyNewHandle(0);
    long                        sampleDataSize;
    TimeValue                   srcMediaTime    = 0;
    TimeValue                   srcSampleDuration, dstSampleDuration;
    
    ClearMoviesStickyError();
    
    /* get the first video track in the source movie */
    srcTrack = GetMovieIndTrackType(srcMovie, 1, VideoMediaType, movieTrackMediaType);
    if (srcTrack == NULL)
        return(paramErr);
    
    /* get the track's media and dimensions */
    srcMedia = GetTrackMedia(srcTrack);
    GetTrackDimensions(srcTrack, &trackWidth, &trackHeight);
    
    /* create a destination track */
    dstTrack = NewMovieTrack(dstMovie, trackWidth, trackHeight, GetTrackVolume(srcTrack));
    if (pDstTrackWidth  != NULL)        *pDstTrackWidth  = trackWidth  >> 16;
    if (pDstTrackHeight != NULL)        *pDstTrackHeight = trackHeight >> 16;
    if (pDstTrack       != NULL)        *pDstTrack       = dstTrack;
 
    /* create a destination media */
    GetMediaHandlerDescription(srcMedia, &mediaType, NULL, NULL);
    dstMedia = NewTrackMedia(dstTrack, mediaType, kQTVRStandardTimeScale, 0, 0);
    
    /* determine the number of samples */
    if (numSamples == 0)
        numSamples = CountTrackSamples(srcTrack);
    dstSampleDuration = dstSequenceDuration / numSamples;
 
    
    /* extract samples one at a time */
    BeginMediaEdits(dstMedia);
    while (numSamples--) {
        GetMediaSample(srcMedia, sampleData, 0, &sampleDataSize, srcMediaTime, NULL, &srcSampleDuration, sampleDesc, NULL, 1, NULL, NULL);
        srcMediaTime += srcSampleDuration;
        AddMediaSample(dstMedia, sampleData, 0, sampleDataSize, dstSampleDuration, sampleDesc, 1, 0, NULL);
    }
    EndMediaEdits(dstMedia);
    
    InsertMediaIntoTrack(dstTrack, 0, 0, GetMediaDuration(dstMedia), 1L << 16);
 
    DisposeHandle((Handle)sampleDesc);
    DisposeHandle(sampleData);
 
    /* a panorama image track should always be disabled */
    SetTrackEnabled(dstTrack, false);
 
    return(GetMoviesStickyError());
}
 
 
/*******************************************************************************
 * ImportFirstVideoTrackFromFile
 *******************************************************************************/
 
static OSErr
ImportFirstVideoTrackFromFile(
    const FSSpec    *srcMovieSpec,          /* Transfer the first video track from this movie... */
    Movie           dstMovie,               /* ... to this movie */
    TimeValue       dstSequenceDuration,    /* Desired time duration of the sequence */
    long            numSamples,             /* The number of samples, or zero if the whole track is desired */
    long            *pDstFrameWidth,        /* Return the width  of the track frame */
    long            *pDstFrameHeight,       /* Return the height of the track frame */
    Track           *pDstTrack              /* Return the new track */
)
{
    Movie   srcMovie    = NULL;
    short   srcRefNum   = 0;
    short   srcResID    = 0;
    OSErr   err;
 
    err = OpenMovieFile(srcMovieSpec, &srcRefNum, fsRdPerm);
    if (err != noErr)
        goto bail;
    
    err = NewMovieFromFile(&srcMovie, srcRefNum, &srcResID, 0, 0, 0);
    if (err != noErr)
        goto bail;
 
    SetMoviePlayHints(srcMovie, hintsHighQuality, hintsHighQuality);
    
    err = ImportFirstVideoTrackFromMovie(srcMovie, dstMovie, dstSequenceDuration, numSamples,
                                        pDstFrameWidth, pDstFrameHeight, pDstTrack
                                    );
 
bail:
    if (srcMovie  != NULL)  DisposeMovie(srcMovie);
    if (srcRefNum != 0)     CloseMovieFile(srcRefNum);
    
    return(err);
}
 
 
/*******************************************************************************
 * VerifyPanoParameters
 *******************************************************************************/
 
static void
VerifyPanoParameters(register MakeQTVRParams *qtvrParams, const MyPanoTrackDescriptor *panoDesc)
{
    float circumference, axialHeight, focalLengthCircumferential, focalLengthAxial, maxFOV;
 
 
    if (qtvrParams->trackDuration <= 0)
        qtvrParams->trackDuration           = 7200;
 
    /* Set default pan range, if not set appropriately */
    if (qtvrParams->maxPan == qtvrParams->minPan) {
        qtvrParams->minPan                  =   0.0f;   /* Default: 360 degree wrapping pano */
        qtvrParams->maxPan                  = 360.0f;
        qtvrParams->wraps                   = 1;
    }
 
 
    if ((panoDesc->versionToCreate == kQTVRCylindricalVersion2v) || (panoDesc->versionToCreate == kQTVRCylindricalVersion2h)) { /* we're building a version 2.0 cylindrical panorama */
 
        /* Check for valid parameters, and assign defaults. */
        if (panoDesc->versionToCreate == kQTVRCylindricalVersion2h) {   /* non-rotated pano */
            circumference                   = panoDesc->imageTileWidth  * qtvrParams->tilesH; if (!qtvrParams->wraps) circumference--;
            axialHeight                     = panoDesc->imageTileHeight * qtvrParams->tilesV;
        }
        else {                                              /* rotated pano */
            circumference                   = panoDesc->imageTileHeight * qtvrParams->tilesV; if (!qtvrParams->wraps) circumference--;
            axialHeight                     = panoDesc->imageTileWidth  * qtvrParams->tilesH;
        }
        focalLengthCircumferential          = circumference / (qtvrParams->maxPan - qtvrParams->minPan) * D_DEGREES_PER_RADIAN;
        if (qtvrParams->maxTilt == qtvrParams->minTilt) {
            focalLengthAxial                = focalLengthCircumferential;               /* Default: square pixels */
            qtvrParams->maxTilt             = atan((axialHeight - 1) / (2.0f * focalLengthAxial)) * D_DEGREES_PER_RADIAN;
            qtvrParams->minTilt             = -qtvrParams->maxTilt;
        }
        else {
            focalLengthAxial                = (axialHeight - 1) / (tan(qtvrParams->maxTilt * D_RADIANS_PER_DEGREE) - tan(qtvrParams->minTilt * D_RADIANS_PER_DEGREE));
        }
        maxFOV = qtvrParams->maxTilt - qtvrParams->minTilt;
        if ((qtvrParams->maxFieldOfView     <= 0.0f) || (qtvrParams->maxFieldOfView > maxFOV))
            qtvrParams->maxFieldOfView      = maxFOV;
        if (qtvrParams->minFieldOfView      <= 0.0f)                                            /* Set minimum to 2:1 pixel zoom */
            qtvrParams->minFieldOfView      = PixelZoomToFOV(focalLengthAxial, qtvrParams->windowHeight, 2.0f) * D_DEGREES_PER_RADIAN;
        if (qtvrParams->minFieldOfView      > qtvrParams->maxFieldOfView)
            qtvrParams->minFieldOfView      = qtvrParams->maxFieldOfView;
        if (qtvrParams->defaultFieldOfView  == 0.0f)                                        /* Set default to 1:1 pixel zoom */
            qtvrParams->defaultFieldOfView  = PixelZoomToFOV(focalLengthAxial, qtvrParams->windowHeight, 1.0f) * D_DEGREES_PER_RADIAN;
        if (qtvrParams->defaultFieldOfView  > qtvrParams->maxFieldOfView)
            qtvrParams->defaultFieldOfView  = qtvrParams->maxFieldOfView;
        if (qtvrParams->defaultFieldOfView  < qtvrParams->minFieldOfView)
            qtvrParams->defaultFieldOfView  = qtvrParams->minFieldOfView;
    }
 
    else {  /* We're building a cubic panorama */
    
        /* Check for valid parameters, and set to default if not */
        if (qtvrParams->maxTilt == qtvrParams->minTilt) {       /* Default: peg at the poles */
            qtvrParams->minTilt             = -90.0f;
            qtvrParams->maxTilt             =  90.0f;
        }
        focalLengthAxial                    = (panoDesc->imageTileHeight * qtvrParams->tilesV - 1) * 0.5f;  /* These are questionable for partial cubic panos */
        maxFOV = qtvrParams->maxTilt - qtvrParams->minTilt;
        if (maxFOV > 170.1f)                            /* 170.1 degrees (= 3 radians) limited by numerical precision in the computations */
            maxFOV = 170.1f;
        if (qtvrParams->maxFieldOfView <= 0.0f) {
            qtvrParams->maxFieldOfView          = maxFOV;
            if (qtvrParams->maxFieldOfView > 120.0)
                qtvrParams->maxFieldOfView      = 120.0f;   /* 120 degrees is the default maximum FOV */
        }
        else if (qtvrParams->maxFieldOfView > maxFOV)
            qtvrParams->maxFieldOfView          = maxFOV;
        if (qtvrParams->minFieldOfView <= 0.0f)                                     /* Set minimum to 2:1 pixel zoom */
            qtvrParams->minFieldOfView          = PixelZoomToFOV(focalLengthAxial, qtvrParams->windowHeight, 2.0f) * D_DEGREES_PER_RADIAN;
        if (qtvrParams->minFieldOfView > qtvrParams->maxFieldOfView)
            qtvrParams->minFieldOfView          = qtvrParams->maxFieldOfView;
        if (qtvrParams->defaultFieldOfView <= 0.0f)                                 /* Set default to 1:1 pixel zoom */
            qtvrParams->defaultFieldOfView      = PixelZoomToFOV(focalLengthAxial, qtvrParams->windowHeight, 1.0f) * D_DEGREES_PER_RADIAN;
        if (qtvrParams->defaultFieldOfView > qtvrParams->maxFieldOfView)
            qtvrParams->defaultFieldOfView      = qtvrParams->maxFieldOfView;
        if (qtvrParams->defaultFieldOfView < qtvrParams->minFieldOfView)
            qtvrParams->defaultFieldOfView      = qtvrParams->minFieldOfView;
    }
}
 
 
/*******************************************************************************
 * SetCubicViewData
 *******************************************************************************/
 
static void
SetCubicViewData(const MakeQTVRParams *qtvrParams, QTVRCubicViewAtom *cubicViewData)
{
    cubicViewData->minPan               = EndianF32_NtoB(qtvrParams->minPan);
    cubicViewData->maxPan               = EndianF32_NtoB(qtvrParams->maxPan);
    cubicViewData->minTilt              = EndianF32_NtoB(qtvrParams->minTilt);
    cubicViewData->maxTilt              = EndianF32_NtoB(qtvrParams->maxTilt);
    cubicViewData->minFieldOfView       = EndianF32_NtoB(qtvrParams->minFieldOfView);
    cubicViewData->maxFieldOfView       = EndianF32_NtoB(qtvrParams->maxFieldOfView);
    cubicViewData->defaultPan           = EndianF32_NtoB(qtvrParams->defaultPan);
    cubicViewData->defaultTilt          = EndianF32_NtoB(qtvrParams->defaultTilt);
    cubicViewData->defaultFieldOfView   = EndianF32_NtoB(qtvrParams->defaultFieldOfView);
}
 
 
/********************************************************************************
 * SetOneCubicFaceData
 *
 *  This assumes that the faces are equal subtiles of 6 cube faces.
 ********************************************************************************/
 
static void
SetOneCubicFaceData(
    QTVRCubicFaceData   *face,
    float               x,          /* axis of rotation */
    float               y,
    float               z,  
    float               degrees,    /* rotation about axis */
    long                tilesH,     /* the number of subdivisions to each face */
    long                tilesV,
    long                h,          /* The horizontal index of this sub-face */
    long                v           /* The vertical   index of this sub-face */
)
{
    double halfAngle, norm, sqrtCotVFOV, s, c;
    
    sqrtCotVFOV = sqrt((double)tilesV);
    halfAngle = degrees * D_PI / 360.0;
    norm = x*x + y*y + z*z;
    if (norm != 0.0)
        norm = 1.0 / sqrt(norm);
    if (fabs(s = sin(halfAngle)) < 1.0e-8)  s = 0.0;    /* Toss out numerically insignificant values ... */
    if (fabs(c = cos(halfAngle)) < 1.0e-8)  c = 0.0;    /* ... so the file format looks nice */
    norm *= s * sqrtCotVFOV;
 
    face->orientation[0]    = EndianF32_NtoB(c * sqrtCotVFOV);
    face->orientation[1]    = EndianF32_NtoB(x * norm);
    face->orientation[2]    = EndianF32_NtoB(y * norm);
    face->orientation[3]    = EndianF32_NtoB(z * norm);
    
    /* Center, normalized by the vertical dimension */
    face->center[0]         = EndianF32_NtoB((2 * h - tilesH + 1) * (float)tilesV / (float)tilesH);
    face->center[1]         = EndianF32_NtoB( 2 * v - tilesV + 1);
    face->aspect            = EndianF32_NtoB(1.0f);
    face->skew              = EndianF32_NtoB(0.0f);
}
 
 
/*******************************************************************************
 * SetCubicFaceData
 *******************************************************************************/
 
static void
SetCubicFaceData(const MakeQTVRParams *qtvrParams, QTVRCubicFaceData* faceData)
{
    long            i, j, k;
    FaceOrientation *fo;
    
    for (k = 0, fo = faceOrientation; k < 6; k++, fo++)
        for (j = 0; j < qtvrParams->tilesV; j++)
            for (i = 0; i < qtvrParams->tilesH; i++, faceData++)
                SetOneCubicFaceData(faceData, fo->x, fo->y, fo->z, fo->theta, qtvrParams->tilesH, qtvrParams->tilesV, i, j);
}
 
 
/*******************************************************************************
 * SetPanoSampleData
 *******************************************************************************/
 
static void
SetPanoSampleData(
    const MakeQTVRParams        *qtvrParams,
    const MyPanoTrackDescriptor *panoDescriptor,
    QTVRPanoSampleAtom          *panoSampleData
)
{
    panoSampleData->majorVersion            = EndianU16_NtoB(kQTVRMajorVersion);
    panoSampleData->minorVersion            = EndianU16_NtoB(kQTVRMinorVersion);
    panoSampleData->imageRefTrackIndex      = EndianU32_NtoB(panoDescriptor->imageRefTrackIndex);
    panoSampleData->hotSpotRefTrackIndex    = EndianU32_NtoB(panoDescriptor->hotSpotRefTrackIndex);
 
 
    if ((panoDescriptor->versionToCreate == kQTVRCylindricalVersion2v) || (panoDescriptor->versionToCreate == kQTVRCylindricalVersion2h)) { /* we're building a version 2.0 cylindrical panorama */
        /* Set cylindrical parameters in Pano Sample Atom */
        panoSampleData->minPan              = EndianF32_NtoB(qtvrParams->minPan);
        panoSampleData->maxPan              = EndianF32_NtoB(qtvrParams->maxPan);
        panoSampleData->minTilt             = EndianF32_NtoB(qtvrParams->minTilt);
        panoSampleData->maxTilt             = EndianF32_NtoB(qtvrParams->maxTilt);
        panoSampleData->minFieldOfView      = EndianF32_NtoB(qtvrParams->minFieldOfView);
        panoSampleData->maxFieldOfView      = EndianF32_NtoB(qtvrParams->maxFieldOfView);
        panoSampleData->defaultPan          = EndianF32_NtoB(qtvrParams->defaultPan);
        panoSampleData->defaultTilt         = EndianF32_NtoB(qtvrParams->defaultTilt);
        panoSampleData->defaultFieldOfView  = EndianF32_NtoB(qtvrParams->defaultFieldOfView);
 
        panoSampleData->imageSizeX          = EndianU32_NtoB(panoDescriptor->imageTileWidth  * qtvrParams->tilesH);
        panoSampleData->imageSizeY          = EndianU32_NtoB(panoDescriptor->imageTileHeight * qtvrParams->tilesV);
        panoSampleData->imageNumFramesX     = EndianU16_NtoB(qtvrParams->tilesH);
        panoSampleData->imageNumFramesY     = EndianU16_NtoB(qtvrParams->tilesV);
        
        panoSampleData->hotSpotSizeX        = EndianU32_NtoB(panoDescriptor->hotSpotTileWidth  * qtvrParams->tilesH);
        panoSampleData->hotSpotSizeY        = EndianU32_NtoB(panoDescriptor->hotSpotTileHeight * qtvrParams->tilesV); 
        panoSampleData->hotSpotNumFramesX   = EndianU16_NtoB(qtvrParams->tilesH);
        panoSampleData->hotSpotNumFramesY   = EndianU16_NtoB(qtvrParams->tilesV);
            
        if (panoDescriptor->versionToCreate == kQTVRCylindricalVersion2h) {
            panoSampleData->panoType        = EndianU32_NtoB(kQTVRHorizontalCylinder);  /* non-rotated pano */
            panoSampleData->flags           = EndianU32_NtoB((qtvrParams->wraps ? kQTVRPanoFlagAlwaysWrap : 0L) | kQTVRPanoFlagHorizontal); // include for compatibility with QT 4.0
        }
        else {
            panoSampleData->panoType        = EndianU32_NtoB(kQTVRVerticalCylinder);    /* rotated pano */
            panoSampleData->flags           = EndianU32_NtoB(qtvrParams->wraps ? kQTVRPanoFlagAlwaysWrap : 0L);
        }
        panoSampleData->reserved2           = EndianU32_NtoB(0L);
    }
 
    else {  /* We're building a cubic panorama: build backward-compatible pano sample data */
        panoSampleData->minPan              = EndianF32_NtoB(qtvrParams->minPan);
        panoSampleData->maxPan              = EndianF32_NtoB(qtvrParams->maxPan);
        panoSampleData->minTilt             = EndianF32_NtoB(qtvrParams->minTilt        < -45.0f ? -45.0f : qtvrParams->minTilt);
        panoSampleData->maxTilt             = EndianF32_NtoB(qtvrParams->maxTilt        >  45.0f ?  45.0f : qtvrParams->maxTilt);
        panoSampleData->minFieldOfView      = EndianF32_NtoB(qtvrParams->minFieldOfView);
        panoSampleData->maxFieldOfView      = EndianF32_NtoB(qtvrParams->maxFieldOfView >  90.0f ?  90.0f : qtvrParams->maxFieldOfView);
        panoSampleData->defaultPan          = EndianF32_NtoB(qtvrParams->defaultPan);
        panoSampleData->defaultTilt         = EndianF32_NtoB(qtvrParams->defaultTilt);
        panoSampleData->defaultFieldOfView  = EndianF32_NtoB(qtvrParams->defaultFieldOfView);
 
        panoSampleData->imageSizeX          = EndianU32_NtoB(panoDescriptor->imageTileWidth  * qtvrParams->tilesH * 4);
        panoSampleData->imageSizeY          = EndianU32_NtoB(panoDescriptor->imageTileHeight * qtvrParams->tilesV);
        panoSampleData->imageNumFramesX     = EndianU16_NtoB(qtvrParams->tilesH * 4);
        panoSampleData->imageNumFramesY     = EndianU16_NtoB(qtvrParams->tilesV);
        
        panoSampleData->hotSpotSizeX        = EndianU32_NtoB(panoDescriptor->hotSpotTileWidth  * qtvrParams->tilesH * 4);
        panoSampleData->hotSpotSizeY        = EndianU32_NtoB(panoDescriptor->hotSpotTileHeight * qtvrParams->tilesV); 
        panoSampleData->hotSpotNumFramesX   = EndianU16_NtoB(qtvrParams->tilesH * 4);
        panoSampleData->hotSpotNumFramesY   = EndianU16_NtoB(qtvrParams->tilesV);
            
        /* Set the horizontal flag so that pre-cubic QTVR engine can at least display the four sides of the cube */
        panoSampleData->flags               = EndianU32_NtoB(kQTVRPanoFlagHorizontal);
        panoSampleData->panoType            = EndianU32_NtoB(kQTVRCube);
        panoSampleData->reserved2           = EndianU32_NtoB(0L);
    }
}
 
 
/********************************************************************************
 * TileGWorldsToNewTrack
 ********************************************************************************/
 
static OSErr
TileGWorldsToNewTrack(
    long        numGWs,                 /* the number of GWorlds */
    GWorldPtr   *srcGWs,                /* Source GWorlds - in standard order */
    long        overlapTiles,           /* Whether the tiles should overlap or not */
    CodecType   tileCodec,
    CodecQ      tileQuality,
    long        tilesH,
    long        tilesV,
    long        tileDepth,
    Movie       dstMovie,               /* to be tiled and added to this movie. */
    TimeValue   dstSequenceDuration,    /* duration of the whole sequence */
    long        *pDstTrackWidth,        /* returned track width,  in integral units */
    long        *pDstTrackHeight,       /* returned track height, in integral units */
    Track       *pDstTrack              /* the track added to the dstMovie */
)
{
    PixMapHandle                pm;
    Rect                        tileRect;
    Track                       tileTrack       = NULL;
    Media                       tileMedia       = NULL;
    ImageDescriptionHandle      tileImageDesc   = NULL;
    Handle                      tileData        = NULL;
    Ptr                         tileDataPtr     = NULL;
    long                        tileFrameSize;
    TimeValue                   dstTileDuration = dstSequenceDuration / (tilesH * tilesV);
    OSErr                       err             = noErr;
    long                        i, j, k;
    #if USE_TILE_DISPLAY_WINDOW
        WindowRef               tileWindow;
    #endif /* USE_TILE_DISPLAY_WINDOW */
    
    /* Determine tile size */
    #if ACCESSOR_CALLS_ARE_FUNCTIONS
        GetPortBounds(srcGWs[0], &tileRect);
    #else /* ACCESSOR_CALLS_ARE_FUNCTIONS */
        tileRect = srcGWs[0]->portRect;
    #endif /* ACCESSOR_CALLS_ARE_FUNCTIONS */
    MacOffsetRect(&tileRect, -tileRect.left, -tileRect.top);
    if (overlapTiles) {
        tileRect.right  += tilesH - 1;  /* Adjust for overlap at seams */
        tileRect.bottom += tilesV - 1;  /* Adjust for overlap at seams */
    }
    if ((tileRect.right % tilesH != 0) || (tileRect.bottom % tilesV != 0)) {    /* Check for evenly divisible tiles */
        err = paramErr;     /* Oops: tiles are not evenly divisible */
        goto bail;
    }
    tileRect.right  /= tilesH;
    tileRect.bottom /= tilesV;
    if (pDstTrackWidth  != NULL)    *pDstTrackWidth  = tileRect.right;
    if (pDstTrackHeight != NULL)    *pDstTrackHeight = tileRect.bottom;
    
    /* Create tile view, if desired */
    #if USE_TILE_DISPLAY_WINDOW
        MacOffsetRect(&tileRect, 20, 100);
        tileWindow = NewCWindow(NULL, &tileRect, "\pUncompressed tiles", true, noGrowDocProc, (WindowPtr)-1L, true, 0);
        MacOffsetRect(&tileRect, -20, -100);
    #endif /* USE_TILE_DISPLAY_WINDOW */
    
    /* Create the tile movie track and media */
    tileTrack = NewMovieTrack(dstMovie, (long)tileRect.right << 16, (long)tileRect.bottom << 16, kNoVolume);    
    tileMedia = NewTrackMedia(tileTrack, VideoMediaType, 600, NULL, 0);
    if ((tileTrack == NULL) || (tileMedia == NULL))
        goto bail;
    if (pDstTrack != NULL)  *pDstTrack = tileTrack;
    
    /*
     * dice the picture into pieces, compress them, and add the compressed tiles as video samples to the movie
     */
 
    /* create an image description handle; this is resized and filled in below by FCompressImage */
    tileImageDesc = (ImageDescriptionHandle)MyNewHandleClear(4);
    if (tileImageDesc == NULL)
        goto bail;
 
    pm = GetGWorldPixMap(srcGWs[0]);
    LockPixels(pm);
    err = GetMaxCompressionSize(pm, &tileRect, tileDepth, tileQuality, tileCodec, (CompressorComponent)anyCodec, &tileFrameSize);
    UnlockPixels(pm);
    if (err != noErr)
        goto bail;
    tileData = MyNewHandle(tileFrameSize);
    if (tileData == NULL)
        goto bail;
    HLock(tileData);
    tileDataPtr = *tileData;
    BeginMediaEdits(tileMedia);
            
    /* tile, compress, and write tiles */
    for (k = 0; k < numGWs; k++) {
        pm = GetGWorldPixMap(srcGWs[k]);
        for (j = 0; j < tilesV; j++) {
            for (i = 0; i < tilesH; i++) {
                Rect r;
                if (overlapTiles) {
                    r.left = i * (tileRect.right  - 1);
                    r.top  = j * (tileRect.bottom - 1);
                }
                else {
                    r.left = i * tileRect.right;
                    r.top  = j * tileRect.bottom;
                }
                r.right  = r.left + tileRect.right;
                r.bottom = r.top  + tileRect.bottom;
                
                LockPixels(pm);
 
                /* Draw the picture into the window we've created */
                #if USE_TILE_DISPLAY_WINDOW
                    if (tileWindow != NULL) {
                        GrafPtr savePort;
                        GetPort(&savePort);
                    #if TARGET_API_MAC_CARBON
                        MacSetPort(GetWindowPort(tileWindow));
                        CopyBits((BitMap*)(*(pm)),(BitMap*)*GetPortPixMap(GetWindowPort(tileWindow)), &(**(pm)).bounds, GetPortBounds(GetWindowPort(tileWindow), NULL), ditherCopy, NULL);
                    #else
                        MacSetPort(tileWindow);
                        CopyBits((BitMap*)(*(pm)),&(tileWindow->portBits), &(**(pm)).bounds, &tileWindow->portRect, ditherCopy, NULL);
                    #endif
                        MacSetPort(savePort);
                    }
                #endif /* USE_TILE_DISPLAY_WINDOW */
 
                err = FCompressImage(pm, &r, tileDepth, tileQuality, tileCodec, (CompressorComponent)anyCodec, NULL, 0, 0, NULL, NULL, tileImageDesc, tileDataPtr);
                UnlockPixels(pm);
                if (err != noErr)
                    goto bail;
                err = AddMediaSample(tileMedia, tileData, 0, (**tileImageDesc).dataSize, dstTileDuration, (SampleDescriptionHandle)tileImageDesc, 1L, 0, NULL);
                if (err != noErr)
                    goto bail;
            }
        }       
    }
    
    HUnlock(tileData);
    
    EndMediaEdits(tileMedia);
    
    
    /* add the media to the track, at time 0 */
    err = InsertMediaIntoTrack(tileTrack, 0, 0, GetMediaDuration(tileMedia), fixed1);
    if (err != noErr)
        goto bail;
 
    /* a panorama image track should always be disabled */
    SetTrackEnabled(tileTrack, false);
    
bail:
    if (tileData      != NULL)  DisposeHandle(tileData);
    if (tileImageDesc != NULL)  DisposeHandle((Handle)tileImageDesc);
    #if USE_TILE_DISPLAY_WINDOW
        if (tileWindow!= NULL)  DisposeWindow(tileWindow);
    #endif /* USE_TILE_DISPLAY_WINDOW */
 
    return(err);
}
 
 
/*******************************************************************************
 * TilePictsToNewTrack
 *******************************************************************************/
 
static OSErr
TilePictsToNewTrack(
    long        numPicts,
    FSSpec      **srcPictSpecs,
    long        overlapTiles,           /* Whether the tiles should overlap or not */
    CodecType   tileCodec,
    CodecQ      tileQuality,
    long        tilesH,
    long        tilesV,
    short       tileDepth,
    Movie       dstMovie,               /* to be tiled and added to this movie. */
    TimeValue   dstSequenceDuration,    /* duration of the whole sequence */
    long        *pDstTrackWidth,        /* returned track width,  in integral units */
    long        *pDstTrackHeight,       /* returned track height, in integral units */
    Track       *pDstTrack              /* the track added to the dstMovie */
)
{
    GraphicsImportComponent     theImporter     = NULL;
    Rect                        pictRect, tileRect, drawRect;
    long                        tileWidth, tileHeight;
    GWorldPtr                   gw              = NULL;
    PixMapHandle                pm              = NULL;
    Track                       tileTrack       = NULL;
    Media                       tileMedia       = NULL;
    ImageDescriptionHandle      tileImageDesc   = NULL;
    Handle                      tileData        = NULL;
    Ptr                         tileDataPtr     = NULL;
    long                        tileFrameSize;
    TimeValue                   dstTileDuration = dstSequenceDuration / (tilesH * tilesV);
    OSErr                       err             = noErr;
    long                        i, j, k;
    FSSpec                      fsSpec;
    #if USE_TILE_DISPLAY_WINDOW
        WindowRef               tileWindow;
    #endif /* USE_TILE_DISPLAY_WINDOW */
    
    if ((srcPictSpecs == NULL) || (srcPictSpecs[0] == NULL) || (srcPictSpecs[0][0].name[0] == 0)) {
        err = paramErr;
        goto bail;
    }
 
    /* Create a GWorld to draw the uncompressed tiles into */
    fsSpec = srcPictSpecs[0][0];
    err = GetGraphicsImporterForFile(&fsSpec, &theImporter);
    if (err != noErr)
        goto bail;
    err = GraphicsImportGetBoundsRect(theImporter, &pictRect);
    if (err != noErr)
        goto bail;
    CloseComponent(theImporter);
    theImporter = NULL;
    tileWidth   = pictRect.right  - pictRect.left;
    tileHeight  = pictRect.bottom - pictRect.top;
    if (overlapTiles) {
        tileWidth  += tilesH - 1;   /* Adjust for overlap at seams */
        tileHeight += tilesV - 1;   /* Adjust for overlap at seams */
    }
    if ((tileWidth % tilesH != 0) || (tileHeight % tilesV != 0)) {  /* Check for evenly divisible tiles */
        err = paramErr;     /* Oops: tiles are not evenly divisible */
        goto bail;
    }
    tileWidth  /= tilesH;
    tileHeight /= tilesV;
    if (pDstTrackWidth  != NULL)    *pDstTrackWidth  = tileWidth;
    if (pDstTrackHeight != NULL)    *pDstTrackHeight = tileHeight;
    MacSetRect(&tileRect, 0, 0, tileWidth, tileHeight);
    err = NewGWorld(&gw, tileDepth, &tileRect, NULL, NULL, 0L);
    if (err != noErr)
        goto bail;
 
    
    /* Debugging or progress window */
    #if USE_TILE_DISPLAY_WINDOW
        MacSetRect(&tileRect, 0, 0, tileWidth, tileHeight);
        MacOffsetRect(&tileRect, 20, 100);
        tileWindow = NewCWindow(NULL, &tileRect, "\pUncompressed tiles", true, noGrowDocProc, (WindowPtr)-1L, true, 0);
        MacOffsetRect(&tileRect, -20, -100);
    #endif /* USE_TILE_DISPLAY_WINDOW */
 
    tileTrack = NewMovieTrack(dstMovie, tileWidth << 16, tileHeight << 16, kNoVolume);  
    tileMedia = NewTrackMedia(tileTrack, VideoMediaType, 600, NULL, 0);
    if ((tileTrack == NULL) || (tileMedia == NULL))
        goto bail;
    if (pDstTrack != NULL)  *pDstTrack = tileTrack;
    
    /* Dice the picture into pieces, compress them, and add the compressed tiles as video samples to the movie */
    tileImageDesc = (ImageDescriptionHandle)MyNewHandleClear(4); /* this is resized and filled in below by FCompressImage */
    if (tileImageDesc == NULL)
        goto bail;
    pm = GetGWorldPixMap(gw);
    LockPixels(pm);
    err = GetMaxCompressionSize(pm, &tileRect, tileDepth, tileQuality, tileCodec, (CompressorComponent)anyCodec, &tileFrameSize);
    UnlockPixels(pm);
    if (err != noErr)
        goto bail;
    tileData = MyNewHandle(tileFrameSize);
    if (tileData == NULL)
        goto bail;
    BeginMediaEdits(tileMedia);
    
    for (k = 0; k < numPicts; k++) {
        fsSpec = srcPictSpecs[0][k];
        err = GetGraphicsImporterForFile(&fsSpec, &theImporter);
        if (err != noErr)
            goto bail;
        for (j = 0; j < tilesV; j++) {
            for (i = 0; i < tilesH; i++) {
                /* Offset to the proper subrect for this tile */
                drawRect = pictRect;
                if (overlapTiles)   MacOffsetRect(&drawRect, -i * (tileWidth-1), -j * (tileHeight-1)); 
                else                MacOffsetRect(&drawRect, -i *  tileWidth,    -j *  tileHeight); 
                
                /* Draw the picture into the window we've created */
                #if USE_TILE_DISPLAY_WINDOW
                    if (tileWindow != NULL) {
                        GraphicsImportSetGWorld(theImporter, (CGrafPtr)tileWindow, NULL);
                        GraphicsImportSetBoundsRect(theImporter, &drawRect);
                        GraphicsImportDraw(theImporter);
                    }
                #endif /* USE_TILE_DISPLAY_WINDOW */
 
                /* Draw the picture into the uncompressed tile GWorld */
                GraphicsImportSetGWorld(theImporter, gw, NULL);
                GraphicsImportSetBoundsRect(theImporter, &drawRect);
                GraphicsImportDraw(theImporter);
                
                /* Compress the tile and add it tile to the movie */
                HLock(tileData);
                tileDataPtr = *tileData;
                LockPixels(pm);
                err = FCompressImage(pm, &tileRect, tileDepth, tileQuality, tileCodec, (CompressorComponent)anyCodec,
                    NULL, 0, 0, NULL, NULL, tileImageDesc, tileDataPtr
                );
                UnlockPixels(pm);
                if (err != noErr)
                    goto bail;
                err = AddMediaSample(tileMedia, tileData, 0, (**tileImageDesc).dataSize, dstTileDuration,
                    (SampleDescriptionHandle)tileImageDesc, 1L, 0, NULL
                );
                if (err != noErr)
                    goto bail;
            }
        }
        CloseComponent(theImporter);
        theImporter = NULL;
    }
    
    EndMediaEdits(tileMedia);
    DisposeHandle(tileData);
    tileData = NULL;
    
    /* add the media to the track, at time 0 */
    err = InsertMediaIntoTrack(tileTrack, 0, 0, GetMediaDuration(tileMedia), fixed1);
    if (err != noErr)
        goto bail;
 
    /* a panorama image track should always be disabled */
    SetTrackEnabled(tileTrack, false);
    
bail:
    if (theImporter   != NULL)  CloseComponent(theImporter);
    if (gw            != NULL)  DisposeGWorld(gw);
    if (tileData      != NULL)  DisposeHandle(tileData);
    if (tileImageDesc != NULL)  DisposeHandle((Handle)tileImageDesc);
    #if USE_TILE_DISPLAY_WINDOW
        if (tileWindow!= NULL)  DisposeWindow(tileWindow);
    #endif /* USE_TILE_DISPLAY_WINDOW */
 
    return(err);
}
 
 
/********************************************************************************
 * FlattenQTVRMovie
 ********************************************************************************/
 
static OSErr
FlattenQTVRMovie(Movie srcMovie, FSSpec *dstMovieSpec)
{
    ComponentDescription desc;
    Component flattener;
    ComponentInstance qtvrExport = nil;
    OSErr err;
 
    desc.componentType          = MovieExportType;
    desc.componentSubType       = MovieFileType;
    desc.componentManufacturer  = QTVRFlattenerType; 
    flattener = FindNextComponent(NULL, &desc);
    
    /* Use the specialized QuickTimeVR flattener if it is available */
    if (flattener && ((qtvrExport = OpenComponent(flattener)) != NULL)) {
        /* http://developer.apple.com/techpubs/quicktime/qtdevdocs/VR/tp_qt3ref_vrcre8mov.13.htm */
        #if 0   /* setting a preview image - this code put here in case someone wants to do it - not tested */
            Boolean yes = true;
            QTAtomContainer exportData;
            QTAtom parent;
            err = QTNewAtomContainer(&exportData);
            err = QTInsertChild (exportData, kParentAtomIsContainer, QTVRFlattenerParentAtomType, 1, 0, 0, nil, &parent);
            err = QTInsertChild (exportData, parent, QTVRImportPreviewAtomType, 1, 0, sizeof (yes), &yes, nil);
            err = QTInsertChild (exportData, parent, QTVRImportSpecAtomType, 1, 0, sizeof (previewSpec), &previewSpec, nil);
            MovieExportSetSettingsFromAtomContainer (qtvrExport, exportData);
 
            /* Overriding the compression settings */
            ComponentInstance sc;
            QTAtomContainer compressorData;
            SCSpatialSettings ss;
            sc = OpenDefaultComponent(StandardCompressionType,StandardCompressionSubType);
            ss.codecType = kCinepakCodecType;
            ss.codec = nil;
            ss.depth = 0;
            ss.spatialQuality = codecHighQuality
            err = SCSetInfo(sc, scSpatialSettingsType, &ss);
            err = SCGetSettingsAsAtomContainer(sc, &compressorData);
            MovieExportSetSettingsFromAtomContainer (qtvrExport, compressorData);
        #endif /* 0 */
        err = MovieExportToFile(qtvrExport, dstMovieSpec, srcMovie, NULL, 0, 0);
    }
    else {  /* Use the generic movie flattener */
        Movie dstMovie;
        SetMovieProgressProc(srcMovie, (MovieProgressUPP)-1, 0L);
        dstMovie = FlattenMovieData(srcMovie,
            flattenDontInterleaveFlatten | flattenAddMovieToDataFork | flattenForceMovieResourceBeforeMovieData,
            dstMovieSpec, FOUR_CHAR_CODE('TVOD'),
            smSystemScript, createMovieFileDeleteCurFile | createMovieFileDontCreateResFile
        );
        if ((dstMovie == NULL) || ((err = GetMoviesError()) == fnOpnErr)) {
            DeleteMovieFile(dstMovieSpec);  /* I don't know why, but sometimes this is necessary */
            dstMovie = FlattenMovieData(srcMovie,
                flattenDontInterleaveFlatten | flattenAddMovieToDataFork | flattenForceMovieResourceBeforeMovieData,
                dstMovieSpec, FOUR_CHAR_CODE('TVOD'),
                smSystemScript, createMovieFileDeleteCurFile | createMovieFileDontCreateResFile
            );
            if ((dstMovie == NULL) && ((err = GetMoviesError()) == noErr))
                err = writErr; /* Something is wrong */
        }
        if (dstMovie != NULL)
            DisposeMovie(dstMovie);
    }
    return(err);
}
 
 
#pragma mark -
/********************************************************************************
 * CreateVRWorld
 *
 *  Create a VR world atom container and add the basic required atoms to it
 *  Also, create a node information atom container and add a node header atom to it.
 *  Return both atom containers.
 *
 *  The caller is responsible for disposing of the VR world and the node information atom
 *  (by calling QTDisposeAtomContainer).
 *
 *  This function assumes that the scene described by the VR world contains
 *  a single node whose type is specified by the theNodeType parameter.
 ********************************************************************************/
 
static OSErr
CreateVRWorld(
    OSType                  theNodeType,
    const MakeQTVRParams    *qtvrParams,
    QTAtomContainer         *theVRWorld,
    QTAtomContainer         *theNodeInfo
)
{
    QTAtomContainer         myVRWorld           = NULL;
    QTAtomContainer         myNodeInfo          = NULL;
    QTVRWorldHeaderAtom     myVRWorldHeaderAtom;
    QTAtom                  myImagingParentAtom;
    QTAtom                  myNodeParentAtom;
    QTAtom                  myHSParentAtom;
    QTAtom                  myNodeAtom;
    QTVRPanoImagingAtom     myPanoImagingAtom;
    QTVRNodeLocationAtom    myNodeLocationAtom;
    QTVRNodeHeaderAtom      myNodeHeaderAtom;
    UInt32                  myIndex;
    OSErr                   err                 = noErr;
 
    /* Create a VR world atom container */
    err = QTNewAtomContainer(&myVRWorld);
    if (err != noErr)
        goto bail;
 
    /* Add a VR world header atom to the VR world */
    myVRWorldHeaderAtom.majorVersion = EndianU16_NtoB(kQTVRMajorVersion);
    myVRWorldHeaderAtom.minorVersion = EndianU16_NtoB(kQTVRMinorVersion);
 
    /* Insert the scene name string, if we have one; if not, set nameAtomID to 0 */
    if (qtvrParams->sceneName) {
        QTAtomID            myID;
        
        err = AddCStringToAtomContainer(myVRWorld, kParentAtomIsContainer, qtvrParams->sceneName, &myID);
        myVRWorldHeaderAtom.nameAtomID  = EndianU32_NtoB(myID);
    } else {
        myVRWorldHeaderAtom.nameAtomID  = EndianU32_NtoB(0L);
    }
    
    myVRWorldHeaderAtom.defaultNodeID   = EndianU32_NtoB(kDefaultNodeID);
    myVRWorldHeaderAtom.vrWorldFlags    = EndianU32_NtoB(0L);
    myVRWorldHeaderAtom.reserved1       = EndianU32_NtoB(0L);
    myVRWorldHeaderAtom.reserved2       = EndianU32_NtoB(0L);
 
    /* Add the atom to the atom container (the VR world) */
    err = QTInsertChild(myVRWorld, kParentAtomIsContainer, kQTVRWorldHeaderAtomType, 1, 1, sizeof(QTVRWorldHeaderAtom), &myVRWorldHeaderAtom, NULL);
    if (err != noErr)
        goto bail;
        
    /* Add an imaging parent atom to the VR world and insert imaging atoms into it
     *
     * Imaging atoms describe the default imaging characteristics for the different types of nodes in the scene;
     * currently, only the panorama imaging atoms are defined, so we'll include those (even in object movies)
     */
 
    err = QTInsertChild(myVRWorld, kParentAtomIsContainer, kQTVRImagingParentAtomType, 1, 1, 0, NULL, &myImagingParentAtom);
    if (err != noErr)
        goto bail;
        
    /* Fill in the fields of the panorama imaging atom structure */
    myPanoImagingAtom.majorVersion      = EndianU16_NtoB(kQTVRMajorVersion);
    myPanoImagingAtom.minorVersion      = EndianU16_NtoB(kQTVRMinorVersion);
    myPanoImagingAtom.correction        = EndianU32_NtoB(kQTVRFullCorrection);
    myPanoImagingAtom.imagingValidFlags = EndianU32_NtoB(kQTVRValidCorrection | kQTVRValidQuality | kQTVRValidDirectDraw);
    myPanoImagingAtom.reserved1         = EndianU32_NtoB(0L);
    myPanoImagingAtom.reserved2         = EndianU32_NtoB(0L);
    for (myIndex = 0; myIndex < 6; myIndex++)
        myPanoImagingAtom.imagingProperties[myIndex] = EndianU32_NtoB(0L);
    
    /* Add a panorama imaging atom for kQTVRMotion state */
    myPanoImagingAtom.quality       = EndianU32_NtoB(qtvrParams->dynamicQuality);
    myPanoImagingAtom.directDraw    = EndianU32_NtoB(true);
    myPanoImagingAtom.imagingMode   = EndianU32_NtoB(kQTVRMotion);
    err = QTInsertChild(myVRWorld, myImagingParentAtom, kQTVRPanoImagingAtomType, 0, 0, sizeof(QTVRPanoImagingAtom), &myPanoImagingAtom, NULL);
    if (err != noErr)
        goto bail;
        
    /* Add a panorama imaging atom for kQTVRStatic state */
    myPanoImagingAtom.quality       = EndianU32_NtoB(qtvrParams->staticQuality);
    myPanoImagingAtom.directDraw    = EndianU32_NtoB(false);
    myPanoImagingAtom.imagingMode   = EndianU32_NtoB(kQTVRStatic);
    err = QTInsertChild(myVRWorld, myImagingParentAtom, kQTVRPanoImagingAtomType, 0, 0, sizeof(QTVRPanoImagingAtom), &myPanoImagingAtom, NULL);
    if (err != noErr)
        goto bail;
        
    /* Add a node parent atom to the VR world and insert node ID atoms into it */
    err = QTInsertChild(myVRWorld, kParentAtomIsContainer, kQTVRNodeParentAtomType, 1, 1, 0, NULL, &myNodeParentAtom);
    if (err != noErr)
        goto bail;
        
    /* Add a node ID atom */
    err = QTInsertChild(myVRWorld, myNodeParentAtom, kQTVRNodeIDAtomType, kDefaultNodeID, 0, 0, NULL, &myNodeAtom);
    if (err != noErr)
        goto bail;
    
    /* Add a single node location atom to the node ID atom */
    myNodeLocationAtom.majorVersion     = EndianU16_NtoB(kQTVRMajorVersion);
    myNodeLocationAtom.minorVersion     = EndianU16_NtoB(kQTVRMinorVersion);
    myNodeLocationAtom.nodeType         = EndianU32_NtoB(theNodeType);
    myNodeLocationAtom.locationFlags    = EndianU32_NtoB(kQTVRSameFile);
    myNodeLocationAtom.locationData     = EndianU32_NtoB(0);
    myNodeLocationAtom.reserved1        = EndianU32_NtoB(0);
    myNodeLocationAtom.reserved2        = EndianU32_NtoB(0);
    
    /* Insert the node location atom into the node ID atom */
    err = QTInsertChild(myVRWorld, myNodeAtom, kQTVRNodeLocationAtomType, 1, 1, sizeof(QTVRNodeLocationAtom), &myNodeLocationAtom, NULL);
    if (err != noErr)
        goto bail;
    
    /*  Create a node information atom container and add a node header atom to it */
    
    err = QTNewAtomContainer(&myNodeInfo);
    if (err != noErr)
        goto bail;
 
    myNodeHeaderAtom.majorVersion   = EndianU16_NtoB(kQTVRMajorVersion);
    myNodeHeaderAtom.minorVersion   = EndianU16_NtoB(kQTVRMinorVersion);
    myNodeHeaderAtom.nodeType       = EndianU32_NtoB(theNodeType);
    myNodeHeaderAtom.nodeID         = EndianU32_NtoB(kDefaultNodeID);
    myNodeHeaderAtom.commentAtomID  = EndianU32_NtoB(0L);
    myNodeHeaderAtom.reserved1      = EndianU32_NtoB(0L);
    myNodeHeaderAtom.reserved2      = EndianU32_NtoB(0L);
    
    /* Insert the node name string into the node info atom container */
    if (qtvrParams->nodeName) {
        QTAtomID            myID;
        
        err = AddCStringToAtomContainer(myNodeInfo, kParentAtomIsContainer, qtvrParams->nodeName, &myID);
        myNodeHeaderAtom.nameAtomID = EndianU32_NtoB(myID);
    } else {
        myNodeHeaderAtom.nameAtomID = EndianU32_NtoB(0L);
    }
    
    /* Insert the node header atom into the node info atom container */
    err = QTInsertChild(myNodeInfo, kParentAtomIsContainer, kQTVRNodeHeaderAtomType, 1, 1, sizeof(QTVRNodeHeaderAtom), &myNodeHeaderAtom, NULL);
    if (err != noErr)
        goto bail;
    
    /*
     * Add a hot spot parent atom to the node information atom container and insert hot spot atoms into it
     */
        
    /* Insert the hot spot parent atom into the node info atom container */
    err = QTInsertChild(myNodeInfo, kParentAtomIsContainer, kQTVRHotSpotParentAtomType, 1, 1, 0, NULL, &myHSParentAtom);
    if (err != noErr)
        goto bail;
        
#ifdef ADD_ALL_HOTSPOTS
    /* The following loop adds all possible hot spot atoms to the hot spot parent atom;
     * we do this because we don't know how many hot spots are in the hot spot image or
     * what their color-table indices are; you will want to handle a real hot spot image
     * differently, no doubt....
     */
    
    for (myIndex = 1; myIndex < 255; myIndex++) {
        char myHSName[100];
        char myURL[] = "http://www.apple.com";
        
        sprintf(myHSName, "Hot Spot Index %d", myIndex);
 
        err = MakeQTVRHotSpot2x0(myNodeInfo, myHSParentAtom, myURL, myHSName, myIndex);
        if (err != noErr)
            goto bail;
    }
#endif /* ADD_ALL_HOTSPOTS */
    
bail:
    /* return the atom containers that we've created and configured here */
    *theVRWorld     = myVRWorld;
    *theNodeInfo    = myNodeInfo;
    
    return(err);
}
 
 
/*******************************************************************************
 * CreateQTVRTrack
 *******************************************************************************/
 
static OSErr
CreateQTVRTrack(const MakeQTVRParams *qtvrParams, TimeValue dstTrackDuration, Movie movie, Track *qtvrTrack)
{
    Track                           myQTVRTrack;
    Media                           myQTVRMedia;
    QTAtomContainer                 myVRWorld           = NULL;
    QTAtomContainer                 myNodeInfo          = NULL;
    QTVRSampleDescriptionHandle     myQTVRDesc          = NULL;
    long                            mySize;
    OSErr                           err;
 
    myQTVRTrack = NewMovieTrack(movie, qtvrParams->windowWidth << 16, qtvrParams->windowHeight << 16, kFullVolume);
    myQTVRMedia = NewTrackMedia(myQTVRTrack, kQTVRQTVRType, kQTVRStandardTimeScale, NULL, 0);
    if ((myQTVRTrack == NULL) || (myQTVRMedia == NULL)) {
        err = GetMoviesError();
        goto bail;
    }
    
    SetMovieTimeScale(movie, kQTVRStandardTimeScale);
 
    /* create a VR world atom container and a node information atom container;
     * remember that the VR world becomes part of the QTVR sample description,
     * and the node information atom container becomes the media sample data
     */
    err = CreateVRWorld(kQTVRPanoramaType, qtvrParams, &myVRWorld, &myNodeInfo);
    if ((err != noErr) || (myVRWorld == NULL) || (myNodeInfo == NULL))
        goto bail;
    
    /* create a QTVR sample description */
    mySize = sizeof(QTVRSampleDescription) + GetHandleSize((Handle)myVRWorld) - sizeof(long);
    myQTVRDesc = (QTVRSampleDescriptionHandle)MyNewHandleClear(mySize);
    if (myQTVRDesc == NULL)
        goto bail;
        
    (**myQTVRDesc).descSize     = mySize;
    (**myQTVRDesc).descType     = kQTVRQTVRType;
    (**myQTVRDesc).reserved1    = 0;
    (**myQTVRDesc).reserved2    = 0;
    (**myQTVRDesc).dataRefIndex = 0;
 
    /* copy the VR world atom container into the data field of the QTVR sample description */
    BlockMoveData(*((Handle)myVRWorld), &((**myQTVRDesc).data), GetHandleSize((Handle)myVRWorld));
    
    /* Create the media sample and add it to the QTVR track */
    BeginMediaEdits(myQTVRMedia);
    err = AddMediaSample(myQTVRMedia, (Handle)myNodeInfo, 0, GetHandleSize((Handle)myNodeInfo), dstTrackDuration, (SampleDescriptionHandle)myQTVRDesc, 1, 0, NULL);
    if (err != noErr)
        goto bail;
    EndMediaEdits(myQTVRMedia);
    err = InsertMediaIntoTrack(myQTVRTrack, 0, 0, dstTrackDuration, fixed1);
    
bail:
    if (myQTVRDesc  != NULL)    DisposeHandle((Handle)myQTVRDesc);
    if (myVRWorld   != NULL)    QTDisposeAtomContainer(myVRWorld);
    if (myNodeInfo  != NULL)    QTDisposeAtomContainer(myNodeInfo);
    *qtvrTrack = myQTVRTrack;
    return(err);
}
 
 
/*******************************************************************************
 * CreatePanoMediaSample
 *******************************************************************************/
 
static OSErr
CreatePanoMediaSample(
    const MakeQTVRParams        *qtvrParams,
    const MyPanoTrackDescriptor *panoDescriptor,
    QTAtomContainer             *thePanoSample
)
{
    OSErr               err;
    QTAtomContainer     panoSample;
    QTVRPanoSampleAtom  myPanoSampleData;
 
 
    /* Create a pano media sample container */
    err = QTNewAtomContainer(&panoSample);
    if (err != noErr)
        goto bail;
    
    /* Insert the pano sample atom into the pano sample atom container */
    SetPanoSampleData(qtvrParams, panoDescriptor, &myPanoSampleData);
    err = QTInsertChild(panoSample, kParentAtomIsContainer, kQTVRPanoSampleDataAtomType, 1, 1, sizeof(QTVRPanoSampleAtom), &myPanoSampleData, NULL);
    if (err != noErr)
        goto bail;
 
 
    /* Attach a fast start preview track, if applicable */
    if (panoDescriptor->fastStartRefTrackIndex) {
        QTVRTrackRefEntry trackRefEntry;
        trackRefEntry.trackRefType    = kQTVRImageTrackRefType;
        trackRefEntry.trackResolution = kQTVRPreviewTrackRes;
        trackRefEntry.trackRefIndex   = panoDescriptor->fastStartRefTrackIndex;
        err = QTInsertChild(panoSample, kParentAtomIsContainer,
            kQTVRTrackRefArrayAtomType, 1, 1, sizeof(QTVRTrackRefEntry), &trackRefEntry, nil);
    }
 
 
    /* Set the view and face orientation for cubic panoramas;
     * this atom will be ignored in pre-cubic versions of QTVR
     */
    if (panoDescriptor->versionToCreate == kQTVRCubicVersion1) {
 
        /* Specify the default and media-constrained view parameters */
        QTVRCubicViewAtom       myCubicViewAtom;
        SetCubicViewData(qtvrParams, &myCubicViewAtom);
        err = QTInsertChild(panoSample, kParentAtomIsContainer, kQTVRCubicViewAtomType, 1, 1, sizeof(QTVRCubicViewAtom), &myCubicViewAtom, NULL);
        if (err != noErr)
            goto bail;
 
        /* Specify the orientation of each face */
        #define FORCE_CUBIC_FACE_DATA
        #ifndef FORCE_CUBIC_FACE_DATA
            if (qtvrParams->tilesH * qtvrParams->tilesV != 1)       /* We use the default if the faces are not subtiled */
        #endif /* FORCE_CUBIC_FACE_DATA */
        {
            QTVRCubicFaceData   *cubicData;
            cubicData = (QTVRCubicFaceData*)NewPtr(sizeof(QTVRCubicFaceData) * 6 * qtvrParams->tilesH * qtvrParams->tilesV);
            SetCubicFaceData(qtvrParams, cubicData);
            err = QTInsertChild(panoSample, kParentAtomIsContainer,
                kQTVRCubicFaceDataAtomType, 1, 1, sizeof(QTVRCubicFaceData) * 6 * qtvrParams->tilesH * qtvrParams->tilesV, cubicData, nil
            );
            DisposePtr((Ptr)cubicData);
            if (err != noErr)
                goto bail;
        }
    }
 
 
    /* add pan constraint, tilt constraint, and field-of-view constraint atoms here
     * [left as an exercise for the reader; here's the basic idea:]
     */
    if (false) {
        QTVRAngleRangeAtom      myPanRangeAtom;
        
        myPanRangeAtom.minimumAngle = EndianF32_NtoB(  0.0);
        myPanRangeAtom.maximumAngle = EndianF32_NtoB(270.0);
        
        err = QTInsertChild(panoSample, kParentAtomIsContainer, kQTVRPanConstraintAtomType, 1, 1, sizeof(QTVRAngleRangeAtom), &myPanRangeAtom, NULL);
    }
    
bail:
    *thePanoSample = (err == noErr) ? panoSample : NULL;
    return(err);
}
 
 
/*******************************************************************************
 * InitMyPanoDesc
 *
 *  Create a (version 2.0) panorama track, appending the given video tracks.
 *******************************************************************************/
 
static void
InitMyPanoDesc(MyPanoTrackDescriptor *myPanoDesc, long versionToCreate)
{
    myPanoDesc->versionToCreate         = versionToCreate;
    myPanoDesc->imageTileWidth          = 0;
    myPanoDesc->imageTileHeight         = 0;
    myPanoDesc->imageRefTrackIndex      = 0;
    myPanoDesc->hotSpotTileWidth        = 0;
    myPanoDesc->hotSpotTileHeight       = 0;
    myPanoDesc->hotSpotRefTrackIndex    = 0;
    myPanoDesc->fastStartTileWidth      = 0;
    myPanoDesc->fastStartTileHeight     = 0;
    myPanoDesc->fastStartRefTrackIndex  = 0;
}
 
 
/*******************************************************************************
 * CreatePanoTrackFromMovies
 *
 *  Create a (version 2.0) panorama track, appending the given video tracks.
 *******************************************************************************/
 
static OSErr
CreatePanoTrackFromMovies(
    const FSSpec            *srcMovieSpec,
    const FSSpec            *srcHSMovieSpec,
    const FSSpec            *srcFSMovieSpec,
    const MakeQTVRParams    *qtvrParams,
    long                    versionToCreate,
    Movie                   dstMovie,
    Track                   qtvrTrack,
    Track                   *thePanoTrack
)
{
    Track                       panoTrack           = NULL;
    Track                       imageTrack          = NULL;
    Track                       hsImageTrack        = NULL;
    Track                       fsImageTrack        = NULL;
    Media                       panoMedia           = NULL;
    SampleDescriptionHandle     sampleDesc          = NULL;
    QTAtomContainer             myPanoSample        = NULL;
    OSErr                       err                 = noErr;
    MakeQTVRParams              myParams            = *qtvrParams;
    MyPanoTrackDescriptor       myPanoDesc;
 
 
    /* Initialize myPanoDesc */
    InitMyPanoDesc(&myPanoDesc, versionToCreate);
 
    /* Create a new pano track and media */
    panoTrack = NewMovieTrack(dstMovie, myParams.windowWidth<<16, myParams.windowHeight<<16, 0);
    if (panoTrack == NULL) {
        err = GetMoviesError();
        goto bail;
    }
    panoMedia = NewTrackMedia(panoTrack, kQTVRPanoramaType, kQTVRStandardTimeScale, 0, 0);
    if (panoMedia == NULL) {
        err = GetMoviesError();
        goto bail;
    }
    
    /* Create a fast start image track, if given; a reference to it is contained in the pano track */
    if ((srcFSMovieSpec != NULL) && (srcFSMovieSpec->name[0] != 0)) {
        err = ImportFirstVideoTrackFromFile(srcFSMovieSpec, dstMovie, myParams.trackDuration, 0,
            &myPanoDesc.fastStartTileWidth, &myPanoDesc.fastStartTileHeight, &fsImageTrack
        );
        if (err != noErr)
            goto bail;
    }   
    
    /* Create a panorama image track; a reference to it is contained in the pano track */
    err = ImportFirstVideoTrackFromFile(srcMovieSpec, dstMovie, myParams.trackDuration, 0,
        &myPanoDesc.imageTileWidth, &myPanoDesc.imageTileHeight, &imageTrack
    );
    if (err != noErr)
        goto bail;
    
    /* Create a hot spot image track, if given; a reference to it is contained in the pano track */
    if ((srcHSMovieSpec != NULL) && (srcHSMovieSpec->name[0] != 0)) {
        err = ImportFirstVideoTrackFromFile(srcHSMovieSpec, dstMovie, myParams.trackDuration, 0,
            &myPanoDesc.hotSpotTileWidth, &myPanoDesc.hotSpotTileHeight, &hsImageTrack
        );
        if (err != noErr)
            goto bail;
    }   
    
    /* Create track references from QTVR track to panorama track,
     * and from the panorama track to the panorama image track and the hot spot image track
     */
    if (panoTrack    != NULL)   AddTrackReference(qtvrTrack, panoTrack,    kQTVRPanoramaType,        NULL);
    if (imageTrack   != NULL)   AddTrackReference(panoTrack, imageTrack,   kQTVRImageTrackRefType,   &myPanoDesc.imageRefTrackIndex);
    if (hsImageTrack != NULL)   AddTrackReference(panoTrack, hsImageTrack, kQTVRHotSpotTrackRefType, &myPanoDesc.hotSpotRefTrackIndex);
    if (fsImageTrack != NULL)   AddTrackReference(panoTrack, fsImageTrack, kQTVRImageTrackRefType,   &myPanoDesc.fastStartRefTrackIndex);
 
 
    /* Create a sample description; this contains no real info, but AddMediaSample requires it. */
    sampleDesc = (SampleDescriptionHandle)MyNewHandleClear(sizeof(SampleDescription));
 
    /* Check pano parameters */
    VerifyPanoParameters(&myParams, &myPanoDesc);
 
    /* Create panorama media sample and add it to the panorama track */
    err = CreatePanoMediaSample(&myParams, &myPanoDesc, &myPanoSample);
    if (err != noErr)
        goto bail;
    BeginMediaEdits(panoMedia);
    err = AddMediaSample(panoMedia, (Handle)myPanoSample, 0, GetHandleSize((Handle)myPanoSample), myParams.trackDuration, (SampleDescriptionHandle)sampleDesc, 1, 0, NULL);
    if (err != noErr)
        goto bail;
    EndMediaEdits(panoMedia);
    err = InsertMediaIntoTrack(panoTrack, 0, 0, myParams.trackDuration, fixed1);
    QTDisposeAtomContainer(myPanoSample);
    DisposeHandle((Handle)sampleDesc);
    
bail:
    *thePanoTrack = panoTrack;
    return(err);
}
 
 
/*******************************************************************************
 * CreatePanoTrackFromPicts
 *
 *  Create a (version 2.0) panorama track, appending the given Picts.
 *******************************************************************************/
 
static OSErr
CreatePanoTrackFromPicts(
    long                    numFaces,
    FSSpec                  **srcPictSpecs,     /* Source     images */
    FSSpec                  **srcHSPictSpecs,   /* Hot spot   images */
    FSSpec                  **srcFSPictSpecs,   /* Fast start images */
    long                    versionToCreate,
    const MakeQTVRParams    *qtvrParams,
    TimeValue               trackDuration,
    Movie                   dstMovie,
    Track                   qtvrTrack,
    Track                   *thePanoTrack
)
{
    Track                       imageTrack          = NULL;
    Track                       hsImageTrack        = NULL;
    Track                       fsImageTrack        = NULL;
    Track                       panoTrack           = NULL;
    Media                       panoMedia           = NULL;
    long                        imageRefIndex       = 0;
    long                        hsRefIndex          = 0;
    long                        fsRefIndex          = 0;
    SampleDescriptionHandle     sampleDesc          = NULL;
    QTAtomContainer             myPanoSample        = NULL;
    OSErr                       err                 = noErr;
    long                        overlapTiles        = (long)(versionToCreate == kQTVRCubicVersion1);
    MakeQTVRParams              myParams            = *qtvrParams;
    MyPanoTrackDescriptor       myPanoDesc;
 
 
    /* Initialize myPanoDesc */
    InitMyPanoDesc(&myPanoDesc, versionToCreate);
 
    /* Create a new pano track and media */
    panoTrack = NewMovieTrack(dstMovie, myParams.windowWidth<<16, myParams.windowHeight<<16, 0);
    if (panoTrack == NULL) {
        err = GetMoviesError();
        goto bail;
    }
    panoMedia = NewTrackMedia(panoTrack, kQTVRPanoramaType, kQTVRStandardTimeScale, 0, 0);
    if (panoTrack == NULL) {
        err = GetMoviesError();
        goto bail;
    }
    
    /* Create a fast start image track, if given; a reference to it is contained in the pano track */
    if ((srcFSPictSpecs != NULL) && (srcFSPictSpecs[0][0].name[0] != 0)) {
        err = TilePictsToNewTrack(numFaces, srcFSPictSpecs, overlapTiles, kJPEGCodecType, codecLowQuality,
            myParams.tilesH, myParams.tilesV, 0, dstMovie, myParams.trackDuration,
            &myPanoDesc.fastStartTileWidth, &myPanoDesc.fastStartTileHeight, &fsImageTrack
        );
        if (err != noErr)
            goto bail;
    }   
    
    /* Create a panorama image track; a reference to it is contained in the pano track */
    err = TilePictsToNewTrack(numFaces, srcPictSpecs, overlapTiles, myParams.tileCodec, myParams.tileQuality,
        myParams.tilesH, myParams.tilesV, 0, dstMovie, myParams.trackDuration,
        &myPanoDesc.imageTileWidth, &myPanoDesc.imageTileHeight, &imageTrack
    );
    if (err != noErr)
        goto bail;
    
    /* Create a hot spot image track, if given; a reference to it is contained in the pano track */
    if ((srcHSPictSpecs != NULL) && (srcHSPictSpecs[0][0].name[0] != 0)) {
        err = TilePictsToNewTrack(numFaces, srcHSPictSpecs, overlapTiles, kGraphicsCodecType, codecLosslessQuality,
            myParams.tilesH, myParams.tilesV, 8, dstMovie, myParams.trackDuration,
            &myPanoDesc.hotSpotTileWidth, &myPanoDesc.hotSpotTileHeight, &hsImageTrack
        );
        if (err != noErr)
            goto bail;
    }   
    
    
    /* Create track references from QTVR track to panorama track,
     * and from the panorama track to the panorama image track and the hot spot image track
     */
    if (panoTrack    != NULL)   AddTrackReference(qtvrTrack, panoTrack,    kQTVRPanoramaType,        NULL);
    if (imageTrack   != NULL)   AddTrackReference(panoTrack, imageTrack,   kQTVRImageTrackRefType,   &myPanoDesc.imageRefTrackIndex);
    if (hsImageTrack != NULL)   AddTrackReference(panoTrack, hsImageTrack, kQTVRHotSpotTrackRefType, &myPanoDesc.hotSpotRefTrackIndex);
    if (fsImageTrack != NULL)   AddTrackReference(panoTrack, fsImageTrack, kQTVRImageTrackRefType,   &myPanoDesc.fastStartRefTrackIndex);
 
 
    /* Create a sample description; this contains no real info, but AddMediaSample requires it. */
    sampleDesc = (SampleDescriptionHandle)MyNewHandleClear(sizeof(SampleDescription));
 
    /* Check pano parameters */
    VerifyPanoParameters(&myParams, &myPanoDesc);
 
    /* Create panorama media sample and add it to the panorama track */
    err = CreatePanoMediaSample(&myParams, &myPanoDesc, &myPanoSample);
    if (err != noErr)
        goto bail;
    BeginMediaEdits(panoMedia);
    err = AddMediaSample(panoMedia, (Handle)myPanoSample, 0, GetHandleSize((Handle)myPanoSample), trackDuration, (SampleDescriptionHandle)sampleDesc, 1, 0, NULL);
    if (err != noErr)
        goto bail;
    EndMediaEdits(panoMedia);
    err = InsertMediaIntoTrack(panoTrack, 0, 0, trackDuration, fixed1);
    
bail:
    if (myPanoSample != NULL)   QTDisposeAtomContainer(myPanoSample);
    if (sampleDesc   != NULL)   DisposeHandle((Handle)sampleDesc);
    *thePanoTrack = panoTrack;
    return(err);
}
 
 
/*******************************************************************************
 * CreatePanoTrackFromGWorlds
 *
 *  Create a (version 2.0) panorama track, appending the given GWorlds.
 *******************************************************************************/
 
static OSErr
CreatePanoTrackFromGWorlds(
    long                    numFaces,
    GWorldPtr               *panoGWs,
    GWorldPtr               *hsGWs,
    GWorldPtr               *fsGWs,
    long                    versionToCreate,
    const MakeQTVRParams    *qtvrParams,
    TimeValue               trackDuration,
    Movie                   dstMovie,
    Track                   qtvrTrack,
    Track                   *thePanoTrack
)
{
    Track                       imageTrack          = NULL;
    Track                       hsImageTrack        = NULL;
    Track                       fsImageTrack        = NULL;
    Track                       panoTrack           = NULL;
    Media                       panoMedia           = NULL;
    long                        imageRefIndex       = 0;
    long                        hsRefIndex          = 0;
    long                        fsRefIndex          = 0;
    SampleDescriptionHandle     sampleDesc          = NULL;
    QTAtomContainer             myPanoSample        = NULL;
    OSErr                       err                 = noErr;
    long                        overlapTiles        = (long)(versionToCreate == kQTVRCubicVersion1);
    MakeQTVRParams              myParams            = *qtvrParams;
    MyPanoTrackDescriptor       myPanoDesc;
 
 
    /* Initialize myPanoDesc */
    InitMyPanoDesc(&myPanoDesc, versionToCreate);
 
    /* Create a new pano track and media */
    panoTrack = NewMovieTrack(dstMovie, myParams.windowWidth<<16, myParams.windowHeight<<16, 0);
    if (panoTrack == NULL) {
        err = GetMoviesError();
        goto bail;
    }
    panoMedia = NewTrackMedia(panoTrack, kQTVRPanoramaType, kQTVRStandardTimeScale, 0, 0);
    if (panoTrack == NULL) {
        err = GetMoviesError();
        goto bail;
    }
    
    /* Create a fast start image track, if given; a reference to it is contained in the pano track */
    if ((fsGWs != NULL) && (fsGWs[0] != NULL)) {
        err = TileGWorldsToNewTrack(numFaces, fsGWs, overlapTiles, kJPEGCodecType, codecLowQuality,
            myParams.tilesH, myParams.tilesV, 0, dstMovie, myParams.trackDuration,
            &myPanoDesc.fastStartTileWidth, &myPanoDesc.fastStartTileHeight, &fsImageTrack
        );
        if (err != noErr)
            goto bail;
    }   
    
    /* Create a panorama image track; a reference to it is contained in the pano track */
    err = TileGWorldsToNewTrack(numFaces, panoGWs, overlapTiles, myParams.tileCodec, myParams.tileQuality,
        myParams.tilesH, myParams.tilesV, 0, dstMovie, myParams.trackDuration,
        &myPanoDesc.imageTileWidth, &myPanoDesc.imageTileHeight, &imageTrack
    );
    if (err != noErr)
        goto bail;
    
    /* Create a hot spot image track, if given; a reference to it is contained in the pano track */
    if ((hsGWs != NULL) && (hsGWs[0] != NULL)) {
        err = TileGWorldsToNewTrack(numFaces, hsGWs, overlapTiles, kGraphicsCodecType, codecLosslessQuality,
            myParams.tilesH, myParams.tilesV, 8, dstMovie, myParams.trackDuration,
            &myPanoDesc.hotSpotTileWidth, &myPanoDesc.hotSpotTileHeight, &hsImageTrack
        );
        if (err != noErr)
            goto bail;
    }   
    
    
    /* Create track references from QTVR track to panorama track,
     * and from the panorama track to the panorama image track and the hot spot image track
     */
    if (panoTrack    != NULL)   AddTrackReference(qtvrTrack, panoTrack,    kQTVRPanoramaType,        NULL);
    if (imageTrack   != NULL)   AddTrackReference(panoTrack, imageTrack,   kQTVRImageTrackRefType,   &myPanoDesc.imageRefTrackIndex);
    if (hsImageTrack != NULL)   AddTrackReference(panoTrack, hsImageTrack, kQTVRHotSpotTrackRefType, &myPanoDesc.hotSpotRefTrackIndex);
    if (fsImageTrack != NULL)   AddTrackReference(panoTrack, fsImageTrack, kQTVRImageTrackRefType,   &myPanoDesc.fastStartRefTrackIndex);
 
 
    /* Create a sample description; this contains no real info, but AddMediaSample requires it. */
    sampleDesc = (SampleDescriptionHandle)MyNewHandleClear(sizeof(SampleDescription));
 
    /* Check pano parameters */
    VerifyPanoParameters(&myParams, &myPanoDesc);
 
    /* Create panorama media sample and add it to the panorama track */
    err = CreatePanoMediaSample(&myParams, &myPanoDesc, &myPanoSample);
    if (err != noErr)
        goto bail;
    BeginMediaEdits(panoMedia);
    err = AddMediaSample(panoMedia, (Handle)myPanoSample, 0, GetHandleSize((Handle)myPanoSample), trackDuration, (SampleDescriptionHandle)sampleDesc, 1, 0, NULL);
    if (err != noErr)
        goto bail;
    EndMediaEdits(panoMedia);
    err = InsertMediaIntoTrack(panoTrack, 0, 0, trackDuration, fixed1);
    
bail:
    if (myPanoSample != NULL)   QTDisposeAtomContainer(myPanoSample);
    if (sampleDesc   != NULL)   DisposeHandle((Handle)sampleDesc);
    *thePanoTrack = panoTrack;
    return(err);
}
 
 
#if 0
/*******************************************************************************
 * CreateTileMovieFromPictFiles
 *
 *  Create a QuickTime movie containing tiles from the specified image files.
 *******************************************************************************/
 
static OSErr
CreateTileMovieFromPictFiles(
    long        numPicts,
    FSSpec      **srcPictSpecs,
    CodecType   tileCodec,
    CodecQ      tileQuality,
    long        tilesH,
    long        tilesV,
    short       tileDepth,
    FSSpec      *dstTileSpec
)
{
    GraphicsImportComponent     theImporter     = NULL;
    Rect                        pictRect, tileRect, drawRect;
    long                        tileWidth, tileHeight;
    GWorldPtr                   gw              = NULL;
    PixMapHandle                pm              = NULL;
    Movie                       movie           = NULL;
    short                       myResRefNum     = -1;
    short                       myResID         = movieInDataForkResID;
    Track                       myTrack         = NULL;
    Media                       myMedia         = NULL;
    ImageDescriptionHandle      myImageDesc     = NULL;
    Handle                      myData          = NULL;
    Ptr                         myDataPtr       = NULL;
    long                        myFrameSize;
    TimeValue                   dstTileDuration = 300;
    OSErr                       err = noErr;
    long                        i, j, k;
    FSSpec                      fsSpec;
    #if USE_TILE_DISPLAY_WINDOW
        WindowRef               myWindow;
    #endif /* USE_TILE_DISPLAY_WINDOW */
    
    if ((srcPictSpecs == NULL) || (srcPictSpecs[0] == NULL) || (srcPictSpecs[0][0].name[0] == 0)) {
        err = paramErr;
        goto bail;
    }
 
    /* Create a GWorld to draw the uncompressed tiles into */
    fsSpec = srcPictSpecs[0][0];
    err = GetGraphicsImporterForFile(&fsSpec, &theImporter);
    if (err != noErr)
        goto bail;
    err = GraphicsImportGetBoundsRect(theImporter, &pictRect);
    if (err != noErr)
        goto bail;
    CloseComponent(theImporter);
    theImporter = NULL;
    tileWidth   = (pictRect.right  - pictRect.left) / tilesH;
    tileHeight  = (pictRect.bottom - pictRect.top ) / tilesV;
    MacSetRect(&tileRect, 0, 0, tileWidth, tileHeight);
    err = NewGWorld(&gw, tileDepth, &tileRect, NULL, NULL, 0L);
    if (err != noErr)
        goto bail;
 
    
    /* Debugging or progress window */
    #if USE_TILE_DISPLAY_WINDOW
        MacSetRect(&tileRect, 0, 0, tileWidth, tileHeight);
        MacOffsetRect(&tileRect, 20, 100);
        myWindow = NewCWindow(NULL, &tileRect, "\pUncompressed tiles", true, noGrowDocProc, (WindowPtr)-1L, true, 0);
        MacOffsetRect(&tileRect, -20, -100);
    #endif /* USE_TILE_DISPLAY_WINDOW */
 
 
    /* Create a new movie to contain the compressed tiles as video samples */
    err = CreateMovieFile(dstTileSpec, FOUR_CHAR_CODE('TVOD'), smCurrentScript, kCreateMovieFlags, &myResRefNum, &movie);
    if (err != noErr)
        goto bail;
    myTrack = NewMovieTrack(movie, tileWidth << 16, tileHeight << 16, kNoVolume);   
    myMedia = NewTrackMedia(myTrack, VideoMediaType, 600, NULL, 0);
    if ((myTrack == NULL) || (myMedia == NULL))
        goto bail;
 
    
    /* Dice the picture into pieces, compress them, and add the compressed tiles as video samples to the movie */
    myImageDesc = (ImageDescriptionHandle)MyNewHandleClear(4); /* this is resized and filled in below by FCompressImage */
    if (myImageDesc == NULL)
        goto bail;
    pm = GetGWorldPixMap(gw);
    LockPixels(pm);
    err = GetMaxCompressionSize(pm, &tileRect, tileDepth, tileQuality, tileCodec, (CompressorComponent)anyCodec, &myFrameSize);
    UnlockPixels(pm);
    if (err != noErr)
        goto bail;
    myData = MyNewHandle(myFrameSize);
    if (myData == NULL)
        goto bail;
    BeginMediaEdits(myMedia);
    
    for (k = 0; k < numPicts; k++) {
        fsSpec = srcPictSpecs[0][k];
        err = GetGraphicsImporterForFile(&fsSpec, &theImporter);
        if (err != noErr)
            goto bail;
        for (j = 0; j < tilesV; j++) {
            for (i = 0; i < tilesH; i++) {
                /* Offset to the proper subrect for this tile */
                drawRect = pictRect;
                MacOffsetRect(&drawRect, -i * tileWidth, -j * tileHeight); 
                
                /* Draw the picture into the window we've created */
                #if USE_TILE_DISPLAY_WINDOW
                    if (myWindow != NULL) {
                        GraphicsImportSetGWorld(theImporter, (CGrafPtr)myWindow, NULL);
                        GraphicsImportSetBoundsRect(theImporter, &drawRect);
                        GraphicsImportDraw(theImporter);
                    }
                #endif /* USE_TILE_DISPLAY_WINDOW */
 
                /* Draw the picture into the uncompressed tile GWorld */
                GraphicsImportSetGWorld(theImporter, gw, NULL);
                GraphicsImportSetBoundsRect(theImporter, &drawRect);
                GraphicsImportDraw(theImporter);
                
                /* Compress the tile and add it tile to the movie */
                HLock(myData);
                myDataPtr = *myData;
                LockPixels(pm);
                err = FCompressImage(pm, &tileRect, tileDepth, tileQuality, tileCodec, (CompressorComponent)anyCodec,
                    NULL, 0, 0, NULL, NULL, myImageDesc, myDataPtr
                );
                UnlockPixels(pm);
                if (err != noErr)
                    goto bail;
                err = AddMediaSample(myMedia, myData, 0, (**myImageDesc).dataSize, dstTileDuration,
                    (SampleDescriptionHandle)myImageDesc, 1L, 0, NULL
                );
                if (err != noErr)
                    goto bail;
            }
        }
        CloseComponent(theImporter);
        theImporter = NULL;
    }
    
    EndMediaEdits(myMedia);
    DisposeHandle(myData);
    myData = NULL;
    
    /* add the media to the track, at time 0 */
    err = InsertMediaIntoTrack(myTrack, 0, 0, GetMediaDuration(myMedia), fixed1);
    if (err != noErr)
        goto bail;
 
    /* add the movie resource */
    err = AddMovieResource(movie, myResRefNum, &myResID, NULL);
    
bail:
    if (theImporter != NULL)    CloseComponent(theImporter);
    if (gw          != NULL)    DisposeGWorld(gw);
    if (myResRefNum != -1)      CloseMovieFile(myResRefNum);
    if (myData      != NULL)    DisposeHandle(myData);
    if (myImageDesc != NULL)    DisposeHandle((Handle)myImageDesc);
    if (movie       != NULL)    DisposeMovie(movie);
    #if USE_TILE_DISPLAY_WINDOW
        if (myWindow!= NULL)    DisposeWindow(myWindow);
    #endif /* USE_TILE_DISPLAY_WINDOW */
 
    return(err);
}
 
 
/*******************************************************************************
 * CreateTileMovieFromOnePictFile
 *
 *  Create a QuickTime movie containing tiles from the specified image file.
 *******************************************************************************/
 
static OSErr
CreateTileMovieFromOnePictFile(
    FSSpec          *srcPictSpec,
    CodecType       tileCodec,
    CodecQ          tileQuality,
    long            tilesH,
    long            tilesV,
    short           tileDepth,
    FSSpec          *dstTileSpec
)
{
    FSSpec  *specArray = srcPictSpec;
    return(CreateTileMovieFromPictFiles(1, &specArray, tileCodec, tileQuality, tilesH, tilesV, tileDepth, dstTileSpec));
}
#endif 
 
#pragma mark -
#pragma mark ### Cylindrical API ###
#pragma mark -
/********************************************************************************
 ********************************************************************************
 ********************************************************************************
 ***        API
 ********************************************************************************
 ********************************************************************************
 ********************************************************************************/
 
 
/*******************************************************************************
 * VRMovieToQTVRCylPano1x0
 * Create a single-node panoramic QTVR movie from the specified tile movie(s).
 *
 * NOTE: This function builds a movie that conforms to version 1.0 of the QuickTime VR file format.
 *
 * The newly-created movie contains references to the original tile movie, not the actual movie data.
 * We do this because we assume that the caller will flatten the movie into a third movie, which will
 * contain the movie data. Also, the interim file is much smaller than it would be if we copied the data,
 * thus saving time and disk space.
 *******************************************************************************/
 
OSErr
VRMovieToQTVRCylPano1x0(
    MakeQTVRParams  *qtvrParams,    /* Parameters used to create the movie */
    FSSpec          *srcTileSpec,   /* Cylindrical tile movie for the panoramic image */
    FSSpec          *srcHSTileSpec, /* Cylindrical tile movie for the hot spot image */
    FSSpec          *dstMovieSpec   /* Destination movie */
)
{
    PanoramaDescriptionHandle   myPanoDesc          = NULL;
    PanoSampleHeaderAtomHandle  myPanoHeader            = NULL;
    HotSpotTableAtomHandle      myHotSpotTable      = NULL;
    StringTableAtomHandle       myStringTable       = NULL;
    short                       myResRefNum         = -1;
    short                       myResID             = movieInDataForkResID;
    short                       myHSResRefNum       = -1;
    short                       myTilResRefNum      = -1;
    short                       myHSTilResRefNum    = -1;
    Movie                       myMovie             = NULL;
    Movie                       myTileMovie         = NULL;
    Movie                       myHSTileMovie       = NULL;
    Track                       myPanoTrack;
    Media                       myPanoMedia;
    Track                       myImageTrack;
    Track                       myHSImageTrack;
    Fixed                       myTheta;
    UInt16                      myIndex;
    long                        myTrackWidth, myTrackHeight;
    ComponentResult             err                 = noErr;
    
    /*
     * create a new movie
     */
 
    /* create a movie file for the destination movie */
    err = CreateMovieFile(dstMovieSpec, FOUR_CHAR_CODE('TVOD'), smCurrentScript, kCreateMovieFlags, &myResRefNum, &myMovie);
    if (err != noErr)
        goto bail;
 
    /*
     * copy the video track from the tile movie to the new movie; this is the "scene track"
     */
 
    /* open the tile movie file */
    err = ImportFirstVideoTrackFromFile(srcTileSpec, myMovie, qtvrParams->trackDuration, 0, &myTrackWidth, &myTrackHeight, &myImageTrack);
    if (err != noErr)
        goto bail;
    
    /*
     * copy the hot spot image track from the tile movie to the new movie
     */
    if ((srcHSTileSpec != NULL) && (srcHSTileSpec->name[0] != 0)) {
        err = ImportFirstVideoTrackFromFile(srcHSTileSpec, myMovie, qtvrParams->trackDuration, 0, &myTrackWidth, &myTrackHeight, &myHSImageTrack);
        if (err != noErr)
            goto bail;
    }
    
    /*
     * create a panorama track and add it to the movie
     */
    
    /* create a panorama track and media */
    myPanoTrack = NewMovieTrack(myMovie, qtvrParams->windowWidth << 16, qtvrParams->windowHeight << 16, 0);
    myPanoMedia = NewTrackMedia(myPanoTrack, kQTVROldPanoType, kQTVRStandardTimeScale, NULL, 0);
    if (((err = GetMoviesError()) != noErr) || (myPanoTrack == NULL) || (myPanoMedia == NULL))
        goto bail;
 
    /* create a panorama sample description */
    myPanoDesc = (PanoramaDescriptionHandle)MyNewHandleClear(sizeof(PanoramaDescription));
    if (myPanoDesc == NULL)
        goto bail;
 
    /* fill in the panorama sample description;
     * all the data in this sample description that follows the first 4 long words must be in big-endian format;
     * the first four long words are used by AddMediaSample and must therefore be in native-endian format
     */
    (**myPanoDesc).size                 = sizeof(PanoramaDescription);
    (**myPanoDesc).type                 = kPanDescType;
    (**myPanoDesc).reserved1            = 0L;
    (**myPanoDesc).reserved2            = 0L;
    
    (**myPanoDesc).majorVersion         = EndianU16_NtoB(0);
    (**myPanoDesc).minorVersion         = EndianU16_NtoB(0);
    (**myPanoDesc).sceneTrackID         = EndianU32_NtoB(GetTrackID(myImageTrack));
    (**myPanoDesc).loResSceneTrackID    = EndianU32_NtoB(0L);       /* no lo-res video track */
    
    for (myIndex = 1; myIndex < 6; myIndex++) {
        (**myPanoDesc).reserved3[myIndex] = EndianU32_NtoB(0L);
        (**myPanoDesc).reserved4[myIndex] = EndianU32_NtoB(0L);
    }
    
    (**myPanoDesc).hotSpotTrackID       = EndianU32_NtoB(GetTrackID(myHSImageTrack));
    (**myPanoDesc).loResHotSpotTrackID  = EndianU32_NtoB(0L);   /* no lo-res hot spot track */
 
    myTheta = 180.0 * (atan((myTrackWidth * qtvrParams->tilesH) * D_PI / (myTrackHeight * qtvrParams->tilesV))) / D_PI * 65536.0;
    
    (**myPanoDesc).hPanStart            = EndianF32NtoFixedB(qtvrParams->minPan);
    (**myPanoDesc).hPanEnd              = EndianF32NtoFixedB(qtvrParams->maxPan);
    (**myPanoDesc).vPanTop              = EndianF32NtoFixedB(qtvrParams->maxTilt);
    (**myPanoDesc).vPanBottom           = EndianF32NtoFixedB(qtvrParams->minTilt);
    (**myPanoDesc).minimumZoom          = EndianF32NtoFixedB(qtvrParams->minFieldOfView);
    (**myPanoDesc).maximumZoom          = EndianF32NtoFixedB(qtvrParams->maxFieldOfView);
    
    (**myPanoDesc).sceneSizeX           = EndianU32_NtoB((long)(myTrackWidth  * qtvrParams->tilesH));
    (**myPanoDesc).sceneSizeY           = EndianU32_NtoB((long)(myTrackHeight * qtvrParams->tilesV));
    (**myPanoDesc).numFrames            = EndianU32_NtoB((long)qtvrParams->tilesV);
    (**myPanoDesc).sceneNumFramesX      = EndianU16_NtoB(qtvrParams->tilesH);
    (**myPanoDesc).sceneNumFramesY      = EndianU16_NtoB(qtvrParams->tilesV);
    (**myPanoDesc).sceneColorDepth      = EndianU16_NtoB(32);
    
    (**myPanoDesc).hotSpotSizeX         = EndianU32_NtoB((long)(myTrackWidth  * qtvrParams->tilesH));
    (**myPanoDesc).hotSpotSizeY         = EndianU32_NtoB((long)(myTrackHeight * qtvrParams->tilesV));
    (**myPanoDesc).hotSpotNumFramesX    = EndianU16_NtoB(qtvrParams->tilesH);
    (**myPanoDesc).hotSpotNumFramesY    = EndianU16_NtoB(qtvrParams->tilesV);
    (**myPanoDesc).hotSpotColorDepth    = EndianU16_NtoB(8);
 
    /* create a panorama header atom;
     * the data in this atom (which is *not* a QTAtom) must be in big-endian format
     */
    myPanoHeader = (PanoSampleHeaderAtomHandle)MyNewHandleClear(sizeof(PanoSampleHeaderAtom));
    if (myPanoHeader == NULL)
        goto bail;
        
    (**myPanoHeader).size               = EndianU32_NtoB(sizeof(PanoSampleHeaderAtom));
    (**myPanoHeader).type               = EndianU32_NtoB(kPanHeaderType);
    (**myPanoHeader).nodeID             = EndianU32_NtoB(0L);
 
    /* set the default pan, tilt, and zoom */
    (**myPanoHeader).defHPan            = EndianF32NtoFixedB(qtvrParams->defaultPan);
    (**myPanoHeader).defVPan            = EndianF32NtoFixedB(qtvrParams->defaultTilt);
    (**myPanoHeader).defZoom            = EndianF32NtoFixedB(qtvrParams->defaultFieldOfView);
 
    /* set constraints for this node; use 0 for the default constraints */
    (**myPanoHeader).minHPan            = EndianS32_NtoB(0L);
    (**myPanoHeader).minVPan            = EndianS32_NtoB(0L);
    (**myPanoHeader).maxHPan            = EndianS32_NtoB(0L);
    (**myPanoHeader).maxVPan            = EndianS32_NtoB(0L);
    (**myPanoHeader).maxZoom            = EndianS32_NtoB(0L);
 
    (**myPanoHeader).nameStrOffset      = EndianU32_NtoB(0L);       /* no name or comment */
    (**myPanoHeader).commentStrOffset   = EndianU32_NtoB(0L);
 
    /* create a string table atom;
     * the data in this atom (which is *not* a QTAtom) must be in big-endian format
     */
    myStringTable = (StringTableAtomHandle)MyNewHandleClear(sizeof(StringTableAtom));
    if (myStringTable == NULL)
        goto bail;
        
    (**myStringTable).size              = EndianU32_NtoB(sizeof(StringTableAtom));
    (**myStringTable).type              = EndianU32_NtoB(kStringTableType);
    (**myStringTable).bunchOstrings[0]  = EndianU32_NtoB(0);
 
    /* create a hot spot table atom and expand the string table atom to include the
     * hot spot names and comments
     */
    myHotSpotTable = MakeQTVRHotSpot1x0(myStringTable);
 
    /* append the hot spot table atom and the string table atom to the panorama header atom
     * [left as an exercise for the reader]
     */
    
    /* Add the media sample to the panorama track */
    BeginMediaEdits(myPanoMedia);
    err = AddMediaSample(myPanoMedia, (Handle)myPanoHeader, 0, GetHandleSize((Handle)myPanoHeader), qtvrParams->trackDuration, (SampleDescriptionHandle)myPanoDesc, 1, 0, NULL);
    if (err != noErr)
        goto bail;
    EndMediaEdits(myPanoMedia);
    err = InsertMediaIntoTrack(myPanoTrack, 0, 0, qtvrParams->trackDuration, fixed1);
    
    /* Add a user data item that identifies the QTVR movie controller */
    err = SetQTControllerType(myMovie, kQTVROldPanoType);
    if (err != noErr)
        goto bail;
        
    /* Add the movie resource to the panorama movie */
    err = AddMovieResource(myMovie, myResRefNum, &myResID, NULL);
    
bail:
    if (myPanoDesc       != NULL)   DisposeHandle((Handle)myPanoDesc);  
    if (myPanoHeader     != NULL)   DisposeHandle((Handle)myPanoHeader);    
    if (myResRefNum      != -1)     CloseMovieFile(myResRefNum);    
    if (myHSResRefNum    != -1)     CloseMovieFile(myHSResRefNum);  
    if (myMovie          != NULL)   DisposeMovie(myMovie);      
    if (myTilResRefNum   != -1)     CloseMovieFile(myTilResRefNum); 
    if (myHSTilResRefNum != -1)     CloseMovieFile(myHSTilResRefNum);
    if (myTileMovie      != NULL)   DisposeMovie(myTileMovie);
    if (myHSTileMovie    != NULL)   DisposeMovie(myHSTileMovie);
        
    return(err);
}
 
 
/*******************************************************************************
 * VRMovieToQTVRCylPano2
 * Create a single-node panoramic QTVR movie from the specified tile movie(s).
 *
 * NOTE: This function builds a movie that conforms to version 2.0 of the QuickTime VR file format.
 *
 * The newly-created movie contains references to the original tile movie, not the actual movie data.
 * We do this because we assume that the caller will flatten the movie into a third movie, which will
 * contain the movie data. Also, the interim file is much smaller than it would be if we copied the data,
 * thus saving time and disk space.
 *******************************************************************************/
 
static OSErr
VRMovieToQTVRCylPano2(
    MakeQTVRParams  *qtvrParams,    /* Parameters used to create the movie */
    long            panoType,       /* Parameters used to create the movie */
    FSSpec          *srcTileSpec,   /* Cylindrical tile movie for the panoramic image */
    FSSpec          *srcHSTileSpec, /* Cylindrical tile movie for the hot spot image */
    FSSpec          *srcFSTileSpec, /* Cylindrical tile movie for the fast start image */
    FSSpec          *dstMovieSpec   /* Destination movie */
)
{
    short               myResRefNum         = -1;
    Movie               tmpMovie            = NULL;
    Track               myQTVRTrack;
    Track               myPanoTrack;
    ComponentResult     err                 = noErr;
    FSSpec              tmpSpec;
    
    /* Create a movie file for the destination movie */
    MakeTempFSSpec(dstMovieSpec, ".MV~", &tmpSpec);
    err = CreateMovieFile(&tmpSpec, FOUR_CHAR_CODE('TVOD'), smCurrentScript, kCreateMovieFlags, &myResRefNum, &tmpMovie);
    if (err != noErr)
        goto bail;
 
    /* Create the QTVR movie track and media */
    err = CreateQTVRTrack(qtvrParams, qtvrParams->trackDuration, tmpMovie, &myQTVRTrack);
    if (err != noErr)
        goto bail;
    
    /* Create panorama track and media, and add them to the movie */
    err = CreatePanoTrackFromMovies(srcTileSpec, srcHSTileSpec, srcFSTileSpec,
        qtvrParams, panoType, tmpMovie, myQTVRTrack,
        &myPanoTrack
    );
    if (err != noErr)
        goto bail;
        
    /* Add a user data item that identifies the QTVR movie controller */
    err = SetQTControllerType(tmpMovie, kQTVRQTVRType);
    if (err != noErr)
        goto bail;
        
    /* Create the final, flattened movie, from the temporary file into a new movie file;
     * put the movie resource first so that FastStart is possible
     */
    err = FlattenQTVRMovie(tmpMovie, dstMovieSpec);
    
bail:
    if (myResRefNum != -1)      CloseMovieFile(myResRefNum);
    if (tmpMovie    != NULL)    DisposeMovie(tmpMovie);
    DeleteMovieFile(&tmpSpec);
        
    return(err);
}
 
 
/*******************************************************************************
 * VRMovieToQTVRCylPano2v0
 * Create a single-node panoramic QTVR movie from the specified tile movie(s).
 *
 * NOTE: This function builds a movie that conforms to version 2.0 of the QuickTime VR file format.
 *
 * The newly-created movie contains references to the original tile movie, not the actual movie data.
 * We do this because we assume that the caller will flatten the movie into a third movie, which will
 * contain the movie data. Also, the interim file is much smaller than it would be if we copied the data,
 * thus saving time and disk space.
 *******************************************************************************/
 
OSErr
VRMovieToQTVRCylPano2v0(
    MakeQTVRParams  *qtvrParams,    /* Parameters used to create the movie */
    FSSpec          *srcTileSpec,   /* Cylindrical tile movie for the panoramic image */
    FSSpec          *srcHSTileSpec, /* Cylindrical tile movie for the hot spot image */
    FSSpec          *srcFSTileSpec, /* Cylindrical tile movie for the fast start image */
    FSSpec          *dstMovieSpec   /* Destination movie */
)
{
    return(VRMovieToQTVRCylPano2(qtvrParams, kQTVRCylindricalVersion2v, srcTileSpec, srcHSTileSpec, srcFSTileSpec, dstMovieSpec));
}
 
 
/*******************************************************************************
 * VRMovieToQTVRCylPano2h0
 *
 *  Create a single-node QuickTime VR panoramic movie from the specified image file.
 *******************************************************************************/
 
OSErr
VRMovieToQTVRCylPano2h0(
    MakeQTVRParams  *qtvrParams,    /* Parameters used to create the movie */
    FSSpec          *srcTileSpec,   /* Cylindrical tile movie for the panoramic image */
    FSSpec          *srcHSTileSpec, /* Cylindrical tile movie for the hot spot image */
    FSSpec          *srcFSTileSpec, /* Cylindrical tile movie for the fast start image */
    FSSpec          *dstMovieSpec   /* Destination movie */
)
{
    return(VRMovieToQTVRCylPano2(qtvrParams, kQTVRCylindricalVersion2h, srcTileSpec, srcHSTileSpec, srcFSTileSpec, dstMovieSpec));
}
 
 
/*******************************************************************************
 * VRPictToQTVRCylPano2
 *
 *  Create a single-node QuickTime VR panoramic movie from the specified image file.
 *******************************************************************************/
 
static OSErr
VRPictToQTVRCylPano2(
    MakeQTVRParams  *qtvrParams,        /* Parameters used to create the movie */
    long            panoType,           /* Version 1, or version 2 (recommended) */
    FSSpec          *srcPictSpec,       /* Source   image */
    FSSpec          *srcHSPictSpec,     /* Hot spot image */
    FSSpec          *srcFSPictSpec,     /* Fast start image */
    FSSpec          *dstMovieSpec       /* Destination movie */
)
{
    short                           myResRefNum         = -1;
    Movie                           tmpMovie            = NULL;
    Track                           myQTVRTrack;
    Track                           myPanoTrack;
    ComponentResult                 err                 = noErr;
    FSSpec                          *srcH, *hsH, *fsH;
    FSSpec                          tmpSpec;
    
    /* Create a movie file for the destination movie */
    MakeTempFSSpec(dstMovieSpec, ".MV~", &tmpSpec);
    err = CreateMovieFile(&tmpSpec, FOUR_CHAR_CODE('TVOD'), smCurrentScript, kCreateMovieFlags, &myResRefNum, &tmpMovie);
    if (err != noErr)
        goto bail;
 
    /* Create the QTVR movie track and media */
    err = CreateQTVRTrack(qtvrParams, qtvrParams->trackDuration, tmpMovie, &myQTVRTrack);
    if (err != noErr)
        goto bail;
    
    /* Create panorama track and media, and add them to the movie */
    srcH = srcPictSpec; hsH = srcHSPictSpec; fsH = srcFSPictSpec;
    err = CreatePanoTrackFromPicts(1, &srcH, (hsH ? &hsH : NULL), (fsH ? &fsH : NULL),
        panoType, qtvrParams, qtvrParams->trackDuration, tmpMovie, myQTVRTrack,
        &myPanoTrack
    );
    if (err != noErr)
        goto bail;
        
    /* Add a user data item that identifies the QTVR movie controller */
    err = SetQTControllerType(tmpMovie, kQTVRQTVRType);
    if (err != noErr)
        goto bail;
        
    /* Create the final, flattened movie, from the temporary file into a new movie file;
     * put the movie resource first so that FastStart is possible
     */
    err = FlattenQTVRMovie(tmpMovie, dstMovieSpec);
    
bail:
    if (myResRefNum != -1)      CloseMovieFile(myResRefNum);
    if (tmpMovie     != NULL)   DisposeMovie(tmpMovie);
    DeleteMovieFile(&tmpSpec);
 
    return(err);
}
 
 
/*******************************************************************************
 * VRPictToQTVRCylPano2v0
 *******************************************************************************/
 
OSErr
VRPictToQTVRCylPano2v0(
    MakeQTVRParams  *qtvrParams,    /* Parameters used to create the movie */
    FSSpec          *srcPictSpec,   /* Source   image, rotated 90 degrees counter-clockwise (classic orientation) */
    FSSpec          *srcHSPictSpec, /* Hot spot image, rotated 90 degrees counter-clockwise (classic orientation) */
    FSSpec          *srcFSPictSpec, /* Fast start image, rotated 90 degrees counter-clockwise (classic orientation) */
    FSSpec          *dstMovieSpec   /* Destination movie */
)
{
    return(VRPictToQTVRCylPano2(qtvrParams, kQTVRCylindricalVersion2v, srcPictSpec, srcHSPictSpec, srcFSPictSpec, dstMovieSpec));
}
 
 
/*******************************************************************************
 * VRPictToQTVRCylPano2h0
 *******************************************************************************/
 
OSErr
VRPictToQTVRCylPano2h0(
    MakeQTVRParams  *qtvrParams,    /* Parameters used to create the movie */
    FSSpec          *srcPictSpec,   /* Source   image, non-rotated (modern orientation) */
    FSSpec          *srcHSPictSpec, /* Hot spot image, non-rotated (modern orientation)) */
    FSSpec          *srcFSPictSpec, /* Fast start image, non-rotated (modern orientation)) */
    FSSpec          *dstMovieSpec   /* Destination movie */
)
{
    return(VRPictToQTVRCylPano2(qtvrParams, kQTVRCylindricalVersion2h, srcPictSpec, srcHSPictSpec, srcFSPictSpec, dstMovieSpec));
}
 
 
/*******************************************************************************
 * VRGWorldToQTVRCylPano2
 *
 *  Create a single-node QuickTime VR panoramic movie from the specified image file.
 *******************************************************************************/
 
static OSErr
VRGWorldToQTVRCylPano2(
    MakeQTVRParams  *qtvrParams,        /* Parameters used to create the movie */
    long            panoType,           /* Version 1, or version 2 (recommended) */
    GWorldPtr       srcGW,              /* Source GWorld */
    GWorldPtr       srcHSGW,            /* Source hot spot GWorld */
    GWorldPtr       srcFSGW,            /* Source fast start GWorld */
    FSSpec          *dstMovieSpec       /* Destination movie */
)
{
    short                           myResRefNum         = -1;
    Movie                           tmpMovie            = NULL;
    Track                           myQTVRTrack;
    Track                           myPanoTrack;
    ComponentResult                 err;
    GWorldPtr                       srGW, hsGW, fsGW;
    FSSpec                          tmpSpec;
    
    /* Create a movie file for the destination movie */
    MakeTempFSSpec(dstMovieSpec, ".MV~", &tmpSpec);
    err = CreateMovieFile(&tmpSpec, FOUR_CHAR_CODE('TVOD'), smCurrentScript, kCreateMovieFlags, &myResRefNum, &tmpMovie);
    if (err != noErr)
        goto bail;
 
    /* Create the QTVR movie track and media */
    err = CreateQTVRTrack(qtvrParams, qtvrParams->trackDuration, tmpMovie, &myQTVRTrack);
    if (err != noErr)
        goto bail;
    
    /* Create panorama track and media, and add them to the movie */
    srGW = srcGW; hsGW = srcHSGW; fsGW = srcFSGW;
    err = CreatePanoTrackFromGWorlds(1, &srcGW, (hsGW ? &hsGW : NULL), (fsGW ? &fsGW : NULL),
        panoType, qtvrParams, qtvrParams->trackDuration, tmpMovie, myQTVRTrack,
        &myPanoTrack
    );
    if (err != noErr)
        goto bail;
        
    /* Add a user data item that identifies the QTVR movie controller */
    err = SetQTControllerType(tmpMovie, kQTVRQTVRType);
    if (err != noErr)
        goto bail;
        
    /* Create the final, flattened movie, from the temporary file into a new movie file;
     * put the movie resource first so that FastStart is possible
     */
    err = FlattenQTVRMovie(tmpMovie, dstMovieSpec);
    
bail:
    if (myResRefNum != -1)      CloseMovieFile(myResRefNum);
    if (tmpMovie     != NULL)   DisposeMovie(tmpMovie);
    DeleteMovieFile(&tmpSpec);
 
    return(err);
}
 
 
/*******************************************************************************
 * VRGWorldToQTVRCylPano2v0
 *******************************************************************************/
 
OSErr
VRGWorldToQTVRCylPano2v0(
    MakeQTVRParams  *qtvrParams,    /* Parameters used to create the movie */
    GWorldPtr       srcGW,          /* Source GWorld, rotated 90 degrees counter-clockwise (classic orientation) */
    GWorldPtr       srcHSGW,        /* Source hot spot GWorld, rotated 90 degrees counter-clockwise (classic orientation) */
    GWorldPtr       srcFSGW,        /* Source fast start GWorld, rotated 90 degrees counter-clockwise (classic orientation) */
    FSSpec          *dstMovieSpec   /* Destination movie */
)
{
    return(VRGWorldToQTVRCylPano2(qtvrParams, kQTVRCylindricalVersion2v, srcGW, srcHSGW, srcFSGW, dstMovieSpec));
}
 
 
/*******************************************************************************
 * VRGWorldToQTVRCylPano2h0
 *******************************************************************************/
 
OSErr
VRGWorldToQTVRCylPano2h0(
    MakeQTVRParams  *qtvrParams,    /* Parameters used to create the movie */
    GWorldPtr       srcGW,          /* Source GWorld, non-rotated (modern orientation) */
    GWorldPtr       srcHSGW,        /* Source hot spot GWorld, non-rotated (modern orientation) */
    GWorldPtr       srcFSGW,        /* Source fast start GWorld, non-rotated (modern orientation) */
    FSSpec          *dstMovieSpec   /* Destination movie */
)
{
    return(VRGWorldToQTVRCylPano2(qtvrParams, kQTVRCylindricalVersion2h, srcGW, srcHSGW, srcFSGW, dstMovieSpec));
}
 
 
#pragma mark -
#pragma mark ### Cubic API ###
#pragma mark -
/*******************************************************************************
 * VRMovieToQTVRCubicPano
 *
 *  Create a single-node cubic panoramic QTVR movie from the specified 6-frame movie.
 *******************************************************************************/
 
OSErr
VRMovieToQTVRCubicPano(
    MakeQTVRParams  *qtvrParams,        /* Parameters used to create the movie */
    FSSpec          *srcFramesSpec,     /* Source movie with the panorama faces */
    FSSpec          *srcHSFramesSpec,   /* Source movie with the hot spot faces */
    FSSpec          *srcFSFramesSpec,   /* Source movie with the fast start faces */
    FSSpec          *dstMovieSpec       /* Destination movie */
)
{
    FSSpec              tmpSpec;
    short               myResRefNum         = -1;
    Movie               tmpMovie                = NULL;
    Track               tmpQTVRTrack;
    Track               tmpPanoTrack;
    ComponentResult     err;
    
    /* Create a temporary version of the panorama movie file,
     * located in the same directory as the destination panorama movie file
     */
    MakeTempFSSpec(dstMovieSpec, ".MV~", &tmpSpec);
    err = CreateMovieFile(&tmpSpec, FOUR_CHAR_CODE('TVOD'), smCurrentScript, kCreateMovieFlags, &myResRefNum, &tmpMovie);
    if (err != noErr)
        goto bail;
 
    /* Create the QTVR movie track and media */
    err = CreateQTVRTrack(qtvrParams, qtvrParams->trackDuration, tmpMovie, &tmpQTVRTrack);
    if (err != noErr)
        goto bail;
    
    /* Create panorama track and media, and add them to the movie */
    err = CreatePanoTrackFromMovies(srcFramesSpec, srcHSFramesSpec, srcFSFramesSpec,
        qtvrParams, kQTVRCubicVersion1, tmpMovie, tmpQTVRTrack,
        &tmpPanoTrack
    );
    if (err != noErr)
        goto bail;
        
    /* Add a user data item that identifies the QTVR movie controller */
    err = SetQTControllerType(tmpMovie, kQTVRQTVRType);
    if (err != noErr)
        goto bail;
        
    /* Create the final, flattened movie, from the temporary file into a new movie file;
     * put the movie resource first so that FastStart is possible
     */
    err = FlattenQTVRMovie(tmpMovie, dstMovieSpec);
 
bail:
    if (myResRefNum != -1)      CloseMovieFile(myResRefNum);
    if (tmpMovie     != NULL)   DisposeMovie(tmpMovie);
    DeleteMovieFile(&tmpSpec);
    
    return(err);
}
 
 
/********************************************************************************
 * VRPictsToQTVRCubicPano
 *
 * This converts a set of 6 picture files to a cubic QuickTime VR panorama movie.
 *
 * This implements tiling of the faces, but the face dimensions must be appropriately divisible:
 *      (width  - tilesH + 1) / tilesH = integer
 *      (height - tilesV + 1) / tilesV = integer
 *  e.g. {dim=512,tiles=1}, {dim=511,tiles=2}, {dim=510,tiles=3}
 ********************************************************************************/
 
OSErr
VRPictsToQTVRCubicPano(
    MakeQTVRParams  *qtvrParams,        /* Parameters used to create the movie */
    FSSpec          **srcPictSpecs,     /* Source     images */
    FSSpec          **srcHSPictSpecs,   /* Hot spot   images */
    FSSpec          **srcFSPictSpecs,   /* Fast start images */
    FSSpec          *dstMovieSpec       /* Destination movie */
)
{
    FSSpec                      tmpSpec;
    short                       tmpRefNum           = -1;
    Movie                       tmpMovie            = NULL;
    Track                       qtvrTrack           = NULL;
    Track                       panoTrack           = NULL;
    ComponentResult             err;
    
    /* Create a temporary version of the panorama movie file,
     * located in the same directory as the destination panorama movie file
     */
    MakeTempFSSpec(dstMovieSpec, ".MV~", &tmpSpec);
    err = CreateMovieFile(&tmpSpec, FOUR_CHAR_CODE('TVOD'), smCurrentScript, kCreateMovieFlags, &tmpRefNum, &tmpMovie);
    if (err != noErr)
        goto bail;
 
    /* Create the QTVR movie track and media */
    err = CreateQTVRTrack(qtvrParams, qtvrParams->trackDuration, tmpMovie, &qtvrTrack);
    if (err != noErr)
        goto bail;
    
    /*  Create panorama track and media, and add them to the movie */
    err = CreatePanoTrackFromPicts(6, srcPictSpecs, srcHSPictSpecs, srcFSPictSpecs, kQTVRCubicVersion1,
        qtvrParams, qtvrParams->trackDuration, tmpMovie, qtvrTrack, &panoTrack
    );
    if (err != noErr)
        goto bail;
        
    /* Add a user data item that identifies the QTVR movie controller */
    err = SetQTControllerType(tmpMovie, kQTVRQTVRType);
    if (err != noErr)
        goto bail;
        
    /* Create the final, flattened movie, from the temporary file into a new movie file;
     * put the movie resource first so that FastStart is possible
     */
    err = FlattenQTVRMovie(tmpMovie, dstMovieSpec);
 
bail:
    if (tmpRefNum != -1)    CloseMovieFile(tmpRefNum);
    if (tmpMovie  != NULL)  DisposeMovie(tmpMovie);
    DeleteMovieFile(&tmpSpec);
    
    return(err);
}
 
 
/********************************************************************************
 * VRGWorldsToQTVRCubicPano
 *
 *  Given 6 GWorlds (and possible 6 hotspot GWorlds), create a cubic panorama movie.
 *
 *  This implements tiling of the faces, but the face dimensions must be appropriately divisible:
 *      (width  - tilesH + 1) / tilesH = integer
 *      (height - tilesV + 1) / tilesV = integer
 *  e.g. {dim=512,tiles=1}, {dim=511,tiles=2}, {dim=510,tiles=3}
 ********************************************************************************/
 
OSErr
VRGWorldsToQTVRCubicPano(
    MakeQTVRParams  *qtvrParams,    /* Parameters used to create the movie */
    GWorldPtr       *srcGWs,        /* 6 Source GWorlds - in standard order */
    GWorldPtr       *srcHSGWs,      /* 6 Source hot spot GWorlds - in standard order */
    GWorldPtr       *srcFSGWs,      /* 6 Source fast start GWorlds - in standard order */
    FSSpec          *dstMovieSpec   /* Destination movie */
)
{
    FSSpec                      tmpSpec;
    short                       tmpRefNum           = -1;
    Movie                       tmpMovie            = NULL;
    Track                       qtvrTrack           = NULL;
    Track                       panoTrack           = NULL;
    ComponentResult             err;
    
    /* Create a temporary version of the panorama movie file,
     * located in the same directory as the destination panorama movie file
     */
    MakeTempFSSpec(dstMovieSpec, ".MV~", &tmpSpec);
    err = CreateMovieFile(&tmpSpec, FOUR_CHAR_CODE('TVOD'), smCurrentScript, kCreateMovieFlags, &tmpRefNum, &tmpMovie);
    if (err != noErr)
        goto bail;
 
    /* Create the QTVR movie track and media */
    err = CreateQTVRTrack(qtvrParams, qtvrParams->trackDuration, tmpMovie, &qtvrTrack);
    if (err != noErr)
        goto bail;
    
    /*  Create panorama track and media, and add them to the movie */
    err = CreatePanoTrackFromGWorlds(6, srcGWs, srcHSGWs, srcFSGWs, kQTVRCubicVersion1,
        qtvrParams, qtvrParams->trackDuration, tmpMovie, qtvrTrack, &panoTrack
    );
    if (err != noErr)
        goto bail;
        
    /* Add a user data item that identifies the QTVR movie controller */
    err = SetQTControllerType(tmpMovie, kQTVRQTVRType);
    if (err != noErr)
        goto bail;
        
    /* Create the final, flattened movie, from the temporary file into a new movie file;
     * put the movie resource first so that FastStart is possible
     */
    err = FlattenQTVRMovie(tmpMovie, dstMovieSpec);
 
bail:
    if (tmpRefNum != -1)    CloseMovieFile(tmpRefNum);
    if (tmpMovie  != NULL)  DisposeMovie(tmpMovie);
    DeleteMovieFile(&tmpSpec);
    
    return(err);
}