さて、いきなりこういうのもなんですが、演算対象の頂点数が1万(だいたい60万頂点/秒)以下という程度なら、
GL.glVertex()でも大丈夫なんじゃないかという気もしてはおります。
最近のGPUとCPUの性能は強烈で、何も考えないような適当なコードを書いても平然と動かせてしまいますので。
では、VBOを利用したときの性能向上ですが、ディスプレイリストとだいたい同じくらいのようです。なんか「だったらディスプレイリストでいいじゃん」という声が聞こえてきそうです。
ただ、ディスプレイリストは変更を加えるときにまたコマンドを実行しなおさないといけないです。となると、
GL.glBegin()〜GL.glVertex()〜GL.glEnd()と同じことになってしまいます。そう考えれば、動的なオブジェクトに対してはVBOの方が高速になると思われます。
さらに、「動的」の部分に頂点シェーダを使ってしまえば、頂点をVRAMに送り込んだあとはGPUに処理を任せてしまうことができます。こうなると、VRAMとメインメモリの行き来が激減するため、かなりの効率向上が見込めます。
実際、最近のゲームではスキニングを行う際には頂点処理は頂点シェーダを利用するのが普通になっているようです。
VBOを利用した表示のサンプルを以下に示します。これは4章のサイコロ表示サンプルをVBO化したものです。
「…4章と変わらないじゃん」
はい、そういう感想が出てくると思います。
4章と同様にFPSAnimatorを利用してタイミングを調整しているのですが、どうやらこの程度の負荷だと速度差が出てこないようです。
ちなみに、
MQOViewerを作ったときにVBOを初めて導入したときには、数万ポリゴン程度のハイポリゴンモデルの表示が数倍速くなりました。
今回はこのようなコードになりましたが、VBOが真価を発揮するのは頂点数がもう二桁か三桁多いような場合でしょう。
sample6_2.java
〜〜前略〜〜
/** COLORCUBESをテクスチャつきで表示します。
* VBOを利用すバージョンです */
public class Sample6_2 extends JApplet implements GLEventListener,KeyListener{
private GLJPanel panel=new GLJPanel();
private GL gl;
// 各バッファオブジェクトの番号です
private int vboIdVertex;
private int vboIdNormal;
private int vboIdTexcoord;
private FloatBuffer vertexBuffer=null;
private FloatBuffer normalBuffer=null;
private FloatBuffer texCoordBuffer=null;
// 立方体を構成する頂点です
private float[][] vertices=new float[][]{{1,1,1},{-1,1,1},{-1,-1,1},{1,-1,1},
{1,1,-1},{-1,1,-1},{-1,-1,-1},{1,-1,-1}};
// 頂点のインデックスです
private int[] vertexIndex=new int[]{0,1,2,3,7,6,5,4,0,3,7,4,5,6,2,1,4,5,1,0,6,7,3,2};
// 各面の法線です
private float[][] normals=new float[][]{{1,0,0},{0,1,0},{0,0,1},{-1,0,0},{0,-1,0},{0,0,-1}};
// 法線のインデックスです
private int[] normalIndex=new int[]{2,2,2,2,5,5,5,5,0,0,0,0,3,3,3,3,1,1,1,1,4,4,4,4};
// 立方体各面の色です
private float[] white=new float[]{1.0f,1.0f,1.0f,1.0f};
// テクスチャの設定です
private float[][] texCoords=new float[][]{{0.0f,0.0f},{0.25f,0.0f},{0.25f,0.25f},{0.0f,0.25f}, // 1
{0.25f,0.25f},{0.5f,0.25f},{0.5f,0.0f},{0.25f,0.0f}, // 2
{0.5f,0.25f},{0.75f,0.25f},{0.75f,0.0f},{0.5f,0.0f}, // 3
{0.75f,0.25f},{1.0f,0.25f},{1.0f,0.0f},{0.75f,0.0f}, // 4
{0.0f,0.5f},{0.25f,0.5f},{0.25f,0.25f},{0.0f,0.25f}, // 5
{0.25f,0.5f},{0.5f,0.5f},{0.5f,0.25f},{0.25f,0.25f}, // 6
};
// 光源の設定です
private float[] lightPosition = { -5.0f, 5.0f, 5.0f, 0.0f }; // 平行光源です
private float[] lightSpecular = { 0.3f, 0.3f, 0.3f, 1.0f }; // 反射光の強さです
private float[] lightDiffuse = { 0.3f, 0.3f, 0.3f, 1.0f }; // 拡散光の強さです
private float[] lightAmbient = { 0.1f, 0.1f, 0.1f, 1.0f }; // 環境光の強さです
private float[] lightSpecular2 = { 1.0f, 1.0f, 1.0f, 1.0f }; // 反射光の強さです
private float[] lightDiffuse2 = { 0.5f, 0.5f, 0.5f, 1.0f }; // 拡散光の強さです
// 立方体の反射率です
private final float[] cubeSpecular = { 0.5f, 0.5f, 0.5f, 1.0f };
private final float[] cubeDiffuse = { 0.5f, 0.5f, 0.5f, 1.0f };
private final float[] cubeAmbient = { 0.1f, 0.1f, 0.1f, 1.0f };
private final float cubeShiness = 5.0f; // ハイライトの強さです
private int counter=0; // 表示回数のカウンターです
private float cameraX=0.0f,cameraZ=20.0f; // カメラの座標です
private float cameraAngle=0.0f; // カメラの角度です
private final float CAMERASPEED=0.3f; // カメラの移動速度です
private FPSAnimator anim; // アニメーション用のオブジェクトです
private final float RADIUS=4.0f; // 回転半径です
private final float RADIUSLIGHT=30.0f; // ライトの回転半径です
private final int NUM_OF_CUBES=10;
/** アプレットの初期化処理です */
public void init() {
panel.addGLEventListener(this);
this.addKeyListener(this);
anim=new FPSAnimator(panel,60,false); // JPanelをアニメーション対象にセットします
anim.start(); // アニメーションをスタートさせます
this.add(panel);
}
/** JOGLの初期化処理です */
public void init(GLAutoDrawable gla) {
gl=gla.getGL();
gl.glEnable(GL.GL_LIGHTING); //光源を有効にします
gl.glEnable(GL.GL_COLOR_MATERIAL); //カラーマテリアルを有効にします
gl.glEnable(GL.GL_LIGHT0); //0番のライトを有効にします
gl.glEnable(GL.GL_LIGHT1); //1番のライトを有効にします
gl.glLightfv(GL.GL_LIGHT0, GL.GL_SPECULAR, lightSpecular, 0); // 反射光の強さを設定します
gl.glLightfv(GL.GL_LIGHT0, GL.GL_DIFFUSE, lightDiffuse, 0); // 拡散光の強さを設定します
gl.glLightfv(GL.GL_LIGHT0, GL.GL_AMBIENT, lightAmbient, 0); // 環境光の強さを設定します
gl.glLightfv(GL.GL_LIGHT1, GL.GL_SPECULAR, lightSpecular2, 0); // 反射光の強さを設定します
gl.glLightfv(GL.GL_LIGHT1, GL.GL_DIFFUSE, lightDiffuse2, 0); // 拡散光の強さを設定します
gl.glLightfv(GL.GL_LIGHT1, GL.GL_AMBIENT, lightAmbient, 0); // 環境光の強さを設定します
// テクスチャを設定します
try {
String url = getCodeBase() + "dice.png";
System.out.println(url);
Texture texture = TextureIO.newTexture(new URL(url), true, "png");
texture.enable();
texture.bind();
} catch (Exception e) {
e.printStackTrace();
}
// 頂点バッファオブジェクトを作成します
this.compile(gl);
}
/** 画面サイズ変更時の処理です */
public void reshape(GLAutoDrawable arg0, int x, int y, int width,int height) {
〜〜中略〜〜
}
/** 再描画の際に呼ばれる処理です */
public void display(GLAutoDrawable arg0) {
gl.glClear(GL.GL_COLOR_BUFFER_BIT |GL.GL_DEPTH_BUFFER_BIT ); // 画面をクリアします
gl.glEnable(GL.GL_DEPTH_TEST); // 奥行き判定を有効にします
gl.glEnable(GL.GL_CULL_FACE); // 裏返ったポリゴンを描画しません
gl.glLoadIdentity(); // 単位行列を読み込みます
gl.glRotatef(-cameraAngle, 0, 1.0f, 0); // 座標系をカメラの向きと逆に回転させます
gl.glTranslatef(-cameraX,0,-cameraZ); // 原点をカメラの位置とは逆に移動させます
gl.glLightfv(GL.GL_LIGHT0, GL.GL_POSITION, lightPosition, 0); // 平行光源を設定します
gl.glPushMatrix();
gl.glTranslated(RADIUSLIGHT*Math.cos((float)(counter)/50),0,RADIUSLIGHT*Math.sin((float)(counter)/50)); // 点光源を移動させます
gl.glLightfv(GL.GL_LIGHT1, GL.GL_POSITION, new float[]{0,0,0,1.0f}, 0); // 点光源を設定します
this.drawLightCube();
gl.glPopMatrix();
for(int i=0;i < NUM_OF_CUBES;i++){
float x=(float)(RADIUS*Math.cos((float)(counter+i*30)/30));
float y=(float)(RADIUS*Math.sin((float)(counter+i*30)/30));
gl.glPushMatrix(); // 行列を退避します
gl.glTranslatef(x, y, -i*5.0f+25); // 移動させます
gl.glPushMatrix(); // 行列を退避します
gl.glRotatef((counter+i*30), 0.0f, 1.0f, 0.0f); // 回転させます
this.drawCubes();
gl.glPopMatrix(); // 行列を復帰します
gl.glPopMatrix();
}
gl.glFlush(); // 描き込みを指示します
counter++;
}
/** displayメソッドから呼び出されるように分離します */
public void drawCubes(){
// 物体の材質の、光源に対する反射率を決めます
gl.glMaterialfv(GL.GL_FRONT, GL.GL_SPECULAR, cubeSpecular, 0); //ポリゴンの前面にどのくらいの反射か
gl.glMaterialfv(GL.GL_FRONT, GL.GL_DIFFUSE, cubeDiffuse, 0); //ポリゴンの前面にどのくらいの拡散か
gl.glMaterialfv(GL.GL_FRONT, GL.GL_AMBIENT, cubeAmbient, 0); //ポリゴンの前面にどのくらいの環境光か
gl.glMaterialf(GL.GL_FRONT, GL.GL_SHININESS, cubeShiness); //ポリゴンの前面にどのくらいのハイライトか
// 頂点データ,法線データ,テクスチャ座標の配列を有効にします
gl.glEnableClientState(GL.GL_VERTEX_ARRAY);
gl.glEnableClientState(GL.GL_NORMAL_ARRAY);
gl.glEnableClientState(GL.GL_TEXTURE_COORD_ARRAY);
// 頂点データオブジェクトのIDを指定します
gl.glBindBuffer(GL.GL_ARRAY_BUFFER, vboIdVertex);
gl.glVertexPointer(3, GL.GL_FLOAT, 0, 0);
// 法線データオブジェクトのIDを指定します
gl.glBindBuffer(GL.GL_ARRAY_BUFFER, vboIdNormal);
gl.glNormalPointer(GL.GL_FLOAT, 0, 0);
// テクスチャ座標オブジェクトのIDを指定します
gl.glBindBuffer(GL.GL_ARRAY_BUFFER, vboIdTexcoord);
gl.glTexCoordPointer(2, GL.GL_FLOAT, 0, 0);
// 描画します
gl.glDrawArrays(GL.GL_QUADS, 0, vertexIndex.length);
// 頂点データ,法線データ,テクスチャ座標の配列を無効にします
gl.glDisableClientState(GL.GL_VERTEX_ARRAY);
gl.glDisableClientState(GL.GL_NORMAL_ARRAY);
gl.glDisableClientState(GL.GL_TEXTURE_COORD_ARRAY);
}
/** 点光源の位置を示すキューブを表示します */
public void drawLightCube(){
gl.glDisable(GL.GL_LIGHTING);
gl.glColor4fv(white,0); // 頂点の色を決定します
// 頂点データの配列を有効にします
gl.glEnableClientState(GL.GL_VERTEX_ARRAY);
// 頂点データオブジェクトのIDを指定します
gl.glBindBuffer(GL.GL_ARRAY_BUFFER, vboIdVertex);
gl.glVertexPointer(3, GL.GL_FLOAT, 0, 0);
// 描画します
gl.glDrawArrays(GL.GL_QUADS, 0, vertexIndex.length);
gl.glEnable(GL.GL_LIGHTING);
gl.glDisableClientState(GL.GL_VERTEX_ARRAY);
}
/** ディスプレイが変更された際に呼ばれる処理です */
public void displayChanged(GLAutoDrawable arg0, boolean arg1, boolean arg2) {
// TODO Auto-generated method stub
}
/** VBOを作成します */
public void compile(GL gl){
/** VBO番号をリセットします */
vboIdVertex=0;
vboIdNormal=0;
vboIdTexcoord=0;
System.out.println("debug:VBO関係のバッファをリセットします");
// バッファオブジェクトの名前を3つ作成します
int[] vboIdBuff=new int[3];
gl.glGenBuffers(3, vboIdBuff,0);
// 1つ目のバッファオブジェクトに頂点データ配列を転送します
gl.glBindBuffer(GL.GL_ARRAY_BUFFER, vboIdBuff[0]);
vertexBuffer=this.createVertexBuffer();
vertexBuffer.flip();
gl.glBufferData(GL.GL_ARRAY_BUFFER, vertexBuffer.capacity()*BufferUtil.SIZEOF_FLOAT, vertexBuffer, GL.GL_STATIC_DRAW);
// 2つ目のバッファオブジェクトに法線データ配列を転送します
gl.glBindBuffer(GL.GL_ARRAY_BUFFER, vboIdBuff[1]);
normalBuffer=this.createNormalBuffer();
normalBuffer.flip();
gl.glBufferData(GL.GL_ARRAY_BUFFER, normalBuffer.capacity()*BufferUtil.SIZEOF_FLOAT, normalBuffer, GL.GL_STATIC_DRAW);
// 3つ目のバッファオブジェクトにテクスチャ座標配列を転送します
gl.glBindBuffer(GL.GL_ARRAY_BUFFER, vboIdBuff[2]);
texCoordBuffer=this.createTexCoordBuffer();
texCoordBuffer.flip();
gl.glBufferData(GL.GL_ARRAY_BUFFER, texCoordBuffer.capacity()*BufferUtil.SIZEOF_FLOAT, texCoordBuffer, GL.GL_STATIC_DRAW);
vboIdVertex=vboIdBuff[0];
vboIdNormal=vboIdBuff[1];
vboIdTexcoord=vboIdBuff[2];
}
/** 頂点バッファの作成です */
public FloatBuffer createVertexBuffer(){
float[] floatArray=new float[vertexIndex.length*3];
for(int i=0;i < vertexIndex.length;i++){
floatArray[i*3]=vertices[vertexIndex[i]][0];
floatArray[i*3+1]=vertices[vertexIndex[i]][1];
floatArray[i*3+2]=vertices[vertexIndex[i]][2];
}
FloatBuffer fBuff=FloatBuffer.wrap(floatArray);
System.out.println("debug:頂点バッファを構築しました");
return fBuff;
}
/** 法線バッファの作成です */
public FloatBuffer createNormalBuffer(){
float[] floatArray=new float[normalIndex.length*3];
for(int i=0;i < normalIndex.length;i++){
floatArray[i*3]=normals[normalIndex[i]][0];
floatArray[i*3+1]=normals[normalIndex[i]][1];
floatArray[i*3+2]=normals[normalIndex[i]][2];
}
FloatBuffer fBuff=FloatBuffer.wrap(floatArray);
System.out.println("debug:法線バッファを構築しました ");
return fBuff;
}
/** テクスチャバッファの作成です */
public FloatBuffer createTexCoordBuffer(){
float[] floatArray=new float[texCoords.length*2];
for(int i=0;i < texCoords.length;i++){
floatArray[i*2]=texCoords[i][0];
floatArray[i*2+1]=texCoords[i][1];
}
FloatBuffer fBuff=FloatBuffer.wrap(floatArray);
System.out.println("debug:テクスチャ座標バッファを構築しました ");
return fBuff;
}
/** キーを押したときに呼ばれる処理です */
〜〜後略〜〜
}
結構長いですね。では、内容について解説を始めます。
実際のVBOの使用の仕方ですが、ディスプレイリストや頂点配列に近いものがあります。
まずは準備です。クラスの最初にVBOの名前(というか値)を格納するための変数を用意しています。
private int vboIdVertex;
private int vboIdNormal;
private int vboIdTexcoord;
VBOを作成したあとは、その後の描画等は番号で管理することになります。
実体はVRAMに置かれますので、つまり一種のポインタみたいなものと言えばいいでしょうか。
今回は頂点と法線とテクスチャ座標でオブジェクトを作成するため、3種類作ります。
続いて、Bufferを作ります。
これは、頂点データ配列を入れるための容器と考えてください。
private FloatBuffer vertexBuffer=null;
private FloatBuffer normalBuffer=null;
private FloatBuffer texCoordBuffer=null;
「float型の配列じゃ駄目なの?」
はい、それは当然出てくる疑問です。
Javaプログラマとしては、そちらのほうが遥かに扱いやすいです。
たぶんですが、JNIのインターフェースとしてポインタ渡しになっているんだと思います。
Bufferは内部的にはポインタ扱いですので、Cで書かれたOpenGLと親和性が良いのでしょう。
ここまでが準備です。
次に、VBOの作成です。このサンプルでは
init()メソッドから呼ばれる
compile()メソッドで行っています。
VRAMが初期化されるたびに作り直すということになります。
VBOを利用する場合、一番面倒なところが、VBOの作成です。作ってしまえば、それを利用するのは簡単です。
では、
compile()メソッドの中を見ていきましょう。
/** VBOを作成します */
public void compile(GL gl){
/** VBO番号をリセットします */
vboIdVertex=0;
vboIdNormal=0;
vboIdTexcoord=0;
System.out.println("debug:VBO関係のバッファをリセットします");
ここまでは何の問題もありませんね。各オブジェクトの名前をゼロクリアしています。
// バッファオブジェクトの名前を3つ作成します
int[] vboIdBuff=new int[3];
gl.glGenBuffers(3, vboIdBuff,0);
ここで、オブジェクトの名前を作成します。作成と言うか、予約するという感じですね。この時点では名前を用意しただけで、まだ実体はありません。
作られた名前は、引数で渡した整数配列
vboIdBuffに入って帰ってきます。
では、その実体をどこで作るのでしょうか?
それは、そのちょっと先にあります。
// 1つ目のバッファオブジェクトに頂点データ配列を転送します
gl.glBindBuffer(GL.GL_ARRAY_BUFFER, vboIdBuff[0]);
vertexBuffer=this.createVertexBuffer();
vertexBuffer.flip();
gl.glBufferData(GL.GL_ARRAY_BUFFER, vertexBuffer.capacity()*BufferUtil.SIZEOF_FLOAT, vertexBuffer, GL.GL_STATIC_DRAW);
この部分です。頂点バッファの実体はここで作っています。
vertexBufferはFloatBuffe型のバッファオブジェクトです。そこに、頂点データをセットしているわけです。では
createVertexBuffer()の中を見てみましょう。
/** 頂点バッファの作成です */
public FloatBuffer createVertexBuffer(){
float[] floatArray=new float[vertexIndex.length*3];
for(int i=0;i < vertexIndex.length;i++){
floatArray[i*3]=vertices[vertexIndex[i]][0];
floatArray[i*3+1]=vertices[vertexIndex[i]][1];
floatArray[i*3+2]=vertices[vertexIndex[i]][2];
}
FloatBuffer fBuff=FloatBuffer.wrap(floatArray);
System.out.println("debug:頂点バッファを構築しました");
return fBuff;
}
中身は、特別なことは何もやっていませんね。いったんfloat型の配列を作って、そこにひたすら頂点を詰めています。
verticesというのは、最初に定義した頂点一覧ですね。そこに、これも最初に宣言したvertexIndex配列を使って座標を参照していきます。一次元配列なので、座標がxyzxyzxyz....と並んでいます。
オブジェクト指向のJavaでえらい原始的なデータ構造を使うわけですが、OpenGLという比較的プリミティブなAPIを扱うため、この辺はハードよりの考え方になっているようです。
もっとも、実際にゲームを作るときは、この辺はゲームエンジンでラップすることになるでしょうから、面倒なのは最初にエンジンを作るときだけとは思います。
今回は、頂点インデックスを参照しながら頂点座標を読み込んで、それを配列に詰めていくという面倒なことをしています。OpenGLでは頂点インデックスでVBOを作ることもできるようなのですが、今回はそれは使用していません。
なぜかというと、OpenGLの場合、頂点というのは「座標+法線+テクスチャ座標」がセットになっていて、フラットシェーディングとは相性が悪いのです。これら全てが同一でないと、同じ頂点として再利用することができません。
フラットシェーディングの場合、当然ですが、「座標が同じだけど、法線が違う」という頂点がたくさん出てきます。ローポリモデルなんかでは、「座標が同じだけど、テクスチャ座標が違う」という頂点もたくさんでてきます。OpenGLでは、それらは「別個の頂点」として扱わなければなりません。
…この辺、何かやりようは無いのかと考えるところですね。大量の重複座標がある場合、ジオメトリ変換の際にGPUパワーを無駄に消費してしまいます。
さて、なにはともあれ、下準備は終わりました。vertexBufferにバッファオブジェクトが返ってきましたので、
flip()して巻き戻しておきます。正直、バッファオブジェクトはお約束が多くて、今ひとつ完全に理解していません。
これで、OpenGLが理解できる形で頂点座標のパッケージを作ることができました。
満を持して
gl.glBufferData(GL.GL_ARRAY_BUFFER, vertexBuffer.capacity()*BufferUtil.SIZEOF_FLOAT, vertexBuffer, GL.GL_STATIC_DRAW);
を呼び出します。引数が多いですが、最初のGL.GL_ARRAY_BUFFERはデータの種類の指定、2番目はデータサイズ、3番目はオブジェクトの名前。最後は、データがあんまり変わらないという意味らしいです。このメソッドを呼び出すことによって、作成したバッファオブジェクトをGPUのVRAMに転送します。
ここでは頂点バッファの例を挙げましたが、法線バッファやテクスチャ座標バッファも同様です。一次元の配列にデータをひたすら詰め込んでいきます。
さて、どうにかデータをオブジェクトにしてVRAMに転送することができました。では、いよいよ表示です。
VBOを使うためには、まずそれを宣言する必要があります。
GL.glEnableClientState()メソッドを利用します。
gl.glEnableClientState(GL.GL_VERTEX_ARRAY);
gl.glEnableClientState(GL.GL_NORMAL_ARRAY);
gl.glEnableClientState(GL.GL_TEXTURE_COORD_ARRAY);
ここでは、頂点、法線、テクスチャ座標についてオブジェクト化することを宣言しています。
宣言する場所は
display()メソッドの中。さらに言うと、描画直前になります。
そして、宣言したら描画命令を出します。頂点、法線、テクスチャ座標のそれぞれについて
glBindBuffer()を呼び出したら、
gl.glDrawArrays()です。
// 頂点データオブジェクトのIDを指定します
gl.glBindBuffer(GL.GL_ARRAY_BUFFER, vboIdVertex);
gl.glVertexPointer(3, GL.GL_FLOAT, 0, 0);
// 法線データオブジェクトのIDを指定します
gl.glBindBuffer(GL.GL_ARRAY_BUFFER, vboIdNormal);
gl.glNormalPointer(GL.GL_FLOAT, 0, 0);
// テクスチャ座標オブジェクトのIDを指定します
gl.glBindBuffer(GL.GL_ARRAY_BUFFER, vboIdTexcoord);
gl.glTexCoordPointer(2, GL.GL_FLOAT, 0, 0);
// 描画します
gl.glDrawArrays(GL.GL_QUADS, 0, vertexIndex.length);
// 頂点データ,法線データ,テクスチャ座標の配列を無効にします
gl.glDisableClientState(GL.GL_VERTEX_ARRAY);
gl.glDisableClientState(GL.GL_NORMAL_ARRAY);
gl.glDisableClientState(GL.GL_TEXTURE_COORD_ARRAY);
これで、画面に描画することができます。座標、法線、テクスチャ座標をばらばらに指定して大丈夫なのかと思われるかもしれませんが、n番目の座標、n番目の法線、n番目のテクスチャ座標を勝手にリンクしてくれます。となると、長さが足りないのとかあったりすると多分エラーはいて落ちますね。注意する必要がありそうです。なお、描画後はVBOのフラグをDisableしておくのがいいらしいです。
とりあえず、ソースの解説はこんな感じです。
VBOはゲームを作るにはかなり必須のスキルですので、ちゃんと押さえておく必要があります。私も良く分かっていない部分が多いので、勉強が必要です。
なお、OpenGL3.xになると、VBOが標準です。というか、ライティングとか全部シェーダを使えという恐ろしい世界です。三角ポリゴン一枚を描画するために、シェーダを書く必要があります。
私も含めたアマチュアプログラマには敷居がどかーんと高くなってしまう感じなのですが、GLUTとかどうなってしまうんでしょうかね。
なお、このページを記述するのに和歌山大学の床井先生のサイト
http://marina.sys.wakayama-u.ac.jp/~tokoi/?date=20080830を参考にさせていただきました。
この場を借りてお礼申し上げます。