TypeScriptとWebGLでポリゴンを描画する方法

f:id:shogonir:20200120005139p:plain

目次

  1. はじめに
  2. webpack + TypeScriptのプロジェクトを作成する
  3. シェーダのソースコードをimportする
  4. 実際にポリゴンを描画する
  5. さいごに

1. はじめに

この記事では、WebGLでポリゴンを描画する方法を紹介します。
言語はTypeScriptを使います。
わざわざJavaScriptではなくTypeScriptを使う理由を書きます。

WebGLは1つのポリゴンを描画するだけでも結構難しかったりします。
その理由はGLの使い方が難しいことだと思います。

こういうときにTypeScriptが役に立ちます。
写経したコードの中に出てくる変数がどんな型なのかが分かるからです。
型が分かれば変数の役割が分かりますし、補完が効くのでタイポなども少なくなります。

この記事の内容を実装したソースコードはこちらにあります。

github.com

また、この記事で扱うWebGLでの描画については下記の記事と同じです。
この記事のJavaScript版で、GLの概念なども丁寧に解説されています。

sbfl.net

最終的には次のような画面になる想定です。

f:id:shogonir:20200120001203p:plain

それでは早速TypeScriptとWebGLでポリゴンを描画していきます。

2. webpack + TypeScriptのプロジェクトを作成する

依存するライブラリを設定します。
下記のコマンドで依存するライブラリを落とします。

npm install --save-dev webpack webpack-cli typescript ts-loader gl-matrix @types/gl-matrix

package.json は下記のようになりました。

{
  "name": "p01-ts-one-polygon",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack",
    "watch": "webpack -w"
  },
  "author": "shogonir",
  "license": "MIT",
  "devDependencies": {
    "@types/gl-matrix": "^2.4.5",
    "gl-matrix": "^3.1.0",
    "ts-loader": "^6.2.1",
    "typescript": "^3.7.4",
    "webpack": "^4.41.4",
    "webpack-cli": "^3.3.10"
  }
}

webpackの設定ファイル webpack.config.js は次のようにします。

module.exports = {
  mode: 'development',
  entry: './src/main.ts',
  module: {
    rules: [
      {
        test: /\.ts$/,
        use: 'ts-loader'
      }
    ]
  },
  resolve: {
    extensions: [
      '.ts'
    ]
  }
};

TypeScriptの設定ファイル tsconfig.json は次のようにします。

{
  "compilerOptions": {
    "sourceMap": true,
    "target": "es2015",
    "module": "es2015"
  }
}

ソースコード src/main.ts を作成します。
このファイルはとりあえずコンソールにログを出力するようにしましょう。

console.log('hello')

この状態で下記のコマンドを打つとビルドできます。

npm run build

ビルドが成功すると dist/main.js ファイルが出来上がっています。
このファイルはリポジトリに含めたくないので .gitignore を準備します。
ついでに依存するライブラリが格納されている node_modules ディレクトリも記載します。

dist
node_modules

最後に、ビルド済みのjsファイルを使うhtmlファイル html/index.html を準備します。

<!DOCTYPE html>
<html>
  <head>
    <style>
      #canvas {
        border: 1px solid black;
      }
    </style>
  </head>
  <body>
    <canvas id="canvas" width="512" height="512"></canvas>
    <script src="../dist/main.js"></script>
  </body>
</html>

このHTMLをブラウザで表示したときに、コンソールに hello と表示されれば成功です。

3. シェーダのソースコードをimportする

シェーダのソースコードを管理しやすくするために、 .glsl 拡張子のファイルに記述することにします。
webpackでシェーダをimportするときは、ファイルの内容を文字列で取得する必要があります。

そのために必要な作業を紹介します。

まずは依存性を1つ追加します。

npm install --save-dev ts-shader-loader

シェーダのソースコードをまとめて入れるディレクトリを src/shader とすることにします。
その中に簡単なシェーダのソースコードを追加します。

まずはバーテックスシェーダ src/shader/VertexShader.glsl です。

#version 300 es

in vec3 vertexPosition;
in vec4 color;

out vec4 vColor;

void main() {
  vColor = color;
  gl_Position = vec4(vertexPosition, 1.0);
}

次にフラグメントシェーダ src/shader/FragmentShader.glsl です。

#version 300 es

precision highp float;

in vec4 vColor;

out vec4 fragmentColor;

void main() {
  fragmentColor = vColor;
}

しかしこれらのファイルは .ts ではないのでimportできません。
なので、このディレクトリのファイルは特別なimportをするということをファイル src/shader/glsl.d.ts で記述します。

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

webpackの設定ファイルは次のように変えます。
.glsl という拡張子もimportする対象であることを設定するためです。

module.exports = {
  mode: 'development',
  entry: './src/main.ts',
  module: {
    rules: [
      {
        test: /\.ts$/,
        use: 'ts-loader'
      },
      {
        test: /.glsl$/,
        use: 'ts-shader-loader'
      }
    ]
  },
  resolve: {
    extensions: [
      '.ts'
    ]
  }
};

src/main.ts からシェーダのソースコードをimportします。

import vertexShaderSource from './shader/VertexShader.glsl'
import fragmentShaderSource from './shader/FragmentShader.glsl'

console.log('hello')
console.log(vertexShaderSource)
console.log(fragmentShaderSource)

再度ビルドして確認してみましょう。
シェーダのソースコードが文字列としてimportできたのが分かると思います。

4. 実際にポリゴンを描画する

ここから先は下記の記事の通りに実装していけば大丈夫です。

sbfl.net

実装していくとソースコードは下記の通りになります。

import vertexShaderSource from './shader/VertexShader.glsl'
import fragmentShaderSource from './shader/FragmentShader.glsl'

const main = () => {

  const mayBeCanvas = document.getElementById('canvas')
  if (mayBeCanvas === null) {
    console.warn('not found canvas element')
    return
  }

  const canvas: HTMLCanvasElement = mayBeCanvas as HTMLCanvasElement

  const mayBeContext = canvas.getContext('webgl2')
  if (mayBeContext === null) {
    console.warn('could not get context')
    return
  }

  const context: WebGL2RenderingContext = mayBeContext

  const vertexShader = context.createShader(context.VERTEX_SHADER)
  context.shaderSource(vertexShader, vertexShaderSource)
  context.compileShader(vertexShader)

  const vertexShaderCompileStatus = context.getShaderParameter(vertexShader, context.COMPILE_STATUS)
  if(!vertexShaderCompileStatus) {
    const info = context.getShaderInfoLog(vertexShader)
    console.warn(info)
    return
  }

  const fragmentShader = context.createShader(context.FRAGMENT_SHADER)
  context.shaderSource(fragmentShader, fragmentShaderSource)
  context.compileShader(fragmentShader)

  const fragmentShaderCompileStatus = context.getShaderParameter(fragmentShader, context.COMPILE_STATUS)
  if(!fragmentShaderCompileStatus) {
    const info = context.getShaderInfoLog(fragmentShader)
    console.warn(info)
    return
  }

  const program = context.createProgram()
  context.attachShader(program, vertexShader)
  context.attachShader(program, fragmentShader)
  context.linkProgram(program)

  const linkStatus = context.getProgramParameter(program, context.LINK_STATUS)
  if(!linkStatus) {
    const info = context.getProgramInfoLog(program)
    console.warn(info)
    return
  }

  context.useProgram(program)

  const vertexBuffer = context.createBuffer()
  const colorBuffer = context.createBuffer()

  const vertexAttribLocation = context.getAttribLocation(program, 'vertexPosition')
  const colorAttribLocation  = context.getAttribLocation(program, 'color')

  const VERTEX_SIZE = 3 // vec3
  const COLOR_SIZE  = 4 // vec4

  context.bindBuffer(context.ARRAY_BUFFER, vertexBuffer)
  context.enableVertexAttribArray(vertexAttribLocation)
  context.vertexAttribPointer(vertexAttribLocation, VERTEX_SIZE, context.FLOAT, false, 0, 0)

  context.bindBuffer(context.ARRAY_BUFFER, colorBuffer)
  context.enableVertexAttribArray(colorAttribLocation)
  context.vertexAttribPointer(colorAttribLocation, COLOR_SIZE, context.FLOAT, false, 0, 0)

  const halfSide = 0.5
  const vertices = new Float32Array([
    -halfSide, halfSide,  0.0,
    -halfSide, -halfSide, 0.0,
    halfSide,  halfSide,  0.0,
    -halfSide, -halfSide, 0.0,
    halfSide,  -halfSide, 0.0,
    halfSide,  halfSide,  0.0
  ])

  const colors = new Float32Array([
    1.0, 0.0, 0.0, 1.0,
    0.0, 1.0, 0.0, 1.0,
    0.0, 0.0, 1.0, 1.0,
    0.0, 1.0, 0.0, 1.0,
    0.0, 0.0, 0.0, 1.0,
    0.0, 0.0, 1.0, 1.0
  ])

  context.bindBuffer(context.ARRAY_BUFFER, vertexBuffer)
  context.bufferData(context.ARRAY_BUFFER, vertices, context.STATIC_DRAW)

  context.bindBuffer(context.ARRAY_BUFFER, colorBuffer)
  context.bufferData(context.ARRAY_BUFFER, colors, context.STATIC_DRAW)

  const VERTEX_NUMS = 6
  context.drawArrays(context.TRIANGLES, 0, VERTEX_NUMS)

  context.flush()
}

main()

再度ビルドして確認すると、ポリゴンが描画できているはずです。

f:id:shogonir:20200120001203p:plain

5. さいごに

今回はTypeScriptとWebGLでポリゴンを描画しました。
TypeScriptのおかげで、どの変数がなんのためにあるのか分かりやすかったです。
WebGLを使うと結構ソースコードが肥大化しがちですが、
TypeScriptを使えばどんどん改修しても耐えられると思います。

今回は簡単なサンプルになりましたが、今後はもう少し複雑なこともやっていきたいです。