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