/* ** Command & Conquer Renegade(tm) ** Copyright 2025 Electronic Arts Inc. ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . */ /*********************************************************************************************** *** Confidential - Westwood Studios *** *********************************************************************************************** * * * Project Name : LightMap * * * * $Archive:: /Commando/Code/Tool $* * * * $Author:: Ian_l $* * * * $Modtime:: 8/24/01 9:24p $* * * * $Revision:: 60 $* * * *---------------------------------------------------------------------------------------------* * Functions: * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ // Includes. #include "StdAfx.h" #include "LightMap.h" #include "Lightscape.h" #include "LightMapPacker.h" #include "OptionsDialog.h" #include "PerlinNoise.h" #include "StringBuilder.h" #include "TextureNameNode.h" #include "matrix3.h" #include "matrix3d.h" #include "lightexclude.h" #include "tri.h" #include "vector3.h" #include "wwmath.h" #include #include #include // Static data. LightscapeSolve *LightscapeSolve::_ActiveImporter = NULL; /*********************************************************************************************** * LightscapeSolve::LightscapeSolve -- * * * * INPUT: * * * * OUTPUT: * * * * WARNINGS: * * * * HISTORY: * * 7/27/99 IML : Created. * *=============================================================================================*/ LightscapeSolve::LightscapeSolve (const char *solvedirectoryname, const char *solvefilenamelist, CStatusBar* statusptr, const char *statusbarmessage, bool blendnoise) : LightmapPacker() { const char *notsolutiontext = "One or more solve files are not solution (.ls) files."; const int materialtexturenamegrowthstep = 256; const int patchfacegrowthstep = 1024; const int vertexgrowthstep = 2048; const int lightgrowthstep = 32; static char _messagebuffer [1024]; CRect srect, trect; BOOL success; StringBuilder errormessage (_messagebuffer, sizeof (_messagebuffer)); const char *solvefilename; LtTBuilderFactory *factory = NULL; OptionsDialog options; // Initialize. SmoothingAngle = DEG_TO_RAD (options.Get_Smoothing_Angle()); // Convert from degrees to radians. SpatialTolerance = options.Get_Spatial_Tolerance(); // No mapping required. FilterSharpness = pow (options.Get_Filter_Sharpness() / 50.0l, 5.0l); // Map from linear to exponential scale. // If noise has been requested create a Perlin Noise generator. if (blendnoise) { ProceduralTexture = new PerlinNoise; ASSERT (ProceduralTexture != NULL); } else { ProceduralTexture = NULL; } if (options.Get_Light_Export_Selective()) { LightExclusionString = options.Get_Light_Exclusion_String(); } else { LightExclusionString = NULL; } statusptr->GetItemRect (0, &srect); statusptr->SetPaneText (0, statusbarmessage); statusptr->GetDC()->DrawText (statusbarmessage, -1, &trect, DT_CALCRECT); CRect prect (srect.TopLeft().x + trect.Width(), srect.TopLeft().y, srect.BottomRight().x, srect.BottomRight().y); ProgressBar = new CProgressCtrl; ASSERT (ProgressBar != NULL); success = ProgressBar->Create (WS_CHILD | WS_VISIBLE | PBS_SMOOTH, prect, statusptr, 0); ASSERT (success); // Specify how many elements should be added to dynamic arrays when they are resized. MaterialTextureNames.Set_Growth_Step (materialtexturenamegrowthstep); PatchFaces.Set_Growth_Step (patchfacegrowthstep); Vertices.Set_Growth_Step (vertexgrowthstep); VertexIndices.Set_Growth_Step (vertexgrowthstep); Lights.Set_Growth_Step (lightgrowthstep); _ActiveImporter = this; try { char solvepathname [_MAX_PATH]; int totalpatchclustercount = 0; // Parse the files, check that they are solve files and sum the patch cluster count for the progress bar. solvefilename = solvefilenamelist; while (strlen (solvefilename) > 0) { strcpy (solvepathname, solvedirectoryname); strcat (solvepathname, solvefilename); factory = new LsInformationFactory; ASSERT (factory != NULL); ::LtSolutionImport (solvepathname, *factory); if (!IsSolution) throw (notsolutiontext); totalpatchclustercount += PatchClusterCount; errormessage.Copy (factory->GetErrorMsg()); delete factory; factory = NULL; if (strlen (errormessage.String()) > 0) throw (errormessage.String()); // Advance to next solve file. solvefilename += strlen (solvefilename) + 1; } // Set-up the progress bar so that it can be updated for every patch cluster. ProgressBar->SetRange32 (0, totalpatchclustercount); ProgressBar->SetStep (1); // For each solve filename in the list... solvefilename = solvefilenamelist; while (strlen (solvefilename) > 0) { strcpy (solvepathname, solvedirectoryname); strcat (solvepathname, solvefilename); // Parse the Lightscape solve file and process materials. // NOTE: Material processing must be done first because mesh processing is dependant on it. // In order to guarantee this, the solution file is parsed twice, so that there is no // implicit reliance on the material data preceeding the mesh data in the file. factory = new LsPreparationFactory; ASSERT (factory != NULL); ::LtSolutionImport (solvepathname, *factory); errormessage.Copy (factory->GetErrorMsg()); delete factory; factory = NULL; if (strlen (errormessage.String()) > 0) throw (errormessage.String()); // Sort the material-texture names by a key defined by the comparison function Compare_Material_Names(). // NOTE: qsort MUST (and does) allow for duplicate keys. if (MaterialTextureNames.Count() > 0) { qsort (&MaterialTextureNames [0], MaterialTextureNames.Count(), sizeof (char*), Compare_Material_Texture_Names); } // Parse the Lightscape solve file and add all vertices and faces that have been // processed by Mesh to Texture. factory = new LsMainFactory; ASSERT (factory != NULL); ::LtSolutionImport (solvepathname, *factory); errormessage.Copy (factory->GetErrorMsg()); delete factory; factory = NULL; if (strlen (errormessage.String()) > 0) throw (errormessage.String()); // Clean-up the the material-texture names. They are no longer required. for (int i = 0; i < MaterialTextureNames.Count(); i++) { delete [] MaterialTextureNames [i]; } MaterialTextureNames.Clear(); // Advance to next solve file. solvefilename += strlen (solvefilename) + 1; } if (Vertices.Count() > 0) { // Create a vertex index table and initialize the indices to identity. for (int v = 0; v < Vertices.Count(); v++) { VertexIndices.Insert (v, v); } // Sort the vertex pointers by a key defined by the comparison function Compare_Vertices(). // NOTE: qsort MUST (and does) allow for duplicate keys. qsort (&VertexIndices [0], VertexIndices.Count(), sizeof (unsigned), Compare_Vertices); } // Sort the patch faces by a key defined by the comparison function Compare_Patch_Faces(). // NOTE: qsort MUST (and does) allow for duplicate keys. if (PatchFaces.Count() > 0) { qsort (&PatchFaces [0], PatchFaces.Count(), sizeof (PatchFaceStruct), Compare_Patch_Faces); } } catch (const char *errormessage) { // Clean-up. if (factory != NULL) delete factory; if (ProgressBar != NULL) { delete ProgressBar; ProgressBar = NULL; } // Re-throw the message to the caller. throw (errormessage); } _ActiveImporter = NULL; delete ProgressBar; ProgressBar = NULL; } /*********************************************************************************************** * LightscapeSolve::Finish -- * * * * INPUT: * * * * OUTPUT: * * * * WARNINGS: * * * * HISTORY: * * 7/27/99 IML : Created. * *=============================================================================================*/ LightscapeSolve::Finish() { LightmapPacker::Finish(); } /*********************************************************************************************** * LightscapeSolve::~LightscapeSolve -- * * * * INPUT: * * * * OUTPUT: * * * * WARNINGS: Do not throw any exceptions in this (and any other) destructor because this * * destructor can itself be called during the exception as part of its 'clean-up'. * * * * HISTORY: * * 7/27/99 IML : Created. * *=============================================================================================*/ LightscapeSolve::~LightscapeSolve() { int i, l; // Clean-up anything that didn't get cleaned up before because an exception was thrown. for (i = 0; i < MaterialTextureNames.Count(); i++) { delete [] MaterialTextureNames [i]; } _ActiveImporter = NULL; // Clean-up the texture names. for (i = 0; i < TextureNames.Length(); i++) { TextureNameNode *texturenamenode, *removalnode; texturenamenode = TextureNames [i]; while (texturenamenode != NULL) { removalnode = texturenamenode; texturenamenode = texturenamenode->Next; delete removalnode; } } // Release references to lights. for (l = 0; l < Lights.Count(); l++) { Lights [l]->Release_Ref(); } if (ProceduralTexture != NULL) delete ProceduralTexture; } /*********************************************************************************************** * LightscapeSolve::Add_Material_Texture_Name -- * * * * INPUT: * * * * OUTPUT: * * * * WARNINGS: * * * * HISTORY: * * 7/27/99 IML : Created. * *=============================================================================================*/ void LightscapeSolve::Add_Material_Texture_Name (const char *materialname, const char *texturepathname) { const char *invalidtexturepathname = ""; unsigned materialnamelength, texturepathnamelength; char *name; // The material name must exist. ASSERT (materialname != NULL); // If the material has no associated texture use the invalid texture pathname. if (texturepathname == NULL) texturepathname = invalidtexturepathname; // Calculate length of strings including terminator. materialnamelength = strlen (materialname) + 1; texturepathnamelength = strlen (texturepathname) + 1; name = new char [materialnamelength + texturepathnamelength]; ASSERT (name != NULL); strcpy (name, materialname); // Append the texture name to the material name but leave the material name's terminator // intact so that conventional string functions only 'see' the material name. strcpy (name + materialnamelength, texturepathname); MaterialTextureNames.Add (name); } /*********************************************************************************************** * LightscapeSolve::Add_Texture_Name -- * * * * INPUT: * * * * OUTPUT: * * * * WARNINGS: * * * * HISTORY: * * 9/16/99 IML : Created. * *=============================================================================================*/ void LightscapeSolve::Add_Texture_Name (unsigned patchindex, const char *materialname) { const char *materialnotfoundtext = "There is a face in the model that has a material name that cannot be found."; int length; char **materialnameptr; char *texturepathname; unsigned texturepathnamelength; // Can the texture name data be accomodated by the texture name array? length = TextureNames.Length(); if ((int) patchindex >= length) { const int growthstep = 1024; // Add this many elements if the vector array runs out of room. bool success = TextureNames.Resize (patchindex + growthstep); ASSERT (success); // Null out the new elements. for (int i = length; i < TextureNames.Length(); i++) { TextureNames [i] = NULL; } } // NOTE: The material-texture names MUST have been sorted prior to calling bsearch(). if (MaterialTextureNames.Count() > 0) { const char **keyptr; const char *key; key = materialname; keyptr = &key; materialnameptr = (char**) bsearch (keyptr, &MaterialTextureNames [0], MaterialTextureNames.Count(), sizeof (char*), Compare_Material_Texture_Names); } else { materialnameptr = NULL; } // If the material cannot be found then either the database has malfunctioned or the solve // file is corrupt. if (materialnameptr == NULL) { ASSERT (FALSE); throw (materialnotfoundtext); } // Extract the texture name. texturepathname = *materialnameptr + strlen (*materialnameptr) + 1; texturepathnamelength = strlen (texturepathname) + 1; // Add the name to the database only if it exists (has non-zero length excluding terminator). if (texturepathnamelength > 1) { TextureNameNode *node; node = new TextureNameNode (texturepathname); ASSERT (node != NULL); // Add the texture name to the database. node->Next = TextureNames [patchindex]; TextureNames [patchindex] = node; } } /*********************************************************************************************** * LightscapeSolve::Add_Light -- * * * * INPUT: * * * * OUTPUT: * * * * WARNINGS: * * * * HISTORY: * * 3/24/00 IML : Created. * *=============================================================================================*/ void LightscapeSolve::Add_Light (LightClass *light) { Lights.Add (light); light->Add_Ref(); } /*********************************************************************************************** * LightscapeSolve::Add_Vertex -- * * * * INPUT: * * * * OUTPUT: * * * * WARNINGS: * * * * HISTORY: * * 1/25/00 IML : Created. * *=============================================================================================*/ bool LightscapeSolve::Add_Vertex (unsigned vertexindex, const Vector3 &p, const Vector3 &n, unsigned patchindex, const Vector2 &t) { bool success; // If adding a M2T vertex, ensure that this is a M2T solve. ASSERT (Importer()->Is_M2T_Solve()); // Has the element already been inserted? if (((int) vertexindex) < Vertices.Count()) { // Check that the new element data matches the existing data. success = (Vertices [vertexindex].Point == p) && (Vertices [vertexindex].FaceNormal == n) && (Vertices [vertexindex].PatchIndex == patchindex); } else { VerticesStruct vertex; vertex.Point = p; vertex.FaceNormal = n; vertex.PatchIndex = patchindex; vertex.ValidColor = false; vertex.ValidUV = false; success = Vertices.Insert (vertexindex, vertex); } if (success) { Vertices [vertexindex].UV = t; Vertices [vertexindex].ValidUV = true; } return (success); } /*********************************************************************************************** * LightscapeSolve::Add_Vertex -- * * * * INPUT: * * * * OUTPUT: * * * * WARNINGS: * * * * HISTORY: * * 1/25/00 IML : Created. * *=============================================================================================*/ bool LightscapeSolve::Add_Vertex (unsigned vertexindex, const Vector3 &p, const Vector3 &n, unsigned patchindex, const ColorVector &c) { bool success; // Has the element already been inserted? if (((int) vertexindex) < Vertices.Count()) { // Check that the new element data matches the existing data. success = (Vertices [vertexindex].Point == p) && (Vertices [vertexindex].FaceNormal == n) && (Vertices [vertexindex].PatchIndex == patchindex); } else { VerticesStruct vertex; vertex.Point = p; vertex.FaceNormal = n; vertex.PatchIndex = patchindex; vertex.ValidColor = false; vertex.ValidUV = false; success = Vertices.Insert (vertexindex, vertex); } if (success) { // Only add/set the color if this is not a M2T solve. if (!Importer()->Is_M2T_Solve()) { // Add or set the color? if (Vertices [vertexindex].ValidColor) { const ColorVector clamp (1.0f, 1.0f, 1.0f); // Add and clamp. Vertices [vertexindex].Color += c; Vertices [vertexindex].Color.Update_Min (clamp); } else { Vertices [vertexindex].Color = c; Vertices [vertexindex].ValidColor = true; } } } return (success); } /*********************************************************************************************** * LightscapeSolve::Add_Patch_Face -- * * * * INPUT: * * * * OUTPUT: * * * * WARNINGS: * * * * HISTORY: * * 9/16/99 IML : Created. * *=============================================================================================*/ bool LightscapeSolve::Add_Patch_Face (unsigned patchfaceindex, unsigned patchindex, unsigned vertexindexcount, unsigned *vertexindices) { bool success; // Only 3 or 4 vertex indices are supported. ASSERT (vertexindexcount == 3 || vertexindexcount == 4); // Has the element already been inserted? if (((int) patchfaceindex) < PatchFaces.Count()) { // Check that the new element data matches the existing data. success = (patchindex == PatchFaces [patchfaceindex].PatchIndex) && (vertexindices [0] == PatchFaces [patchfaceindex].VertexIndices [0]) && (vertexindices [1] == PatchFaces [patchfaceindex].VertexIndices [1]) && (vertexindices [2] == PatchFaces [patchfaceindex].VertexIndices [2]); if (vertexindexcount == 4) { success &= (vertexindices [3] == PatchFaces [patchfaceindex].VertexIndices [3]); } } else { PatchFaceStruct patchface; patchface.PatchIndex = patchindex; patchface.VertexIndices [0] = vertexindices [0]; patchface.VertexIndices [1] = vertexindices [1]; patchface.VertexIndices [2] = vertexindices [2]; if (vertexindexcount == 4) { patchface.VertexIndices [3] = vertexindices [3]; } else { patchface.VertexIndices [3] = PatchFaceStruct::VERTEX_INDEX_INVALID; } success = PatchFaces.Insert (patchfaceindex, patchface); } return (success); } /*********************************************************************************************** * LightscapeSolve::Find_Vertex -- * * * * INPUT: * * * * OUTPUT: * * * * WARNINGS: * * * * HISTORY: * * 1/07/00 IML : Created. * *=============================================================================================*/ void LightscapeSolve::Find_Vertex (const Vector3 &point, const Vector3 *smoothingnormalptr, W3dRGBStruct &vertexcolor, SolveStatistics &solvestatistics) { const int growthstep = 256; const W3dRGBStruct pink (255, 0, 255); DynamicVectorClass vertexset; ColorVector color; unsigned count; vertexset.Set_Growth_Step (growthstep); Find_Vertices (point, SpatialTolerance, vertexset); if (vertexset.Count() > 0) { color.Set (0.0f, 0.0f, 0.0f); count = 0; if (smoothingnormalptr != NULL) { // Sum the colors of all vertices that lie within the smoothing normal. for (int v = 0; v < vertexset.Count(); v++) { if (vertexset [v]->ValidColor) { float angle; // Is the face that is associated with this vertex within the smoothing angle // of the supplied vertex normal? angle = acosf (Vector3::Dot_Product (*smoothingnormalptr, vertexset [v]->FaceNormal)); if (angle <= SmoothingAngle) { color += vertexset [v]->Color; count++; } } } if (count > 0) { color /= ((float) count); vertexcolor.Set (color.X, color.Y, color.Z); return; } } // Sum the colors of all vertices regardless of smoothing normal. for (int v = 0; v < vertexset.Count(); v++) { if (vertexset [v]->ValidColor) { color += vertexset [v]->Color; count++; } } if (count > 0) { color /= ((float) count); vertexcolor.Set (color.X, color.Y, color.Z); solvestatistics.Count [SolveStatistics::VERTEX_NOT_SMOOTH]++; return; } else { // There are no vertices with a valid color. Return a place-holder color. vertexcolor = pink; solvestatistics.Count [SolveStatistics::VERTEX_NO_COLOR]++; return; } } // Vertex not found. Return a place-holder color. vertexcolor = pink; solvestatistics.Count [SolveStatistics::VERTEX_NOT_FOUND]++; return; } /*********************************************************************************************** * LightscapeSolve::Find_Triangle -- * * * * INPUT: * * * * OUTPUT: * * * * WARNINGS: * * * * HISTORY: * * 06/07/00 IML : Created. * *=============================================================================================*/ void LightscapeSolve::Find_Triangle (const Vector3 *points, const Vector3 &normal, PackingTriangle &triangle, SolveStatistics &solvestatistics) { const unsigned notextureid = 0xffffffff; // Arbitrary ID used to indicate that there is no associated texture. VerticesStruct trianglevertices [Triangle::VERTICES_COUNT]; FaceResultEnum faceresult; bool notexture; unsigned v; ASSERT (points != NULL); // First, attempt a match with the face itself. switch (Face_Size (points, Triangle::VERTICES_COUNT)) { case FACE_UNDERSIZED: case FACE_NOT_UNDERSIZED: Find_Triangle (points, normal, trianglevertices, faceresult); break; case FACE_DEGENERATE: solvestatistics.Count [SolveStatistics::FACE_DEGENERATE]++; faceresult = FACE_NOT_FOUND; break; } notexture = false; switch (faceresult) { case FACE_AMBIGUOUS: solvestatistics.Count [SolveStatistics::FACE_AMBIGUOUS]++; // No break. case FACE_FOUND: { TextureNameNode *texturenamenodeptr; bool validuvs; // Check that texture exists and that all UV's are valid. texturenamenodeptr = (TextureNameNode*) Texture_Name (trianglevertices [0].PatchIndex); if (texturenamenodeptr != NULL) { validuvs = true; for (v = 0; v < Triangle::VERTICES_COUNT; v++) { validuvs &= trianglevertices [v].ValidUV; } if (validuvs) { triangle.TextureNameNodePtr = texturenamenodeptr; triangle.TextureID = trianglevertices [0].PatchIndex; triangle.Normal = trianglevertices [0].FaceNormal; for (v = 0; v < Triangle::VERTICES_COUNT; v++) { triangle.Vertices [v].Point = trianglevertices [v].Point; triangle.Vertices [v].UV = trianglevertices [v].UV; } break; } } notexture = true; solvestatistics.Count [SolveStatistics::FACE_NO_LIGHTMAP]++; break; } case FACE_NOT_FOUND: notexture = true; solvestatistics.Count [SolveStatistics::FACE_NOT_FOUND]++; break; } if (notexture) { // Return the original triangle with no associated texture. triangle.Normal = normal; for (v = 0; v < Triangle::VERTICES_COUNT; v++) { triangle.Vertices [v].Point = points [v]; } triangle.TextureNameNodePtr = NULL; triangle.TextureID = notextureid; } } /*********************************************************************************************** * LightscapeSolve::Submit_Triangle -- * * * * INPUT: * * * * OUTPUT: * * * * WARNINGS: * * * * HISTORY: * * 06/07/00 IML : Created. * *=============================================================================================*/ void LightscapeSolve::Submit_Triangle (PackingTriangle &triangle) { DynamicVectorClass adjtriangles; adjtriangles.Set_Growth_Step (32); Find_Adjacent_Triangles (triangle, adjtriangles); Submit (&triangle, adjtriangles); } /*********************************************************************************************** * LightscapeSolve::Find_Adjacent_Triangles -- * * * * INPUT: * * * * OUTPUT: * * * * WARNINGS: * * * * HISTORY: * * 6/05/00 IML : Created. * *=============================================================================================*/ void LightscapeSolve::Find_Adjacent_Triangles (const PackingTriangle &principaltriangle, DynamicVectorClass &adjtriangles) { DynamicVectorClass spatialvertexset; // For each principal triangle vertex... for (unsigned v = 0; v < Triangle::VERTICES_COUNT; v++) { // Find all vertices that lie within spatial tolerance of this principal triangle vertex. spatialvertexset.Clear(); Find_Vertices (principaltriangle.Vertices [v].Point, SpatialTolerance, spatialvertexset); for (int s = 0; s < spatialvertexset.Count(); s++) { Vector3 normal; float angle; // Is the face that is associated with this spatial vertex within the smoothing angle of the principal triangle's normal? normal = spatialvertexset [s]->FaceNormal; angle = acosf (Vector3::Dot_Product (principaltriangle.Normal, normal)); if (angle <= SmoothingAngle) { TextureNameNode *texturenamenodeptr; unsigned textureid; // Is the texture name valid? texturenamenodeptr = (TextureNameNode*) Texture_Name (spatialvertexset [s]->PatchIndex); textureid = spatialvertexset [s]->PatchIndex; if (texturenamenodeptr != NULL) { PatchFaceStruct p, *patchfaceptr; // Find a patch face in the database with patch index that matches the patch index of the spatial vertex. p.PatchIndex = spatialvertexset [s]->PatchIndex; patchfaceptr = (PatchFaceStruct*) bsearch (&p, &PatchFaces [0], PatchFaces.Count(), sizeof (PatchFaceStruct), Compare_Patch_Faces); // Does a patch exist? if (patchfaceptr != NULL) { // Step backwards in the database to the first patch that matches. // NOTE: bsearch() does not necessarily return the first key in the array that matches when there are duplicate keys. while (patchfaceptr > &PatchFaces [0]) { if (Compare_Patch_Faces (&p, patchfaceptr - 1) != 0) break; patchfaceptr--; } // Step forwards in the database while there is a patch match. while ((patchfaceptr < &PatchFaces [0] + PatchFaces.Count()) && (Compare_Patch_Faces (&p, patchfaceptr) == 0)) { unsigned trianglecount; // Is this patch face a triangle or a quadrilateral? trianglecount = (patchfaceptr->VertexIndices [PatchFaceStruct::VERTICES_PER_PATCH_FACE - 1] == PatchFaceStruct::VERTEX_INDEX_INVALID) ? 1 : 2; // For each triangle... for (unsigned t = 0; t < trianglecount; t++) { const unsigned _vertexindex [2][3] = {{0, 1, 2}, {0, 2, 3}}; bool validuvs; unsigned v; validuvs = true; for (v = 0; v < Triangle::VERTICES_COUNT; v++) { validuvs &= Vertices [patchfaceptr->VertexIndices [_vertexindex [t][v]]].ValidUV; } if (validuvs) { Triangle triangle; bool hasequivalent; triangle.TextureNameNodePtr = texturenamenodeptr; triangle.TextureID = textureid; triangle.Normal = normal; for (v = 0; v < Triangle::VERTICES_COUNT; v++) { triangle.Vertices [v].Point = Vertices [patchfaceptr->VertexIndices [_vertexindex [t][v]]].Point; triangle.Vertices [v].UV = Vertices [patchfaceptr->VertexIndices [_vertexindex [t][v]]].UV; } // Linearly search the existing adjacent triangles to see if this triangle already exists. hasequivalent = false; for (int a = 0; a < adjtriangles.Count(); a++) { if (triangle.Is_Equivalent (adjtriangles [a])) { hasequivalent = true; break; } } if (!hasequivalent) adjtriangles.Add (triangle); } } // Advance to next patch face. patchfaceptr++; } } } } } } } /*********************************************************************************************** * LightscapeSolve::Find_Triangle -- * * * * INPUT: * * * * OUTPUT: * * * * WARNINGS: * * * * HISTORY: * * 7/27/99 IML : Created. * *=============================================================================================*/ void LightscapeSolve::Find_Triangle (const Vector3 *points, const Vector3 &facenormal, VerticesStruct *vertices, FaceResultEnum &faceresult) { const unsigned attemptcount = 14; // No. of binary search attempts (see note below). ASSERT (points != NULL); // Attempt to find the face with zero tolerance. Find_Triangle (points, 0.0f, true, &facenormal, vertices, faceresult); // If the face was not found then attempt to find a face by iterating over a range of // tolerances using a binary search technique. if (faceresult == FACE_NOT_FOUND) { bool faceambiguous; float maxtolerance = SpatialTolerance; float mintolerance = 0.00f; float tolerance; tolerance = (maxtolerance + mintolerance) * 0.5f; // NOTE: The tolerance will be increased or decreased, depending upon whether the // current result is 'not found' or 'ambiguous'. On each iteration the tolerance // range (max tolerance - min tolerance) is halved. Thus, after n iterations, the // tolerance range will be (max tolerance - min tolerance) / 2 ^ n. I have purposely // set n, the number of attempts, to yield a range at the limit of floating point // accuracy. faceambiguous = false; for (unsigned attempt = 0; attempt < attemptcount; attempt++) { Find_Triangle (points, tolerance, false, NULL, vertices, faceresult); if (faceresult == FACE_AMBIGUOUS) { // Flag that an ambiguous result occured. faceambiguous = true; // Decrease the tolerance. maxtolerance = tolerance; tolerance = (tolerance + mintolerance) * 0.5f; } else { if (faceresult == FACE_NOT_FOUND) { // Increase the tolerance. mintolerance = tolerance; tolerance = (tolerance + maxtolerance) * 0.5f; } else { // The face has been found. ASSERT (faceresult == FACE_FOUND); break; } } } // If an ambiguous result occured on ANY iteration then the maximum tolerance is the // smallest tolerance at which the ambiguity exists. Try to resolve the ambiguity // using the centroid and normal tests. if ((faceresult != FACE_FOUND) && faceambiguous) { Find_Triangle (points, maxtolerance, true, &facenormal, vertices, faceresult); } } } /*********************************************************************************************** * LightscapeSolve::Find_Triangle -- * * * * INPUT: * * * * OUTPUT: * * * * WARNINGS: * * * * HISTORY: * * 7/27/99 IML : Created. * *=============================================================================================*/ void LightscapeSolve::Find_Triangle (const Vector3 *points, float tolerance, bool centroidtest, const Vector3 *facenormal, VerticesStruct *vertices, FaceResultEnum &faceresult) { const int growthstep = 256; DynamicVectorClass vertexset [Triangle::VERTICES_COUNT]; DynamicVectorClass *candidatevertexset = &vertexset [0]; unsigned patchindex; int n, c, v; bool success; ASSERT (points != NULL); // Create a set of vertex indices that correspond to the zeroth point. candidatevertexset->Set_Growth_Step (growthstep); Find_Vertices (points [0], tolerance, *candidatevertexset); // Remove any duplicate patches from the candidate set. c = 0; while (c < candidatevertexset->Count()) { patchindex = (*candidatevertexset) [c]->PatchIndex; v = c + 1; while (v < candidatevertexset->Count()) { if (patchindex == (*candidatevertexset) [v]->PatchIndex) { success = candidatevertexset->Delete (v); ASSERT (success); } else { v++; } } c++; } // For all other points... for (n = 1; n < Triangle::VERTICES_COUNT; n++) { // Create a set of vertex indices that correspond to n'th pair. vertexset [n].Set_Growth_Step (growthstep); Find_Vertices (points [n], tolerance, vertexset [n]); // Delete all indexes in the candidate index set that have materials that DO NOT occur in the vertex index set. c = 0; while (c < candidatevertexset->Count()) { bool found; patchindex = (*candidatevertexset) [c]->PatchIndex; found = false; for (v = 0; v < vertexset [n].Count(); v++) { if (patchindex == vertexset [n][v]->PatchIndex) { found = true; break; } } if (!found) { success = candidatevertexset->Delete (c); ASSERT (success); } else { c++; } } } // If there is more than one candidate and the centroid test can be applied then apply it. if ((candidatevertexset->Count() > 1) && centroidtest) { // If there is a patch-face database... if (PatchFaces.Count() > 0) { // Attempt to eliminate the ambiguity by applying a centroid test. Vector3 centroid; centroid = points [0]; for (n = 1; n < Triangle::VERTICES_COUNT; n++) { centroid += points [n]; } centroid /= (float) Triangle::VERTICES_COUNT; // For each candidate... c = 0; while (c < candidatevertexset->Count()) { bool contained; PatchFaceStruct p; PatchFaceStruct *patchfaceptr; unsigned trianglecount; patchindex = (*candidatevertexset) [c]->PatchIndex; contained = false; // Find a face in the array that matches the patch. p.PatchIndex = patchindex; patchfaceptr = (PatchFaceStruct*) bsearch (&p, &PatchFaces [0], PatchFaces.Count(), sizeof (PatchFaceStruct), Compare_Patch_Faces); if (patchfaceptr != NULL) { // Step backwards in the array to the first patch that matches. // NOTE: bsearch() does not necessarily return the first key in the array that matches when there are duplicate keys. while (patchfaceptr > &PatchFaces [0]) { if (Compare_Patch_Faces (&p, patchfaceptr - 1) != 0) break; patchfaceptr--; } // Step forwards in the array while there is a patch match. // Find all vertices whose points and normals approximately match the requested normal and add their patch indices to the patch array. while ((patchfaceptr < &PatchFaces [0] + PatchFaces.Count()) && (Compare_Patch_Faces (&p, patchfaceptr) == 0)) { // NOTE: The candidate point may not match the any of the material face // points because Lightscape may have changed the topology. Thus it is // necessary to search ALL of the faces that have this material. // For each triangle in the patch face... // NOTE: Define the first triangle to be the points 0-1-2. If the patch face is a quadrilateral, // then define the second triangle to be the points 0-2-3. trianglecount = (patchfaceptr->VertexIndices [3] == PatchFaceStruct::VERTEX_INDEX_INVALID) ? 1 : 2; for (unsigned t = 0; t < trianglecount; t++) { // Create a triangle from the material face and test it against the centroid for containment. TriClass triangle; Vector3 normal; triangle.V [0] = &Vertices [patchfaceptr->VertexIndices [0]].Point; triangle.V [1] = &Vertices [patchfaceptr->VertexIndices [1 + t]].Point; triangle.V [2] = &Vertices [patchfaceptr->VertexIndices [2 + t]].Point; triangle.N = &normal; triangle.Compute_Normal(); if (triangle.Contains_Point (centroid)) { contained = true; break; } } if (contained) break; // Advance to next patch face. patchfaceptr++; } } if (!contained) { success = candidatevertexset->Delete (c); ASSERT (success); } else { c++; } } } } // If there is still more than one candidate and the normal exists then apply the normal test. if ((candidatevertexset->Count() > 1) && (facenormal != NULL)) { float largestdp; // Find the smallest angle (largest dot product) between the vertex face normal and the target face normal. largestdp = (*facenormal) * ((*candidatevertexset) [0]->FaceNormal); for (c = 1; c < candidatevertexset->Count(); c++) { largestdp = MAX (largestdp, (*facenormal) * ((*candidatevertexset) [c]->FaceNormal)); } // Delete all candidates that do not have the largest dot product. c = 0; while (c < candidatevertexset->Count()) { float dp = (*facenormal) * ((*candidatevertexset) [c]->FaceNormal); if (dp < largestdp) { success = candidatevertexset->Delete (c); ASSERT (success); } else { c++; } } } // Now if there are no candidates then return failure. if (candidatevertexset->Count() == 0) { faceresult = FACE_NOT_FOUND; } else { unsigned candidateindex = 0; // NOTE: If there is >1 candidate at this point the result is still ambiguous. // If the face normal has been supplied, select the candidate with closest // face normal - otherwise select the zeroth candidate. if (facenormal != NULL) { float mindistance = FLT_MAX; for (n = 0; n < candidatevertexset->Count(); n++) { float distance; distance = Manhatten_Distance (*facenormal - (*candidatevertexset) [n]->FaceNormal); if (distance < mindistance) { mindistance = distance; candidateindex = n; } } } // For the zeroth point... patchindex = (*candidatevertexset) [candidateindex]->PatchIndex; vertices [0] = *((*candidatevertexset) [candidateindex]); // For all other points... for (n = 1; n < Triangle::VERTICES_COUNT; n++) { bool assigned = false; for (v = 0; v < vertexset [n].Count(); v++) { if (vertexset [n][v]->PatchIndex == patchindex) { vertices [n] = *(vertexset [n][v]); assigned = true; break; } } ASSERT (assigned); } // Flag if the result was ambiguous. if (candidatevertexset->Count() > 1) { faceresult = FACE_AMBIGUOUS; } else { // There must be exactly one candidate. ASSERT (candidatevertexset->Count() == 1); faceresult = FACE_FOUND; } } } /*********************************************************************************************** * LightscapeSolve::Find_Vertices -- * * * * INPUT: * * * * OUTPUT: * * * * WARNINGS: * * * * HISTORY: * * 7/27/99 IML : Created. * *=============================================================================================*/ void LightscapeSolve::Find_Vertices (const Vector3 &point, float tolerance, DynamicVectorClass &vertices) { if (VertexIndices.Count() > 0) { unsigned *vertexindexptr = NULL; int b0, b1, b; float x; // There should be a 1-1 mapping between vertex indices and vertices. ASSERT (VertexIndices.Count() == Vertices.Count()); // Find a vertex that matches point to within tolerance using binary search technique. b0 = 0; b1 = VertexIndices.Count() - 1; do { b = (b0 + b1) >> 1; x = Vertices [VertexIndices [b]].Point.X; if (point.X - x < -tolerance) { b1 = b - 1; } else { if (point.X - x > tolerance) { b0 = b + 1; } else { vertexindexptr = &VertexIndices [b]; break; } } } while (b0 <= b1); // Step backwards in the array to the first vertex that meets the match criteria defined by Compare_Vertices(). // NOTE: Above search did not necessarily return the first key in the array that matches when there are duplicate keys. if (vertexindexptr != NULL) { while (vertexindexptr > &VertexIndices [0]) { x = Vertices [*(vertexindexptr - 1)].Point.X; if (point.X - x > tolerance) break; vertexindexptr--; } // Step forwards in the array while there is an approximate vertex match. // Add all vertices that are valid and whose points match the requested point within tolerance. while (vertexindexptr < &VertexIndices [0] + VertexIndices.Count()) { VerticesStruct *vertexptr; vertexptr = &Vertices [*vertexindexptr]; if (point.X - vertexptr->Point.X < -tolerance) break; if (Manhatten_Distance (vertexptr->Point - point) <= tolerance) { vertices.Add (vertexptr); } // Advance to next vertex index. vertexindexptr++; } } } } /*********************************************************************************************** * LightscapeSolve::Face_Size -- * * * * INPUT: * * * * OUTPUT: * * * * WARNINGS: * * * * HISTORY: * * 9/17/99 IML : Created. * *=============================================================================================*/ LightscapeSolve::FaceSizeEnum LightscapeSolve::Face_Size (const Vector3 *points, unsigned count) { const float undersizedarea = 0.0016f; // An area << the area of a lightmap lumel (assuming // that lumel density is less than 625 lumels/metre). float area; // Test for degeneracy. // NOTE: Assume that the points are ordered (for triangles this is guaranteed). for (unsigned n = 0; n < count; n++) { if (points [n] == points [(n + 1) % count]) return (FACE_DEGENERATE); } // Currently, the area calculation requires that the polygon is a triangle. ASSERT (count == 3); // Test for a small area. area = 0.5f * Vector3::Cross_Product (points [1] - points [0], points [2] - points [0]).Length(); if (area < undersizedarea) return (FACE_UNDERSIZED); return (FACE_NOT_UNDERSIZED); } /*********************************************************************************************** * LightscapeSolve::Longest_Edge -- * * * * INPUT: * * * * OUTPUT: * * * * WARNINGS: * * * * HISTORY: * * 9/17/99 IML : Created. * *=============================================================================================*/ unsigned LightscapeSolve::Longest_Edge (const Vector3 *points, unsigned count, bool *usededges) { float maxlength = -1.0f; float length; unsigned edgeindex; for (unsigned e = 0; e < count; e++) { if (!usededges [e]) { length = (points [(e + 1) % count] - points [e]).Length(); if (length > maxlength) { maxlength = length; edgeindex = e; } } } // An edge must have been found. ASSERT (maxlength >= 0.0f); usededges [edgeindex] = true; return (edgeindex); } /*********************************************************************************************** * LightscapeSolve::Texture_Name -- * * * * INPUT: * * * * OUTPUT: * * * * WARNINGS: * * * * HISTORY: * * 7/27/99 IML : Created. * *=============================================================================================*/ const TextureNameNode *LightscapeSolve::Texture_Name (unsigned patchindex) { // If the array element does not exist yet then the name has not been set. if ((int) patchindex >= TextureNames.Length()) return (NULL); return TextureNames [patchindex]; } /*********************************************************************************************** * LightscapeSolve::Compare_Material_Texture_Names -- * * * * INPUT: * * * * OUTPUT: * * * * WARNINGS: * * * * HISTORY: * * 7/27/99 IML : Created. * *=============================================================================================*/ int LightscapeSolve::Compare_Material_Texture_Names (const void *materialname0, const void *materialname1) { return (strcmp (*((const char**) materialname0), *((const char**) materialname1))); } /*********************************************************************************************** * LightscapeSolve::Compare_Vertices * * * * INPUT: * * * * OUTPUT: * * * * WARNINGS: * * * * HISTORY: * * 7/27/99 IML : Created. * *=============================================================================================*/ int LightscapeSolve::Compare_Vertices (const void *vertexindexptr0, const void *vertexindexptr1) { float x0, x1; ASSERT (_ActiveImporter != NULL); x0 = _ActiveImporter->Vertices [*((unsigned*) vertexindexptr0)].Point.X; x1 = _ActiveImporter->Vertices [*((unsigned*) vertexindexptr1)].Point.X; if (x0 < x1) { return (-1); } else { if (x0 > x1) { return (1); } else { return (0); } } } /*********************************************************************************************** * LightscapeSolve::Compare_Patch_Faces -- * * * * INPUT: * * * * OUTPUT: * * * * WARNINGS: * * * * HISTORY: * * 9/17/99 IML : Created. * *=============================================================================================*/ int LightscapeSolve::Compare_Patch_Faces (const void *patchface0, const void *patchface1) { unsigned patchindex0 = ((PatchFaceStruct*) patchface0)->PatchIndex; unsigned patchindex1 = ((PatchFaceStruct*) patchface1)->PatchIndex; if (patchindex0 < patchindex1) { return (-1); } else { if (patchindex0 > patchindex1) { return (1); } else { return (0); } } } /*********************************************************************************************** * LightscapeSolve::Pack -- * * * * INPUT: * * * * OUTPUT: * * * * WARNINGS: * * * * HISTORY: * * 09/07/00 IML : Created. * *=============================================================================================*/ void LightscapeSolve::Pack() { LightmapPacker::Pack (ProceduralTexture); } /*********************************************************************************************** * LsInformationFactory::OnGet*Builder -- * * * * INPUT: * * * * OUTPUT: * * * * WARNINGS: * * * * HISTORY: * * 9/16/99 IML : Created. * *=============================================================================================*/ LtTInfoBuilderApi* LsInformationFactory::OnGetInfoBuilder() { return (new LsInfoBuilder (this)); } LtTParameterBuilderApi* LsInformationFactory::OnGetParameterBuilder() { return (new LsParameterBuilder (this)); } /*********************************************************************************************** * LsPreparationFactory::OnGet*Builder -- * * * * INPUT: * * * * OUTPUT: * * * * WARNINGS: * * * * HISTORY: * * 7/27/99 IML : Created. * *=============================================================================================*/ LtTInfoBuilderApi *LsPreparationFactory::OnGetInfoBuilder() { return (new LsInfoBuilder (this)); } LtTParameterBuilderApi *LsPreparationFactory::OnGetParameterBuilder() { return (new LsParameterBuilder (this)); } LtTMaterialBuilderApi *LsPreparationFactory::OnGetMaterialBuilder() { return (new LsMaterialBuilder (this)); } LtTLampBuilderApi *LsPreparationFactory::OnGetLampBuilder() { return (new LsLampBuilder (this)); } LtTMeshBuilderApi *LsPreparationFactory::OnGetMeshBuilder() { return (new LsMeshInquirer (this)); } /*********************************************************************************************** * LsMainFactory::LsMainFactory -- * * * * INPUT: * * * * OUTPUT: * * * * WARNINGS: * * * * HISTORY: * * 7/23/99 IML : Created. * *=============================================================================================*/ LsMainFactory::LsMainFactory() : LtTBuilderFactory() { PatchFaceIndex = 0; VertexIndex = 0; PatchIndex = 0; } /*********************************************************************************************** * LsMainFactory::OnGet*Builder -- * * * * INPUT: * * * * OUTPUT: * * * * WARNINGS: * * * * HISTORY: * * 7/23/99 IML : Created. * *=============================================================================================*/ LtTInfoBuilderApi* LsMainFactory::OnGetInfoBuilder() { return (new LsInfoBuilder (this)); } LtTParameterBuilderApi* LsMainFactory::OnGetParameterBuilder() { return (new LsParameterBuilder (this)); } LtTMeshBuilderApi* LsMainFactory::OnGetMeshBuilder() { return (new LsMeshBuilder (this)); } /*********************************************************************************************** * LsInfoBuilder::LsInfoBuilder -- * * * * INPUT: * * * * OUTPUT: * * * * WARNINGS: * * * * HISTORY: * * 9/17/99 IML : Created. * *=============================================================================================*/ LsInfoBuilder::LsInfoBuilder (LtTBuilderFactory *factory) : LtTBaseInfoBuilder (factory) { // Do any special initialization here. } /*********************************************************************************************** * LsInfoBuilder::Finish -- * * * * INPUT: * * * * OUTPUT: * * * * WARNINGS: * * * * HISTORY: * * 9/17/99 IML : Created. * *=============================================================================================*/ LtTBool LsInfoBuilder::Finish() { // Read any info values here. LightscapeSolve::Importer()->Set_Is_Solution (IsSolution() ? true : false); LightscapeSolve::Importer()->Set_Patch_Cluster_Count (GetNumGroups()); LightscapeSolve::Importer()->Set_Brightness (GetBrightness()); LightscapeSolve::Importer()->Set_Contrast (GetContrast()); // Return success. return (TRUE); } /*********************************************************************************************** * LsParameterBuilder::LsParameterBuilder -- * * * * INPUT: * * * * OUTPUT: * * * * WARNINGS: * * * * HISTORY: * * 9/17/99 IML : Created. * *=============================================================================================*/ LsParameterBuilder::LsParameterBuilder (LtTBuilderFactory *factory) : LtTBaseParameterBuilder (factory) { // Do any special initialization here. } /*********************************************************************************************** * LsParameterBuilder::Finish -- * * * * INPUT: * * * * OUTPUT: * * * * WARNINGS: * * * * HISTORY: * * 9/17/99 IML : Created. * *=============================================================================================*/ LtTBool LsParameterBuilder::Finish() { // Read any parameter values here. LightscapeSolve::Importer()->Set_Is_Daylight ((GetFlags() & LT_PROCESS_DAYLIGHT) != 0); LightscapeSolve::Importer()->Set_Is_Exterior ((GetFlags() & LT_PROCESS_EXTERIOR) != 0); // Return success. return (TRUE); } /*********************************************************************************************** * LsMateriaBuilder::LsMaterialBuilder -- * * * * INPUT: * * * * OUTPUT: * * * * WARNINGS: * * * * HISTORY: * * 7/27/99 IML : Created. * *=============================================================================================*/ LsMaterialBuilder::LsMaterialBuilder (LtTBuilderFactory *factory) : LtTBaseMaterialBuilder (factory) { // Do any special initialization here. } /*********************************************************************************************** * LsMateriaBuilder::Finish -- * * * * INPUT: * * * * OUTPUT: * * * * WARNINGS: * * * * HISTORY: * * 7/27/99 IML : Created. * *=============================================================================================*/ LtTBool LsMaterialBuilder::Finish() { ASSERT (LightscapeSolve::Importer() != NULL); LightscapeSolve::Importer()->Add_Material_Texture_Name (GetName(), GetTextureName()); // Return success. return (TRUE); } /*********************************************************************************************** * LsLampBuilder::LsLampBuilder -- * * * * INPUT: * * * * OUTPUT: * * * * WARNINGS: * * * * HISTORY: * * 3/24/00 IML : Created. * *=============================================================================================*/ LsLampBuilder::LsLampBuilder (LtTBuilderFactory *factory) : LtTBaseLampBuilder (factory) { // Do any special initialization here. } /*********************************************************************************************** * LsLampBuilder::Finish -- * * * * INPUT: * * * * OUTPUT: * * * * WARNINGS: * * * * HISTORY: * * 3/24/00 IML : Created. * *=============================================================================================*/ LtTBool LsLampBuilder::Finish() { const char *unknownlighttext = "There is a light in the solve with unknown type."; const Vector3 oodistancesquaredfactors (0.0f, 0.0f, 1.0f); const Vector3 zaxis (0.0f, 0.0f, 1.0f); const ColorVector black (0.0f, 0.0f, 0.0f); const ColorVector white (1.0f, 1.0f, 1.0f); float dir [3][3]; LightClass *light; Vector3 position; ColorVector color; Matrix3D transform; ASSERT (LightscapeSolve::Importer() != NULL); // If the light name begins with a '@' skip it - it is not intended for export. if (LightscapeSolve::Importer()->Light_Exclusion_String() != NULL) { if (strstr (GetName(), LightscapeSolve::Importer()->Light_Exclusion_String())) return (TRUE); } // Skip skylight - it cannot be meaningfully translated into a LightClass object. if ((GetType() == LT_LAMP_DAYLIGHT) && (strcmp (GetName(), "Skylight") == 0)) return (TRUE); // If the lamp does not store direct illumination and has not been raytraced then // it has not been included in the solve - skip it. if ((GetFlags() & LT_LAMP_CAST_NO_DIRECT) && !(GetFlags() & LT_LAMP_RAY_TRACE_DIRECT)) return (TRUE); // If this light has non-zero intensity then add the light to the solve database. if (GetIntensity() > 0.0f) { // NOTE 0: Lightscape uses a right-handed coordinate system. // NOTE 1: For directional lights (including sunlight), multiplying the direction matrix // by (0, 0, 1) will yield the direction of the light rays. GetDirection (dir); Matrix3 direction (dir [0][0], dir [0][1], dir [0][2], dir [1][0], dir [1][1], dir [1][2], dir [2][0], dir [2][1], dir [2][2]); // Create light of appropriate type. switch (GetType()) { case LT_LAMP_ISOTROPIC: // Spherical equal intensity distribution. light = new LightClass (LightClass::POINT); ASSERT (light != NULL); break; case LT_LAMP_DIFFUSE: // Hemispherical distribution with intensity proportional to the cosine to the direction of light. // Interpret this light as a spotlight with a 180 degree spot angle. light = new LightClass (LightClass::SPOT); ASSERT (light != NULL); light->Set_Spot_Angle (WWMATH_PI); light->Set_Spot_Direction (zaxis); // For efficiency, use unity as exponent. light->Set_Spot_Exponent (1.0f); break; case LT_LAMP_SPOTLIGHT: // Spotlight distribution. Intensity at beam angle is one-half maximum intensity. Intensity is truncated to 0 at field angle. light = new LightClass (LightClass::SPOT); ASSERT (light != NULL); // NOTE: Field angle is defined as angle swept from center of beam to outside. light->Set_Spot_Angle (GetFieldAngle() * 2.0f); light->Set_Spot_Direction (zaxis); // For efficiency, use unity as exponent. light->Set_Spot_Exponent (1.0f); break; case LT_LAMP_GENERAL: // General distribution is determined from table if intensities in given directions. { float angle; // Get the widest angle - this is normally, but not always, the field angle. angle = MAX (GetBeamAngle(), GetFieldAngle()); if (angle > WWMATH_PI / 2.0f) { // Interpret light as an isometric light. light = new LightClass (LightClass::POINT); ASSERT (light != NULL); } else { // Interpret light as a spotlight. light = new LightClass (LightClass::SPOT); ASSERT (light != NULL); light->Set_Spot_Angle (angle * 2.0f); light->Set_Spot_Direction (zaxis); // For efficiency, use unity as exponent. light->Set_Spot_Exponent (1.0f); } } break; case LT_LAMP_DAYLIGHT: // Special value used to flag daylight when import an LS file. light = new LightClass (LightClass::DIRECTIONAL); ASSERT (light != NULL); break; default: ASSERT (FALSE); throw (unknownlighttext); break; } // Set transform matrix that defines direction and position of light source. position.Set (GetLocation().GetX(), GetLocation().GetY(), GetLocation().GetZ()); transform.Set (direction, position); light->Set_Transform (transform); // Set intensity. light->Set_Intensity (1.0f); RadianceMap radiancemap (LightscapeSolve::Importer()->Get_Brightness(), LightscapeSolve::Importer()->Get_Contrast(), LightscapeSolve::Importer()->Is_Daylight(), LightscapeSolve::Importer()->Is_Exterior()); float i = GetIntensity(); LtTRGBColor irradiance (i * GetFilterColor().GetR(), i * GetFilterColor().GetG(), i * GetFilterColor().GetB()); // Set colors. color.Set (irradiance, radiancemap); // Clamp to white. color.Update_Min (white); light->Set_Ambient (black); light->Set_Diffuse (color); light->Set_Specular (color); // Set attenuation model. if (GetType() != LT_LAMP_DAYLIGHT) { const float oocutoff = 1.0f / 0.025f; // Reciprocal of light intensity at far attenuation. // NOTE: Convert Lightscape 1/d^2 attenuation to an equivalent MAX type light. light->Set_Flag (LightClass::NEAR_ATTENUATION, false); light->Set_Near_Attenuation_Range (0.0f, 0.0f); light->Set_Flag (LightClass::FAR_ATTENUATION, true); // Calculate attenuation range. // The far attenuation is calculated as the distance at which intensity falls to a given // ratio (cut-off), assuming an attenuation of c/d^2, where c = monochrome intensity of light source. float d = sqrt (oocutoff * color.Length()); light->Set_Far_Attenuation_Range (d * 0.25f, d); } else { light->Set_Flag (LightClass::NEAR_ATTENUATION, false); light->Set_Flag (LightClass::FAR_ATTENUATION, false); } // Miscellaneous attributes. light->Enable_Shadows ((GetFlags() & LT_LAMP_CAST_NO_SHADOWS) ? false : true); LightscapeSolve::Importer()->Add_Light (light); // Clean-up. light->Release_Ref(); } // Return success. return (TRUE); } /*********************************************************************************************** * LsMeshInquirer::LsMeshInquirer -- * * * * INPUT: * * * * OUTPUT: * * * * WARNINGS: * * * * HISTORY: * * 04/19/00 IML : Created. * *=============================================================================================*/ LsMeshInquirer::LsMeshInquirer (LtTBuilderFactory *factory) : LtTBaseMeshBuilder (factory) { // Assume initially that the solve is NOT an M2T solve. LightscapeSolve::Importer()->Set_Is_M2T_Solve (false); } /*********************************************************************************************** * LsMeshInquirer::LsMeshInquirer -- * * * * INPUT: * * * * OUTPUT: * * * * WARNINGS: * * * * HISTORY: * * 04/19/00 IML : Created. * *=============================================================================================*/ LtTBool LsMeshInquirer::Finish() { if (GetProperties() & LT_MESH_RAWTEXTURE) { LightscapeSolve::Importer()->Set_Is_M2T_Solve (true); } // Return success. return (TRUE); } /*********************************************************************************************** * LsMeshBuilder::LsMeshBuilder -- * * * * INPUT: * * * * OUTPUT: * * * * WARNINGS: * * * * HISTORY: * * 7/23/99 IML : Created. * *=============================================================================================*/ LsMeshBuilder::LsMeshBuilder (LtTBuilderFactory *factory) : LtTBaseMeshBuilder (factory) { ASSERT (strcmp (typeid (*factory).name(), "class LsMainFactory") == 0); FactoryPtr = (LsMainFactory*) factory; Faces = NULL; FaceCount = 0; AllocatedCount = 0; } /*********************************************************************************************** * LsMeshBuilder::~LsMeshBuilder -- * * * * INPUT: * * * * OUTPUT: * * * * WARNINGS: * * * * HISTORY: * * 9/16/99 IML : Created. * *=============================================================================================*/ LsMeshBuilder::~LsMeshBuilder() { if (Faces != NULL) delete [] Faces; } /*********************************************************************************************** * LsMeshBuilder::SetFaces -- * * * * INPUT: * * * * OUTPUT: * * * * WARNINGS: * * * * HISTORY: * * 9/16/99 IML : Created. * *=============================================================================================*/ void LsMeshBuilder::SetFaces (const int facecount, const LtTFace *faces) { int topcount, f, g; // Count the number of faces in the top-level hierarchy. topcount = 0; for (f = 0; f < facecount; f++) { topcount++; f += faces [f].GetDescendents(); } // If the block allocated for faces is not big enough delete it and allocate a new one. if (topcount > AllocatedCount) { if (Faces != NULL) delete [] Faces; Faces = new LtTFace [topcount]; ASSERT (Faces != NULL); AllocatedCount = topcount; } // Copy the faces in the top-level hierarchy into the block allocated for faces. g = 0; for (f = 0; f < facecount; f++) { Faces [g] = faces [f]; g++; f += faces [f].GetDescendents(); } ASSERT (g == topcount); FaceCount = topcount; } /*********************************************************************************************** * LsMeshBuilder::GetFaceCount -- * * * * INPUT: * * * * OUTPUT: * * * * WARNINGS: * * * * HISTORY: * * 9/16/99 IML : Created. * *=============================================================================================*/ int LsMeshBuilder::GetFaceCount() const { return (FaceCount); } /*********************************************************************************************** * LsMeshBuilder::GetFaces -- * * * * INPUT: * * * * OUTPUT: * * * * WARNINGS: * * * * HISTORY: * * 9/16/99 IML : Created. * *=============================================================================================*/ const LtTFace* LsMeshBuilder::GetFaces() const { return (Faces); } /*********************************************************************************************** * LsMeshBuilder::Finish -- Called by the Lightscape framework after each patch cluster read. * * * * Called by the Lightscape framework after each patch cluster read from a .ls file. The file * * read is initiated by a LtSolutionImport() call. The clustering and the mesh structure is * * entirely under the control of Lightscape. Each patch cluster may or may not reference a * * texture and may or may not contain a radiosity mesh. If the .ls file has been produced by * * Lightscape's 'Mesh to Texture' tool (M2T) with the "Generate Illumination Map" option * * enabled then the material definition will (should) reference a lightmap texture. * * * * INPUT: * * * * OUTPUT: * * * * WARNINGS: * * * * HISTORY: * * 7/23/99 IML : Created. * *=============================================================================================*/ LtTBool LsMeshBuilder::Finish() { const char *invalidmappingtext = "There is a face in the model with an invalid texture mapping. Please ensure that the\ Mesh to Texture option called 'Convert each surface to a single texture per surface' was\ used."; const char *notnormalizedtext = "There is a vertex in the model with a valid texture mapping but a non-normalized texture\ coordinate."; const char *unmatchedmodeltext = "The solve files do not contain the same model information. For any 2 solve files, the first\ n patches must match, where n is the smallest patch count of the two files."; VertexUser usedvertices (*this); bool success; ASSERT (LightscapeSolve::Importer() != NULL); // Add the vertices to the Lightscape solve database. // If this mesh is an M2T patch... if (GetProperties() & LT_MESH_RAWTEXTURE) { int vertexindex; LtTVector lsv; // Lightscape scratch vertex. Vector3 v; // Scratch vertex. Vector3 origin, du, dv; // Definition of texture projection. Vector2 length; // bool normalized; LtTUnitVector lsn; Vector3 n; // Lightscape definition of texture projection. LtTTextureProjection lsprojection = GetTextureProjection(); // If the texture mapping is invalid (ie. not orthographic)... if ((lsprojection.GetFlags() & LT_PROJECTION_TYPE_MASK) != LT_PROJECTION_ORTHOGRAPHIC) { throw (invalidmappingtext); } // Define origin and texture vectors for a planar mapping. lsv = lsprojection.GetOrigin(); origin.Set (lsv.GetX(), lsv.GetY(), lsv.GetZ()); lsv = lsprojection.GetDu(); du.Set (lsv.GetX(), lsv.GetY(), lsv.GetZ()); lsv = lsprojection.GetDv(); dv.Set (lsv.GetX(), lsv.GetY(), lsv.GetZ()); // Normalize the texture vectors. length.U = du.Length(); length.V = dv.Length(); du /= length.U; dv /= length.V; // Step thru the vertices, calculate UV from the texture projection and add the information // to the Lightscape solve database. GetPlane (lsn); n.Set (lsn.GetX(), lsn.GetY(), lsn.GetZ()); for (vertexindex = 0; vertexindex < GetVertexCount(); vertexindex++) { // Add only if the vertex is referenced by a face. if (usedvertices.Is_Used (vertexindex)) { LtTPoint lsp = GetVertices() [vertexindex]; Vector3 p (lsp.GetX(), lsp.GetY(), lsp.GetZ()); Vector2 t; v = p - origin; t.U = (du * v) / length.U; t.V = (dv * v) / length.V; // UV's must be normalized (in the range 0..1) in order to reference the lightmap properly. normalized = ((t.U >= 0.0f) && (t.U <= 1.0f)); normalized &= ((t.V >= 0.0f) && (t.V <= 1.0f)); if (!normalized) throw (notnormalizedtext); // Assign an index for this vertex which will uniquely identify it in the vertex database. usedvertices.Set_Index (vertexindex, FactoryPtr->Vertex_Count()); // Add the vertex to the database. success = LightscapeSolve::Importer()->Add_Vertex (FactoryPtr->Vertex_Count(), p, n, FactoryPtr->Patch_Count(), t); if (!success) throw (unmatchedmodeltext); // Advance vertex index counter. FactoryPtr->Vertex_Increment(); } } // Find the texture name that corresponds to the material name and add it for this patch. LightscapeSolve::Importer()->Add_Texture_Name (FactoryPtr->Patch_Count(), GetMaterial()); } else { const double filtersharpness = LightscapeSolve::Importer()->Filter_Sharpness(); ProceduralTexture *blendtexture; DynamicVectorClass facevertices; double *distanceratios; int vertexindex; int facevertexindex; RadianceMap radiancemap (LightscapeSolve::Importer()->Get_Brightness(), LightscapeSolve::Importer()->Get_Contrast(), LightscapeSolve::Importer()->Is_Daylight(), LightscapeSolve::Importer()->Is_Exterior()); LtTPoint lsp; LtTRGBColor irradiance; LtTUnitVector lsn; Vector3 n; blendtexture = LightscapeSolve::Importer()->Procedural_Texture(); // NOTE: This mesh is not an M2T patch so it must instead contain valid vertex color information. // Build an array of face vertices initialized to their respective vertex colors. for (vertexindex = 0; vertexindex < GetVertexCount(); vertexindex++) { if (usedvertices.Is_Used (vertexindex)) { FaceVertexStruct facevertex; lsp = GetVertices() [vertexindex]; irradiance = GetIrradiances() [vertexindex]; facevertex.Point.Set (lsp.GetX(), lsp.GetY(), lsp.GetZ()); facevertex.Color.Set (irradiance, radiancemap); // Should the vertex color be blended with a sample from a procedural texture? if (blendtexture != NULL) { facevertex.Color *= blendtexture->Value (facevertex.Point); } facevertex.Weight = 1.0f; facevertices.Add (facevertex); } } // There must be at least one face vertex. ASSERT (facevertices.Count() > 0); // For every non-face vertex calculate its color contribution to each face vertex. distanceratios = new double [facevertices.Count()]; ASSERT (distanceratios != NULL); for (vertexindex = 0; vertexindex < GetVertexCount(); vertexindex++) { if (!usedvertices.Is_Used (vertexindex)) { Vector3 point; ColorVector color; double d, s; lsp = GetVertices() [vertexindex]; irradiance = GetIrradiances() [vertexindex]; point.Set (lsp.GetX(), lsp.GetY(), lsp.GetZ()); color.Set (irradiance, radiancemap); // Should the vertex color be blended with a sample from a procedural texture? if (blendtexture != NULL) { color *= blendtexture->Value (point); } d = (point - facevertices [0].Point).Length(); distanceratios [0] = s = 1.0f; for (facevertexindex = 1; facevertexindex < facevertices.Count(); facevertexindex++) { distanceratios [facevertexindex] = d / (point - facevertices [facevertexindex].Point).Length(); // Raise the distance ratio to the user specified filter sharpness. distanceratios [facevertexindex] = pow (distanceratios [facevertexindex], filtersharpness); s += distanceratios [facevertexindex]; } for (facevertexindex = 0; facevertexindex < facevertices.Count(); facevertexindex++) { float weight; weight = distanceratios [facevertexindex] / s; facevertices [facevertexindex].Color += color * weight; facevertices [facevertexindex].Weight += weight; } } } // Add the face vertices to the solve database. GetPlane (lsn); n.Set (lsn.GetX(), lsn.GetY(), lsn.GetZ()); facevertexindex = 0; for (vertexindex = 0; vertexindex < GetVertexCount(); vertexindex++) { if (usedvertices.Is_Used (vertexindex)) { ColorVector c = facevertices [facevertexindex].Color; c /= facevertices [facevertexindex].Weight; // Assign an index for this vertex which will uniquely identify it in the vertex database. usedvertices.Set_Index (vertexindex, FactoryPtr->Vertex_Count()); // Add the vertex to the database. success = LightscapeSolve::Importer()->Add_Vertex (FactoryPtr->Vertex_Count(), facevertices [facevertexindex].Point, n, FactoryPtr->Patch_Count(), c); if (!success) { delete [] distanceratios; throw (unmatchedmodeltext); } // Advance the vertex index counters. FactoryPtr->Vertex_Increment(); facevertexindex++; } } // Clean-up. delete [] distanceratios; } // Add the faces to the Lightscape solve database. for (int faceindex = 0; faceindex < GetFaceCount(); faceindex++) { const unsigned facevertexindexcount = 4; LtTFace face; int vertexindex, facevertexcount; unsigned facevertexindices [facevertexindexcount]; ASSERT (faceindex >= 0 && faceindex < GetFaceCount()); face = GetFaces() [faceindex]; // If the face is not a triangle then it must be a quadrilateral. It is safe to assume // that these are the only polygon types that Lightscape supports. facevertexcount = face.IsTriangle() ? 3 : 4; for (int v = 0; v < facevertexcount; v++) { vertexindex = face.GetVert (v); ASSERT (vertexindex >= 0 && vertexindex < GetVertexCount()); facevertexindices [v] = usedvertices.Get_Index (vertexindex); } success = LightscapeSolve::Importer()->Add_Patch_Face (FactoryPtr->Patch_Face_Count(), FactoryPtr->Patch_Count(), facevertexcount, facevertexindices); if (!success) throw (unmatchedmodeltext); // Advance patch face counter. FactoryPtr->Patch_Face_Increment(); } // Advance the patch index counter. FactoryPtr->Patch_Increment(); // Return success. return (TRUE); } /*********************************************************************************************** * VertexUser::VertexUser -- * * * * INPUT: * * * * OUTPUT: * * * * WARNINGS: * * * * HISTORY: * * 1/05/00 IML : Created. * *=============================================================================================*/ VertexUser::VertexUser (const LsMeshBuilder &meshbuilder) { LtTFace face; unsigned v, vertexindex, facevertexcount; Count = meshbuilder.GetVertexCount(); UsedVertexIndices = new int [Count]; ASSERT (UsedVertexIndices != NULL); // Initialize. All vertices unused. for (v = 0; v < Count; v++) { UsedVertexIndices [v] = UNUSED; } // For each face... for (int faceindex = 0; faceindex < meshbuilder.GetFaceCount(); faceindex++) { face = meshbuilder.GetFaces() [faceindex]; // If the face is not a triangle then it must be a quadrilateral. It is safe to assume // that these are the only polygon types that Lightscape supports. facevertexcount = face.IsTriangle() ? 3 : 4; // For each vertex in the face, flag that the vertex is referenced by a face. for (v = 0; v < facevertexcount; v++) { vertexindex = face.GetVert (v); ASSERT (vertexindex >= 0 && vertexindex < Count); UsedVertexIndices [vertexindex] = USED; } } } /*********************************************************************************************** * LightscapeMeshSolve::LightscapeMeshSolve -- * * * * INPUT: * * * * OUTPUT: * * * * WARNINGS: * * * * HISTORY: * * 7/30/99 IML : Created. * *=============================================================================================*/ LightscapeMeshSolve::LightscapeMeshSolve (LightscapeSolve &solve, ChunkClass &trianglechunk, ChunkClass &vertexchunk) : Statistics (vertexchunk.Get_Size() / sizeof (W3dVectorStruct), trianglechunk.Get_Size() / sizeof (W3dTriStruct)) { const unsigned verticesperface = 3; // No. of vertices in a triangle. PackingTriangle *triangles; W3dTriStruct *w3dfaces; W3dVectorStruct *w3dvertices; unsigned f, v; // Calculate no. of faces and face-vertices from triangle chunk. VertexCount = vertexchunk.Get_Size() / sizeof (W3dVectorStruct); FaceCount = trianglechunk.Get_Size() / sizeof (W3dTriStruct); FaceVertexCount = FaceCount * verticesperface; // Allocate data tables for this mesh. VertexColors = new W3dRGBStruct [VertexCount]; ASSERT (VertexColors != NULL); FaceVertexUVs = new Vector2 [FaceVertexCount]; ASSERT (FaceVertexUVs != NULL); FaceRemapLightmapIndices = new unsigned [FaceCount]; ASSERT (FaceRemapLightmapIndices != NULL); LightmapIndices = new unsigned [FaceCount]; ASSERT (LightmapIndices != NULL); triangles = new PackingTriangle [FaceCount]; ASSERT (triangles != NULL); w3dfaces = (W3dTriStruct*) trianglechunk.Get_Data(); ASSERT (w3dfaces != NULL); w3dvertices = (W3dVectorStruct*) vertexchunk.Get_Data(); ASSERT (w3dvertices != NULL); // For each vertex in the vertex chunk... for (unsigned vertexindex = 0; vertexindex < VertexCount; vertexindex++) { const Vector3 zero (0.0f, 0.0f, 0.0f); Vector3 *smoothingnormalptr; Vector3 point; W3dVectorStruct *w3dvertexptr; W3dRGBStruct vertexcolor; // Iterate over the faces and calculate a smoothing normal for this vertex based on // the faces that reference this vertex. smoothingnormalptr = NULL; for (f = 0; f < FaceCount; f++) { for (v = 0; v < verticesperface; v++) { if (w3dfaces [f].Vindex [v] == vertexindex) { Vector3 facenormal; // Do not consider face normals that are zero (face is degenerate). facenormal.Set (w3dfaces [f].Normal.X, w3dfaces [f].Normal.Y, w3dfaces [f].Normal.Z); if (facenormal != zero) smoothingnormalptr = &facenormal; break; } } if (smoothingnormalptr != NULL) break; } w3dvertexptr = w3dvertices + vertexindex; point.Set (w3dvertexptr->X, w3dvertexptr->Y, w3dvertexptr->Z); solve.Find_Vertex (point, smoothingnormalptr, vertexcolor, Statistics); VertexColors [vertexindex] = vertexcolor; } // For each face in the w3d triangle chunk, match it up with a face in the Lightscape solve database. for (f = 0; f < FaceCount; f++) { Vector3 points [verticesperface]; Vector3 facenormal; Vector2 uvs [verticesperface]; PackingTriangle triangle; // Extract points and face normal from vertex chunk. for (v = 0; v < verticesperface; v++) { W3dVectorStruct *w3dvertexptr; unsigned vindex; vindex = w3dfaces [f].Vindex [v]; w3dvertexptr = w3dvertices + vindex; points [v].Set (w3dvertexptr->X, w3dvertexptr->Y, w3dvertexptr->Z); } facenormal.Set (w3dfaces [f].Normal.X, w3dfaces [f].Normal.Y, w3dfaces [f].Normal.Z); solve.Find_Triangle (points, facenormal, triangles [f], Statistics); } if (Statistics.Valid_Lightmap_Solve()) { unsigned lightmapindex, remaplightmapindex; bool found; // Submit the triangles for packing. for (f = 0; f < FaceCount; f++) { solve.Submit_Triangle (triangles [f]); } // Now pack. solve.Pack(); // Extract the lightmap UV's from the triangles. v = 0; for (f = 0; f < FaceCount; f++) { FaceVertexUVs [v + 0] = triangles [f].PackedUVs [0]; FaceVertexUVs [v + 1] = triangles [f].PackedUVs [1]; FaceVertexUVs [v + 2] = triangles [f].PackedUVs [2]; v += verticesperface; } // The face-lightmap indices are currently non-zero based, unordered, non-contiguous indices // ie. an unknown list of indexes that have been returned from the packing process. // The mesh requires face-lightmap indices that are zero-based, ordered, contiguous indices // so remap the indices to create face-remaplightmap indices. Also, create a table of 'real' // lightmap indices that will be indexed by the face-remaplightmap table. remaplightmapindex = 0; for (f = 0; f < FaceCount; f++) { int g; lightmapindex = triangles [f].PackedTextureID; // Look backwards thru the face-lightmap table to see if this index has already occured? found = false; g = ((int) f) - 1; while (g >= 0) { if (triangles [g].PackedTextureID == lightmapindex) { found = true; break; } g--; } // Was the index found? if (found) { FaceRemapLightmapIndices [f] = FaceRemapLightmapIndices [g]; } else { FaceRemapLightmapIndices [f] = remaplightmapindex; ASSERT (remaplightmapindex < FaceCount); LightmapIndices [remaplightmapindex] = lightmapindex; remaplightmapindex++; } } ASSERT (remaplightmapindex > 0); LightmapCount = remaplightmapindex; } else { LightmapCount = 0; } // Clean-up. delete [] triangles; } /*********************************************************************************************** * LightscapeMeshSolve::~LightscapeMeshSolve -- * * * * INPUT: * * * * OUTPUT: * * * * WARNINGS: * * * * HISTORY: * * 7/30/99 IML : Created. * *=============================================================================================*/ LightscapeMeshSolve::~LightscapeMeshSolve() { // Clean-up. delete [] VertexColors; delete [] LightmapIndices; delete [] FaceRemapLightmapIndices; delete [] FaceVertexUVs; } /*********************************************************************************************** * LightscapeMeshSolve::Vertex_Color -- * * * * INPUT: * * * * OUTPUT: * * * * WARNINGS: * * * * HISTORY: * * 1/06/00 IML : Created. * *=============================================================================================*/ W3dRGBStruct LightscapeMeshSolve::Vertex_Color (unsigned vertexindex) const { ASSERT (Statistics.Valid_Vertex_Solve()); ASSERT (vertexindex < VertexCount); return (VertexColors [vertexindex]); } /*********************************************************************************************** * LightscapeMeshSolve::LightscapeMeshSolve -- * * * * INPUT: * * * * OUTPUT: * * * * WARNINGS: * * * * HISTORY: * * 7/30/99 IML : Created. * *=============================================================================================*/ const char *LightscapeMeshSolve::Lightmap_Pathname (unsigned lightmapindex) const { ASSERT (Statistics.Valid_Lightmap_Solve()); ASSERT (lightmapindex < FaceCount); return (LightscapeSolve::Lightmap_Pathname (LightmapIndices [lightmapindex])); } /*********************************************************************************************** * LightscapeMeshSolve::LightscapeMeshSolve -- * * * * INPUT: * * * * OUTPUT: * * * * WARNINGS: * * * * HISTORY: * * 7/30/99 IML : Created. * *=============================================================================================*/ unsigned LightscapeMeshSolve::Lightmap_Index (unsigned faceindex) const { ASSERT (Statistics.Valid_Lightmap_Solve()); ASSERT (faceindex < FaceCount); return (FaceRemapLightmapIndices [faceindex]); } /*********************************************************************************************** * LightscapeMeshSolve::LightscapeMeshSolve -- * * * * INPUT: * * * * OUTPUT: * * * * WARNINGS: * * * * HISTORY: * * 7/30/99 IML : Created. * *=============================================================================================*/ W3dTexCoordStruct LightscapeMeshSolve::Lightmap_UV (unsigned facevertexindex) const { W3dTexCoordStruct w3duv; ASSERT (Statistics.Valid_Lightmap_Solve()); ASSERT (facevertexindex < FaceVertexCount); w3duv.U = FaceVertexUVs [facevertexindex].U; w3duv.V = FaceVertexUVs [facevertexindex].V; return (w3duv); }