TypeScriptで記述できるWebGL環境をwebpackで構築する

グラフィックスの勉強をちゃんとしたいとふと思い(思い立つが手が動かせてなかった)、興味があったWebGLを触ろうと思い、この環境をwebpackで整えました。

自分がウェブを仕事でやっているわけでなくて詳しくないので、ツッコミなどは大歓迎です。@yucchiy_ までご連絡いただけると幸いです。

やりたいこと

  • TypeScriptベースで開発したい
  • モデルやテクスチャのロード対応
  • シェーダーについては別シェーダーのインポート・エクスポートもできるようにしたい

グラフィックス系の実装なので比較的記述するコードが複雑でかつ量も多くなりそうなのと、WebGLのAPIを叩くときの引数や戻り値のヒントとして、ある程度型がついていて、IDEで確認できると良いな、ということでTypeScriptを選択しました。

必要なモジュールのインストール

下記モジュールをインストールします。

$ npm install --save-dev webpack webpack-cli  webpack-dev-server \
    typescript ts-loader \
    html-webpack-plugin \
    raw-loader glslify-loader

主に下記用途です。

  • webpackwebpack-cliwebpack-dev-server
    • webpack本体やwebpackでのローカルサーバー構築。
  • typescriptts-loader
    • TypeScriptでアプリを開発するため。
  • html-webpack-plugin
    • webpackでHTMLの生成も行う。複数HTMLも生成できる。
  • raw-loaderglslify-loader
    • テクスチャやモデル、シェーダー読み込みと、シェーダーのモジュール化のため。

webpackの設定

そこまで複雑ではないので、まず全文記載します。

var path = require('path');
var HtmlWebpackPlugin = require('html-webpack-plugin');

var debug = process.env.NODE_ENV !== 'production';

module.exports = {
    entry: path.resolve(__dirname, 'src/entrypoint.ts'),
    plugins: [
        new HtmlWebpackPlugin({
            templateParameters: {
                title: 'MiniEngein',
            },
            template: path.resolve(__dirname, 'src/index.html'),
            inject: 'body',
        })
    ],
    output: {
        path: path.resolve(__dirname, 'public'),
        filename: 'bundle.js',
    },
    resolve: {
        extensions: ['.tsx', '.ts', '.js']
    },
    module: {
        rules: [
            {
                test: /\.(frag|vert|glsl)$/,
                exclude: '/node_modules/',
                use: [
                    'raw-loader',
                    'glslify-loader'
                ]
            },
            {
                test: /\.(tsx|ts)$/,
                exclude: /node_modules/,
                use: [
                    'ts-loader'
                ]
            }
        ]
    },
    mode: debug ? 'development' : 'production',
    devtool: debug ? 'eval-source-map' : 'source-map',
};

yucchiy/MiniEngineでも確認できます

上記の設定について解説していきます。

ディレクトリ構成とHTML

今回用意した環境は、大まかに下記のディレクトリ構成となります。

.
├── package-lock.json
├── package.json
├── src
│   ├── app
│   │   ├── glsl
│   │   └── ts
│   ├── entrypoint.ts
│   └── index.html
├── tsconfig.json
└── webpack.config.js
  • src/entrypoint.ts がエントリーポイントとなるTypeScipt
    • src/index.html が上記エントリーポイントを読み込むHTMLファイル
  • src/app 以下に実際のアプリを開発する
    • src/app/glsl にシェーダーファイルを配置する
    • src/app/ts 以下にTypeScriptを配置する

上記の設定を、 webpack.config.js 内で下記のように設定しています。

module.exports = {
    entry: path.resolve(__dirname, 'src/entrypoint.ts'),
    plugins: [
        new HtmlWebpackPlugin({
            templateParameters: {
                title: 'MiniEngein',
            },
            template: path.resolve(__dirname, 'src/index.html'),
            inject: 'body',
        })
    ],

TypeScriptのロード

開発にTypeScriptを利用したいため、 ts-loader を設定します。

    resolve: {
        extensions: ['.tsx', '.ts', '.js']
    },
    module: {
        rules: [
            {
                test: /\.(tsx|ts)$/,
                exclude: /node_modules/,
                use: [
                    'ts-loader'
                ]
            }

この設定を行うことで、TypeScriptを配置してロードすると読み込み時に自動でJavaScriptにトランスパイルして利用されます。

TypeScriptの設定である tsconfig.json は下記のようにしました。

{
  "compilerOptions": {
    "target": "ES2015",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "$schema": "https://json.schemastore.org/tsconfig",
}

シェーダーのロード

シェーダーもテクスチャと同様に、 raw-loader を利用することで、TypeScriptコード中に import を用いてロードできるようにしています。ただしシェーダーの場合、シェーダー中に別シェーダーの関数を呼び出したいので、さらに glslify-loader を通しています。

下記を webpack.config.js に記載します。

    module: {
        rules: [
            {
                test: /\.(frag|vert|glsl)$/,
                exclude: '/node_modules/',
                use: [
                    'raw-loader',
                    'glslify-loader'
                ]
            },

シェーダーコードは、インポート時に string として利用したいので、 src/app/glsl 以下に glsl.d.ts を配置して、下記を記述します。

declare module '*.vert' {
    const src: string;
    export default src;
}

declare module '*.frag' {
    const src: string;
    export default src;
}

declare module '*.glsl' {
    const src: string;
    export default src;
}

これで、 vertfragglsl 拡張子のシェーダーファイルをインポートで読み込む際は、下記コードのように string としてインポートされます。

// "../glsl/HelloTriangleScene/vertex.glsl"と
// "../glsl/HelloTriangleScene/fragment.glsl"をロード
import VertexShaderSource from "../glsl/HelloTriangleScene/vertex.glsl";
import FragmentShaderSource from "../glsl/HelloTriangleScene/fragment.glsl";

export class HelloTriangleScene extends BaseScene {
    constructor(ctx: GameContext) {
        super();
        // VertexShaderSourceおよびFragmentShaderSourceに
        // stringとしてシェーダーコードが格納されている
        var vertexShader = ctx.createVertexShader(VertexShaderSource);
        var fragmentShader = ctx.createVertexShader(fragmentShaderSource);

また、 glslify-loader を用いることで、下記のようにシェーダーコード内での別シェーダーファイルの読み込みを行えます。

// my-function.glsl
float myFunction(vec3 normal) {
  return dot(vec3(0, 1, 0), normal);
}

// myFunctionをエクスポートして別ファイルから利用できるようにする
#pragma glslify: export(myFunction)


// 同フォルダ内のglslファイル
// my-function.glslのmyFunctionをインポートして利用する
#pragma glslify: topDot = require(./my-function.glsl)

topDot(vec3(0, 1, 0)); // 1

ローカルの開発サーバーの立ち上げ

webpack-dev-server でローカルの開発サーバーを立ち上げて開発します。また、サーバーを立ち上げている最中にコードを書き換えた場合は、自動でリロードが走るようにします。

package.jsonscripts 項目に下記を追記します。

"scripts": {
    "develop": "webpack-dev-server --progress --hot"

実際にサーバーを立ち上げるには、下記コマンドを実行します。

# `webpack-dev-server --progress --hot` が呼び出される
$ npm run develop

特に設定をしていない場合は、 http://localhost:8080 で確認できます。

成果物の作成

成果物を public ディレクトリに書き出すようにします。まず、 webpack.config.jsoutput 項目に下記を記述します。

    output: {
        path: path.resolve(__dirname, 'public'),
        filename: 'bundle.js',
    },

次に package.jsonscripts に下記を記述します。

"scripts": {
    "build": "webpack",

実際にビルドするには下記コマンドを実行します。成功すると public に成果物が吐き出されるので、これを適当なサーバーに配置します。

$ npm run build

エディタ環境

VSCodeにTypeScriptのプラグインを導入して開発しています。これによって、WebGLのAPI呼び出し時の補完や型によるヒントが下記のように表示されます。

D3192936BD76B505E822189B3B42641A

シンボル名のリネームなどのリファクタリングもある程度しっかりと行えるので、開発体験としてはかなりいい感じです。

参考