CDR6275

Michio SHIRAISHI Official Site


OpenGL 4.1とOpenGL ES SL 1.0で学ぶ3次元コンピュータグラフィックス

東邦大学理学部情報科学科 白石路雄
最終更新: 2014年2月20日

6. 投影変換

 この章では三角形をウィンドウに表示するプログラムを通して、基本的な部分について説明します。

6.1 平行投影する

 3次元コンピュータグラフィックスでは、3次元空間に定義された座標から、ウィンドウ上の2次元の座標に変換をする必要があります。このことを投影といいます。

 これまでのプログラムではどのようになっていたかというと、3次元空間内の座標が(0,0,0)、(1,0,0)、(0,1,0)となっていましたが、それを描いたところ次のような結果となったのを思い出してください。

orthogonal-projection-1

つまり、3次元空間のx座標とy座標の範囲がそれぞれ-1から1までだったものがウィンドウいっぱいに表示されているのですから、2次元のウィンドウの座標でもx座標とy座標の範囲がそれぞれ-1から1までとなっていることが分かります。このように、x軸が-1から1、y軸が-1から1、z軸が-1から1の範囲からなる立方体の内部にあるものが、画面上に表示されることになります。この座標系のことを、正規化デバイス座標系と言います。

 それでは、ウィンドウのアスペクト比を変更してみるとどうなるでしょうか。

orthogonal-projection-2

 このような結果となります。つまり、正規化デバイス座標系からウィンドウ座標系への変換が、x軸の-1から1の範囲が横方向にいっぱいになるように、また、y軸の-1から1の範囲が縦方向にいっぱいになるように、変換されていることが分かります。この変換のことを、ビューポート変換と言います。

 さて、3次元空間で二等辺三角形だったものがゆがんで表示されてしまうのは困ります。これを避けるためには、ウィンドウの幅がwidth、高さがheightのときに、3次元空間のyの範囲を-1から1までとしたときに、xの範囲が-width/heightからwidth/heightである直方体が、正規化デバイス座標系である立方体になるように変換しなければなりません。このような変換のことを投影変換、特に、平行投影変換、と呼びます。

平行投影

 さて、ここで問題を一般的に考えます。次のような範囲で指定される3次元空間の中の直方体を考えます。

\[
l \le x \le r \\
t \le y \le b \\
n \le z \le f
\]

 この空間を正規化デバイス座標系における次の立方体に変換します。

\[
-1 \le x^\prime \le 1 \\
-1 \le y^\prime \le 1 \\
-1 \le z^\prime \le 1
\]

まずxの変換式を考えてみましょう。変換式は次のようになります。

\[
x^\prime = \frac{2}{r-l}\cdot x – \frac{r+l}{r-l}
\]

 xにlを代入すると-1に、rを代入すると1になることを確認してください。このような式がyとzについても作られます。このような変換は同次座標系を使うと次のような行列の形で書くことができます。

平行投影のプログラム

 まず、頂点シェーダを変更する必要があります。OpenGL ES Shading Language 1.0では、行列の乗算もかんたんに行えます。

#version 100
attribute vec3 vertexCoordinate;
uniform mat4 projectionMatrix;
void main(){
  gl_Position = projectionMatrix * vec4(vertexCoordinate, 1.0);
}

 ここで4×4行列を表すmat4型の変数としてprojectionMatrixが宣言されています。ここに、メインのプログラムから行列を渡します。しかし、attributeではなく、uniformとなっています。頂点座標であるvertexCoordinateは頂点ごとに値が変化しますが、投影変換を行う行列を表すprojectionMatrixは頂点ごとに変化するわけではありません。したがって、このような宣言となっています。

 それでは次にメインのプログラムを見てみましょう。

#include <iostream>

#include "pmd.h"
#include "OrthogonalProjection.h"
#include "SimpleProgramObject.h"
#include "Matrix.h"

enum
{
  ATTRIBUTE_VERTEX_COORDINATE,
  ATTRIBUTE_VERTEX_TEXTURE_COORDINATE,
  ATTRIBUTE_VERTEX_NORMAL,
  ATTRIBUTE_VERTEX_COLOR,
  NUM_ATTRIBUTES
};

enum
{
  UNIFORM_PROJECTION_MATRIX,
  UNIFORM_MODEL_MATRIX,
  UNIFORM_NORMAL_MATRIX,
  UNIFORM_LIGHT_DIRECTION,
  UNIFORM_VIEW_DIRECTION,
  UNIFORM_LIGHT_AMBIENT_COLOR,
  UNIFORM_LIGHT_DIFFUSE_COLOR,
  UNIFORM_LIGHT_SPECULAR_COLOR,
  UNIFORM_OBJECT_AMBIENT_COLOR,
  UNIFORM_OBJECT_DIFFUSE_COLOR,
  UNIFORM_OBJECT_SPECULAR_COLOR,
  UNIFORM_OBJECT_SHINNESS,
  UNIFORM_TEXTURE_ENABLED,
  UNIFORM_SHADING_ENABLED,
  NUM_UNIFORMS
};
GLint uniforms[NUM_UNIFORMS];

bool OrthogonalProjection::initialize(){
#if !TARGET_OS_IPHONE
  BaseApplication::initializeWindow(640, 960, "Orthogonal Projection");
#endif
  
  polygonModel.load(modelSearchPath, "miku.pmd");
  
  textures = new GLuint[polygonModel.getNumMaterials()];
  for(int i=0; i<polygonModel.getNumMaterials(); i++){
    unsigned char* data = polygonModel.getTextureDatas()[i];
    if(data==0){
      textures[i] = 0;
    }
    else{
      unsigned int width = polygonModel.getTextureWidths()[i];
      unsigned int height = polygonModel.getTextureHeights()[i];
      glGenTextures(1, &textures[i]);
      glBindTexture(GL_TEXTURE_2D, textures[i]);
      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
      glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
      glBindTexture(GL_TEXTURE_2D, 0);
      //      std::cerr << i << ": " << textures[i] << std::endl;
    }
  }
  
  SimpleProgramObject programObject;
  const char* vertexShaderFileName = "phong-shading-model-with-projection.vs";
  const char* fragmentShaderFileName = "texture-mapping.fs";
  
  program = programObject.createProgram(vertexShaderFileName, fragmentShaderFileName, shaderSearchPath);
  
  // シェーダコード内の変数にインデックスを設定する
  glBindAttribLocation(program, ATTRIBUTE_VERTEX_COORDINATE, "vertexCoordinate");
  glBindAttribLocation(program, ATTRIBUTE_VERTEX_TEXTURE_COORDINATE, "vertexTextureCoordinate");
  glBindAttribLocation(program, ATTRIBUTE_VERTEX_NORMAL, "vertexNormal");
  
  programObject.linkProgram();
  
  // 頂点配列オブジェクトを作成して設定する
#if TARGET_OS_IPHONE
  glGenVertexArraysOES(1, &vertexArrayObject);
  glBindVertexArrayOES(vertexArrayObject);
#else
  glGenVertexArrays(1, &vertexArrayObject);
  glBindVertexArray(vertexArrayObject);
#endif
  
  // 頂点バッファオブジェクトを作成する
  glGenBuffers(3, vertexBufferObjects);
  glBindBuffer(GL_ARRAY_BUFFER, vertexBufferObjects[0]);
  glBufferData(GL_ARRAY_BUFFER, sizeof(float)*polygonModel.numVertices()*3, polygonModel.vertexCoordinates(), GL_STATIC_DRAW);
  
  // 頂点バッファオブジェクトにシェーダ内の変数vertexCoodrinateを結びつける
  glEnableVertexAttribArray(ATTRIBUTE_VERTEX_COORDINATE);
  glVertexAttribPointer(ATTRIBUTE_VERTEX_COORDINATE, 3, GL_FLOAT, GL_FALSE, sizeof(float)*3, 0);
  
  // 頂点バッファオブジェクトを作成する
  glBindBuffer(GL_ARRAY_BUFFER, vertexBufferObjects[1]);
  glBufferData(GL_ARRAY_BUFFER, sizeof(float)*polygonModel.numVertices()*3, polygonModel.vertexNormals(), GL_STATIC_DRAW);
  
  // 頂点バッファオブジェクトにシェーダ内の変数vertexCoodrinateを結びつける
  glEnableVertexAttribArray(ATTRIBUTE_VERTEX_NORMAL);
  glVertexAttribPointer(ATTRIBUTE_VERTEX_NORMAL, 3, GL_FLOAT, GL_FALSE, sizeof(float)*3, 0);
  
  // 頂点バッファオブジェクトを作成する
  glBindBuffer(GL_ARRAY_BUFFER, vertexBufferObjects[2]);
  glBufferData(GL_ARRAY_BUFFER, sizeof(float)*polygonModel.numVertices()*2, polygonModel.vertexTextureCoordinates(), GL_STATIC_DRAW);
  
  // 頂点バッファオブジェクトにシェーダ内の変数vertexCoodrinateを結びつける
  glEnableVertexAttribArray(ATTRIBUTE_VERTEX_TEXTURE_COORDINATE);
  glVertexAttribPointer(ATTRIBUTE_VERTEX_TEXTURE_COORDINATE, 3, GL_FLOAT, GL_FALSE, sizeof(float)*2, 0);
  
  glGenBuffers(1, &elementArrayBufferObject);
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, elementArrayBufferObject);
  glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(GLuint)*3*polygonModel.numFaces(), polygonModel.faceVertexIndices(), GL_STATIC_DRAW);
  
  // 背景色の設定
  glClearColor(0.75f, 0.75f, 0.75f, 1.0f);
  
  // 頂点シェーダ内のuniform変数の場所を保存する
  uniforms[UNIFORM_PROJECTION_MATRIX] = glGetUniformLocation(program, "projectionMatrix");
  uniforms[UNIFORM_MODEL_MATRIX] = glGetUniformLocation(program, "modelMatrix");
  uniforms[UNIFORM_NORMAL_MATRIX] = glGetUniformLocation(program, "normalMatrix");
  uniforms[UNIFORM_VIEW_DIRECTION] = glGetUniformLocation(program, "viewDirection");
  uniforms[UNIFORM_LIGHT_DIRECTION] = glGetUniformLocation(program, "lightDirection");
  uniforms[UNIFORM_LIGHT_AMBIENT_COLOR] = glGetUniformLocation(program, "lightAmbientColor");
  uniforms[UNIFORM_LIGHT_DIFFUSE_COLOR] = glGetUniformLocation(program, "lightDiffuseColor");
  uniforms[UNIFORM_LIGHT_SPECULAR_COLOR] = glGetUniformLocation(program, "lightSpecularColor");
  uniforms[UNIFORM_OBJECT_AMBIENT_COLOR] = glGetUniformLocation(program, "objectAmbientColor");
  uniforms[UNIFORM_OBJECT_DIFFUSE_COLOR] = glGetUniformLocation(program, "objectDiffuseColor");
  uniforms[UNIFORM_OBJECT_SPECULAR_COLOR] = glGetUniformLocation(program, "objectSpecularColor");
  uniforms[UNIFORM_OBJECT_SHINNESS] = glGetUniformLocation(program, "objectShinness");
  uniforms[UNIFORM_TEXTURE_ENABLED] = glGetUniformLocation(program, "textureEnabled");
  uniforms[UNIFORM_SHADING_ENABLED] = glGetUniformLocation(program, "shadingEnabled");
  
  Matrix::setIdentityM(modelMatrix, 0);
  Matrix::scaleM(modelMatrix, 0, 0.07f, 0.07f, 0.07f);
  Matrix::translateM(modelMatrix, 0, 0.0f, -polygonModel.getMaxHeight()/2.0f, 0.0f);
  Matrix::rotateM(modelMatrix, 0, 180.0, 0.0, 1.0, 0.0);
  
  if(windowWidth > windowHeight){
    Matrix::orthoM(projectionMatrix, 0, -(float)windowWidth / windowHeight, (float)windowWidth / windowHeight, -1.0f, 1.0f, -1.0f, 1.0f);
  }
  
  else{
    Matrix::orthoM(projectionMatrix, 0, -1.0f, 1.0f, -(float)windowHeight / windowWidth, (float)windowHeight / windowWidth, -1.0f, 1.0f);
  }
  

  float invertMatrix[16];
  Matrix::invertM(invertMatrix, 0, modelMatrix, 0);
  Matrix::transposeM(normalMatrix, 0, invertMatrix, 0);
  
  glActiveTexture(GL_TEXTURE0);
  glUniform1i(ATTRIBUTE_VERTEX_TEXTURE_COORDINATE, 0);
  
  return true;
}


void OrthogonalProjection::update(){
  
}

void OrthogonalProjection::draw(){
  glEnable(GL_CULL_FACE);
  glEnable(GL_DEPTH_TEST);
  // 背景のクリア
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  // 使用するプログラムオブジェクトをする
  glUseProgram(program);
  // 投影行列を設定する
  glUniformMatrix4fv(uniforms[UNIFORM_PROJECTION_MATRIX], 1, 0, projectionMatrix);
  glUniformMatrix4fv(uniforms[UNIFORM_MODEL_MATRIX], 1, 0, modelMatrix);
  glUniformMatrix4fv(uniforms[UNIFORM_NORMAL_MATRIX], 1, 0, normalMatrix);
  glUniform3f(uniforms[UNIFORM_VIEW_DIRECTION], 0.0f, 0.0f, 1.0f);
  glUniform3f(uniforms[UNIFORM_LIGHT_DIRECTION], 1.0f, 0.0f, 1.0f);
  glUniform3f(uniforms[UNIFORM_LIGHT_AMBIENT_COLOR], 1.0f, 1.0f, 1.0f);
  glUniform3f(uniforms[UNIFORM_LIGHT_DIFFUSE_COLOR], 1.0f, 1.0f, 1.0f);
  glUniform3f(uniforms[UNIFORM_LIGHT_SPECULAR_COLOR], 1.0f, 1.0f, 1.0f);
  glUniform1i(uniforms[UNIFORM_SHADING_ENABLED], 1);
  
  unsigned int* vertexCounts = polygonModel.getVertexCounts();
  float* ambientColors = polygonModel.getAmbientColors();
  float* diffuseColors = polygonModel.getDiffuseColors();
  float* specularColors = polygonModel.getSpecularColors();
  float* shinness = polygonModel.getShinness();
  int sum = 0;
  for(int i=0; i<polygonModel.getNumMaterials(); i++){
    glUniform3f(uniforms[UNIFORM_OBJECT_AMBIENT_COLOR], ambientColors[3*i+0], ambientColors[3*i+1], ambientColors[3*i+2]);
    glUniform3f(uniforms[UNIFORM_OBJECT_DIFFUSE_COLOR], diffuseColors[3*i+0], diffuseColors[3*i+1], diffuseColors[3*i+2]);
    glUniform3f(uniforms[UNIFORM_OBJECT_SPECULAR_COLOR], specularColors[3*i+0], specularColors[3*i+1], specularColors[3*i+2]);
    glUniform1f(uniforms[UNIFORM_OBJECT_SHINNESS], shinness[i]);
    if(textures[i]==0){
      glUniform1i(uniforms[UNIFORM_TEXTURE_ENABLED], 0);
      glDrawElements(GL_TRIANGLES, vertexCounts[i], GL_UNSIGNED_INT, (GLvoid*)(sum*sizeof(GLuint)));
    }
    else{
      glUniform1i(uniforms[UNIFORM_TEXTURE_ENABLED], 1);
      //      glEnable(GL_BLEND);
      //      glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
      glBindTexture(GL_TEXTURE_2D, textures[i]);
      //      glUniform1i(ATTRIBUTE_VERTEX_TEXTURE_COORDINATE, 0);
      glEnable(ATTRIBUTE_VERTEX_TEXTURE_COORDINATE);
      glDrawElements(GL_TRIANGLES, vertexCounts[i], GL_UNSIGNED_INT, (GLvoid*)(sum*sizeof(GLuint)));
      glDisable(ATTRIBUTE_VERTEX_TEXTURE_COORDINATE);
      //      glDisable(GL_BLEND);
      glBindTexture(GL_TEXTURE_2D, 0);
    }
    sum += vertexCounts[i];
  }
  
  // 使用するプログラムオブジェクトを解除する
  glUseProgram(0);
#if _WIN32 || (TARGET_OS_MAC && !TARGET_OS_IPHONE)
  // バッファの入れ替え
  glfwSwapBuffers(window);
  // イベントの取得
  glfwPollEvents();
#endif
}

 これまでのプログラムと違う点を解説していきます。まず、シェーダのファイル名を変更しています。

 このプログラムでは新しくuniform変数を使用しているので、

enum
{
  UNIFORM_PROJECTION_MATRIX,
  NUM_UNIFORMS
};
GLint uniforms[NUM_UNIFORMS];

という宣言が入っており、インデックスを取得するために、

uniforms[UNIFORM_PROJECTION_MATRIX] = glGetUniformLocation(program, "projectionMatrix");

という1行が初期化の際に含まれています。

 次に投影変換を表す行列について見てみます。OrthogonalProjection.hファイルを見ると分かるように、float projectionMatrix[16]というように、float型の配列として4×4行列が宣言されています。この配列に上で述べた平行投影の行列を設定するために、

Matrix::orthoM(projectionMatrix, 0, -1.0f, 1.0f, -(float)windowWidth/windowHeight, (float)windowWidth/windowHeight, -1.0f, 1.0f);

ここで、4×4行列のそれぞれの要素の値を自分で計算してもいいのですが、本稿ではMatrixクラスを使うことにします。このクラスはAndroidで使用されているものと同じように使えるようにしてあります (AndroidのものはJavaで書かれているわけですが、それをCに私がポーティングしました)。使い方としては、Androidのドキュメントに次のように書かれていますので、最初の引数に配列へのポインタを指定し、2番目の引数にオフセットとして0を与え、さらに3番目以降の引数に平行投影に必要なそれぞれの要素を与えることで、1番目の引数に渡した配列に行列が設定されます。

orthoM(float[] m, int mOffset, float left, float right, float bottom, float top, float near, float far)

ここで、fovyは、

 そして、その行列を頂点シェーダに渡すために、drawメソッドの中で次のような命令を行っています。

glUniformMatrix4fv(uniforms[UNIFORM_PROJECTION_MATRIX], 1, 0, projectionMatrix);

やってみよう

  1. 背景の色のグレーをもっと明るくするにはどのようにしたらいいですか?
  2. ウィンドウの大きさを変えてみましょう。