OpenGL ES 2.0で波紋を作る#7 板ポリゴンに動きをつける
最終章です。板ポリゴンへの動きのつけ方を解説します。
ポイント
・3D座標のz軸
波紋
板ポリゴンでは、x軸とy軸を利用して2Dポリゴンを画面に描画しました。
この板ポリゴンのz軸に対して高さをつけると、板ポリゴンが3Dポリゴンに変わります。
今回は、このz軸の算出がポイントです。
z軸には、中心座標から距離と時間に応じて高低差をつけます。高低差はサイン波を利用します。サイン波により波の広がりが表現できます。
では、板ポリゴンに波の広がりを付けてみましょう。
波紋作成
今回は、以下のファイルを修正します。
・Shader.vsh
・Shader.fsh
・GameViewController.m
変更箇所について解説します。
変更箇所は、背景を反転させています。
Shader.vsh
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
attribute vec4 attr_pos; attribute vec4 attr_color; uniform mat4 unif_pm; varying lowp vec4 vary_color; uniform float drow_count; #define WAVE_HEIGHT 0.05 #define WAVE_WIDTH 8.0 #define WAVE_SPEED 0.06 #define PI 3.14159265 void main() { vec4 p = attr_pos; float height = WAVE_HEIGHT * (1.0 - abs(p.x * 1.3)) * (1.0 - abs(p.y * 1.3)); float r = sqrt(dot(p.xy, p.xy)); float t = drow_count * WAVE_SPEED; p.z = height * sin((r - t) * PI * WAVE_WIDTH); gl_Position = unif_pm * p; vary_color = attr_color; vary_color.z = p.z; } |
定数は、WAVE_HEIGHT:波の高さ、WAVE_WIDTH:波の幅、WAVE_SPEED:波の速度、PI:円周率を設定します。
mainメソッドには、以下を追加します。
・波の広がりであるz軸の算出
・フラグメントシェーダに連携するためのカラーの設定
波の広がりの算出は、以下のとおりです。
波の広がり(z軸の高さ) = 波の高さ ✕ sin(半径 ー 時間)
波の高さは、座標0から離れるに従ってなくなるようにします。係数を1.0にすると端の波がなくなります。今回は、波紋に見せるため1.3の係数を掛けます。
半径は、p.xy におけるベクトル空間の距離を算出します。
時間は、drow_count と定数を利用して速度を調整します。
サイン波の算出は、定数を利用して波の幅を調整します。
フラグメントシェーダには、z軸の高さを連携します。
Shader.fsh
1 2 3 4 5 6 7 8 9 |
varying lowp vec4 vary_color; #define COLOR_WEIGHT 0.05 void main() { mediump float i = ((COLOR_WEIGHT) + vary_color.z) * 0.5; mediump float wc = i / COLOR_WEIGHT; gl_FragColor = vec4(0.5 * wc, 0.9 * wc, 0.9, 0.0); } |
カラーは、頂点シェーダから連携されたz軸の高さを利用します。
z軸の範囲は、頂点シェーダの定数 WAVE_HEIGHT になります。ここでは、 -0.05〜0.05の間で設定されます。
カラーは0.0〜1.0で設定されるため、-0.05〜0.05が0.0〜1.0になるように変換処理を追加します。
また、水の色としてRGBカラーの赤:0.5、緑:0.9、青:0.9を設定します。この時、波の低い箇所は暗く、高い箇所は明るくなるように gl_FragColor を調整します。
GameViewController.m
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
#import "GameViewController.h" #import <OpenGLES/ES2/glext.h> typedef struct { // X値 GLfloat x; // Y値 GLfloat y; // Z値 GLfloat z; // U値 GLfloat u; // V値 GLfloat v; } VertexData; // 分割数 static const int kDivCount = 120; // 頂点数 = Y頂点数 * X頂点数 static const int kVertexCount = (kDivCount+1) * (kDivCount+1); // 頂点インデックス数 = Y座標のインデックス数 * X座標のインデックス数 static const int kIndexCount = (kDivCount) * (3+(((kDivCount+1)-2)*2)+3); // カウント float count = 0; @interface GameViewController () { // レンダリング用プログラムシェーダー GLuint _program; // 頂点バッファ GLuint vertices_buffer; // インデックスバッファ GLuint indices_buffer; // 視点 GLKMatrix4 projection_modelview; } @property (strong, nonatomic) EAGLContext *context; @property (strong, nonatomic) GLKBaseEffect *effect; - (void)setupGL; - (void)tearDownGL; - (BOOL)loadShaders; - (BOOL)compileShader:(GLuint *)shader type:(GLenum)type file:(NSString *)file; - (BOOL)linkProgram:(GLuint)prog; - (BOOL)validateProgram:(GLuint)prog; @end |
頂点シェーダで波の広がりを算出するための変数 count を定義します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 |
- (void)setupGL { [EAGLContext setCurrentContext:self.context]; [self loadShaders]; // 板ポリゴン作成 { { // 頂点バッファ { // 頂点バッファ VertexData vertrices[kVertexCount]; // 頂点間の長さ float divisions = 1.0f / kDivCount; // 頂点作成 int i = 0; for (int y = 0; y <= kDivCount; y++){ for (int x = 0; x <= kDivCount; x++){ float tx = (x * divisions) * 2 - 1.0f; float ty = (y * divisions) * 2 - 1.0f; float tz = 0.0f; vertrices[i].x = tx; vertrices[i].y = ty; vertrices[i].z = tz; vertrices[i].u = tx; vertrices[i].v = ty; i++; } } // 頂点バッファ作成 glGenBuffers(1, &vertices_buffer); assert(glGetError() == GL_NO_ERROR); assert(vertices_buffer != 0); // バインド glBindBuffer(GL_ARRAY_BUFFER, vertices_buffer); assert(glGetError() == GL_NO_ERROR); // アップロード glBufferData(GL_ARRAY_BUFFER, sizeof(VertexData) * kVertexCount, &vertrices, GL_STATIC_DRAW); assert(glGetError() == GL_NO_ERROR); // バインド解除 glBindBuffer(GL_ARRAY_BUFFER, 0); } // インデックスバッファ { // インデックスバッファ int indices[kIndexCount]; // y座標最大値 int maxYposition = kDivCount + 1; int i = 0; for (int y = 0; y < kDivCount; y++){ for (int x = 0; x < kDivCount+1; x++){ int idx0 = x + (y * maxYposition); int idx1 = idx0 + maxYposition; if(x == 0) { // 先頭x座標の場合、縮退三角形の対応をする indices[i++] = idx0; indices[i++] = idx0; indices[i++] = idx1; } else if (x == kDivCount) { // 後尾x座標の場合、縮退三角形の対応をする indices[i++] = idx0; indices[i++] = idx1; indices[i++] = idx1; } else { indices[i++] = idx0; indices[i++] = idx1; } } } // インデックスバッファ glGenBuffers(1, &indices_buffer); assert(glGetError() == GL_NO_ERROR); assert(indices_buffer != 0); // バインド glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indices_buffer); assert(glGetError() == GL_NO_ERROR); // アップロード glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(int) * kIndexCount, &indices, GL_STATIC_DRAW); assert(glGetError() == GL_NO_ERROR); // バインド解除 glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); } } // カメラ初期化 { // 画角 float degree = 25.0f; //カメラ範囲 float near = 0.0f; float far = 10.0f; // カメラ位置 float eyeX = 0.0f; float eyeY = 2.7f; float eyeZ = 1.7f; // ターゲット位置 float targetX = 0.0f; float targetY = 0.0f; float targetZ = 0.0f; // カメラベクトル float upX = 0.0f; float upY = -1.0f; float upZ = 0.0f; // カメラ初期値設定 float aspect = (GLfloat) self.view.bounds.size.width / (GLfloat) self.view.bounds.size.height; GLKMatrix4 projectionMatrix = GLKMatrix4MakePerspective(GLKMathDegreesToRadians(degree), aspect, near, far); GLKMatrix4 modelViewMatrix = GLKMatrix4MakeLookAt(eyeX, eyeY, eyeZ, targetX, targetY, targetZ, upX, upY, upZ); projection_modelview = GLKMatrix4Multiply(projectionMatrix, modelViewMatrix); } } } |
板ポリゴンをズームアップするため、カメラの設定値を修正します。
画角とカメラの位置に注目してください。
1 2 3 4 5 6 7 |
- (void)update { // 描画回数更新 { count = count + 0.1; } } |
updateメソッドは、描画前に処理されるメソッドです。
count は頂点シェーダで利用する時間の代わりに使用します。描画されるタイミングでカウントアップします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect { glClearColor(0.6f, 0.8f, 1.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // シェーダー利用開始 glUseProgram(_program); assert(glGetError() == GL_NO_ERROR); // バッファバインド glBindBuffer(GL_ARRAY_BUFFER, vertices_buffer); // 描画回数アップロード { // シェーダのuniform変数取得 GLint drow_count = glGetUniformLocation(_program, "drow_count"); // カウント設定 glUniform1f(drow_count, count); } // カメラ設定 { // シェーダのuniform変数取得 GLint unif_pm = glGetUniformLocation(_program, "unif_pm"); // カメラ指定 glUniformMatrix4fv(unif_pm, 1, GL_FALSE, projection_modelview.m); } // 頂点データ読み込み設定 { // シェーダのattribute変数取得 GLint attr_pos = glGetAttribLocation(_program, "attr_pos"); // シェーダ内属性のアクセス許可有効化 glEnableVertexAttribArray(attr_pos); // 利用箇所指定 glVertexAttribPointer(attr_pos, 3, GL_FLOAT, GL_FALSE, sizeof(VertexData), (GLvoid *) 0); } // フラグメントデータ読み込み設定 { // シェーダのattribute変数取得 GLint attr_color = glGetAttribLocation(_program, "attr_color"); // シェーダ内属性のアクセス許可有効化 glEnableVertexAttribArray(attr_color); // 利用箇所指定 glVertexAttribPointer(attr_color, 2, GL_FLOAT, GL_FALSE, sizeof(VertexData), (GLvoid *) (sizeof(float)*3)); } // インデックスバッファバインド glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indices_buffer); // シェーダー描画 glDrawElements(GL_TRIANGLE_STRIP, kIndexCount, GL_UNSIGNED_INT, (GLvoid *) 0); assert(glGetError() == GL_NO_ERROR); } |
updateメソッドで算出した count 変数を頂点シェーダへ連携するための処理を追加します。「unif_pm」と同様の設定です。
波の広がりを算出するための処理は以上です。
波紋の実行
それでは、シミュレータを起動してみましょう!
シミュレータを起動すると、波紋が画面に描画されます。
※動画を作ってみました!こちらから動画を見れます。
まとめ
今回は、波紋をテーマにObjective-CとOpenGL でプログラムを書いてみました。
OpenGLの開発は、最初に理解しなければいけないことが多いように感じました。
例えば、Javaで最初に学ぶことは、『Hello World !』をコンソールに出力することです。これは、数行あれば完成します。おまじないが1行、『Hello World !』の出力が1行です。
OpenGLは、まずはOpenGLの言語仕様、初期化から描画までの流れを理解しなければなりません。
さらに動きのあるポリゴンを作成するとなるとカメラ、光、影をベクトル、行列によって計算しなければなりません。
さらに、複雑な動きになると、高レベルな数学の知識が必要になります。
Javaとは違い、OpenGLは最初に乗り越えないといけない壁が高いと思います。
ですが、エンジニアとして、自由に描画できるOpenGLの魅力は尽きません。また、提供されるライブラリなどでは限界があります。
必要な時に、求められることができるようになるためには知識が必要です。
そのためには、OpenGLのような低レベル言語を経験し、エンジニアとしての底力を上げておきましょう!
バックナンバー
OpenGL ES2.0で波紋を作る#4 OpenGL ES を組み込むベースを作成
OpenGL ES2.0で波紋を作る#5 格子状の板ポリゴンを作る
OpenGL ES2.0で波紋を作る#7 板ポリゴンに動きをつける