C#でWebGLコンテンツのJSと画像を１ファイルにパックしてHTMLで１タグで表示する

#はじめに
何番煎じか知りませんが、WebGLでFlashみたいな使いかたができないものかとやってみました。
ファイルが分散するということがWebGLとFlashの大きな違いであると感じたことがあったからです。

具体的な用途はやはり広告でしょうか。
ブログパーツのような使い方もできそうです。
いわゆるWebGL出力の一種かもしれません。


[動くデモのページ](https://warotarokz.sakura.ne.jp/experimental/webgl-packing/)

#仕組みの概要
結論からいうと、次のようなスクリプトを生成し、

```ts:webgl_content_packed.js
(function (win, doc) {
    var images = {};
	images['tex.png'] = new Image();
	images['tex.png'].src = 'data:image/png;base64,iVBORw0KGgoA ..... ';
	
    doc.write('<canvas style=\"width: 100%;height: 100%;\"></canvas>');
    var canvas_list = doc.getElementsByTagName('canvas');
    var canvas = canvas_list[canvas_list.length - 1];

    var script_text = win.atob('KG5ldyBmdW5jdGlvbiAoKSB7DQoNCiA ..... ');
    var js = eval(script_text);
    var env = {
        'window': win,
        'document': doc,
        'canvas': canvas,
        'script': js,
        'resources': {
        'images': images
        }
    };
    js.initialize(env);
})(window, document, null);
```

次のようにHTMLに組み込みます。

```html:index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Packing test</title>
    <style>
        div#container {
            width: 480px;
            height: 480px;
        }
    </style>
</head>
<body>
    <div id="container">
        <script src="webgl_content_packed.js"></script>
    </div>
</body>
</html>
```

以下の特徴があると思います。
WebGLと関係なかったりしますが筆者はWebGL界隈の人なので…

* canvasタグを生成しているのでscriptタグ１つで表示できる
* 画像データをData URI schemeで読み込んでいるので画像が盗まれない（そんなことはない
* スクリプトはData URI schemeで読み込んでいるのでソースコードが見られない（そんなことはない



#C#によるパッキング
inputImageFileNames で指定した画像ファイルをbase64の文字列に変換します。
また、inputScriptFileNames で指定したスクリプトを文字列で繋げたものをbase64の文字列に変換します。
debugModeでスクリプトをbase64にしないで出力することができます（デバッグ用）。


```csharp:Program.cs
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;

namespace WebGLPacking
{
    class Program
    {
        static void Main(string[] args)
        {
            // 入出力ファイル設定
            string inputDirectory = @"../../../src";

            var inputImageFileNames = new List<string>()
            {
                @"tex.png",
            };

            var inputScriptFileNames = new List<string>()
            {
                @"packing_head.js",
                @"gl-matrix-min.js",
                @"main.js",
                @"packing_tail.js",
            };

            string outputDirectory = @"../../../";
            string outputFile = @"webgl_content_packed.js";

            bool debugMode = false;

            // 外側のスクリプトのテンプレート
            string templateScript1 = @"
(function (win, doc) {
    var images = {};
";

            string templateScript2 = @"

    doc.write('<canvas style=\""width: 100%;height: 100%;\""></canvas>');
    var canvas_list = doc.getElementsByTagName('canvas');
    var canvas = canvas_list[canvas_list.length - 1];
";
            
            string templateScript3;
            string templateScript4;
            if (debugMode)
            {
                templateScript3 = @"
    var js = ";

                templateScript4 = @";";
            }
            else
            {
                templateScript3 = @"
    var script_text = win.atob('";

                templateScript4 = @"');
    var js = eval(script_text);
";
            }

            string templateScript5 = @"
    var env = {
        'window': win,
        'document': doc,
        'canvas': canvas,
        'script': js,
        'resources': {
        'images': images
        }
    };
    js.initialize(env);
})(window, document, null);
";
            // 画像データ部分のスクリプト生成
            var resultSB = new StringBuilder();
            resultSB.Append(templateScript1);

            foreach (string fileName in inputImageFileNames)
            {
                string imageName = Path.GetFileName(fileName);

                byte[] imageFileBytes = File.ReadAllBytes(Path.Combine(inputDirectory, fileName));
                string imageFileBase64Text = Convert.ToBase64String(imageFileBytes);

                resultSB.Append("\r\n");
                resultSB.Append(string.Format("\timages['{0}'] = new Image();", imageName));
                resultSB.Append("\r\n");
                resultSB.Append(string.Format("\timages['{0}'].src = 'data:image/png;base64,{1}';"
                                                , imageName, imageFileBase64Text));
            }

            resultSB.Append(templateScript2);

            resultSB.Append(templateScript3);

            // スクリプトデータ部分のスクリプト生成
            var scriptSB = new StringBuilder();
            foreach (string fileName in inputScriptFileNames)
            {
                string scriptText = File.ReadAllText(Path.Combine(inputDirectory, fileName));
                scriptSB.Append(scriptText);
                scriptSB.Append("\r\n");
            }

            if (debugMode)
            {
                resultSB.Append(scriptSB.ToString());
            }
            else
            {
                byte[] scriptTextBytes = System.Text.Encoding.UTF8.GetBytes(scriptSB.ToString());
                string scriptBase64Text = Convert.ToBase64String(scriptTextBytes);
                resultSB.Append(scriptBase64Text);
            }

            resultSB.Append(templateScript4);

            // 最後の部分の生成
            resultSB.Append(templateScript5);

            // ファイル出力
            File.WriteAllText(Path.Combine(outputDirectory, outputFile), resultSB.ToString());
        }
    }
}
```

サンプルとして、変換対象のファイルも以下に示します。
WebGLのため長々と書いてありますが、必要なのはこれらのファイルが全て連結することで initialize(env) という関数を持つオブジェクトを生成するようになっていることです。1ファイルにするため、全ての機能をそのオブジェクトの中に入れる必要があります。

```js:packing_head.js
(new function () {
```


```js:gl-matrix-min.js
ライブラリのため省略
```

```js:main.js

    this.onload = function () {

        var screenWidth = _env.canvas.offsetWidth;
        var screenHeight = _env.canvas.offsetHeight;
        _env.canvas.width = screenWidth;
        _env.canvas.height = screenHeight;

        var gl = _env.canvas.getContext('experimental-webgl');

        // initialize model buffers
        var positionData = [
            -1.0, 1.0, 0.0,
            1.0, 1.0, 0.0,
            -1.0, -1.0, 0.0,
            1.0, -1.0, 0.0
        ];
        var textureCoordData = [
            0.0, 0.0,
            1.0, 0.0,
            0.0, 1.0,
            1.0, 1.0
        ];
        var indexData = [
            0, 1, 2,
            3, 2, 1
        ];

        var positionBuffer     = createVertexBuffer(positionData, gl);
        var textureCoordBuffer = createVertexBuffer(textureCoordData, gl);
        var indexBuffer        = createIndexBuffer(indexData, gl);

        // initialize texture
        var texture = createTexture(_env.resources.images["tex.png"], gl);

        // initialize the shader
        var vertexShaderSrc = "\
            attribute vec3  aPosition;\
            attribute vec2  aTextureCoord;\
            uniform   mat4  uMvpMatrix;\
            uniform   float uZoom;\
            varying   vec2  vTextureCoord;\
            void main(void){\
                gl_Position = uMvpMatrix * vec4(aPosition, 1.0);\
                gl_Position.xy *= uZoom;\
                vTextureCoord = aTextureCoord;\
            }\
        ";

        var fragmentShaderSrc = "\
            precision mediump float;\
            uniform sampler2D uTexture;\
            uniform float     uAlpha;\
            varying vec2      vTextureCoord;\
            void main(void){\
                gl_FragColor = texture2D(uTexture, vTextureCoord);\
                gl_FragColor.a = gl_FragColor.a * uAlpha;\
            }\
        ";

        var vertexShader = createShader(vertexShaderSrc, gl.VERTEX_SHADER, gl);
        var fragmentShader = createShader(fragmentShaderSrc, gl.FRAGMENT_SHADER, gl);
        var program = createProgram(vertexShader, fragmentShader, gl);

        var aPosition = setFloatVertexAttribute(program, 'aPosition', positionBuffer, 3, gl);
        var aTextureCoord = setFloatVertexAttribute(program, 'aTextureCoord', textureCoordBuffer, 2, gl);

        var uMvpMatrix = gl.getUniformLocation(program, 'uMvpMatrix');
        var uTexture = gl.getUniformLocation(program, 'uTexture');
        var uZoom = gl.getUniformLocation(program, 'uZoom');
        var uAlpha = gl.getUniformLocation(program, 'uAlpha');

        // main loop
        var eyeLocation = vec3.create();
        var lookatLocation = vec3.create();
        var upVector = vec3.create();
        var modelLocation = vec3.create();
        var modelMatrix = mat4.create();
        var viewMatrix = mat4.create();
        var projMatrix = mat4.create();
        var mvpMatrix = mat4.create();
        var tempVec3 = vec3.create();

        var start_time = new Date().getTime();

        (function () {

            var local_time = (new Date().getTime() - start_time) * 0.001;

            vec3.set(eyeLocation, 0.0, -6.0, 4.0);
            vec3.set(lookatLocation, 0.0, 0.0, 0.0);
            vec3.set(upVector, 0.0, 0.0, 1.0);
            mat4.perspective(projMatrix, 45.0 * Math.PI / 180, screenWidth / screenHeight, 0.1, 100.0);
            mat4.lookAt(viewMatrix, eyeLocation, lookatLocation, upVector);
            mat4.multiply(projMatrix, projMatrix, viewMatrix);

            vec3.set(modelLocation, 0.0, 0.0, 0.0);
            mat4.identity(modelMatrix);
            mat4.translate(modelMatrix, modelMatrix, modelLocation);
            mat4.rotateZ(modelMatrix, modelMatrix, local_time);
            mat4.scale(modelMatrix, modelMatrix, vec3.set(tempVec3, 2.0, 2.0, 2.0));

            mat4.multiply(mvpMatrix, projMatrix, modelMatrix);

            gl.clearColor(0.0, 0.0, 1.0, 1.0);
            gl.clearDepth(1.0);
            gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

            gl.activeTexture(gl.TEXTURE0);
            gl.bindTexture(gl.TEXTURE_2D, texture);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);

            gl.disable(gl.DEPTH_TEST);
            gl.disable(gl.CULL_FACE);
            gl.enable(gl.BLEND);
            gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);

            gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);

            gl.useProgram(program);

            gl.uniformMatrix4fv(uMvpMatrix, false, mvpMatrix);
            gl.uniform1i(uTexture, 0);
            gl.uniform1f(uAlpha, 1.0);
            gl.uniform1f(uZoom, 1.5);

            gl.drawElements(gl.TRIANGLES, indexData.length, gl.UNSIGNED_SHORT, 0);

            setTimeout(arguments.callee, 1000 / 30);
        })();
    };

    var createVertexBuffer = function (data, gl) {

        var buffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);
        gl.bindBuffer(gl.ARRAY_BUFFER, null);

        return buffer;
    };

    var createIndexBuffer = function (data, gl) {

        var buffer = gl.createBuffer();
        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffer);
        gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Int16Array(data), gl.STATIC_DRAW);
        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);

        return buffer;
    };

    var createTexture = function (image, gl) {

        var textrue = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, textrue);
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
        gl.generateMipmap(gl.TEXTURE_2D);
        gl.bindTexture(gl.TEXTURE_2D, null);

        return textrue;
    };

    var createShader = function (sourceCodeText, shaderType, gl) {

        var shader = gl.createShader(shaderType);
        gl.shaderSource(shader, sourceCodeText);
        gl.compileShader(shader);

        if (gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
            return shader;
        } else {
            alert(gl.getShaderInfoLog(shader));
            return null;
        }
    };

    var createProgram = function (vs, fs, gl) {

        var program = gl.createProgram();
        gl.attachShader(program, vs);
        gl.attachShader(program, fs);
        gl.linkProgram(program);

        if (gl.getProgramParameter(program, gl.LINK_STATUS)) {
            return program;
        } else {
            alert(gl.getProgramInfoLog(program));
            return null;
        }
    };

    var setFloatVertexAttribute = function (program, attribName, buffer, elementCount, gl) {

        var attribute = gl.getAttribLocation(program, attribName);
        gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
        gl.enableVertexAttribArray(attribute);
        gl.vertexAttribPointer(attribute, elementCount, gl.FLOAT, false, 4 * elementCount, 0);

        return attribute;
    };

```

```js:packing_tail.js

    var _env;

    this.initialize = function (env) {
        _env = env;
        _env.document.addEventListener('DOMContentLoaded', this.onload, false);
    }
})
```

使用テクスチャです。この下に逃げ出したくなるような何かがあるわけではありません。
![tex.png](https://qiita-image-store.s3.amazonaws.com/0/59191/98629d35-8dfc-36a5-4e27-303d879717ac.png)

#おわりに
もっと良いやり方はあると思いますし、問題も色々あると思います。
ファイルが大きくなるとページの表示に時間がかかるようになる問題もあります。
スクリプトで遅延読み込みすればいいかもしれませんが、それだと１ファイルにした意味がないですし。
ブラウザ側でそういうことをやってくれればいいんですが…。

しかし、ともかく１ファイルになることで配布しやすくなるなどの利点もあると思います。
あるかなぁ。
筆者の気持ちとしては、本当にFlashのような使い方ができるようになって、WebGLの用途が広がればいいなという気持ちでおります。
WebGL界隈の人なので。