【AWS Lambdaと】とにかく動くWebGL【コラッツ予想】

# ごあいさつ

皆さん、こんにちは。メリークリスマス。
viviON開発部の若獅子こと minister_fuji です。

そう。去年の弊社アドヴェントカレンダーに燦然とLispを引っ提げてきて、社内をざわつかせたアイツが帰ってきたのです。

https://qiita.com/minister_fuji/items/97a57379ee7c3d31d994

今回は「コラッツ予想」と「AWS Lambda」を使って、「WebGL」の魅力をお伝えしに来ました。

是非ともお付き合いくださいませ。

# まずはPR

:::note info
viviONグループでは、DLsiteやcomipoなど、
二次元コンテンツを世の中に届けるためのサービスを運営しています。
ともに働く仲間を募集していますので、興味のある方は[こちら](https://vivion.jp/recruit/)まで。
:::

フロントエンド・バックエンド・SREなど幅広く募集中です！ :raised_hand: 
カジュアル面談等で、「Lisp/WebGLのアイツと話したい」とお申し付けいただければ、
**私が出てきます！**

# 今回やること・学ぶこと
- URLにアクセスすると
    - 指定した数字に対してコラッツ予想の計算作業が行われ
    - 様子がWebGLでリアルタイムに描画される

という代物を作ります！
ここから、

- Lambdaによるスタティックコンテンツ返却というテクニック
- WebGLの最初の一歩

を一緒に学ぶことをゴールとしていきます。

# WebGLとはなにか
JavascriptのAPIであり、ブラウザでグラフィックを描画できる技術を指します。
プラグインを入れることなく、ブラウザ種類を問わずに描画することが可能となっています。
RPGツクールなどのゲーム制作シーンにも利用されている技術です！

# GLSLとはなにか
WebGLと類似していますが、こちらはOpneGLの規格でのシェーディング言語 となっており、
記述により、3Dグラフィック描画処理をGPUを操作しながら記述することが可能となっています

余談ですが、DJのグラフィックバージョンとして、V(Visual)Jという存在がおりまして、
GLSLライブコーディングでVJしてクラブシーンを盛り上げるぜ！みたいなことも可能ではあります。
(かなり熟練度がないと厳しい)

ある程度掘り下げていくと数学的知識を要求されるため、使いこなせれば結構ニッチマンになれるのではないかと想定されます（需要が多いかは別）

**・・・そう、Lispのようにねッ！！！**

# コラッツ予想とはなにか
[数学の未解決問題](https://ja.wikipedia.org/wiki/%E3%82%B3%E3%83%A9%E3%83%83%E3%83%84%E3%81%AE%E5%95%8F%E9%A1%8C)のひとつで、
「すべての自然数は、偶数時に2で割り、奇数時に3倍して1加える作業を繰り返すと必ず1になる。はず。」
という「予想」となります。

最近書籍化された「笑わない数学2」という本でも取り上げられました。
今回WebGLの記事を書こうと思っていて、ちょうど見かけたため「今ちょっぴりHOTかもしれない」と思い、採用しました！

問題の難易度としては未解決なだけあって高いですが、内容がシンプルでとっかかりやすい点が、今回のテーマと相性がよさそうでした！

実際、フィットしていたと思います。

# なぜLambdaなのか
実はWebGLに限って言うと、Lambdaと組ませる必要性は高くありません。
HTMLファイルのなかにJavascriptを書いて、ローカルのブラウザで動作させることも可能だからです。

ただ、Lambdaでスタティックコンテンツとして返すことで、S3連携やCloudwatchEventsとも連携ができ、より動的な要素を追加・拡張しやすくなるため、遊びの幅が広がる、という点が大きいと思います。

**個人開発にはLambdaがおすすめ！！**

- 筆者が布教したいLambdaの便利さ・手軽さ
    - 関数URLを振ればすぐに外部共有できる
        - リクエストを受けたときの内容を解析してアクセスを絞ったりできる
    - レスポンスが自由だからスタティックコンテンツも返せるし、JSONも返せる
        - Github Pagesと同じような用途でも活用可能
    - 多少実行するくらいならほぼ無料
        - 実行時間が少ない「チョコっと開発」を一瞬でリリースすることに向いている
    - 社内案件でも、小さい自動化やちょっとしたものだと、筆者「Lambdaだけでなんとかならないかな？」言いがち

Lambdaへの愛も深まったところで、そろそろやっていきましょう。

Let's GO～～～

# 前提条件
- 動かす側のPCのグラボがそれっぽいこと
    - 今回のコード程度ならおそらく問題ないですが、拡張的な開発を重ねたとき、スペックによっては描画がカクつくかもしれません
- AWSアカウントがあること・AWS Lambdaの最低限の初期知識があること
    - Lambdaを動作させるため(Lambdaの解説は巷に譲るものとし、割愛です)
 
# 作業開始
- Lambdaの作成
    - AWS マネジメントコンソールから作成します
- コード(後述)を書く
    - 「JS(WebGL)を包含したスタティックなHTMLファイルを返却する」という処理を記述します
    - 文字数制限さえクリアできていればALBのスタティック返却でも可能ですが、料金が・・・
- 関数URLを設定する
    - Lambdaにブラウザからアクセスできるようにします
- 表示されたURLにブラウザからアクセスする
    - 返却されたスタティックなHTMLが動作します
- 完了

::: note warn
関数URLの設定について、軽く試す分には「認証:NONE」で問題ありませんが、
全世界に解放されるため、ご注意ください。
厳しくする場合は、「IAM認証」にするか、ALBと接続してセキュリティグループを絞ればOKです。
:::

# Code 

```typescript

export const handler = async (event) => {
    const htmlContent = `

    <!DOCTYPE html>
    <html lang="en">

    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Collatz Conjecture Visualization</title>
        <style>
            canvas { width: 100%; height: 100%; }
        </style>
    </head>

    <body>

        <style>
        * {
            background-color: black;
            color: silver;
        }
        </style>
        <pre></pre>
        <input type="number" id="numberInput" placeholder="Enter a number" min="1"/>
        <button onclick="visualize()">Visualize</button>
        <canvas id="glCanvas"></canvas>
        <pre id="now" style="text-align: center;"></pre>
        <div id="stepslist" style="width:1024px;line-break: anywhere;"></div>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/gl-matrix/2.8.1/gl-matrix-min.js"></script>
        <script>
        document.getElementById('numberInput').value = 1;

        function collatz(n) {
            const steps = [];
            while (n !== 1) {
                steps.push(n);
                if (n % 2 === 0) {
                    n = n / 2;
                } else {
                    n = 3 * n + 1;
                }
            }
            steps.push(1);
            return steps;
          }

            function visualize() {
                const number = document.getElementById('numberInput').value;
                let count = number;

                const interval = setInterval(function() {
                    steps = collatz(count);
                    drawGraph(steps);
                    document.getElementById('now').textContent  = count;
                    document.getElementById('stepslist').textContent  = steps;
                    count++;
                    if (count === 301) {
                        clearInterval(interval);
                    }
                }, 100);
            }

            function drawGraph(steps) {
                console.log(steps);
                const canvas = document.getElementById('glCanvas');
                const gl = canvas.getContext('webgl');

                if (!gl) {
                    console.error('WebGL not supported');
                    return;
                }

                // シェーダープログラムの作成
                const vertexShaderSource = \`
                attribute vec2 a_position;
                void main() {
                    gl_Position = vec4(a_position, 0, 1);
                }
                \`;

                // 順番に赤、緑、青、アルファ (1, 0, 0, 1); // 赤色
                const colorText = ''+ Math.random() + ',' + Math.random()+',' + Math.random()+','+Math.random() + '';
                const fragmentShaderSource = \`
                precision mediump float;
                void main() {
                    gl_FragColor = vec4(\`+colorText+\`); 
                }
                \`;

                const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
                const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
                const program = createProgram(gl, vertexShader, fragmentShader);
    
                // シェーダープログラムを使用
                gl.useProgram(program);
    
                // 頂点データの作成
                const vertices = steps.map((step, index) => [index / steps.length * 2 - 1, step / Math.max(...steps) * 2 - 1]).flat();
                const vertexBuffer = gl.createBuffer();
                gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
                gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
    
                // 頂点属性の設定
                const positionLocation = gl.getAttribLocation(program, 'a_position');
                gl.enableVertexAttribArray(positionLocation);
                gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
    
                // 描画
                gl.clear(gl.COLOR_BUFFER_BIT);
                gl.drawArrays(gl.LINE_STRIP, 0, steps.length);
            }
    
            function createShader(gl, type, source) {
                const shader = gl.createShader(type);
                gl.shaderSource(shader, source);
                gl.compileShader(shader);
                if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
                    console.error('Error compiling shader:', gl.getShaderInfoLog(shader));
                    gl.deleteShader(shader);
                    return null;
                }
                return shader;
            }
    
            function createProgram(gl, vertexShader, fragmentShader) {
                const program = gl.createProgram();
                gl.attachShader(program, vertexShader);
                gl.attachShader(program, fragmentShader);
                gl.linkProgram(program);
                if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
                    console.error('Error linking program:', gl.getProgramInfoLog(program));
                    gl.deleteProgram(program);
                    return null;
                }
                return program;
            }

        </script>
    </body>
    </html>
    `;

    return {
        statusCode: 200,
        headers: {
            'Content-Type': 'text/html',
        },
        body: htmlContent,
    };
};

```

# ポイント
- 大きなHTMLファイルを文字列として返すよ、という関数
- HTMLファイルの中でscriptタグからJavascriptを実装している
- コラッツ予想は1つの関数で表現、対象の整数を受け取り、作業結果の配列を返します
- ループ化することで連続描画を実現
- 描画部分では最初にバーテックスシェーダ、フラグメントシェーダをプログラムとしてセットアップ
- セットアップ後、頂点データと頂点属性をglオブジェクトに渡して、描画を実行
- スピード重視のためスタイルの記述などはラフなままです

# 結果
GIFにしてみました。

![Animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616811/787b1686-7735-c718-404f-aae5502c9811.gif)

見てわかる通り、1から順番にコラッツ予想での作業を適用していくと、どこかのタイミングで1を迎えます。
ただ、そのプロセスが不規則だということがよくわかると思います。
すぐ終わるパターンもあれば、長くかかるものもあり、作業中の数字のピークも現れるタイミングが異なっている点についても、視覚的に理解できることがお分かりになったかと思います。

どうです？やってみたくなりましたか？！？！

# 拡張
- Lambdaにこだわりたかったので無理くりLambdaでやっていますが、上述のコードを編集してHTML部分だけ抜き出せば、ローカル環境でも動作させることが可能
    - その際はシェーダー作成時のエスケープに注意すること
- コラッツ予想を返している部分の計算式を変えれば異なる描画をさせることが可能
    - コラッツ予想はINが1個、OUTがn個の整数である関数なので、同様のIOを持つ関数であれば、他の部分を操作せず改修できる
- 細かいところでは描画の間隔秒数もいじれたり、描画カラーをランダムから規則だった色や固定にすることもできる
- また、今回は深く触れておりませんが、描画部分を強化することで、3Dにしたりも可能
    - GLSLの応用的な記述を組み込むなど

これを機に、小さいところからいろいろ遊んでみて欲しいと思います。
WebGL/GLSL をいじる楽しさがわかってもらえると幸いです。

極めたくなってきたら、下記ページなどを参考に、遥かなる一歩を踏み出しましょう！！！

# 参考
- WebGLの教科書的なサイト
    - https://wgld.org/sitemap.html
    - https://webglfundamentals.org/webgl/lessons/ja/

- 試せるサイト
    - https://glslsandbox.com/

# さいごに
※弊社の開発業務ではWebGLを・・・

使用していません。
(筆者の観測範囲では)

あらかじめ、ご了承くださいませ。

以上、minister_fujiでした！
よいお年をッ！！！！！