Revision2020 PC 64K Intro 優勝作品『RE: SIMULATED』の技術解説

4月10日~4月13日に世界最大のデモパーティRevision 2020に参加しました。

Revision 2020内で開催されたコンペのうち、PC 64K Introという64KBの容量制約のある部門で『RE: SIMULATED by gam0022 & sadakkey』という作品を発表しました。

Tokyo Demo Fest 2018に続き、私(@gam0022)が映像を、さだきちさん(@sadakkey)が音楽を制作しました。

……なんと、本作品が参加者投票により1位に選ばれました! 日本人のチームがPC 64K Intro部門で優勝するのは Revision 史上初です。とても嬉しいです!

本記事では、技術解説をメインに、『RE: SIMULATED by gam0022 & sadakkey』を紹介したいと思います。

resimulated-collage

作品へのリンク

WebGLとWebAudioによる64K Introなので、最新のChromeと高性能なGPUがあれば、ブラウザ上で動作します。

高スペックのPCを持っていない方は、YouTubeの動画をご覧ください。

フラクタルをつかった映像のビットレートの高い作品ですが、4K解像度を選ぶことである程度は綺麗な状態で見れます。


こちらはpouet(デモシーンのコミュニティサイト)のリンクです。

技術解説

ソースコードはすべてGitHubに公開しているので、興味がある方はぜひ見てください。

サウンド編についてはさだきちさんが解説されています。あわせてご覧ください!

キーワードとしては、以下の技術が使われています。

TypeScript, WebGL, WebAudio, webpack, pnginator.rb, Raymarching, GLSL Sound

シンプルなWebGLエンジン『Chromatiq』

64KBの容量制約があるため、Unityやthree.jsといった既存のゲームエンジンやフレームワークを利用せずに、描画用のWebGLエンジンと制作用のツール(エディタ機能)を自作する必要がありました。

OpenGLやDirectXを使わずに、WebGLを選択した理由は以下です。

  • WebGLでブラウザ上で動かせれば、手元のPCで動かしてもらえる可能性が高いと考えた
    • 自分の作品は映像のビットレートが高く、動画だと綺麗にならない
    • 手元のPCで実行して綺麗な状態で見てもらいたい
  • Webフロントエンドの技術をキャッチアップしたかった

そこで、64K Intro向けにファイルサイズの最小化を目指したシンプルなWebGLエンジン『Chromatiq』を開発しました。

WebGLエンジンとは言うものの、本当にシンプルで最小限な機能しか “現段階では” 実装していません。

なるべく作品に依存した機能は用意したくなかったので、汎用的な設計になっています。

  • マルチパスのImageShaderによるレンダリング(viewport square)
  • ビルドインのBloomのポストエフェクト
    • どんな作品でも利用できそうなので、これだけビルドインにした
  • TypeScriptからuniformをアニメーションするためのインターフェース
  • Shadertoyと互換性のあるGLSL Sound
  • オーディオファイルの再生(mp3 / ogg)
    • DAWによる音楽の再生用の機能
    • 今回は先にDAWで作曲し、後からGLSLに移植する作戦にした
  • フォントをレンダリングするためのcanvasからのテクスチャ生成

イメージとしてはGLSLエディタを排除したスタンドアローンなShadertoyが近いかもしれません。

ソースコードはこちらです。単一ファイルのTypeScriptで実装しました。

圧縮後のコードサイズを気にして、変な感じの実装になっているので、微妙に読みづらいかもしれません。

例えば、フィールド参照の this を頭につけるとコードサイズが増えるため、コンストラクタの中で動的にインスタンスメソッドを定義することで、this の利用を最小限にしたり、 クラス外から値を参照・設定する必要があるデータのみ、フィールドとして定義する方針とています。enumもコードサイズが増えるので禁止にしました。

製作の終盤から容量が余裕そうなことが判明したので、途中からファイルサイズを考慮するのを止め、mini化の中途半端感は否めないです。 このあたりは、次のデモに向けて改良していきたいと考えています。

uniform名は基本的にはShadertoyと一致させているのですが、テクスチャのサンプラーはShadertoyを踏襲せずに、直前のパスを参照する iPrevPass を定義しました。 これによってGLSLを書き換えずにエフェクトの順番を入れ替えたり、気軽にパスを増やしてエフェクトをチェインしやすくしました。 このあたりの仕様も、作品の需要に応じて変更していく可能性は高いです。

ファイル圧縮のためのビルドプロセス

圧縮にはwebpackpnginator.rbを利用しています。

ビルドプロセスを図にしました。

build-process

webpackですべてのファイルをbundle.jsという単一のJavaScriptに固めてから、pnginator.rbで自己解凍形式のPNGにしています。

TypeScriptのminifyは完全にwebpack任せです。

PNGでは画像データをzlib圧縮するため、画像データではなくても、例えば今回のようなプログラムのソースコードでちゃんと圧縮できます。

GLSLのminifyも検証はしていて、webpackのLoaderを開発する予定もあったのですが、容量が余裕だったのでGLSLの圧縮はPNG(zlib)だけになりました。

また、開発用にしか必要ないコードの削除もwebpackのdefine-pluginで実現できました。

webpackとpnginator.rbを組み合わせる手法は、FMS_CatさんUntilを参考にしました。

当初はnode.jsでGLSLのホットリロード機能付きのWebサーバを開発しようと技術検証していたのですが、 要件はwebpack-dev-serverですべて実現可能だったので、webpackを採用しました。

PRごとに圧縮後のファイルサイズを確認するようにしたら、圧縮後のファイルサイズについて知見が貯まりました(例)

  • コードの自動フォーマットをかけると、圧縮効率が上がってファイルサイズが減る
  • コードをコピペすると圧縮効率が高くなるので、実は無理にコードを共通化する意味は実は薄い
  • 似たよな構造になるようにコードを意識すると圧縮効率が良くなる
  • 関数の順番を入れ替えただけで微妙にサイズが減ったりと謎が多い

制作用のエディタ機能の紹介

chromatiq-editor

製作のイテレーションを高速化するため、必要なエディタ機能は一通り実装しました。

  • 再生位置のシーク機能
    • 再生・一時停止・停止・フレームのコマ送り・時間の表示単位の秒とビートの切り替え
  • GLSLやTypeScriptのホットリロード機能
  • uniformのパラメータのインスペクタ
  • カメラの自由移動
  • デバッグ用に特定のパスの表示

エディタ機能は容量制約に影響しないので、既存のライブラリを積極的に利用しています。

リポジトリをcloneして、 npm run start すれば、エディタ機能が使えますので、興味がある人はお試しください。

git clone git@github.com:gam0022/resimulated.git
cd resimulated
npm install

# 制作用のエディタを起動
npm run start

# 提出用のビルド(dist\resimulated.html)を生成
npm run build

uniformのパラメータのインスペクタ

GLSL上で以下のようなuniformを宣言するだけで、そのままインスペクタに表示されるような仕組みを実装しました。

コメントでは左から順に 初期値 min max カテゴリー名 を指定しています。初期値は必須ですが、それ以外は省略可能としました。

uniform float gEmissiveIntensity;     // 6.0 0 20 emissive
uniform float gEmissiveSpeed;         // 1 0 2
uniform float gEmissiveHue;           // 0.33947042613522904 0 1
uniform float gEmissiveHueShiftBeat;  // 0 0 1
uniform float gEmissiveHueShiftZ;     // 0 0 1
uniform float gEmissiveHueShiftXY;    // 0 0 1

uniform宣言をすると、自動的にインスペクタにパラメータが追加されます。

chromatiq-editor-emissive

私の作品では、フラクタルやIFSといったパラメータの細かな調整が重要になる表現を多用しているため、気軽にパラメータを増やして、気軽に値を調整できるようにしました。

値の当たりをつけた後に、パラメータのアニメーションをTypeScriptのコードに落とし込むワークフローにしました。

これは、インスペクタを動かしている様子の動画です。

動画の保存機能

処理落ちなしに4K解像度で動画を出力したかったので、以下の機能を実装しました。

  • 映像の連番PNG保存機能
  • サウンドの wav 保存機能

.png と .wav を ffmpeg で .mp4 に変換してYouTubeにアップロードしました。

ffmpeg.exe -r 60 -i chromatiq%04d.png -i chromatiq.wav -c:v libx264 -preset slow -profile:v high -coder 1 -pix_fmt yuv420p -movflags +faststart -g 30 -bf 2 -c:a aac -b:a 384k -profile:a aac_low -b:v 68M chromatiq_68M.mp4

YouTube用のffmpegのエンコード設定については、以下を参考にしました。

映像について

映像の3D描画は基本的に全部レイマーチングです。

前半のサイバーなシーンはMandelboxをベースにしました。

後半の宇宙空間とグリーティングのシーンでは、宇宙空間はレイマーチング、惑星の上のグリーティングの文字はAABBとして解析的に衝突判定をするハイブリッドなレイトレをしています。

パスの構成は、最終的にこうなりました。

1パス目と2パス目を分離したのは、シェーダーのコンパイル時間の短縮のためです。

  • 1パス目: 前半のシーンのレイマーチング
  • 2パス目: 後半のシーンのレイマーチング
  • 3パス目: テキストの描画
  • 4~13パス目: Bloomのポストエフェクト
  • 14パス目: ポストエフェクトとトーンマッピング

惑星のバリエーション生成の仕組み

後半のグリーティングでは、自分が特に尊敬しているデモグループをイメージした惑星が合計14パターン登場します。

様々なバリエーションの惑星を効率的に生成するための仕組みを実装しました。

  • 地形の高さマップの自動生成
  • テクスチャの色のグラデーションの自動生成

地形の高さマップの自動生成

2DのValue Noiseを重ね合わせたfbm(Fractal Brownian Motion)で地形の高さマップを生成しました。

さらに、fbm関数をネストして(fbmのUV計算にfbmをつかって)、歪んだような不思議な雰囲気の地形も生成できるようにしました。

左がfbmのネストよる歪みなしで、右がfbmのネストによる歪みありです。

fbm-shift

fbmの各種パラメーター(振幅や周波数、Y方向のスケール、歪み用のfbmの強度)は、乱数ではなく、配列で直接指定することで、イメージ通りの結果に調整できるようにしました。

// fbmAmp, fbmFreq, fbmYScale, fbmShift
vec4[PLANETS_PAT_MAX * PLANETS_NUM_MAX] planetFbmParams = vec4[](
    // MIX_A
    vec4(0.3, 17.0, 1.0, 0.01), vec4(0.05, 10.0, 1.05, 0.0), vec4(0.05, 10.0, 1.05, 0.01),
    vec4(0.05, 10.0, 4.05, 0.02), vec4(0.05, 10.0, 2.05, 00.1), vec4(0.0),
    // MIX_B
    vec4(0.0, 10.0, 1.0, 0.2), vec4(0.0, 10.0, 1.0, 0.01), vec4(0.0, 10.0, 1.0, 0.03),
    vec4(0.05, 10.0, 1.0, 00.2), vec4(0.06, 10.0, 1.0, 0.03), vec4(0.05, 10.0, 1.0, 0.03));

このようなfbmをネストしたシンプルな関数で高さマップを生成しました。

// 惑星の高さマップ(height map)を生成する関数
// pは球体のUV, id は惑星のID
float hPlanetsMix(vec2 p, int id) {
    p.y *= planetFbmParams[id].z;
    return fbm(p + 
        planetFbmParams[id].w * fbm(p, 4.0 * planetFbmParams[id].y), planetFbmParams[id].y);
}

テクスチャの色のグラデーションの自動生成

iqのColor Palettesを使いました。

vec3 pal(in float t, in vec3 a, in vec3 b, in vec3 c, in vec3 d) {
    return a + b * cos(TAU * (c * t + d));
}

pal 関数の使い方は簡単で、a, b, c, d を任意に指定すれば、t を変化することでグラデーションを生成できます。

今回は a, b, c は定数、d は惑星ごとに乱数で決定しました。

a, b, c や乱数のseed値はインスペクタで値を調整しながら、イメージ通りのグラデーションが生成されるまで試行錯誤しました。

t は地形の高さマップにマッピングしました。

無数の小惑星のランダムな配置

宇宙空間がスカスカすぎて寂しかったので、無数の小惑星をランダムに配置しようとしたら、予想外に苦戦しました。

レイマーチングだと空間をmodすることで物体を無限に複製することは簡単なのですが、それでは規則的な配置にしかならず、かなり不自然になってしまいます。

gazさんのシェーダーを参考にして、空間をgridに分割して、gridごとに乱数を生成して、乱数で確率的に物体を間引く手法を採用しました。

また、アーティファクトの回避するために、rayの長さを制限する工夫も必要でした。

最終的に、ランダムな位置と大きさをもつ小惑星の距離関数はこうなりました。

float dGomi(vec3 p) {
    // アーティファクト対策のための固定長の距離
    float d = 1.0;

    // グリット(4m四方の立方体)の計算
    vec3 g = vec3(floor(p / 4.0));

    // 座標の繰り返し
    p = mod(p, 4.0) - 2.0;

    // 確率 rate に応じて球体を配置
    vec3 rand = hash33(g);
    float rate = (gPlanetsId != PLANETS_EARTH) ? 0.08 : 0.01;
    if (rand.x < rate) {
        p -= (rand - 0.5);
        d = sdSphere(p, 0.1 * rand.y);
    }

    // fbmで表面の凹凸のディテールを加える
    // レイが接近したときだけに計算するのは、LODによる負荷対策
    // fbmの計算はかなり高負荷なので、LODをしないと激重になる
    if (d < 0.5) {
        vec2 uv = uvSphere(normalize(p));
        uv.x += dot(rand, vec3(1.0));
        d -= remapTo(rand.z, 0.01, 0.08) * fbm(uv, 5.0);
    }

    return d;
}

音楽について

基盤となるGLSLサウンド用のシーケンサーの実装は私が、それ以外のオシレーターの関数やメロディの実装はさだきちさんが担当しました。

音楽もやはり容量制約のためにGLSLで実装する必要があり、さだきちさんにはコーディングによる作曲をお願いしました。 さだきちさんはプログラミングもGLSLも未経験だったので、それらの習得から始まりました。 かなり無茶なお願いだったにも関わらず、かっこいいトランスミュージックを提供してくれたさだきちさんには感謝しかありません。ありがとうございます!

私が担当したGLSLサウンド用のシーケンサーはGLSLサウンド上で実装されており、GLSLサウンドを鳴らす仕組みについては、AMAGIさん(@amagitakayosi)の記事を参考に、Shadertoy互換のGLSLサウンドの再生機能を実装しました。ありがとうございます!

サウンド用のシーケンサーの利用例

これはベースのパートの波形を生成するGLSLの関数です。

vec2 bass1(float beat, float time) {
// 1つのパターンのビート数
#define BASS1_BEAT_LEN 8

// パターンの種類
#define BASS1_DEV_PAT 10

// 楽曲全体の長さのパターン数
#define BASS1_DEV_LEN 32

    // パターンの定義
    int[BASS1_BEAT_LEN * NOTE_DIV * BASS1_DEV_PAT] notes = int[](
        // パターン0
        F(0), F(33), E(0, 33), S(0, 33, 0, 33),
        F(0), F(33), E(0, 33), S(0, 33, 0, 33),

        // パターン1
        E(33, 33), S(0, 33, 33, 33), S(0, 33, 33, 33), S(0, 33, 33, 33),
        E(33, 33), S(0, 33, 33, 33), S(0, 33, 33, 33), S(0, 33, 33, 33),

        // パターン2
        E(33, 33), S(0, 33, 33, 33), S(0, 33, 33, 33), S(0, 33, 33, 33),
        E(29, 29), S(0, 29, 29, 29), S(0, 31, 31, 31), S(48, 47, 43, 40),

        // パターン3
        E(33, 33), S(0, 33, 33, 33), S(0, 33, 33, 33), S(0, 33, 33, 33),
        E(33, 33), S(0, 33, 33, 33), S(0, 33, 33, 33), S(0, 34, 34, 34),

        // パターン4
        E(33, 33), S(0, 33, 33, 33), S(0, 33, 33, 33), S(0, 33, 33, 33),
        E(33, 33), S(0, 33, 33, 33), S(0, 33, 33, 33), S(0, 36, 36, 36),

        // パターン5
        E(33, 33), S(0, 33, 33, 33), S(0, 33, 33, 33), S(0, 33, 33, 33),
        E(33, 33), S(0, 33, 33, 33), S(0, 34, 34, 34), S(0, 36, 36, 36),

        // パターン6
        E(33, 33), S(0, 33, 33, 33), S(0, 33, 33, 33), S(0, 33, 33, 33),
        E(33, 33), S(0, 33, 33, 33), S(0, 43, 43, 43), S(0, 55, 57, 69),

        // パターン7
        E(29, 29), S(0, 29, 29, 29), S(0, 29, 29, 29), S(0, 31, 33, 45),
        E(29, 29), S(0, 29, 29, 29), S(0, 29, 29, 29), S(0, 31, 31, 31),

        // パターン8
        E(33, 33), S(0, 33, 33, 33), S(0, 33, 33, 33), S(0, 33, 33, 33),
        E(33, 33), S(0, 33, 33, 33), S(0, 33, 33, 33), S(0, 43, 45, 57),

        // パターン9
        E(29, 29), S(0, 29, 29, 29), S(0, 29, 29, 29), S(0, 31, 33, 45),
        E(29, 29), S(0, 29, 29, 29), S(0, 31, 31, 31), S(0, 31, 31, 31));

    // パターンの進行
    int[BASS1_DEV_LEN / DEV_PACK] development = int[](
        D(0, 0, 0, 0, 0, 0, 0, 0), D(1, 1, 1, 2, 3, 4, 5, 6),
        D(7, 0, 7, 8, 7, 0, 9, 0), D(0, 0, 0, 0, 0, 0, 0, 0));

    SEQUENCER(beat, time, BASS1_BEAT_LEN, BASS1_DEV_PAT, BASS1_DEV_LEN,
        notes, development, bass)

    return ret;
}

パターン(2小節分のノートナンバーの並び)の定義と進行は、それぞれ配列で指定できるようにしています。

音の長さは下記の4種類に対応しました。 ノートナンバーに0を指定すれば、同じ長さの休符になります。

  • O: 全音符
  • F: 4分音符
  • E: 8分音符
  • S: 16分音符

GLSLのコンスタントバッファのサイズには上限があり、サウンド用のシェーダー全体で要素数が4096個まででしか配列を宣言できません。

そこで、O, F, E, S を関数マクロとし、16分音符を最小単位として各音符を16bit(うち、ノートナンバーが8bit、音の長さが8bit)ずつパッキングしています。 GLSLのintは32bitなので、int配列の1要素に16分音符なら2つ、8分音符なら1つ入るような設計です。

また、パターン進行の D もマクロにしていて、要素数の節約のために4bitずつパッキングをしています。

続いて、bass は時間とノートナンバーを入力として、波形を出力するオシレーターのGLSL関数です。

SEQUENCER は、時間、パターンの定義の配列、パターンの進行の配列、オシレーターの関数を指定することで、パートごとの波形を生成して vec2 ret に代入する関数マクロです。 GLSLでは関数を引数とするような高階関数は実現できませんが、関数マクロで擬似的に実現しました。

#define SEQUENCER(beat, time, beatLen, devPat, devLen, notes, development, toneFunc)  \
    int indexOffset = development[int(                                                \
        mod(beat / float(beatLen * DEV_PACK), float(devLen / DEV_PACK)))];            \
    indexOffset =                                                                     \
        (indexOffset >> (4 * int(mod(beat / float(beatLen), float(DEV_PACK))))) & 15; \
    indexOffset *= beatLen * NOTE_VDIV;                                               \
                                                                                      \
    for (int i = 0; i < beatLen * NOTE_VDIV;) {                                       \
        int index = i + indexOffset;                                                  \
        int shift = (index % 2 == 1) ? 16 : 0;                                        \
        int div = ((notes[index >> 1] >> shift) >> 8) & 255;                          \
        int len = NOTE_VDIV * NOTE_VDIV / div;                                        \
        for (int j = 0; j < len; j++) {                                               \
            tmpIndexes[i + j] = i;                                                    \
        }                                                                             \
        i += len;                                                                     \
    }                                                                                 \
                                                                                      \
    float indexFloat = mod(beat * float(NOTE_VDIV), float(beatLen * NOTE_VDIV));      \
    int index = int(indexFloat);                                                      \
    int shift = (index % 2 == 1) ? 16 : 0;                                            \
    int note = (notes[(index + indexOffset) >> 1] >> shift) & 255;                    \
    float localTime =                                                                 \
        beatToTime((indexFloat - float(tmpIndexes[index])) / float(NOTE_VDIV));       \
    float amp = (note == 0) ? 0.0 : 1.0;                                              \
    vec2 ret = vec2(toneFunc(float(note), localTime) * amp);

パターンの定義・進行のマクロはこちらです。

// 1ビートを最大何分割するか。16分音符に対応するなら4
#define NOTE_VDIV 4

// 1ビートのpackingを考慮した分割数。32bitのintに16bitずつ詰めているので
// 4 / (32 / 16) = 2
#define NOTE_DIV 2

// 展開用の配列のpacking数。32bitのintに4bitずつ詰めているので
// 32 / 4 = 8
#define DEV_PACK 8

#define MAX_BEAT_LEN 8
int[MAX_BEAT_LEN * NOTE_VDIV] tmpIndexes;

#define O(a)                                                                      \
    (a | 1 << 8) | ((a | 1 << 8) << 16), (a | 1 << 8) | ((a | 1 << 8) << 16),     \
        (a | 1 << 8) | ((a | 1 << 8) << 16), (a | 1 << 8) | ((a | 1 << 8) << 16), \
        (a | 1 << 8) | ((a | 1 << 8) << 16), (a | 1 << 8) | ((a | 1 << 8) << 16), \
        (a | 1 << 8) | ((a | 1 << 8) << 16), (a | 1 << 8) | ((a | 1 << 8) << 16)
#define F(a) (a | 4 << 8) | ((a | 4 << 8) << 16), (a | 4 << 8) | ((a | 4 << 8) << 16)
#define E(a, b) (a | 8 << 8) | ((a | 8 << 8) << 16), (b | 8 << 8) | ((b | 8 << 8) << 16)
#define S(a, b, c, d) \
    (a | 16 << 8) | ((b | 16 << 8) << 16), (c | 16 << 8) | ((d | 16 << 8) << 16)
#define D(a, b, c, d, e, f, g, h) \
    (a) | (b << 4) | (c << 8) | (d << 12) | (e << 16) | (f << 20) | (g << 24) | (h << 28)

『RE: SIMULATED』の意味

タイトルの『RE: SIMULATED』には2つの意味を込めました。

  1. 前作『WORMHOLE』の64K Introとしての「再現」
  2. SIMULATED REALITY

1. 前作『WORMHOLE』の64K Introとしての「再現」

一昨年のTokyo Demo Fest 2018のCombined Demo Compoでも、さだきちさんとチームを組んで『WORMHOLE』という作品を制作しました(記事)。

前半のシーンが顕著ですが、『WORMHOLE』と表現や演出が酷似していると思います。

  • フラクタルの形状変化
  • 光の色の変化
  • シーン転換前の激しい点滅
  • シーン転換後のホワイトイン
  • パーティのロゴの登場

『WORMHOLE』はUnityで制作したので、60.7 MB(zip圧縮で 23.18MB)というファイルサイズでした。

前作では、Unityを利用したことで賛否両論があったので、ツールに頼らなくても同様のビジュアルを再現できることを証明する意図がありました。

また、64K Introなどの容量制限のある部門への参加が個人的にも憧れだったという理由もあります。

今回は自作のシステムで作品を制作することでファイルサイズは26KBになりました。

同じ表現を「再現」しつつも、容量を 12334 まで圧縮する試みのコンセプトは達成できました。

まさか、コンポで優勝するという結果まで「再現」してしまうのは予想外でした(笑)

2. SIMULATED REALITY

Simulated Realityという裏設定もありました。

作品の最後に「RE: SIMULATED」の文字が

RE: SIMULATEDREREALITY

と変化して、REALITYに変化するタイミングで「地球」がフラッシュバックするのは、Simulated Realityの暗喩です。

前半のサイバーなシーンは電子的な仮想空間という設定で、シーン転換時に球体を中心に空間が歪んで圧縮するのは、宇宙誕生の爆発であるビッグバンの暗喩です。

この世界は上位存在によって電子的にシミュレーションされた仮想現実で、最後に自分たちが住む地球を見つけるというストーリーでした(あくまで裏設定だったので、見た人に通じなくても良い)。

できれば現実と見分けがつかないようなリアルなグラフィックで表現できたら良かったのですが、力量不足でした……。

おわりに

Webフロントエンドは久しぶりで、node.jsとwebpackは初めてだったので、新しい技術を学ぶ良い機会となりました。

昔はjQueryが必要だったDOMのセレクターやHTTPアクセスが、標準のAPI(querySelectorFetch)になっていて驚きました。

TypeScript(ECMascript)に苦手意識がありましたが、最近はかなり使いやすい言語になったなぁと認識を改めました。 演算子オーバーロードがないのだけは、3Dプログラミングには必須のベクトル計算の実装の可読性が落ちて苦しい気持ちになったので、早くサポートして欲しいと感じました。

また、64K Introのエントリーは今回が初めてということで、どのくらいのコンテンツが詰め込めるか感覚がつかめず、容量を半分以上も余らせてしまいました。 次の機会には64KBギリギリまで使って、もっと映像としても洗練させて、さらにCoolな作品を発表したいです。

例年のRevisionの64K Introの作品と比較すると、かなり未熟なので、もっと精進して最高のデモを作りたいという気持ちです。

ともあれ、このたびは優勝作品に選んでいただき、とても光栄に思います。

世界中の尊敬するデモチームの方々からいただいたお祝いのコメントも嬉しかったです。わーい!

最後に、世界的に大変な状況の中、オンラインでの開催のためにご尽力いただいた皆様に、心より感謝申し上げます。 とても楽しく充実した3日間を過ごせました。来年はドイツでお会いしましょう!

comments powered by Disqus

gam0022.net's Tag Cloud

gam0022.net

Qiita