環境原猫 59~65日目

今回は、WebGL2.0のTransform Feedbackによりマンデルブロ集合をとりあえず描画することが出来ました。

数値が発散するまでの計算回数を元に適当に色付けしています。

シェーダは以下のようになりました。in_realやtf_realが実部、in_imgやtf_imgが虚部に関する計算値です。

#version 300 es
in vec3 aVertexPosition;
in float in_real;
in float in_img;
in float in_count;

out float tf_real;
out float tf_img;
out float tf_count;

uniform float uReset;
uniform float uCalcs;
uniform float uScale;
uniform float uBaseX;
uniform float uBaseY;

void main(void) {
    float real = in_real;
    float img = in_img;
    float count = in_count;
    for(float i = 0.0; i < uCalcs; i++){
        if (0.0 < uReset && 0.0 == i) {
            real = (aVertexPosition.x + uBaseX) * uScale;
            img = (aVertexPosition.y + uBaseY) * uScale;
            count = 0.0;
        } else {
            float a = real * real - img * img + (aVertexPosition.x + uBaseX) *uScale;
            float b = 2.0 * real * img + (aVertexPosition.y + uBaseY) * uScale;
            real = a;
            img = b;
            if (4.0 < a * a + b * b) {
                count = count + 0.05;
            } else {
                count = count;
            }
        }
    }
    tf_real = real;
    tf_img = img;
    tf_count = count;
    gl_Position = vec4(aVertexPosition, 1.0);
}

シェーダの呼出しコード(概略)は以下の通りです。入力と出力のVBOを描画ごとに入れ替えることでマンデルブロ集合の計算を進めています。

~略~
// --- Transform Feedback ---
gl.useProgram(this.shader_tf);
this.shader_tf.enableAttribute();

// [入力] ユニフォーム変数
gl.uniform1f(this.shader_tf["uReset"], bReset ? 1.0 : 0.0);
gl.uniform1f(this.shader_tf["uCalcs"], calcs);
gl.uniform1f(this.shader_tf["uScale"], scale);
gl.uniform1f(this.shader_tf["uBaseX"], baseX);
gl.uniform1f(this.shader_tf["uBaseY"], baseY);

// [入力] VBO のバインド
gl.bindBuffer(gl.ARRAY_BUFFER, this.vID);
gl.vertexAttribPointer(this.shader_tf["aVertexPosition"], this.v.itemSize, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, this.tf_turn ? this.tf_rID : this.tf_r2ID);
gl.vertexAttribPointer(this.shader_tf["in_real"], this.tf_turn ? this.tf_r.itemSize : this.tf_r2.itemSize, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, this.tf_turn ? this.tf_iID : this.tf_i2ID);
gl.vertexAttribPointer(this.shader_tf["in_img"], this.tf_turn ? this.tf_i.itemSize : this.tf_i2.itemSize, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, this.tf_turn ? this.tf_cID : this.tf_c2ID);
gl.vertexAttribPointer(this.shader_tf["in_count"], this.tf_turn ? this.tf_c.itemSize : this.tf_c2.itemSize, gl.FLOAT, false, 0, 0);

// [出力] 書き込み先の VBO をバインド
gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, this.tf_turn ? this.tf_r2ID : this.tf_rID);
gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 1, this.tf_turn ? this.tf_i2ID : this.tf_iID);
gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 2, this.tf_turn ? this.tf_c2ID : this.tf_cID);

gl.enable(gl.RASTERIZER_DISCARD);
gl.beginTransformFeedback(gl.POINTS);

// 実行
gl.drawArrays(gl.POINTS, 0, this.points);

gl.disable(gl.RASTERIZER_DISCARD);
gl.endTransformFeedback();

this.shader_tf.disableAttribute();

gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, null);
gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 1, null);
gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 2, null);

this.tf_turn = !this.tf_turn;
~略~

動画は画素ごとに1フレーム1回で計算した様子ですが、シェーダ内でループしてまとめて計算もできます。

4Kの解像度で1フレーム1万回の計算をした場合、RTX3070で約18FPSの結果となりました。

これは、 f(z) = z^2 + C の計算を、1秒間に3,840 * 2,160 * 10,000 * 18 = 1,492,992,000,000 回行っていることを意味します。

( -`ω-)キリッ
_人人人人人人_
> 意味します <
 ̄Y^Y^Y^Y^Y^Y ̄

次の週

環境原猫 52~58日目

今回は、マンデルブロ集合をTransform Feedbackで高速描画するための下準備を行いました。

このページでマンデルブロ集合を手軽に描画できるようにWebGL2を利用することとしました。

過去に作成したLIFEGAME LIGHTNINGはWebGL1.0で作成していましたが、このバージョンだとTransform Feedbackが利用できないため土台部分のコードをWebGL2.0で書き直し、その上でTransform Feedbackの基礎処理を実装しました。

中核となるコード(概略)は以下通りです。

// --- Transform Feedback ---
gl.useProgram(this.shader_tf);
this.shader_tf.enableAttribute();

// [入力] VBO のバインド
gl.bindBuffer(gl.ARRAY_BUFFER, this.vID);
gl.vertexAttribPointer(this.shader_tf["aVertexPosition"], this.v.itemSize, gl.FLOAT, false, 0, 0);

// [出力] 書き込み先の VBO をバインド
gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, this.tf_rID);
gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 1, this.tf_iID);
gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 2, this.tf_cID);

gl.enable(gl.RASTERIZER_DISCARD);
gl.beginTransformFeedback(gl.POINTS);

// 実行
gl.drawArrays(gl.POINTS, 0, this.points);

gl.disable(gl.RASTERIZER_DISCARD);
gl.endTransformFeedback();

// テスト出力        
//const view = new Float32Array(this.points);
//gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 2, this.tf_cID);
//gl.getBufferSubData(gl.TRANSFORM_FEEDBACK_BUFFER, 0, view);
//console.log(view);

this.shader_tf.disableAttribute();

gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, null);
gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 1, null);
gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 2, null);

入力に初期座標を、出力にマンデルブロ集合の計算に必要となる複素数の計算結果「実部」と「虚部」と「計算回数」を設定しました。

Transform Feedback用の頂点シェーダはとりあえず以下のようにしました。

const POINT_VS_TF = '#version 300 es\n\
in vec3 aVertexPosition;\
\
out float tf_real;\
out float tf_img;\
out float tf_count;\
\
void main(void) {\
    tf_real = 0.0;\
    tf_img = 0.0;\
    tf_count = 0.0 < aVertexPosition.x && 0.0 < aVertexPosition.y ? 1.0 : 0.0;\
    gl_Position = vec4(aVertexPosition, 1.0);\
}';

描画用の頂点シェーダは以下のようにしました。

const POINT_VS = '#version 300 es\n\
in vec3 aVertexPosition;\
in vec3 aVertexColor;\
in float aVertexAlpha;\
in float tf_count;\
\
out vec4 vColor;\
\
uniform float uPointSize;\
\
uniform mat4 uMVMatrix;\
uniform mat4 uPMatrix;\
\
void main(void) {\
    vColor = vec4(aVertexColor, aVertexAlpha * tf_count);\
    gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);\
    gl_PointSize = uPointSize;\
}';

描画用のフラグメントシェーダは以下のようにし、各点を円形のグラデーションで描画しています

const POINT_FS = '#version 300 es\n\
precision mediump float;\
\
in vec4 vColor;\
out vec4 outColor;\
\
void main(void) {\
    vec3 target;\
    target.xy = (gl_PointCoord - 0.5) * 2.0;\
    float r2 = target.x * target.x + target.y * target.y;\
    if (1.0 < r2) {\
        discard;\
    }\
    outColor = vec4(vColor.r, vColor.g, vColor.b, vColor.a * 0.4 * (1.0 - r2));\
}';

テストで、Transform Feedback用の頂点シェーダでは、入力座標 x y が共に 0以上 ならば tf_count に 1.0 を出力するようにしています。

見事にシェーダでの計算結果が反映されました。

ちなみにフルスクリーン切り替え処理や、ちょっとした設定の切り替え機能(今のところFPSの表示/非表示だけ)も実装しました。今回は超がんばりました。

あとはTransform Feedback用の頂点シェーダに複素数の計算を記述し、描画用のシェーダで解像度を上げていけばマンデルブロ集合の画像が表示されるはずです。

次の週

環境原猫 38~44日目

先週は水分子をTransform Feedbackを利用してFPSをほとんど落とさずにグリグリ動かすことが出来ました。

しかし、私が本当にやりたいことは、水分子が互いに影響を及ぼすような動作です。例えば水の表面張力の、近くの水同士が引っ張り合うような力のシミュレートをしたかったのです。

で、水分子が互いに影響を及ぼすような動作がTransform Feedbackで実現できないか色々調査しましたが、結論としては

他の水分子(座標)の情報は取得できず、水分子同士の相互作用の処理は出来ない

ことが判明しました。

/(^o^)\オワタ

というわけで今後の方針としては

1.CPUをつかって水分子同士の計算を頑張る

2.水分子とか細かい部分はバッサリ切り捨てて単純なゲームを作る。というか分子から作るってどいうこと?いつゲームは完成するの?

3.1と2のバランスを取る

の3つがあるのかなと思います。今週はその方針を考えようと思います。

次の週

環境原猫 32~37日目

29日目でCPUにより水分子をグリグリ動かしましたが、今回はシェーダでTransform Feedbackを利用して動かすことが出来ました!

シェーダは以下のように変更しています。

layout (location = 0) in vec3 Position;
layout (location = 1) in vec2 Speed;
out vec3 feedbackPosition;
uniform float pointSize;

void main()
{
    gl_Position = vec4(Position, 1.0);
    float x = Position.x + Speed.x;
    float y = Position.y + Speed.y;
    if (1.0 < x) { x = -1.0; }
    if (x < -1.0) { x = 1.0; }
    if (1.0 < y) { y = -1.0; }
    if (y < -1.0) { y = 1.0; }
    feedbackPosition = vec3(x, y, 0.0);
    gl_PointSize = pointSize;
}
~略~
let c_string = CString::new("feedbackPosition").unwrap();
let c_string_ptr = &c_string.as_ptr();
gl::TransformFeedbackVaryings(program_id, 1, c_string_ptr, gl::SEPARATE_ATTRIBS);
gl::LinkProgram(program_id);
~略~

out vec3 feedbackPositionに、シェーダによる計算結果が格納されます。

位置情報をもう1つ作って、入力と出力用に分けました。

    let mut vertices: Vec<f32> = Vec::with_capacity(POINTS * 3);
    let mut vertices2: Vec<f32> = Vec::with_capacity(POINTS * 3);
    let mut vertices_speed: Vec<f32> = Vec::with_capacity(POINTS * 2);
    let mut rng = rand::thread_rng();
    for _i in 0..POINTS {
        vertices.push(rng.gen_range(-1.0..1.0));    // x
        vertices.push(rng.gen_range(-1.0..1.0));    // y
        vertices.push(0.0);                         // z
        vertices2.push(0.0);    // x
        vertices2.push(0.0);    // y
        vertices2.push(0.0);    // z
        vertices_speed.push(rng.gen_range(-0.01..0.01));    // speed x
        vertices_speed.push(rng.gen_range(-0.01..0.01));    // speed y
    }

描画時に、入力と出力を毎フレーム入れ替えるようにしました。出力結果が次の入力値になるようにし、その際の出力結果は次の入力に・・・と毎回入れ替えながら分子の位置を更新しています。

let mut b_turn: bool = false;

'running: loop {
    ~略~
    // draw points
    unsafe {
        if b_turn {
            gl::BindVertexArray(vao2);
            gl::BindBufferBase(gl::TRANSFORM_FEEDBACK_BUFFER, 0, vbo);
        } else {
            gl::BindVertexArray(vao);
            gl::BindBufferBase(gl::TRANSFORM_FEEDBACK_BUFFER, 0, vbo2);
        }
        // Transform Feedback 開始
        gl::BeginTransformFeedback(gl::POINTS);
        gl::DrawArrays(
            gl::POINTS,     // mode
            0,              // starting index in the enabled arrays
            POINTS as i32,  // number of indices to be rendered
        );
        // Transform Feedback 終了
        gl::EndTransformFeedback();
        // 後片付け
        gl::BindBuffer(gl::ARRAY_BUFFER, 0);
        gl::BindVertexArray(0);
    }
    // swap window
    window.gl_swap_window();
    }
    b_turn = !b_turn;
}

気になるベンチマークですが、
Transform Feedback未使用(CPUで計算)の場合、

1000万ポイント = 28FPS
1億ポイント = 3FPS

Transform Feedback 使用(シェーダで計算)の場合、

1000万ポイント = 60FPS
1億ポイント = 20FPS

となり、大幅な性能向上を果たしました。なんとこれ、分子を動かす前とFPS値がほぼ変わって無いです。Transform Feedbackにより、余って無駄になっていたGPUリソースを有効活用できました。

( ✌’ω’)✌

次の週

ゲーム制作 環境原猫 31日目

隣の田所さんのサーバが止まっていたので復旧してました。

「隣の田所さん」(ゲーム名)はWebサーバにRust言語で作られたものを使っており性能は良いのですが、いつの間にか止まっていることが多いんですよね・・・

ログインできない場合はこのページにコメント入れていただければ復旧しますね。

あと、このゲーム制作日記(主に制作者のサボり防止用)ですが、毎日だとあまり書くことがないので今後は1週間に1度、月曜日にまとめて書くことにします。

次の週

ゲーム制作 環境原猫 29日目

昨日水分子を、画像を使わずにシェーダのみで描画するように変更しました。

今日は水分子を、空気中を飛び回っているイメージでグリグリ動かしました。

コードの変更箇所は以下の通りです。

let mut vertices: Vec<f32> = Vec::with_capacity(POINTS * 3);
let mut vertices_speed: Vec<f32> = Vec::with_capacity(POINTS * 2);
let mut rng = rand::thread_rng();
for _i in 0..POINTS {
    vertices.push(rng.gen_range(-1.0..1.0));    // x
    vertices.push(rng.gen_range(-1.0..1.0));    // y
    vertices.push(0.0);                         // z
    vertices_speed.push(rng.gen_range(-0.01..0.01));    // speed x
    vertices_speed.push(rng.gen_range(-0.01..0.01));    // speed y
}
// move points
for i in 0..POINTS {
    let base_pos = i * 3;
    let base_speed = i * 2;
    // x方向
    if 1.0 < vertices[base_pos + 0] || vertices[base_pos + 0] < -1.0 {
        vertices_speed[base_speed + 0] *= -1.0;
    }
    vertices[base_pos + 0] += vertices_speed[base_speed + 0];
    // y方向
    if 1.0 < vertices[base_pos + 1] || vertices[base_pos + 1] < -1.0 {
        vertices_speed[base_speed + 1] *= -1.0;
    }
    vertices[base_pos + 1] += vertices_speed[base_speed + 1];
}
unsafe {
    gl::BindBuffer(gl::ARRAY_BUFFER, vbo);
    gl::BufferData(
        gl::ARRAY_BUFFER,                                                       // target
        (vertices.len() * std::mem::size_of::<f32>()) as gl::types::GLsizeiptr, // size of data in bytes
        vertices.as_ptr() as *const gl::types::GLvoid, // pointer to data
        gl::STREAM_DRAW,                               // usage
    );
    gl::BindBuffer(gl::ARRAY_BUFFER, 0);
} 
// draw points
unsafe {
    gl::BindVertexArray(vao);
    gl::DrawArrays(
        gl::POINTS,     // mode
        0,              // starting index in the enabled arrays
        POINTS as i32,  // number of indices to be rendered
    );
}
unsafe {
    gl::BindBuffer(gl::ARRAY_BUFFER, vbo);
    gl::BufferData(
        gl::ARRAY_BUFFER,                                                       // target
        (vertices.len() * std::mem::size_of::<f32>()) as gl::types::GLsizeiptr, // size of data in bytes
        vertices.as_ptr() as *const gl::types::GLvoid, // pointer to data
        gl::STREAM_DRAW,                               // usage
    );
    gl::BindBuffer(gl::ARRAY_BUFFER, 0);
}

Rustのコード上で描画位置を変更していますが、これだとポイント数が増えた場合に負荷がヤバそうです。

明日からはなるべくシェーダに仕事をさせられないか検討します。

次の日

ゲーム制作 環境原猫 28日目

昨日に引き続き、今日は円の半径に伴いアルファ値が変わるようにシェーダを調整しました。

#version 330 core

out vec4 Color;

void main()
{
    vec3 target;
  
    target.xy = (gl_PointCoord - 0.5) * 2.0;                // 座標値変換 (0, 1) -> (-1, 1)
    float r2 = target.x * target.x + target.y * target.y;   // 半径の2乗
    if (1.0 < r2) {
        discard;
    }
    Color = vec4(0.0f, 0.0f, 1.0f, 0.4 * (1.0 - r2));
}

これだけですが効果は抜群で、水分子画像を使わずにシェーダだけで事が済んでしまいました。むしろ水分子画像を拡大したときはシェーダの方が綺麗に描画できます。

そして気になるパフォーマンスですが、画像を使わずにシェーダだけで描画したほうが、1FPSだけですが上回りました。(1億と1000万ポイント描画時に計測)

せっかくGIMPで一生懸命?作りましたが、シェーダ方式を採用することにしました。透明度や色、様々なエフェクトなどシェーダの方が自由度も高いですしね。

次の日

ゲーム制作 環境原猫 27日目

昨日はMacでも開発できるよう環境を整えました。

本日は水分子を画像を使わずにシェーダで描画するようにしてみます。まずポイントスプライトを円形にするため、フラグメントシェーダを以下のように改変しました。

#version 330 core

out vec4 Color;

void main()
{
    vec3 target;
    // 座標値変換 (0, 1) -> (-1, 1)
    target.xy = (gl_PointCoord - 0.5) * 2.0;
    // 半径の2乗
    float r2 = target.x * target.x + target.y * target.y;
    if (1.0 < r2) {
        discard;
    }
    Color = vec4(0.0f, 0.0f, 1.0f, 0.3f);
}

円外の場合はdiscardで色を付けないようにしています。結果は以下のようになりました。

明日はアルファ値を調整します。

次の日

大手には作れないアプリを(気持ちだけは(๑•̀ㅂ•́)و✧)