目次
- はじめに
- webpack + TypeScriptのプロジェクトを作成する
- シェーダのソースコードをimportする
- 実際にポリゴンを描画する
- さいごに
1. はじめに
この記事では、WebGLでポリゴンを描画する方法を紹介します。
言語はTypeScriptを使います。
わざわざJavaScriptではなくTypeScriptを使う理由を書きます。
WebGLは1つのポリゴンを描画するだけでも結構難しかったりします。
その理由はGLの使い方が難しいことだと思います。
こういうときにTypeScriptが役に立ちます。
写経したコードの中に出てくる変数がどんな型なのかが分かるからです。
型が分かれば変数の役割が分かりますし、補完が効くのでタイポなども少なくなります。
この記事の内容を実装したソースコードはこちらにあります。
また、この記事で扱うWebGLでの描画については下記の記事と同じです。
この記事のJavaScript版で、GLの概念なども丁寧に解説されています。
最終的には次のような画面になる想定です。
それでは早速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. 実際にポリゴンを描画する
ここから先は下記の記事の通りに実装していけば大丈夫です。
実装していくとソースコードは下記の通りになります。
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()
再度ビルドして確認すると、ポリゴンが描画できているはずです。
5. さいごに
今回はTypeScriptとWebGLでポリゴンを描画しました。
TypeScriptのおかげで、どの変数がなんのためにあるのか分かりやすかったです。
WebGLを使うと結構ソースコードが肥大化しがちですが、
TypeScriptを使えばどんどん改修しても耐えられると思います。
今回は簡単なサンプルになりましたが、今後はもう少し複雑なこともやっていきたいです。