void
fgLoadImgAnyFormat(
    const FgString &    fname,
    FgImgRgbaUb &       img)
{
    if (!fgFileReadable(fname))
        fgThrow("Unable to read file",fname);
    fgEnsureMagick();
    FgScopePtr<ExceptionInfo>   exception(AcquireExceptionInfo(),DestroyExceptionInfo);
    FgScopePtr<ImageInfo>       image_info(CloneImageInfo(0),DestroyImageInfo);
    (void) strcpy(image_info->filename,fname.as_utf8_string().c_str());
    // TODO: Do we need to convert into a specific colourspace?
    Image *imgPtr = ReadImage(image_info.get(),exception.get());
    if (imgPtr == 0)
        // exception.description is NULL
        fgThrow("Unable to read image file",fname + ": " + exception->reason);
    FgScopeGuard                sg0(boost::bind(DestroyImage,imgPtr));
    FgScopePtr<CacheView>       view(AcquireCacheView(imgPtr),DestroyCacheView);
    img.resize(uint(imgPtr->columns),uint(imgPtr->rows));
    for(uint row = 0; row < imgPtr->rows; ++row) {
        for(uint column = 0; column < imgPtr->columns; ++column) {
            PixelPacket pixel;
            FGASSERT(MagickTrue == 
                    GetOneCacheViewAuthenticPixel(view.get(),column,row,&pixel,exception.get()));
            img.elem(column,row).red() = uchar(pixel.red);
            img.elem(column,row).green() = uchar(pixel.green);
            img.elem(column,row).blue() = uchar(pixel.blue);
            img.elem(column,row).alpha() =  255 - uchar(pixel.opacity);
        }
    }
}
static
void
resize(const FgArgs &)
{
    string              fname("base/test/testorig.jpg");
    FgImgRgbaUb         img;
    fgLoadImgAnyFormat(fgDataDir()+fname,img);
    FgImgRgbaUb         out(img.width()/2+1,img.height()+1);
    fgImgResize(img,out);
    fgImgDisplay(out);
}
void
fgSampler(
    const FgSample &    sample,
    FgImgRgbaUb &       img,
    uint                antiAliasBitDepth)
{
    FgImgRgbaF                  fimg(img.dims());
    fgSampler(sample,fimg,antiAliasBitDepth);
    for (FgIter2UI it(img.dims()); it.valid(); it.next())
    {
        const FgRgbaF & fpix = fimg[it()];
        img[it()] = 
            FgRgbaUB(
                uchar(fgClamp(fpix.red(),0.0f,255.0f)),
                uchar(fgClamp(fpix.green(),0.0f,255.0f)),
                uchar(fgClamp(fpix.blue(),0.0f,255.0f)),
                uchar(fgClamp(fpix.alpha(),0.0f,255.0f)));
    }
}
FgImgRgbaUb
fgOglGetRender()
{
    glReadBuffer(GL_FRONT);
    FgImgRgbaUb     ret;
    GLint           x[4];
    glGetIntegerv(GL_VIEWPORT,x);
    FgVect2UI       dims(x[2],x[3]);
    if (dims.volume() > 0) {
        ret.resize(dims);
        FgRgbaUB *          ptr = ret.dataPtr();
        for (uint yy=0; yy<dims[1]; ++yy) {     // Invert line ordering from OGL:
            glReadPixels(0,dims[1]-1-yy,dims[0],1,GL_RGBA,GL_UNSIGNED_BYTE,ptr);
            ptr += dims[0];
        }
    }
    glReadBuffer(GL_BACK);          // Restore to default
    return ret;
}
void
fgSaveImgAnyFormat(
    const FgString &    fname,
    const FgImgRgbaUb & img)
{
    FGASSERT(fname.length() > 0);

    fgEnsureMagick();
    FgScopePtr<ImageInfo>       image_info(CloneImageInfo(0),DestroyImageInfo);
    FgScopePtr<ExceptionInfo>   exception(AcquireExceptionInfo(),DestroyExceptionInfo);
    FgScopePtr<Image>           image(ConstituteImage(img.width(),img.height(),"RGBA",
                                                   CharPixel,
                                                   img.dataPtr(),
                                                   exception.get()),
                                   DestroyImage);
    (void) strcpy(image_info->filename,fname.as_utf8_string().c_str());
    FGASSERT(
        MagickTrue == 
            WriteImages(image_info.get(),image.get(),fname.as_utf8_string().c_str(),exception.get()));
}
static
void
uvmask(const FgArgs & args)
{
    FgSyntax    syntax(args,
        "<meshIn>.<ext0> <imageIn>.<ext1> <meshOut>.<ext2>\n"
        "    <ext0> = " + fgLoadMeshFormatsDescription() + "\n"
        "    <ext1> = " + fgImgCommonFormatsDescription() + "\n"
        "    <ext2> = " + fgSaveMeshFormatsDescription()
        );
    Fg3dMesh        mesh = fgLoadMeshAnyFormat(syntax.next());
    FgImgRgbaUb     img;
    fgLoadImgAnyFormat(syntax.next(),img);
    FgImage<FgBool> mask = FgImage<FgBool>(img.dims());
    for (FgIter2UI it(img.dims()); it.valid(); it.next()) {
        FgVect4UC   px = img[it()].m_c;
        mask[it()] = (px[0] > 0) || (px[1] > 0) || (px[2] > 0); }
    mask = fgAnd(mask,fgFlipHoriz(mask));
    mesh = fg3dMaskFromUvs(mesh,mask);
    fgSaveMeshAnyFormat(mesh,syntax.next());
}
bool
fgCompareImages(
    const FgImgRgbaUb & test,
    const FgImgRgbaUb & ref,
    uint                maxDelta)
{
    if (test.dims() != ref.dims())
        return false;
    int             lim = int(maxDelta * maxDelta);
    for (FgIter2UI it(test.dims()); it.valid(); it.next())
    {
        FgVect4I delta =
            FgVect4I(test[it()].m_c) -
            FgVect4I(ref[it()].m_c);
        if ((fgSqr(delta[0]) > lim) ||
            (fgSqr(delta[1]) > lim) ||
            (fgSqr(delta[2]) > lim) ||
            (fgSqr(delta[3]) > lim))
            return false;
    }
    return true;
}
//****************************************************************************
//                              saveXsiFile
//****************************************************************************
static bool saveXsiFile(

    const FgString                  &fname,
    const FffMultiObjectC           &model,
    const vector<FffMultiObjectC>   *morphTargets,
    const string                    &appName)
{
    size_t numTargets=0;
    if (morphTargets)
    {
        numTargets = morphTargets->size();
    }

    FgPath      path(fname);
    path.ext = "xsi";
    FgOfstream ofs(path.str());
    if (!ofs)
    {
        return false;
    }

    //
    // Get object's bounding box (for camera and lighting info)
    //
    FgVect3F centroid(0.0f,0.0f,0.0f);
    FgVect3F lowBound(0.0f,0.0f,0.0f);
    FgVect3F hiBound(0.0f,0.0f,0.0f);
    unsigned long numTotalVtx=0;
    for (unsigned long yy=0; yy<model.numObjs(); ++yy)
    {
        // Get aliase names for all the lists.
        const vector<FgVect3F>    &vtxList = model.getPtList(yy);

        // Calculate the info
        for (unsigned long pt=0; pt<vtxList.size(); ++pt)
        {
            centroid += vtxList[pt];
            ++numTotalVtx;
            if (yy==0 && pt==0)
            {
                lowBound = vtxList[pt];
                hiBound = vtxList[pt];
            }
            else
            {
                if (lowBound[0] > vtxList[pt][0]) lowBound[0]=vtxList[pt][0];
                if (lowBound[1] > vtxList[pt][1]) lowBound[1]=vtxList[pt][1];
                if (lowBound[2] > vtxList[pt][2]) lowBound[2]=vtxList[pt][2];
                if (hiBound[0] < vtxList[pt][0]) hiBound[0]=vtxList[pt][0];
                if (hiBound[1] < vtxList[pt][1]) hiBound[1]=vtxList[pt][1];
                if (hiBound[2] < vtxList[pt][2]) hiBound[2]=vtxList[pt][2];
            }
        }
    }
    centroid /= float(numTotalVtx);
    FgVect3F camPos = centroid;
    float zdiff = hiBound[2] - lowBound[2];
    camPos[2] += zdiff*2.0f;
    float camNearPlane = 0.1f;
    float camFarPlane = zdiff*5.0f;

    //
    // Write (version 3.0) file header
    //
    ofs << "xsi 0300txt 0032\n\n" << flush;

    //
    // Output some standard info
    //
    ofs << "\n";
    ofs << "SI_FileInfo\n";
    ofs << "{\n";
    ofs << "   \"\",\n";
    ofs << "   \"" << appName << "\",\n";
    ofs << "}\n";
    ofs << "\n";
    ofs << "SI_Scene scene1\n";
    ofs << "{ \n";
    ofs << "   \"FRAMES\",\n";
    ofs << "   1.000000,\n";
    if (numTargets)
        ofs << "   " << numTargets+1 << ".000000,\n";
    else
        ofs << "   100.000000,\n";
    ofs << "   30.0,\n";
    ofs << "}\n";
    ofs << "\n";
    ofs << "SI_CoordinateSystem coord1\n";
    ofs << "{\n";
    ofs << "   1;\n";
    ofs << "   0;\n";
    ofs << "   1;\n";
    ofs << "   0;\n";
    ofs << "   2;\n";
    ofs << "   5;\n";
    ofs << "}\n";
    ofs << "\n";
    ofs << "SI_Angle\n";
    ofs << "{\n";
    ofs << "   0;\n";
    ofs << "}\n";
    ofs << "\n";
    ofs << "SI_Camera camera1\n";
    ofs << "{\n";
                // Position
    ofs << "   " << floatToString(camPos[0]) << "; " 
                 << floatToString(camPos[1]) << "; " 
                 << floatToString(camPos[2]) << ";;\n";
                // Point of interest (camera direction)
    ofs << "   " << floatToString(centroid[0]) << "; "
                 << floatToString(centroid[1]) << "; "
                 << floatToString(centroid[2]) << ";;\n";
                // Roll
    ofs << "   0.0;\n";
                // Field of view
    ofs << "   55.0;\n";
                // Near plane units
    ofs << "   " << floatToString(camNearPlane) << ";\n";
                // Far plane units
    ofs << "   " << floatToString(camFarPlane) << ";\n";
    ofs << "}\n";
    ofs << "\n" << flush;
    ofs << "SI_Ambience\n";
    ofs << "{\n";
    ofs << "   0.2; 0.2; 0.2;;\n";
    ofs << "}\n";
    ofs << "\n" << flush;
    ofs << "SI_Light light1\n";
    ofs << "{\n";
                // Type
    ofs << "   0;\n";
                // Colour of light
    ofs << "   1.0; 1.0; 1.0;;\n";
                // Position of light (above the camera)
    ofs << "   " << floatToString(camPos[0]) << "; "
                 << floatToString(hiBound[1]) << "; "
                 << floatToString(camPos[2]) << ";;\n";
    ofs << "}\n";
    ofs << "\n" << flush;


    //
    // Now output the material list.
    //
    ofs << "SI_MaterialLibrary MATLIB-scene1\n";
    ofs << "{\n";
    ofs << "   " << model.numObjs() << ",\n";
    unsigned long xx;
    for (xx=0; xx<model.numObjs(); ++xx)
    {
        string objName = model.getModelName(xx);

        ofs << "\n";
        ofs << "   SI_Material " << objName << "\n";
        ofs << "   {\n";
        ofs << "      0.7, 0.7, 0.7, 1.0,\n";   // Diffuse colour
        ofs << "      50.0,\n";                 // Specular decay
        ofs << "      1.0, 1.0, 1.0,\n";        // Specular colour
        ofs << "      0.0, 0.0, 0.0,\n";        // Emissive colour
        ofs << "      2,\n";                    // Shading type (2=phong)
        ofs << "      0.3, 0.3, 0.3,\n";        // Ambient colour

        // Now output texture image info.
        string txtFname = model.getTextureFilename(xx);
        unsigned long imgWd=0, imgHgt=0;
        if (txtFname != "" && txtFname.length() > 0)
        {
            // Get the image size by loading the image
            FgImgRgbaUb     tmpImg;
            fgLoadImgAnyFormat(path.dir()+txtFname,tmpImg);
            {
                imgWd = tmpImg.width();
                imgHgt = tmpImg.height();
            }
        }

        if (imgWd != 0 && imgHgt != 0)
        {
            string objTexName = objName + string(".Material.") 
                              + objName + string(".texture.map");
            ofs << "      SI_Texture2D " << objTexName << "\n";
            ofs << "      {\n";
            ofs << "         \"" << txtFname << "\";\n";
            ofs << "         3;\n";             // UV map (unwrapped)
            ofs << "         " << imgWd << ";" << imgHgt << ";\n";
            ofs << "         0;" << imgWd-1 << ";0;" << imgHgt-1 << ";\n";
            ofs << "         0;\n";             // No UV swap
            ofs << "         1;1;\n";
            ofs << "         0;0;\n";
            ofs << "         1.0;1.0;\n";       // UV scale
            ofs << "         0.0;0.0;\n";       // UV offset
            ofs << "         1.0,0.0,0.0,0.0,\n";   // Project matrix
            ofs << "         0.0,1.0,0.0,0.0,\n";
            ofs << "         0.0,0.0,1.0,0.0,\n";
            ofs << "         0.0,0.0,0.0,1.0;;\n";
            ofs << "         3;\n";             // No mask blending
            ofs << "         1.0;\n";           // Blending value
            ofs << "         0.0;\n";           // Texture ambient
            ofs << "         0.0;\n";           // Texture diffuse
            ofs << "         0.0;\n";           // Texture specular
            ofs << "         0.0;\n";           // Texture transparency
            ofs << "         0.0;\n";           // Texture reflectivity
            ofs << "         0.0;\n";           // Texture roughness
            ofs << "      }\n";
        }

        ofs << "   }\n" << flush;
    }
    ofs << "}\n";
    ofs << "\n" << flush;

    //
    // Now we output the models
    //
    for (xx=0; xx<model.numObjs(); ++xx)
    {
        string objName = model.getModelName(xx);

        // Get aliase names for all the lists.
        const vector<FgVect3F>    &vtxList = model.getPtList(xx);
        const vector<FgVect3UI>    &triList = model.getTriList(xx);
        const vector<FgVect4UI>    &quadList = model.getQuadList(xx);
        const vector<FgVect2F>    &txtList = model.getTextCoord(xx);
        const vector<FgVect3UI>    &txtTriList = model.getTexTriList(xx);
        const vector<FgVect4UI>    &txtQuadList = model.getTexQuadList(xx);
        bool perFacet = false;
        bool perVertex = false;
        if (vtxList.size() == txtList.size() &&
            txtTriList.size() == 0 &&
            txtQuadList.size() == 0)
            perVertex = true;
        else if (txtList.size() > 0 &&
                 txtTriList.size() == triList.size() &&
                 txtQuadList.size() == quadList.size())
            perFacet = true;

        // Create a new list for vertex normals
        vector<FgVect3F> normList;
        normList.resize(vtxList.size());

        // Output the current model
        ofs << "SI_Model MDL-" << objName << endl;
        ofs << "{\n";
        ofs << "   SI_Transform SRT-" << objName << endl;
        ofs << "   {\n";
        ofs << "      1.0, 1.0, 1.0, \n";
        ofs << "      0.0, 0.0, 0.0, \n";
        ofs << "      0.0, 0.0, 0.0, \n";
        ofs << "   }\n";
        ofs << "\n";
        ofs << "   SI_GlobalMaterial\n";
        ofs << "   {\n";
        ofs << "      \"" << objName << "\",\n";
        ofs << "      \"BRANCH\",\n";
        ofs << "   }\n";
        ofs << "\n";
        ofs << "   SI_Visibility\n";
        ofs << "   {\n";
        ofs << "      1, \n";
        ofs << "   }\n";
        ofs << "\n";
        ofs << "   SI_Mesh MSH-" << objName << endl;
        ofs << "   {\n";

            // Coordinates (position, normal, and UV)
        ofs << "      SI_Shape SHP-" << objName << "-ORG\n";
        ofs << "      {\n";
        if (perVertex || perFacet)
            ofs << "         3,\n";
        else
            ofs << "         2,\n";
        ofs << "         \"ORDERED\",\n";
        ofs << "\n";
        ofs << "         " << vtxList.size() << ",\n";
        ofs << "         \"POSITION\",\n";
        unsigned long pt;
        for (pt=0; pt<vtxList.size(); ++pt)
        {
            ofs << "         "
                << floatToString(vtxList[pt][0]) << ", "
                << floatToString(vtxList[pt][1]) << ", "
                << floatToString(vtxList[pt][2]) << ",\n";

            // Initialize the normal list to zero.
            normList[pt] = FgVect3F(0);
        }
        ofs << "\n";
        // Calculate the normal first
        for (unsigned long tt=0; tt<triList.size(); ++tt)
        {
            FgVect3F norm = calTriNormals(vtxList,triList,tt);
            normList[ triList[tt][0] ] += norm;
            normList[ triList[tt][1] ] += norm;
            normList[ triList[tt][2] ] += norm;
        }
        for (unsigned long qq=0; qq<quadList.size(); ++qq)
        {
            FgVect3F norm = calQuadNormals(vtxList,quadList,qq);
            normList[ quadList[qq][0] ] += norm;
            normList[ quadList[qq][1] ] += norm;
            normList[ quadList[qq][2] ] += norm;
            normList[ quadList[qq][3] ] += norm;
        }
        ofs << "         " << normList.size() << ",\n";
        ofs << "         \"NORMAL\",\n";
        for (pt=0; pt<normList.size(); ++pt)
        {
            float ln = normList[pt].length();
            if (ln > 0.0f) normList[pt] /= ln;
            ofs << "         "
                << floatToString(normList[pt][0]) << ", "
                << floatToString(normList[pt][1]) << ", "
                << floatToString(normList[pt][2]) << ",\n";
        }
        if (perVertex || perFacet)
        {
            ofs << "\n";
            ofs << "         " << txtList.size() << ",\n";
            ofs << "         \"TEX_COORD_UV\",\n";
            for (pt=0; pt<txtList.size(); ++pt)
            {
                ofs << "         "
                    << floatToString(txtList[pt][0]) << ", "
                    << floatToString(txtList[pt][1]) << ", \n";
            }
        }
        ofs << "      }\n" << flush;

            //
            // Facets
            //
        if (triList.size())
        {
            ofs << "\n";
            ofs << "      SI_TriangleList " << objName << "\n";
            ofs << "      {\n";
            ofs << "         " << triList.size() << ",\n";
            if (perVertex || perFacet)
                ofs << "         \"NORMAL|TEX_COORD_UV\",\n";
            else
                ofs << "         \"NORMAL\",\n";
            ofs << "         \"" << objName << "\",\n";

            // Triangle vertex id.
            ofs << "\n";
            unsigned long tri;
            for (tri=0; tri<triList.size(); ++tri)
            {
                ofs << "         "
                    << triList[tri][0] << ", "
                    << triList[tri][1] << ", "
                    << triList[tri][2] << ", \n";
            }

            // Triangle's normal id.
            ofs << "\n";
            for (tri=0; tri<triList.size(); ++tri)
            {
                ofs << "         "
                    << triList[tri][0] << ", "
                    << triList[tri][1] << ", "
                    << triList[tri][2] << ", \n";
            }

            // Triangle's texture coordinate id.
            if (perVertex)
            {
                ofs << "\n";
                for (tri=0; tri<triList.size(); ++tri)
                {
                    ofs << "         "
                        << triList[tri][0] << ", "
                        << triList[tri][1] << ", "
                        << triList[tri][2] << ", \n";
                }
            }
            else if (perFacet)
            {
                ofs << "\n";
                for (tri=0; tri<txtTriList.size(); ++tri)
                {
                    ofs << "         "
                        << txtTriList[tri][0] << ", "
                        << txtTriList[tri][1] << ", "
                        << txtTriList[tri][2] << ", \n";
                }
            }
            ofs << "      }\n" << flush;
        }

        if (quadList.size())
        {
            ofs << "\n";
            ofs << "      SI_PolygonList " << objName << "\n";
            ofs << "      {\n";
            ofs << "         " << quadList.size() << ",\n";
            if (perVertex || perFacet)
                ofs << "         \"NORMAL|TEX_COORD_UV\",\n";
            else
                ofs << "         \"NORMAL\",\n";
            ofs << "         \"" << objName << "\",\n";
            ofs << "         " << 4*quadList.size() << ",\n";
            unsigned long qu;
            for (qu=0; qu<quadList.size(); ++qu)
            {
                ofs << "         4,\n";
            }

            // Quad's vertex id.
            ofs << "\n";
            for (qu=0; qu<quadList.size(); ++qu)
            {
                ofs << "         "
                    << quadList[qu][0] << ", "
                    << quadList[qu][1] << ", "
                    << quadList[qu][2] << ", "
                    << quadList[qu][3] << ", \n";
            }

            // Quad's normal id.
            ofs << "\n";
            for (qu=0; qu<quadList.size(); ++qu)
            {
                ofs << "         "
                    << quadList[qu][0] << ", "
                    << quadList[qu][1] << ", "
                    << quadList[qu][2] << ", "
                    << quadList[qu][3] << ", \n";
            }

            // Quad's texture coordinate id.
            if (perVertex)
            {
                ofs << "\n";
                for (qu=0; qu<quadList.size(); ++qu)
                {
                    ofs << "         "
                        << quadList[qu][0] << ", "
                        << quadList[qu][1] << ", "
                        << quadList[qu][2] << ", "
                        << quadList[qu][3] << ", \n";
                }
            }
            else if (perFacet)
            {
                ofs << "\n";
                for (qu=0; qu<txtQuadList.size(); ++qu)
                {
                    ofs << "         "
                        << txtQuadList[qu][0] << ", "
                        << txtQuadList[qu][1] << ", "
                        << txtQuadList[qu][2] << ", "
                        << txtQuadList[qu][3] << ", \n";
                }
            }
            ofs << "      }\n" << flush;
        }

        // Output morph targets as animations
        if (morphTargets)
        {
            size_t numMorphs = morphTargets->size();
            if (numMorphs)
            {
                ofs << "\n";
                ofs << "      SI_ShapeAnimation SHPANIM-" << objName << endl;
                ofs << "      {\n";
                ofs << "         \"LINEAR\",\n";
                ofs << "         " << numMorphs+1 << ",\n";
                for (unsigned long mm=0; mm<numMorphs+1; ++mm)
                {
                    const vector<FgVect3F> *mvtxList = &vtxList;
                    if (mm > 0)
                        mvtxList = &((*morphTargets)[mm-1].getPtList(xx));

                    normList.resize( mvtxList->size() );

                    ofs << "\n";
                    ofs << "         SI_Shape SHP-" << objName 
                                                    << "-" << mm << "\n";
                    ofs << "         {\n";
                    if (perVertex || perFacet)
                        ofs << "            3,\n";
                    else
                        ofs << "            2,\n";
                    ofs << "            \"INDEXED\",\n";
 
                    // New vertex list.
                    ofs << "\n";
                    ofs << "            " << mvtxList->size() << ",\n";
                    ofs << "            \"POSITION\",\n";
                    for (pt=0; pt<mvtxList->size(); ++pt)
                    {
                        ofs << "            " << pt << ", "
                            << floatToString((*mvtxList)[pt][0]) << ", "
                            << floatToString((*mvtxList)[pt][1]) << ", "
                            << floatToString((*mvtxList)[pt][2]) << ",\n";

                        // Initialize the normal list to zero.
                        normList[pt] = FgVect3F(0.0f);
                    }

                    // New normal list.
                    size_t  tt;
                    for (tt=0; tt<triList.size(); ++tt)
                    {
                        FgVect3F norm=calTriNormals(*mvtxList,triList,ulong(tt));
                        normList[ triList[tt][0] ] += norm;
                        normList[ triList[tt][1] ] += norm;
                        normList[ triList[tt][2] ] += norm;
                    }
                    size_t  qq;
                    for (qq=0; qq<quadList.size(); ++qq)
                    {
                        FgVect3F norm=calQuadNormals(*mvtxList,quadList,ulong(qq));
                        normList[ quadList[qq][0] ] += norm;
                        normList[ quadList[qq][1] ] += norm;
                        normList[ quadList[qq][2] ] += norm;
                        normList[ quadList[qq][3] ] += norm;
                    }
                    ofs << "\n";
                    ofs << "            " << normList.size() << ",\n";
                    ofs << "            \"NORMAL\",\n";
                    for (pt=0; pt<normList.size(); ++pt)
                    {
                        float ln = normList[pt].length();
                        if (ln > 0.0f) normList[pt] /= ln;
                        ofs << "            " << pt << ", "
                            << floatToString(normList[pt][0]) << ", "
                            << floatToString(normList[pt][1]) << ", "
                            << floatToString(normList[pt][2]) << ",\n";
                    }

                    // Texture UV
                    if (perVertex || perFacet)
                    {
                        ofs << "\n";
                        ofs << "            " << txtList.size() << ",\n";
                        ofs << "            \"TEX_COORD_UV\",\n";
                        for (pt=0; pt<txtList.size(); ++pt)
                        {
                            ofs << "            " << pt << ", "
                                << floatToString(txtList[pt][0]) << ", "
                                << floatToString(txtList[pt][1]) << ", \n";
                        }
                    }

                    ofs << "         }\n" << flush;
                }
                ofs << "\n";
                ofs << "         SI_FCurve " << objName << "-SHPANIM-1\n";
                ofs << "         {\n";
                ofs << "            \"" << objName << "\",\n";
                ofs << "            \"SHPANIM-1\",\n";
                ofs << "            \"LINEAR\",\n";
                ofs << "            1,1,\n";
                ofs << "            " << numMorphs+1 << ",\n";
                size_t      mm;
                for (mm=0; mm<numMorphs+1; ++mm)
                {
                    ofs << "            " << mm+1 << ", " << mm << ".0,\n";
                }
                ofs << "         }\n";
                ofs << "      }\n" << flush;
            }
        }

        // End of SI_Mesh
        ofs << "   }\n";

        // End of SI_Model
        ofs << "}\n";

        // End of current model
        ofs << "\n" << flush;
    }

    //
    // Done.
    //
    if (ofs.fail())
    {
        ofs.close();
        return false;
    }
    else
    {
        ofs.close();
        return true;
    }
}