1 /*
2 
3 	Copyright (C) 1991-2001 and beyond by Bungie Studios, Inc.
4 	and the "Aleph One" developers.
5 
6 	This program is free software; you can redistribute it and/or modify
7 	it under the terms of the GNU General Public License as published by
8 	the Free Software Foundation; either version 3 of the License, or
9 	(at your option) any later version.
10 
11 	This program is distributed in the hope that it will be useful,
12 	but WITHOUT ANY WARRANTY; without even the implied warranty of
13 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 	GNU General Public License for more details.
15 
16 	This license is contained in the file "COPYING",
17 	which is included with this source code; it is available online at
18 	http://www.gnu.org/licenses/gpl.html
19 
20 	Alias|Wavefront Object Loader
21 
22 	By Loren Petrich, June 16, 2001
23 */
24 
25 #include <ctype.h>
26 #include <stdlib.h>
27 #include <string.h>
28 #include <algorithm>
29 
30 #include "cseries.h"
31 
32 #include "Logging.h"
33 
34 #ifdef HAVE_OPENGL
35 #ifdef __WIN32__
36 #include <windows.h>
37 #endif
38 
39 #include "WavefrontLoader.h"
40 
41 
42 // Which of these is present in the vertex-info data:
43 enum {
44 	Present_Position	= 0x0001,
45 	Present_TxtrCoord	= 0x0002,
46 	Present_Normal		= 0x0004
47 };
48 
49 static const char *Path = NULL;	  // Path to model file.
50 
51 // Input line will be able to stretch as much as necessary
52 static vector<char> InputLine(64);
53 
54 // Compare input-line beginning to a keyword;
55 // returns pointer to rest of line if it was found,
56 // otherwise returns NULL
57 char *CompareToKeyword(const char *Keyword);
58 
59 // Gets a pointer to a string of vertex-index sets and picks off one of them,
60 // returning a pointer to the character just after it. Also returns the presence and values
61 // picked off.
62 // Returns NULL if there are none remaining to be found.
63 char *GetVertIndxSet(char *Buffer, short& Presence,
64 	short& PosIndx, short& TCIndx, short& NormIndx);
65 
66 // Gets a vertex index and returns whether or not an index value was found
67 // what it was if found, and a pointer to the character just after the index value
68 // (either '/' or '\0'). And also whether the scanning hit the end of the set.
69 // Returns NULL if there are none remaining to be found.
70 char *GetVertIndx(char *Buffer, bool& WasFound, short& Val, bool& HitEnd);
71 
72 
73 // The purpose of the sorting is to find all the unique index sets;
74 // this is some data for the STL sorter
75 struct IndexedVertListCompare
76 {
77 	short *VertIndxSets;
78 
79 	// The comparison operation
operator ()IndexedVertListCompare80 	bool operator() (int i1, int i2) const
81 	{
82 		short *VISet1 = VertIndxSets + 4*i1;
83 		short *VISet2 = VertIndxSets + 4*i2;
84 
85 		// Sort by position first, then texture coordinate, then normal
86 
87 		if (VISet1[1] > VISet2[1])
88 			return false;
89 		else if (VISet1[1] < VISet2[1])
90 			return true;
91 
92 		if (VISet1[2] > VISet2[2])
93 			return false;
94 		else if (VISet1[2] < VISet2[2])
95 			return true;
96 
97 		if (VISet1[3] > VISet2[3])
98 			return false;
99 		else if (VISet1[3] < VISet2[3])
100 			return true;
101 
102 		// All equal!
103 //		return true;
104 		return false;
105 	}
106 };
107 
LoadModel_Wavefront(FileSpecifier & Spec,Model3D & Model)108 bool LoadModel_Wavefront(FileSpecifier& Spec, Model3D& Model)
109 {
110 	// Clear out the final model object
111 	Model.Clear();
112 
113 	// Intermediate lists of positions, texture coordinates, and normals
114 	vector<GLfloat> Positions;
115 	vector<GLfloat> TxtrCoords;
116 	vector<GLfloat> Normals;
117 
118 	// Intermediate list of polygon features:
119 	// Polygon sizes (how many vertices):
120 	vector<short> PolygonSizes;
121 	// Vertex indices (how many read, position, txtr-coord, normal)
122 	vector<short> VertIndxSets;
123 
124 	Path = Spec.GetPath();
125 	logNote("Loading Alias|Wavefront model file %s",Path);
126 
127 	OpenedFile OFile;
128 	if (!Spec.Open(OFile))
129 	{
130 		logError("ERROR opening %s",Path);
131 		return false;
132 	}
133 
134 	// Reading loop; create temporary lists of positions, texture coordinates, and normals
135 
136 	// Load the lines, one by one, and then parse them. Be sure to take care of the continuation
137 	// character "\" [Wavefront files follow some Unix conventions]
138 	bool MoreLines = true;
139 	while(MoreLines)
140 	{
141 		InputLine.clear();
142 
143 		// Fill up the line
144 		bool LineContinued = false;
145 		while(true)
146 		{
147 			// Try to read a character; if it is not possible to read anymore,
148 			// the line has ended
149 			char c;
150 			MoreLines = OFile.Read(1,&c);
151 			if (!MoreLines) break;
152 
153 			// End-of-line characters; ignore if the line is to be continued
154 			if (c == '\r' || c == '\n')
155 			{
156 				if (!LineContinued)
157 				{
158 					// If the line is not empty, then break; otherwise ignore.
159 					// Blank lines will be ignored, and this will allow starting a line
160 					// at the first non-end-of-line character
161 					if (!InputLine.empty()) break;
162 				}
163 			}
164 			// Backslash character indicates that the current line continues into the next one
165 			else if (c == '\\')
166 			{
167 				LineContinued = true;
168 			}
169 			else
170 			{
171 				// Continuation will stop if a non-end-of-line character is encounted
172 				LineContinued = false;
173 
174 				// Add that character!
175 				InputLine.push_back(c);
176 			}
177 		}
178 		// Line-end at end of file will produce an empty line, so do this test
179 		if (InputLine.empty()) continue;
180 
181 		// If the line is a comment line, then ignore it
182 		if (InputLine[0] == '#') continue;
183 
184 		// Make the line look like a C string
185 		InputLine.push_back('\0');
186 
187 		// Now parse the line; notice the = instead of == (substitute and test in one line)
188 		// Unhandled keywords are currently commented out for speed;
189 		// many of those are for handling curved surfaces, which are currently ignored.
190 		char *RestOfLine = NULL;
191 		if ((RestOfLine = CompareToKeyword("v")) != NULL) // Vertex position
192 		{
193 			GLfloat Position[3];
194 			objlist_clear(Position,3);
195 
196 			sscanf(RestOfLine," %f %f %f",Position,Position+1,Position+2);
197 
198 			for (int k=0; k<3; k++)
199 				Positions.push_back(Position[k]);
200 		}
201 		else if ((RestOfLine = CompareToKeyword("vt")) != NULL) // Vertex texture coordinate
202 		{
203 			GLfloat TxtrCoord[2];
204 			objlist_clear(TxtrCoord,2);
205 
206 			sscanf(RestOfLine," %f %f",TxtrCoord,TxtrCoord+1);
207 
208 			for (int k=0; k<2; k++)
209 				TxtrCoords.push_back(TxtrCoord[k]);
210 		}
211 		else if ((RestOfLine = CompareToKeyword("vn")) != NULL) // Vertex normal
212 		{
213 			GLfloat Normal[3];
214 			objlist_clear(Normal,3);
215 
216 			sscanf(RestOfLine," %f %f %f",Normal,Normal+1,Normal+2);
217 
218 			for (int k=0; k<3; k++)
219 				Normals.push_back(Normal[k]);
220 		}
221 		/*
222 		else if ((RestOfLine = CompareToKeyword("vp")) // Vertex parameter value
223 		{
224 			// For curved objects, which are not supported here
225 		}
226 		else if ((RestOfLine = CompareToKeyword("deg")) != NULL) // Degree
227 		{
228 			// Curved objects not supported here
229 		}
230 		else if ((RestOfLine = CompareToKeyword("bmat")) != NULL) // Basis matrix
231 		{
232 			// Curved objects not supported here
233 		}
234 		else if ((RestOfLine = CompareToKeyword("step")) != NULL) // Step size
235 		{
236 			// Curved objects not supported here
237 		}
238 		else if ((RestOfLine = CompareToKeyword("cstype")) != NULL) // Curve/surface type
239 		{
240 			// Curved objects not supported here
241 		}
242 		else if ((RestOfLine = CompareToKeyword("p")) != NULL) // Point
243 		{
244 			// Not supported here
245 		}
246 		else if ((RestOfLine = CompareToKeyword("l")) != NULL) // Line
247 		{
248 			// Not supported here
249 		}
250 		*/
251 		else if ((RestOfLine = CompareToKeyword("f")) != NULL) // Face (polygon)
252 		{
253 			// Pick off the face vertices one by one;
254 			// stuff their contents into a token and then process that token
255 			int NumVertices = 0;
256 
257 			short Presence = 0, PosIndx = 0, TCIndx = 0, NormIndx = 0;
258 			while((RestOfLine = GetVertIndxSet(RestOfLine, Presence, PosIndx, TCIndx, NormIndx)) != NULL)
259 			{
260 				NumVertices++;
261 
262 				// Wavefront vertex-index conventions:
263 				// Positive is 1-based indexing
264 				// Negative is from end of current list
265 
266 				if (PosIndx < 0)
267 					PosIndx += static_cast<short>(Positions.size())/3;
268 				else
269 					PosIndx--;
270 
271 				if (TCIndx < 0)
272 					TCIndx += static_cast<short>(TxtrCoords.size())/2;
273 				else
274 					TCIndx--;
275 
276 				if (NormIndx < 0)
277 					NormIndx += static_cast<short>(Normals.size())/3;
278 				else
279 					NormIndx--;
280 
281 				// Add!
282 				VertIndxSets.push_back(Presence);
283 				VertIndxSets.push_back(PosIndx);
284 				VertIndxSets.push_back(TCIndx);
285 				VertIndxSets.push_back(NormIndx);
286 			}
287 			// Polygon complete!
288 			PolygonSizes.push_back(NumVertices);
289 		}
290 		/*
291 		else if ((RestOfLine = CompareToKeyword("curv")) != NULL) // Curve
292 		{
293 			// Curved objects not supported here
294 		}
295 		else if ((RestOfLine = CompareToKeyword("curv2")) != NULL) // 2D Curve
296 		{
297 			// Curved objects not supported here
298 		}
299 		else if ((RestOfLine = CompareToKeyword("surf")) != NULL) // Surface
300 		{
301 			// Curved objects not supported here
302 		}
303 		else if ((RestOfLine = CompareToKeyword("parm")) != NULL) // Parameter values
304 		{
305 			// Curved objects not supported here
306 		}
307 		else if ((RestOfLine = CompareToKeyword("trim")) != NULL) // Outer trimming loop
308 		{
309 			// Curved objects not supported here
310 		}
311 		else if ((RestOfLine = CompareToKeyword("hole")) != NULL) // Inner trimming loop
312 		{
313 			// Curved objects not supported here
314 		}
315 		else if ((RestOfLine = CompareToKeyword("scrv")) != NULL) // Special curve
316 		{
317 			// Curved objects not supported here
318 		}
319 		else if ((RestOfLine = CompareToKeyword("sp")) != NULL) // Special point
320 		{
321 			// Curved objects not supported here
322 		}
323 		else if ((RestOfLine = CompareToKeyword("end")) != NULL) // End statement
324 		{
325 			// Curved objects not supported here
326 		}
327 		else if ((RestOfLine = CompareToKeyword("con")) != NULL) // Connect
328 		{
329 			// Curved objects not supported here
330 		}
331 		else if ((RestOfLine = CompareToKeyword("g")) != NULL) // Group name
332 		{
333 			// Not supported here
334 		}
335 		else if ((RestOfLine = CompareToKeyword("s")) != NULL) // Smoothing group
336 		{
337 			// Not supported here
338 		}
339 		else if ((RestOfLine = CompareToKeyword("mg")) != NULL) // Merging group
340 		{
341 			// Not supported here
342 		}
343 		else if ((RestOfLine = CompareToKeyword("o")) != NULL) // Object name
344 		{
345 			// Not supported here
346 		}
347 		else if ((RestOfLine = CompareToKeyword("bevel")) != NULL) // Bevel interpolation
348 		{
349 			// Not supported here
350 		}
351 		else if ((RestOfLine = CompareToKeyword("c_interp")) != NULL) // Color interpolation
352 		{
353 			// Not supported here
354 		}
355 		else if ((RestOfLine = CompareToKeyword("d_interp")) != NULL) // Dissolve interpolation
356 		{
357 			// Not supported here
358 		}
359 		else if ((RestOfLine = CompareToKeyword("lod")) != NULL) // Level of detail
360 		{
361 			// Not supported here
362 		}
363 		else if ((RestOfLine = CompareToKeyword("usemtl")) != NULL) // Material name
364 		{
365 			// Not supported here
366 		}
367 		else if ((RestOfLine = CompareToKeyword("mtllib")) != NULL) // Material library
368 		{
369 			// Not supported here
370 		}
371 		else if ((RestOfLine = CompareToKeyword("shadow_obj")) != NULL) // Shadow casting
372 		{
373 			// Not supported here
374 		}
375 		else if ((RestOfLine = CompareToKeyword("trace_obje")) != NULL) // Ray tracing
376 		{
377 			// Not supported here
378 		}
379 		else if ((RestOfLine = CompareToKeyword("ctech")) != NULL) // Curve approximation technique
380 		{
381 			// Curved objects not supported here
382 		}
383 		else if ((RestOfLine = CompareToKeyword("stech")) != NULL) // Surface approximation technique
384 		{
385 			// Curved objects not supported here
386 		}
387 		*/
388 	}
389 
390 	if (PolygonSizes.size() <= 0)
391 	{
392 		logError("ERROR: the model in %s has no polygons",Path);
393 		return false;
394 	}
395 
396 	// How many vertices do the polygons have?
397 	for (unsigned k=0; k<PolygonSizes.size(); k++)
398 	{
399 		short PSize = PolygonSizes[k];
400 		if (PSize < 3)
401 		{
402 			logWarning("WARNING: polygon ignored; it had bad size %u: %d in %s",k,PSize,Path);
403 		}
404 	}
405 
406 	// What is the lowest common denominator of the polygon data
407 	// (which is present of vertex positions, texture coordinates, and normals)
408 	short WhatsPresent = Present_Position | Present_TxtrCoord | Present_Normal;
409 
410 	for (unsigned k=0; k<VertIndxSets.size()/4; k++)
411 	{
412 		short Presence = VertIndxSets[4*k];
413 		WhatsPresent &= Presence;
414 		if (!(Presence & Present_Position))
415 		{
416 			logError("ERROR: Vertex has no position index: %u in %s",k,Path);
417 		}
418 	}
419 
420 	if (!(WhatsPresent & Present_Position)) return false;
421 
422 	bool AllInRange = true;
423 
424 	for (unsigned k=0; k<VertIndxSets.size()/4; k++)
425 	{
426 		short PosIndx = VertIndxSets[4*k+1];
427 		if (PosIndx < 0 || PosIndx >= int(Positions.size()))
428 		{
429 			logError("ERROR: Out of range vertex position: %u: %d (0,%lu) in %s",k,PosIndx,(unsigned long)Positions.size()-1,Path);
430 			AllInRange = false;
431 		}
432 
433 		if (WhatsPresent & Present_TxtrCoord)
434 		{
435 			short TCIndx = VertIndxSets[4*k+2];
436 			if (TCIndx < 0 || TCIndx >= int(TxtrCoords.size()))
437 			{
438 				logError("ERROR: Out of range vertex position: %u: %d (0,%lu) in %s",k,TCIndx,(unsigned long)(TxtrCoords.size()-1),Path);
439 				AllInRange = false;
440 			}
441 		}
442 		else
443 			VertIndxSets[4*k+2] = -1; // What "0" gets turned into by the Wavefront-conversion-translation code
444 
445 		if (WhatsPresent & Present_Normal)
446 		{
447 			short NormIndx = VertIndxSets[4*k+3];
448 			if (NormIndx < 0 || NormIndx >= int(Normals.size()))
449 			{
450 				logError("ERROR: Out of range vertex position: %u: %d (0,%lu) in %s",k,NormIndx,(unsigned long)(Normals.size()-1),Path);
451 				AllInRange = false;
452 			}
453 		}
454 		else
455 			VertIndxSets[4*k+3] = -1; // What "0" gets turned into by the Wavefront-conversion-translation code
456 	}
457 
458 	if (!AllInRange) return false;
459 
460 	// Find unique vertex sets:
461 
462 	// First, do an index sort of them
463 	vector<int> VertIndxRefs(VertIndxSets.size()/4);
464 	for (unsigned k=0; k<VertIndxRefs.size(); k++)
465 		VertIndxRefs[k] = k;
466 
467 	IndexedVertListCompare Compare;
468 	Compare.VertIndxSets = &VertIndxSets[0];
469 	sort(VertIndxRefs.begin(),VertIndxRefs.end(),Compare);
470 
471 	// Find the unique entries:
472 	vector<int> WhichUniqueSet(VertIndxRefs.size());
473 
474 	// Previous index values:
475 	short PrevPosIndx = -1, PrevTCIndx = -1, PrevNormIndx = -1;
476 	// For doing zero-based indexing
477 	int NumUnique = -1;
478 
479 	// Scan the vertices in index-sort order:
480 	for (unsigned k=0; k<VertIndxRefs.size(); k++)
481 	{
482 		int n = VertIndxRefs[k];
483 
484 		short *VISet = &VertIndxSets[4*n];
485 		short PosIndx = VISet[1];
486 		short TCIndx = VISet[2];
487 		short NormIndx = VISet[3];
488 
489 		if (PosIndx == PrevPosIndx && TCIndx == PrevTCIndx && NormIndx == PrevNormIndx)
490 		{
491 			WhichUniqueSet[n] = NumUnique;
492 			continue;
493 		}
494 
495 		// Found a unique set
496 		WhichUniqueSet[n] = ++NumUnique;
497 
498 		// These are all for the model object
499 
500 		// Load the positions
501 		{
502 			GLfloat *PosPtr = &Positions[3*PosIndx];
503 			for (int m=0; m<3; m++)
504 				Model.Positions.push_back(*(PosPtr++));
505 		}
506 
507 		// Load the texture coordinates
508 		if (WhatsPresent & Present_TxtrCoord)
509 		{
510 			GLfloat *TCPtr = &TxtrCoords[2*TCIndx];
511 			for (int m=0; m<2; m++)
512 				Model.TxtrCoords.push_back(*(TCPtr++));
513 		}
514 
515 		// Load the normals
516 		if (WhatsPresent & Present_Normal)
517 		{
518 			GLfloat *NormPtr = &Normals[3*NormIndx];
519 			for (int m=0; m<3; m++)
520 				Model.Normals.push_back(*(NormPtr++));
521 		}
522 
523 		// Save these new unique-set values for comparison to the next ones
524 		PrevPosIndx = PosIndx;
525 		PrevTCIndx = TCIndx;
526 		PrevNormIndx = NormIndx;
527 	}
528 
529 	// Decompose the polygons into triangles by turning them into fans
530 	int IndxBase = 0;
531 	for (unsigned k=0; k<PolygonSizes.size(); k++)
532 	{
533 		short PolySize = PolygonSizes[k];
534 		int *PolyIndices = &WhichUniqueSet[IndxBase];
535 
536 		for (int m=0; m<PolySize-2; m++)
537 		{
538 			Model.VertIndices.push_back(PolyIndices[0]);
539 			Model.VertIndices.push_back(PolyIndices[m+1]);
540 			Model.VertIndices.push_back(PolyIndices[m+2]);
541 		}
542 
543 		IndxBase += PolySize;
544 	}
545 
546 	if (Model.VertIndices.size() <= 0)
547 	{
548 		logError("ERROR: the model in %s has no good polygons",Path);
549 		return false;
550 	}
551 
552 	logTrace("Successfully read the file:");
553 	if (WhatsPresent & Present_Position)  logTrace("    Positions");
554 	if (WhatsPresent & Present_TxtrCoord) logTrace("    TxtrCoords");
555 	if (WhatsPresent & Present_Normal)    logTrace("    Normals");
556 	return true;
557 }
558 
559 
CompareToKeyword(const char * Keyword)560 char *CompareToKeyword(const char *Keyword)
561 {
562 	size_t KWLen = strlen(Keyword);
563 
564 	if (InputLine.size() < KWLen) return NULL;
565 
566 	for (unsigned k=0; k<KWLen; k++)
567 		if (InputLine[k] != Keyword[k]) return NULL;
568 
569 	char *RestOfLine = &InputLine[KWLen];
570 
571 	while(RestOfLine - &InputLine[0] < int(InputLine.size()))
572 	{
573 		// End of line?
574 		if (*RestOfLine == '\0') return RestOfLine;
575 
576 		// Other than whitespace -- assume it to be part of the keyword if just after it;
577 		// otherwise, it is to be returned to the rest of the code to work on
578 		if (!(*RestOfLine == ' ' || *RestOfLine == '\t'))
579 			return ((RestOfLine == &InputLine[KWLen]) ? NULL : RestOfLine);
580 
581 		// Whitespace: move on to the next character
582 		RestOfLine++;
583 	}
584 
585 	// Shouldn't happen
586 	return NULL;
587 }
588 
589 
GetVertIndxSet(char * Buffer,short & Presence,short & PosIndx,short & TCIndx,short & NormIndx)590 char *GetVertIndxSet(char *Buffer, short& Presence,
591 	short& PosIndx, short& TCIndx, short& NormIndx)
592 {
593 	// Initialize...
594 	Presence = 0; PosIndx = 0; TCIndx = 0; NormIndx = 0;
595 
596 	// Eat initial whitespace; return NULL if end-of-string was hit
597 	// OK to modify Buffer, since it's called by value
598 	while(*Buffer == ' ' || *Buffer == '\t')
599 	{
600 		Buffer++;
601 	}
602 	if (*Buffer == '\0') return NULL;
603 
604 	// Hit non-whitespace; now grab the individual vertex values
605 	bool WasFound = false, HitEnd = false;
606 	Buffer = GetVertIndx(Buffer,WasFound,PosIndx,HitEnd);
607 	if (WasFound) Presence |= Present_Position;
608 	if (HitEnd) return Buffer;
609 
610 	Buffer = GetVertIndx(Buffer,WasFound,TCIndx,HitEnd);
611 	if (WasFound) Presence |= Present_TxtrCoord;
612 	if (HitEnd) return Buffer;
613 
614 	Buffer = GetVertIndx(Buffer,WasFound,NormIndx,HitEnd);
615 	if (WasFound) Presence |= Present_Normal;
616 	return Buffer;
617 }
618 
GetVertIndx(char * Buffer,bool & WasFound,short & Val,bool & HitEnd)619 char *GetVertIndx(char *Buffer, bool& WasFound, short& Val, bool& HitEnd)
620 {
621 	const int VIBLen = 64;
622 	char VIBuffer[VIBLen];
623 	int VIBIndx = 0;
624 
625 	// Load the vertex-index buffer and make it a C string
626 	HitEnd = false;
627 	WasFound = false;
628 	bool HitInternalBdry = false;	// Use this variable to avoid duplicating an evaluation
629 	while (!(HitInternalBdry = (*Buffer == '/')))
630 	{
631 		HitEnd = (*Buffer == ' ' || *Buffer == '\t' || *Buffer == '\0');
632 		if (HitEnd) break;
633 
634 		if (VIBIndx < VIBLen-1)
635 			VIBuffer[VIBIndx++] = *Buffer;
636 
637 		Buffer++;
638 	}
639 	if (HitInternalBdry) Buffer++;
640 	VIBuffer[VIBIndx] = '\0';
641 
642 	// Interpret it!
643 	WasFound = (sscanf(VIBuffer,"%hd",&Val) > 0);
644 
645 	return Buffer;
646 }
647 
648 // Load a Wavefront model and convert its vertex and texture coordinates from
649 // OBJ's right-handed coordinate system to Aleph One's left-handed system.
LoadModel_Wavefront_RightHand(FileSpecifier & Spec,Model3D & Model)650 bool LoadModel_Wavefront_RightHand(FileSpecifier& Spec, Model3D& Model)
651 {
652 	bool Result = LoadModel_Wavefront(Spec, Model);
653 	if (!Result) return Result;
654 
655 	logTrace("Converting handedness.");
656 
657 	// OBJ files produced by Blender and Wings 3D are oriented with
658 	// y increasing upwards, and the front of a Blender model faces in the
659 	// positive-Z direction.  (Wings 3D does not distinguish a "front"
660 	// view.)  In Aleph One's coordinate system Z increases upwards, and
661 	// items that have been placed with 0 degrees of rotation face in the
662 	// positive-x direction.
663 	for (unsigned XPos = 0; XPos < Model.Positions.size(); XPos += 3)
664 	{
665 		GLfloat X = Model.Positions[XPos];
666 		Model.Positions[XPos] = Model.Positions[XPos + 2];
667 		Model.Positions[XPos + 2] = Model.Positions[XPos + 1];
668 		Model.Positions[XPos + 1] = -X;
669 	}
670 
671 	// Ditto for vertex normals, if present.
672 	for (unsigned XPos = 0; XPos < Model.Normals.size(); XPos += 3)
673 	{
674 		GLfloat X = Model.Normals[XPos];
675 		Model.Normals[XPos] = Model.Normals[XPos + 2];
676 		Model.Normals[XPos + 2] = Model.Normals[XPos + 1];
677 		Model.Normals[XPos + 1] = -X;
678 	}
679 
680 	// Vertices of each face are now listed in clockwise order.
681 	// Reverse them.
682 	for (unsigned IPos = 0; IPos < Model.VertIndices.size(); IPos += 3)
683 	{
684 		int Index = Model.VertIndices[IPos + 1];
685 		Model.VertIndices[IPos + 1] = Model.VertIndices[IPos];
686 		Model.VertIndices[IPos] = Index;
687 	}
688 
689 	// Switch texture coordinates from right-handed (x,y) to
690 	// left-handed (row,column).
691 	for (unsigned YPos = 1; YPos < Model.TxtrCoords.size(); YPos += 2)
692 	{
693 		Model.TxtrCoords[YPos] = 1.0 - Model.TxtrCoords[YPos];
694 	}
695 
696 	return true;
697 }
698 
699 #endif // def HAVE_OPENGL
700