source: nanovis/trunk/VelocityArrowsSlice.cpp @ 4895

Last change on this file since 4895 was 4167, checked in by ldelgass, 6 years ago

Texture environment is not part of texture object stored state, so don't set
environment mode before loading textures, should be done at render time.

  • Property svn:eol-style set to native
File size: 18.8 KB
Line 
1/* -*- mode: c++; c-basic-offset: 4; indent-tabs-mode: nil -*- */
2/*
3 * Copyright (C) 2004-2013  HUBzero Foundation, LLC
4 *
5 */
6#include "nvconf.h"
7
8#include <math.h>
9
10#include <GL/glew.h>
11#include <GL/gl.h>
12
13#include <vrmath/Vector3f.h>
14#include <util/FilePath.h>
15#include <Image.h>
16#include <ImageLoaderFactory.h>
17#include <ImageLoader.h>
18
19#include "nanovis.h"
20#include "VelocityArrowsSlice.h"
21#include "Volume.h"
22#include "Shader.h"
23#include "Camera.h"
24
25using namespace nv;
26using namespace nv::util;
27using namespace vrmath;
28
29static inline float deg2rad(float deg)
30{
31    return ((deg * M_PI) / 180.);
32}
33
34static inline float rad2deg(float rad)
35{
36    return ((rad * 180.) / M_PI);
37}
38
39VelocityArrowsSlice::VelocityArrowsSlice() :
40    _vectorFieldGraphicsID(0),
41    _slicePos(0.5f),
42    _axis(AXIS_Z),
43    _fbo(0),
44    _tex(0),
45    _maxPointSize(1.0f),
46    _renderTargetWidth(128),
47    _renderTargetHeight(128),
48    _velocities(NULL),
49    _projectionVector(1, 1, 0),
50    _tickCountForMinSizeAxis(10),
51    _tickCountX(0),
52    _tickCountY(0),
53    _tickCountZ(0),
54    _pointCount(0),
55    _maxVelocityScale(1, 1, 1),
56    _arrowColor(1, 1, 0),
57    _visible(false),
58    _dirty(true),
59    _vertexBufferGraphicsID(0),
60    _arrowsTex(NULL),
61    _renderMode(LINES)
62{
63    setSliceAxis(AXIS_Z);
64
65    _queryVelocityFP.loadFragmentProgram("queryvelocity.cg");
66
67    // Delay loading of shaders only required for glyph style rendering
68    if (_renderMode == GLYPHS) {
69        _particleShader.loadVertexProgram("velocityslicevp.cg");
70        _particleShader.loadFragmentProgram("velocityslicefp.cg");
71    }
72
73    createRenderTarget();
74
75    std::string path = FilePath::getInstance()->getPath("arrows.bmp");
76    if (!path.empty()) {
77        ImageLoader *loader = ImageLoaderFactory::getInstance()->createLoader("bmp");
78        if (loader != NULL) {
79            Image *image = loader->load(path.c_str(), Image::IMG_RGBA);
80            if (image != NULL) {
81                unsigned char *bytes = (unsigned char *)image->getImageBuffer();
82                if (bytes != NULL) {
83                    _arrowsTex = new Texture2D(image->getWidth(), image->getHeight(),
84                                               GL_UNSIGNED_BYTE, GL_LINEAR, 4, NULL);
85                    _arrowsTex->setWrapS(GL_MIRRORED_REPEAT);
86                    _arrowsTex->setWrapT(GL_MIRRORED_REPEAT);
87                    _arrowsTex->initialize(image->getImageBuffer());
88                }
89                delete image;
90            } else {
91                ERROR("Failed to load arrows image");
92            }
93            delete loader;
94        } else {
95            ERROR("Couldn't find loader for arrows image");
96        }
97    } else {
98        ERROR("Couldn't find arrows image");
99    }
100
101    GLfloat minMax[2];
102    glGetFloatv(GL_ALIASED_POINT_SIZE_RANGE, minMax);
103    TRACE("Aliased point size range: %g %g", minMax[0], minMax[1]);
104    _maxPointSize = minMax[1];
105    glGetFloatv(GL_SMOOTH_POINT_SIZE_RANGE, minMax);
106    TRACE("Smooth point size range: %g %g", minMax[0], minMax[1]);
107    _maxPointSize = minMax[1] > _maxPointSize ? minMax[1] : _maxPointSize;
108    TRACE("Max point size: %g", _maxPointSize);
109
110    TRACE("Leaving VelocityArrowsSlice constructor");
111}
112
113VelocityArrowsSlice::~VelocityArrowsSlice()
114{
115    if (_tex != 0) {
116        glDeleteTextures(1, &_tex);
117    }
118    if (_fbo != 0) {
119        glDeleteFramebuffersEXT(1, &_fbo);
120    }
121    if (_arrowsTex != NULL) {
122        delete _arrowsTex;
123    }
124    if (_vertexBufferGraphicsID != 0) {
125        glDeleteBuffers(1, &_vertexBufferGraphicsID);
126    }
127    if (_velocities != NULL) {
128        delete [] _velocities;
129    }
130}
131
132void VelocityArrowsSlice::createRenderTarget()
133{
134    if (_velocities != NULL) {
135        delete [] _velocities;
136    }
137    _velocities = new Vector3f[_renderTargetWidth * _renderTargetHeight];
138
139    if (_vertexBufferGraphicsID != 0) {
140        glDeleteBuffers(1, &_vertexBufferGraphicsID);
141    }
142
143    glGenBuffers(1, &_vertexBufferGraphicsID);
144    glBindBufferARB(GL_ARRAY_BUFFER_ARB, _vertexBufferGraphicsID);
145    glBufferDataARB(GL_ARRAY_BUFFER_ARB, _renderTargetWidth * _renderTargetHeight * 3 * sizeof(float), 
146                    0, GL_DYNAMIC_DRAW_ARB);
147    glBindBufferARB(GL_ARRAY_BUFFER_ARB, 0);
148
149    if (_tex != 0) {
150        glDeleteTextures(1, &_tex);
151    }
152    if (_fbo != 0) {
153        glDeleteFramebuffersEXT(1, &_fbo);
154    }
155
156    glGenFramebuffersEXT(1, &_fbo);
157    glGenTextures(1, &_tex);
158    int fboOrig;
159    glGetIntegerv(GL_FRAMEBUFFER_BINDING_EXT, &fboOrig);
160    glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, _fbo);
161
162    glBindTexture(GL_TEXTURE_RECTANGLE_ARB, _tex);
163    glTexParameterf(GL_TEXTURE_RECTANGLE_ARB, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
164    glTexParameterf(GL_TEXTURE_RECTANGLE_ARB, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
165    glTexParameteri(GL_TEXTURE_RECTANGLE_ARB, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
166    glTexParameteri(GL_TEXTURE_RECTANGLE_ARB, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
167
168    glTexImage2D(GL_TEXTURE_RECTANGLE_ARB, 0, GL_RGBA32F_ARB, 
169                 _renderTargetWidth, _renderTargetHeight, 0,
170                 GL_RGBA, GL_FLOAT, NULL);
171
172    glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, 
173                              GL_TEXTURE_RECTANGLE_ARB, _tex, 0);
174
175    glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fboOrig);
176}
177
178void VelocityArrowsSlice::setSliceAxis(FlowSliceAxis axis)
179{
180    _axis = axis;
181    switch (_axis) {
182    case AXIS_X:
183        _projectionVector.x = 0;
184        _projectionVector.y = 1;
185        _projectionVector.z = 1;
186        break;
187    case AXIS_Y:
188        _projectionVector.x = 1;
189        _projectionVector.y = 0;
190        _projectionVector.z = 1;
191        break;
192    case AXIS_Z:
193        _projectionVector.x = 1;
194        _projectionVector.y = 1;
195        _projectionVector.z = 0;
196        break;
197    }
198    _dirty = true;
199}
200
201void VelocityArrowsSlice::queryVelocity()
202{
203    glPushAttrib(GL_VIEWPORT_BIT | GL_ENABLE_BIT);
204    int fboOrig;
205    glGetIntegerv(GL_FRAMEBUFFER_BINDING_EXT, &fboOrig);
206
207    glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, _fbo);
208
209    _queryVelocityFP.bind();
210    _queryVelocityFP.setFPTextureParameter("vfield", _vectorFieldGraphicsID);
211
212    glDisable(GL_DEPTH_TEST);
213    glViewport(0, 0, _renderTargetWidth, _renderTargetHeight);
214    glMatrixMode(GL_PROJECTION);
215    glPushMatrix();
216    glLoadIdentity();
217    glOrtho(0, _renderTargetWidth, 0, _renderTargetHeight, -10.0f, 10.0f);
218    glMatrixMode(GL_MODELVIEW);
219    glPushMatrix();
220    glLoadIdentity();
221
222    glBegin(GL_QUADS);
223    switch (_axis) {
224    case 0:
225        glTexCoord3f(_slicePos, 0, 0); glVertex2i(0,                  0);
226        glTexCoord3f(_slicePos, 1, 0); glVertex2i(_renderTargetWidth, 0);
227        glTexCoord3f(_slicePos, 1, 1); glVertex2i(_renderTargetWidth, _renderTargetHeight);
228        glTexCoord3f(_slicePos, 0, 1); glVertex2i(0,                  _renderTargetHeight);
229        break;
230    case 1:
231        glTexCoord3f(0, _slicePos, 0); glVertex2i(0,                  0);
232        glTexCoord3f(1, _slicePos, 0); glVertex2i(_renderTargetWidth, 0);
233        glTexCoord3f(1, _slicePos, 1); glVertex2i(_renderTargetWidth, _renderTargetHeight);
234        glTexCoord3f(0, _slicePos, 1); glVertex2i(0,                  _renderTargetHeight);
235        break;
236    case 2:
237    default:
238        glTexCoord3f(0, 0, _slicePos); glVertex2i(0,                  0);
239        glTexCoord3f(1, 0, _slicePos); glVertex2i(_renderTargetWidth, 0);
240        glTexCoord3f(1, 1, _slicePos); glVertex2i(_renderTargetWidth, _renderTargetHeight);
241        glTexCoord3f(0, 1, _slicePos); glVertex2i(0,                  _renderTargetHeight);
242        break;
243    }
244    glEnd();
245
246    _queryVelocityFP.disableFPTextureParameter("vfield");
247    _queryVelocityFP.unbind();
248
249    glReadPixels(0, 0, _renderTargetWidth, _renderTargetHeight, GL_RGB, GL_FLOAT, _velocities);
250
251    glMatrixMode(GL_PROJECTION);
252    glPopMatrix();
253    glMatrixMode(GL_MODELVIEW);
254    glPopMatrix();
255
256    glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fboOrig);
257
258    glPopAttrib();
259}
260
261static void drawLineArrow(int axis)
262{
263    glBegin(GL_LINES);
264    switch (axis) {
265    case 0: // ZY plane
266        glVertex3f(0.0, 0.0, 0.0);
267        glVertex3f(0.0, 0.0, 1.0);
268
269        glVertex3f(0.0, 0.0, 1.0);
270        glVertex3f(0.0, 0.25, 0.75);
271
272        glVertex3f(0.0, 0.0, 1.0);
273        glVertex3f(0.0, -0.25, 0.75);
274        break;
275    case 1: // XZ plane
276        glVertex3f(0.0, 0.0, 0.0);
277        glVertex3f(1.0, 0.0, 0.0);
278
279        glVertex3f(1.0, 0.0, 0.0);
280        glVertex3f(0.75, 0.0, 0.25);
281
282        glVertex3f(1.0, 0.0, 0.0);
283        glVertex3f(0.75, 0.0, -0.25);
284        break;
285    case 2: // XY plane
286    default:
287        glVertex3f(0.0, 0.0, 0.0);
288        glVertex3f(1.0, 0.0, 0.0);
289
290        glVertex3f(1.0, 0.0, 0.0);
291        glVertex3f(0.75, 0.25, 0.0);
292
293        glVertex3f(1.0, 0.0, 0.0);
294        glVertex3f(0.75, -0.25, 0.0);
295        break;
296    }
297    glEnd();
298}
299
300void VelocityArrowsSlice::render()
301{
302    if (!_visible)
303        return;
304
305    if (_dirty) {
306        computeSamplingTicks();
307        queryVelocity();
308        _dirty = false;
309    }
310
311    TRACE("_scale: %g %g %g", _scale.x, _scale.y, _scale.z);
312    TRACE("_maxVelocityScale: %g %g %g",
313          _maxVelocityScale.x, _maxVelocityScale.y, _maxVelocityScale.z);
314
315    glPushAttrib(GL_ENABLE_BIT | GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
316
317    glMatrixMode(GL_MODELVIEW);
318    glPushMatrix();
319
320    glTranslatef(_origin.x, _origin.y, _origin.z);
321    glScalef(_scale.x, _scale.y, _scale.z);
322
323    if (_renderMode == LINES) {
324        glDisable(GL_TEXTURE_2D);
325        glDisable(GL_LIGHTING);
326        glLineWidth(2.0);
327        glColor3f(_arrowColor.x, _arrowColor.y, _arrowColor.z);
328 
329        Vector3f pos;
330        Vector3f vel;
331        Vector3f refVec;
332        Vector3f blue(0, 0, 1);
333        Vector3f red(1, 0, 0);
334
335        int index = 0, icount, jcount;
336        switch (_axis) {
337        case 0:
338            icount = _tickCountZ;
339            jcount = _tickCountY;
340            refVec.set(0, 0, 1);
341            break;
342        case 1:
343            icount = _tickCountZ;
344            jcount = _tickCountX;
345            refVec.set(1, 0, 0);
346            break;
347        case 2:
348        default:
349            icount = _tickCountY;
350            jcount = _tickCountX;
351            refVec.set(1, 0, 0);
352            break;
353        }
354
355        for (int i = 0; i < icount; ++i) {
356            for (int j = 0; j < jcount; ++j, ++index) {
357                pos = _samplingPositions[index];
358                // Normalized velocity: [-1,1] components
359                // Project 3D vector onto sample plane
360                vel = _velocities[index].scale(_projectionVector);
361                // Length: [0,1]
362                double length = vel.length();
363                if (length < 0.0 || length > 1.0) {
364                    TRACE("***vec: (%g, %g, %g) length: %g***", vel.x, vel.y, vel.z, length);
365                    continue;
366                }
367                if (length > 0.0) {
368                    Vector3f vnorm = vel.normalize();
369                    Vector3f rotationAxis = refVec.cross(vnorm);
370                    double angle = rad2deg(acos(refVec.dot(vnorm)));
371                    Vector3f color = blue * (1.0 - length) + red * length;
372                    float scale = length;
373                    if (scale < 0.10) scale = 0.10;
374                    glMatrixMode(GL_MODELVIEW);
375                    glPushMatrix();
376                    glColor3f(color.x, color.y, color.z);
377                    glTranslatef(pos.x, pos.y, pos.z);
378                    glScalef(2.0 * _maxVelocityScale.x,
379                             2.0 * _maxVelocityScale.y,
380                             2.0 * _maxVelocityScale.z);
381                    glScalef(scale, scale, scale);
382                    if (angle > 1.0e-6 || angle < -1.0e-6)
383                        glRotated(angle, rotationAxis.x, rotationAxis.y, rotationAxis.z);
384                    drawLineArrow(_axis);
385                    glPopMatrix();
386                }
387            }
388        }
389
390        glLineWidth(1.0);
391    } else {
392        glColor4f(_arrowColor.x, _arrowColor.y, _arrowColor.z, 1.0f);
393        glEnable(GL_DEPTH_TEST);
394        glDisable(GL_LIGHTING);
395#if 0
396        glBlendFunc(GL_SRC_ALPHA, GL_ONE);
397        glEnable(GL_BLEND);
398        glDepthMask(GL_FALSE);
399#else
400        glDisable(GL_BLEND);
401#endif
402        glAlphaFunc(GL_GREATER, 0.6);
403        glEnable(GL_ALPHA_TEST);
404        glEnable(GL_POINT_SPRITE_ARB);
405        glPointSize(20);
406        glEnable(GL_VERTEX_PROGRAM_POINT_SIZE_ARB);
407
408        _arrowsTex->activate();
409        glEnable(GL_TEXTURE_2D);
410        glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
411
412        GLfloat atten[] = {1, 0, 0};
413        glPointParameterfvARB(GL_POINT_DISTANCE_ATTENUATION_ARB, atten);
414        glPointParameterfARB(GL_POINT_FADE_THRESHOLD_SIZE_ARB, 0.0f);
415        glPointParameterfARB(GL_POINT_SIZE_MIN_ARB, 1.0f);
416        glPointParameterfARB(GL_POINT_SIZE_MAX_ARB, _maxPointSize);
417        glTexEnvi(GL_POINT_SPRITE_ARB, GL_COORD_REPLACE_ARB, GL_TRUE);
418
419        // FIXME: This vertex shader won't compile with ARB_vertex_program,
420        // so it should use GLSL
421        if (!_particleShader.isVertexProgramLoaded()) {
422            _particleShader.loadVertexProgram("velocityslicevp.cg");
423        }
424        if (!_particleShader.isFragmentProgramLoaded()) {
425            _particleShader.loadFragmentProgram("velocityslicefp.cg");
426        }
427
428        _particleShader.bind();
429        _particleShader.setVPTextureParameter("vfield", _vectorFieldGraphicsID);
430        _particleShader.setFPTextureParameter("arrows", _arrowsTex->id());
431        _particleShader.setVPParameter1f("tanHalfFOV",
432                                         tan(NanoVis::getCamera()->getFov() * 0.5) * NanoVis::winHeight * 0.5);
433        _particleShader.setGLStateMatrixVPParameter("modelview",
434                                                    Shader::MODELVIEW_MATRIX,
435                                                    Shader::MATRIX_IDENTITY);
436        _particleShader.setGLStateMatrixVPParameter("mvp",
437                                                    Shader::MODELVIEW_PROJECTION_MATRIX,
438                                                    Shader::MATRIX_IDENTITY);
439
440        glEnableClientState(GL_VERTEX_ARRAY);
441        glBindBufferARB(GL_ARRAY_BUFFER_ARB, _vertexBufferGraphicsID);
442        glVertexPointer(3, GL_FLOAT, 0, 0);
443        //glEnableClientState(GL_COLOR_ARRAY);
444
445        // TBD..
446        glDrawArrays(GL_POINTS, 0, _pointCount);
447        glPointSize(1);
448
449        glDisableClientState(GL_VERTEX_ARRAY);
450        glDisable(GL_VERTEX_PROGRAM_POINT_SIZE_ARB);
451        glDisable(GL_POINT_SPRITE_ARB);
452
453        _particleShader.disableVPTextureParameter("vfield");
454        _particleShader.disableFPTextureParameter("arrows");
455        _particleShader.unbind();
456
457        glActiveTexture(GL_TEXTURE0);
458        glTexEnvi(GL_POINT_SPRITE_ARB, GL_COORD_REPLACE_ARB, GL_FALSE);
459        _arrowsTex->deactivate();
460    }
461
462    glPopMatrix();
463    glPopAttrib();
464}
465
466void 
467VelocityArrowsSlice::setVectorField(Volume *volume)
468{
469    _vectorFieldGraphicsID = volume->textureID();
470    Vector3f bmin, bmax;
471    volume->getBounds(bmin, bmax);
472    _origin.set(bmin.x, bmin.y, bmin.z);
473    _scale.set(bmax.x-bmin.x, bmax.y-bmin.y, bmax.z-bmin.z);
474
475    _dirty = true;
476}
477
478void VelocityArrowsSlice::computeSamplingTicks()
479{
480    if (_scale.x < _scale.y) {
481        if (_scale.x < _scale.z || _scale.z == 0.0) {
482            // _scale.x
483            _tickCountX = _tickCountForMinSizeAxis;
484
485            float step = _scale.x / (_tickCountX + 1);
486            _tickCountY = (int)(_scale.y/step);
487            _tickCountZ = (int)(_scale.z/step);
488        } else {
489            // _scale.z
490            _tickCountZ = _tickCountForMinSizeAxis;
491
492            float step = _scale.z / (_tickCountZ + 1);
493            _tickCountX = (int)(_scale.x/step);
494            _tickCountY = (int)(_scale.y/step);
495        }
496    } else {
497        if (_scale.y < _scale.z || _scale.z == 0.0) {
498            // _scale.y
499            _tickCountY = _tickCountForMinSizeAxis;
500
501            float step = _scale.y / (_tickCountY + 1);
502            _tickCountX = (int)(_scale.x/step);
503            _tickCountZ = (int)(_scale.z/step);
504        } else {
505            // _scale.z
506            _tickCountZ = _tickCountForMinSizeAxis;
507
508            float step = _scale.z / (_tickCountZ + 1);
509            _tickCountX = (int)(_scale.x/step);
510            _tickCountY = (int)(_scale.y/step);
511        }
512    }
513
514    switch (_axis) {
515    case 0:
516        _tickCountX = 1;
517        _renderTargetWidth = _tickCountY;
518        _renderTargetHeight = _tickCountZ;
519        break;
520    case 1:
521        _tickCountY = 1;
522        _renderTargetWidth = _tickCountX;
523        _renderTargetHeight = _tickCountZ;
524        break;
525    default:
526    case 2:
527        _tickCountZ = 1;
528        _renderTargetWidth = _tickCountX;
529        _renderTargetHeight = _tickCountY;
530        break;
531    }
532
533    _maxVelocityScale.x = (1.0f / _tickCountX) * 0.8f;
534    _maxVelocityScale.y = (1.0f / _tickCountY) * 0.8f;
535    _maxVelocityScale.z = (1.0f / _tickCountZ) * 0.8f;
536
537    TRACE("Tick counts: %d %d %d", _tickCountX, _tickCountY, _tickCountZ);
538
539    int pointCount = _tickCountX * _tickCountY * _tickCountZ;
540    if (_pointCount != pointCount) {
541        _samplingPositions.clear();
542        _samplingPositions.reserve(pointCount);
543        _pointCount = pointCount;
544    }
545
546    createRenderTarget();
547
548    Vector3f pos;
549    Vector3f *pinfo = NULL;
550    if (_renderMode == GLYPHS) {
551        glBindBufferARB(GL_ARRAY_BUFFER_ARB, _vertexBufferGraphicsID);
552        pinfo = (Vector3f *)glMapBufferARB(GL_ARRAY_BUFFER_ARB, GL_WRITE_ONLY_ARB);
553    }
554
555    if (_axis == 2) {
556        for (int y = 1; y <= _tickCountY; ++y) {
557            for (int x = 1; x <= _tickCountX; ++x) {
558                pos.x = (1.0f / (_tickCountX + 1)) * x;
559                pos.y = (1.0f / (_tickCountY + 1)) * y;
560                pos.z = _slicePos;
561                if (_renderMode == LINES) {
562                    _samplingPositions.push_back(pos);
563                } else {
564                    *pinfo = pos;
565                    ++pinfo;
566                }
567            }
568        }
569    } else if (_axis == 1) {
570        for (int z = 1; z <= _tickCountZ; ++z) {
571            for (int x = 1; x <= _tickCountX; ++x) {
572                pos.x = (1.0f / (_tickCountX + 1)) * x;
573                pos.y = _slicePos;
574                pos.z = (1.0f / (_tickCountZ + 1)) * z;
575                if (_renderMode == LINES) {
576                    _samplingPositions.push_back(pos);
577                } else {
578                    *pinfo = pos;
579                    ++pinfo;
580                }
581            }
582        }
583    } else if (_axis == 0) {
584        for (int z = 1; z <= _tickCountZ; ++z) {
585            for (int y = 1; y <= _tickCountY; ++y) {
586                pos.x = _slicePos;
587                pos.y = (1.0f / (_tickCountY + 1)) * y;
588                pos.z = (1.0f / (_tickCountZ + 1)) * z;
589                if (_renderMode == LINES) {
590                    _samplingPositions.push_back(pos);
591                } else {
592                    *pinfo = pos;
593                    ++pinfo;
594                }
595            }
596        }
597    }
598
599    if (_renderMode == GLYPHS) {
600        glUnmapBufferARB(GL_ARRAY_BUFFER_ARB);
601    }
602}
Note: See TracBrowser for help on using the repository browser.