工場裏のアーカイブス

素人によるiPhoneアプリ開発の学習記

OpenGL ES2.0でポイントスプライト(1)

OpenGLでは、GL_POINTS で描かれる点(ポイント)の1つ1つにテクスチャを貼付ける「ポイントスプライト」という機能をサポートしています。これらのポイントは、そのサイズを拡大すると実は正方形として描かれており、その全面にテクスチャを貼付けることが出来ます。この正方形は常に視点に対して垂直な方向に表示される(あくまで点である以上、視点の方向によって見え方が変わらない)ため、ビルボードの代わりとして用いることが出来ます。

またアルファブレンドを有効にした状態で、様々な色を付けた多数の粒子(パーティクル)をポイントスプライトにより描画すると、非常に奇麗で幻想的なグラフィックを作ることが出来ます。更にパーティクルシステム(参考:Wikipedia)をポイントスプライトを用いて実装すると、炎、爆発、流水、霧といった事象を効率的に、かつ奇麗に表現することが出来るようです。

そこで、OpenGL ES2.0で実際にポイントスプライトを用いてグラフィックを描画する方法を模索しました。参考資料が少なく、かなりの試行錯誤と挫折を重ねましたが、最近になってようやくその方法が分かってきました。それについてメモしていきます。

シェーダ利用の必要性について

私は最初、シェーダなどを使わずにGLKitの範疇だけでポイントスプライトを使用することが出来ないか、色々調べて試行錯誤してみました。しかし私が調べた限りでは、GLKitの範疇だけでポイントサイズを変更したり、ポイントへテクスチャを貼付けたりする方法を見つけることが出来ませんでした。OpenGL ES2.0でポイントスプライトを実現するためには、(部分的にでも)シェーダを利用することが避けられないようです。そこで本記事では、GLKitとシェーダを併用する形での方法についてメモします。

簡単なサンプル

ひとまず、ポイントスプライトを利用した簡単なサンプルプログラムを作成しましたので、ソースとその実行手順を示します。なお、こちらのサンプルを作成するにあたっては、「OpenGL ES2.0 プログラミングガイド」という書籍をかなり参考にしています。

  • まずXcode(この記事執筆時点のバージョンは4.6.2)で、「OpenGL Game」テンプレートを用いて新しいプロジェクトを作成します。


  • 次に、こちらのリンク先にある「particle.png」という画像(アルファチャンネル付きのPNG画像として作成した、ポイント用のテクスチャ)を適当な場所に保存します。ファイル名は「particle.png」という名前のままにしておいてください。


  • そして、保存した「particle.png」をプロジェクトに追加します。Xcodeのメニューから「File」→「Add Files to "(プロジェクト名)"」と選択するとファイルを読み込むためのダイアログが開きますので、先程保存した「particle.png」を探して選択し、読み込んでください。


  • 続いて、ViewController.m の中身を、以下のコードに書き換えます(ViewController.h の書き換えは不要)。このコードは「OpenGL Game」テンプレートで自動生成されるサンプルを土台としており、前述のようにGLKitとシェーダを併用しています(シェーダのコンパイルやロード関連の方法については、土台となったサンプルの方法をほぼそのまま流用しています)。
//ViewController.m

#import "ViewController.h"

#define BUFFER_OFFSET(i) ((char *)NULL + (i))
#define VERTEX_NUM 100
#define RADIUS 3.0f

//各シェーダのuniform変数の位置を格納する配列。
enum
{
    UNIFORM_MODELVIEWPROJECTION_MATRIX,
    UNIFORM_S_TEXTURE,
    NUM_UNIFORMS
};
GLint uniforms[NUM_UNIFORMS];

//ポイントの座標および色の格納用配列
GLfloat vertex[VERTEX_NUM * 6];

@interface ViewController () {
    GLuint _program;
        
    GLuint _vertexArray;
    GLuint _vertexBuffer;
    
    GLKMatrix4 _modelViewProjectionMatrix;
    GLKMatrix4 modelViewMatrix;
    GLfloat _rotation;
}
@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

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    self.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];    
    if (!self.context) {
        NSLog(@"Failed to create ES context");
    }
    
    GLKView *view = (GLKView *)self.view;
    view.context = self.context;
    view.drawableDepthFormat = GLKViewDrawableDepthFormat24;
    
    [self setupGL];
}

- (void)dealloc
{
    [self tearDownGL];
    
    if ([EAGLContext currentContext] == self.context) {
        [EAGLContext setCurrentContext:nil];
    }
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];

    if ([self isViewLoaded] && ([[self view] window] == nil)) {
        self.view = nil;
        
        [self tearDownGL];
        
        if ([EAGLContext currentContext] == self.context) {
            [EAGLContext setCurrentContext:nil];
        }
        self.context = nil;
    }

    // Dispose of any resources that can be recreated.
}

- (void)setupGL
{
    [EAGLContext setCurrentContext:self.context];
    
    [self loadShaders];
    
    self.effect = [[GLKBaseEffect alloc] init];
    self.effect.light0.enabled = GL_TRUE;
    self.effect.light0.diffuseColor = GLKVector4Make(1.0f, 0.4f, 0.4f, 1.0f);
    
    //glEnable(GL_DEPTH_TEST); デプステストはオフにしておく。
    
    //テクスチャのロード。これだけで画像をテクスチャとして読み込み、テクスチャユニット0にバインドすることが出来る。便利!
    NSString* filePath = [[NSBundle mainBundle] pathForResource:@"particle" ofType:@"png"];
    GLKTextureInfo *texInfo =
    [GLKTextureLoader textureWithContentsOfFile:filePath options:nil error:nil];
    if (texInfo) {
        NSLog(@"Texture loaded successfully. name = %d size = (%d x %d)",
              texInfo.name, texInfo.width, texInfo.height);
    }
    self.effect.texture2d0.name = texInfo.name;
    
    //vertex[i]〜vertex[i+3]に、各ポイントの座標(半径"RADIUS"の球上のランダムな位置)を設定
    //vertex[i+4]〜vertex[i+6]に、各ポイントの色(RGB各成分に0.5f〜1.0fの間のランダムな値)を設定
    for(int i = 0; i < VERTEX_NUM * 6; i+=6){
        float theta = GLKMathDegreesToRadians(arc4random()%360);
        float phi = GLKMathDegreesToRadians(arc4random()%360);
        
        vertex[i] = RADIUS * sin(theta) * cos(phi);
        vertex[i + 1] = RADIUS * sin(theta) * sin(phi);
        vertex[i + 2] = RADIUS * cos(theta);
        vertex[i + 3] = ((arc4random() % 10000) / 20000.0f) + 0.5f;
        vertex[i + 4] = ((arc4random() % 10000) / 20000.0f) + 0.5f;
        vertex[i + 5] = ((arc4random() % 10000) / 20000.0f) + 0.5f;
    }
    
    glGenVertexArraysOES(1, &_vertexArray);
    glBindVertexArrayOES(_vertexArray);
    
    glGenBuffers(1, &_vertexBuffer);
    glBindBuffer(GL_ARRAY_BUFFER, _vertexBuffer);
    
    glBindBuffer(GL_ARRAY_BUFFER, _vertexBuffer);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertex), vertex, GL_STATIC_DRAW);
    
    //各ポイントの座標、色を頂点配列に格納
    glEnableVertexAttribArray(GLKVertexAttribPosition);
    glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE, sizeof(float)*6, BUFFER_OFFSET(0));
    glEnableVertexAttribArray(GLKVertexAttribColor);
    glVertexAttribPointer(GLKVertexAttribColor, 3, GL_FLOAT, GL_FALSE, sizeof(float)*6, BUFFER_OFFSET(sizeof(float)*3));
    
    glBindVertexArrayOES(0);
}

- (void)tearDownGL
{
    [EAGLContext setCurrentContext:self.context];
    
    glDeleteBuffers(1, &_vertexBuffer);
    glDeleteVertexArraysOES(1, &_vertexArray);
    
    self.effect = nil;
    
    if (_program) {
        glDeleteProgram(_program);
        _program = 0;
    }
}

#pragma mark - GLKView and GLKViewController delegate methods

- (void)update
{
    float aspect = fabsf(self.view.bounds.size.width / self.view.bounds.size.height);
    GLKMatrix4 projectionMatrix = GLKMatrix4MakePerspective(GLKMathDegreesToRadians(65.0f), aspect, 0.1f, 300.0f);
    
    self.effect.transform.projectionMatrix = projectionMatrix;

    modelViewMatrix = GLKMatrix4MakeTranslation(0.0f, 0.0f, -10.0f);
    modelViewMatrix = GLKMatrix4Rotate(modelViewMatrix, _rotation, 1.0f, 1.0f, 1.0f);
    
    _modelViewProjectionMatrix = GLKMatrix4Multiply(projectionMatrix, modelViewMatrix);
    
    _rotation += self.timeSinceLastUpdate * 0.2f;
}

- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect
{
    glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);
    
    //ブレンディングの設定(デプステストはオフにしておく)
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE);
    
    //シェーダをロードして、利用可能にする
    [self loadShaders];
    glUseProgram(_program);
    
    //変数"_modelViewProjectionMatrix"を、頂点シェーダのuniform変数"modelViewProjectionMatrix"(mat4型)に渡す
    //(loadShadersメソッドの中で、"uniforms[UNIFORM_MODELVIEWPROJECTION_MATRIX]"に、uniform変数の場所を格納)
    glUniformMatrix4fv(uniforms[UNIFORM_MODELVIEWPROJECTION_MATRIX], 1, 0, _modelViewProjectionMatrix.m);
    
    //フラグメントシェーダでテクスチャを取り込むためのサンプラ。テクスチャユニット0に設定
    glUniform1i(uniforms[UNIFORM_S_TEXTURE], 0);
    
    //ポイントの描画
    glBindVertexArrayOES(_vertexArray);
    glDrawArrays(GL_POINTS, 0, VERTEX_NUM);
    glBindVertexArrayOES(0);
}

//以下、シェーダのロード関連のメソッド群
#pragma mark -  OpenGL ES 2 shader compilation

- (BOOL)loadShaders
{
    GLuint vertShader, fragShader;
    NSString *vertShaderPathname, *fragShaderPathname;
    
    // Create shader program.
    _program = glCreateProgram();
    
    // Create and compile vertex shader.
    vertShaderPathname = [[NSBundle mainBundle] pathForResource:@"Shader" ofType:@"vsh"];
    if (![self compileShader:&vertShader type:GL_VERTEX_SHADER file:vertShaderPathname]) {
        NSLog(@"Failed to compile vertex shader");
        return NO;
    }
    
    // Create and compile fragment shader.
    fragShaderPathname = [[NSBundle mainBundle] pathForResource:@"Shader" ofType:@"fsh"];
    if (![self compileShader:&fragShader type:GL_FRAGMENT_SHADER file:fragShaderPathname]) {
        NSLog(@"Failed to compile fragment shader");
        return NO;
    }
    
    // Attach vertex shader to program.
    glAttachShader(_program, vertShader);
    
    // Attach fragment shader to program.
    glAttachShader(_program, fragShader);
    
    // Bind attribute locations.
    // This needs to be done prior to linking.
    glBindAttribLocation(_program, GLKVertexAttribPosition, "position");
    
    //頂点配列に格納した各ポイントの色を、頂点シェーダのattribute変数"color"に入力
    glBindAttribLocation(_program, GLKVertexAttribColor, "color"); 
    
    // Link program.
    if (![self linkProgram:_program]) {
        NSLog(@"Failed to link program: %d", _program);
        
        if (vertShader) {
            glDeleteShader(vertShader);
            vertShader = 0;
        }
        if (fragShader) {
            glDeleteShader(fragShader);
            fragShader = 0;
        }
        if (_program) {
            glDeleteProgram(_program);
            _program = 0;
        }
        
        return NO;
    }
    
    //頂点シェーダのuniform変数"modelViewProjectionMatrix"(mat4型)の場所を取得する。
    uniforms[UNIFORM_MODELVIEWPROJECTION_MATRIX] = glGetUniformLocation(_program, "modelViewProjectionMatrix");
    //フラグメントシェーダのuniform変数"s_texture"(テクスチャのサンプラ)の場所を取得する。
    uniforms[UNIFORM_S_TEXTURE] = glGetUniformLocation(_program, "s_texture");
    
    // Release vertex and fragment shaders.
    if (vertShader) {
        glDetachShader(_program, vertShader);
        glDeleteShader(vertShader);
    }
    if (fragShader) {
        glDetachShader(_program, fragShader);
        glDeleteShader(fragShader);
    }
    
    return YES;
}

- (BOOL)compileShader:(GLuint *)shader type:(GLenum)type file:(NSString *)file
{
    GLint status;
    const GLchar *source;
    
    source = (GLchar *)[[NSString stringWithContentsOfFile:file encoding:NSUTF8StringEncoding error:nil] UTF8String];
    if (!source) {
        NSLog(@"Failed to load vertex shader");
        return NO;
    }
    
    *shader = glCreateShader(type);
    glShaderSource(*shader, 1, &source, NULL);
    glCompileShader(*shader);
    
#if defined(DEBUG)
    GLint logLength;
    glGetShaderiv(*shader, GL_INFO_LOG_LENGTH, &logLength);
    if (logLength > 0) {
        GLchar *log = (GLchar *)malloc(logLength);
        glGetShaderInfoLog(*shader, logLength, &logLength, log);
        NSLog(@"Shader compile log:\n%s", log);
        free(log);
    }
#endif
    
    glGetShaderiv(*shader, GL_COMPILE_STATUS, &status);
    if (status == 0) {
        glDeleteShader(*shader);
        return NO;
    }
    
    return YES;
}

- (BOOL)linkProgram:(GLuint)prog
{
    GLint status;
    glLinkProgram(prog);
    
#if defined(DEBUG)
    GLint logLength;
    glGetProgramiv(prog, GL_INFO_LOG_LENGTH, &logLength);
    if (logLength > 0) {
        GLchar *log = (GLchar *)malloc(logLength);
        glGetProgramInfoLog(prog, logLength, &logLength, log);
        NSLog(@"Program link log:\n%s", log);
        free(log);
    }
#endif
    
    glGetProgramiv(prog, GL_LINK_STATUS, &status);
    if (status == 0) {
        return NO;
    }
    
    return YES;
}

- (BOOL)validateProgram:(GLuint)prog
{
    GLint logLength, status;
    
    glValidateProgram(prog);
    glGetProgramiv(prog, GL_INFO_LOG_LENGTH, &logLength);
    if (logLength > 0) {
        GLchar *log = (GLchar *)malloc(logLength);
        glGetProgramInfoLog(prog, logLength, &logLength, log);
        NSLog(@"Program validate log:\n%s", log);
        free(log);
    }
    
    glGetProgramiv(prog, GL_VALIDATE_STATUS, &status);
    if (status == 0) {
        return NO;
    }
    
    return YES;
}

@end


  • 更に今回はシェーダを利用するため、シェーダのコードについても書き換える必要があります。まず頂点シェーダ(Shader.vsh)の中身を以下のコードに書き換えます。
//Shader.vsh

attribute vec4 position;
attribute vec4 color;
varying vec4 vcolor;

uniform mat4 modelViewProjectionMatrix;

void main()
{
    vcolor = color;
    gl_Position = modelViewProjectionMatrix * position;
    
    //ポイントのサイズをここで設定する
    gl_PointSize = 20.0;
}


  • 続いて、フラグメントシェーダ(Shader.fsh)の中身を以下のコードに書き換えます。
//Shader.fsh

precision mediump float;

uniform sampler2D s_texture;
varying vec4 vcolor;

void main()
{
    //サンプラで取り込んだテクスチャを、変数"baseColor"に格納
    vec4 baseColor = texture2D(s_texture, gl_PointCoord);
    
    //アルファ値が0.5未満である場合はフラグメントを破棄(アルファテスト)
    if(baseColor.a < 0.5){
        discard;
    }
    else{
        //元々のテクスチャの色に、各ポイントに設定された色を付けて出力する
        gl_FragColor = baseColor * vec4(vcolor);
    }
}


  • これを実行すると、以下のように球状に配置されたいくつものカラフルな点が描画されます。これらの点の一つ一つが、テクスチャ(particle.png)を貼付けられたポイントであり、各点に(頂点属性として)設定された色に応じた塗り分けがされて表示されます。またアルファブレンドを有効にしてあるため、各ポイントの色はブレンディングされて表示されます。

f:id:fleron:20130711003455p:plain


このサンプルプログラムのソースの詳細については、次回以降の記事にメモしていく予定です。