JavaScriptの3DライブラリThree.jsを使う機会から、そこで使われているシェーダープログラムに、興味をもつようになりました。(GPUで実行される言語なので)
Three.jsはJavaScriptなので、OpenGLではなく厳密にはWebGLと呼びますが、OpenGL ES 2.0がWebGL 1.0相当となるようです。(https://wgld.org/d/webgl2/w001.html)
今回はまずシェーディングの機能を試したかったので、このブログのロゴデータを加工してみました。
参考 : https://wgld.org/sitemap.html
このサイトはWebGLについて、とても詳しく丁寧に書かれており、大変勉強になります。Three.jsのようなライブラリは使わずに、自前のライブラリを使って書かれているので、しくみを一から勉強するのにはとても役に立ちます。ただコードのボリュームは多いので、さらにそこから必要な部分のみを切り取って使わせていただきました。このサイトにあるブラーの記事を参考に、ちょっとカッコいいエフェクトをつくってみました。
scriptタグにGPUで走る頂点シェーダ、フラグメントシェーダのプログラムを書くようになっています。(手軽すぎる・・)
組み込み変数やベクトル演算が特徴的です。attribute変数、uniform変数でJavaScriptの変数の値を受け取ります。
create_shader関数から下はそのまま引用させていただきましたが、結構なボリュームがあります。それだけOpenGLは手順が多いということですね。しかし一つずつ順を追っていくとその仕組みがよくわかるように書かれており、関数が機能ごとによくまとめられています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 |
<html> <head> <script id="vs" type="x-shader/x-vertex"> attribute vec3 position; attribute vec2 texCoord; uniform mat4 mvpMatrix; varying vec2 vTexCoord; void main(void){ vTexCoord = texCoord; gl_Position = mvpMatrix * vec4(position, 1.0); } </script> <script id="fs" type="x-shader/x-fragment"> precision mediump float; uniform sampler2D texture; uniform float strength; varying vec2 vTexCoord; const float num = 10.0; //変更 const float tFrag = 1.0 / 512.0; const float nFrag = 1.0 / num ; void main(void){ vec3 destColor = vec3(0.0); vec2 fc = vec2(gl_FragCoord.s, 512.0 - gl_FragCoord.t); vec2 fcc = fc - vec2(gl_FragCoord.s, 0.0); float totalWeight = 0.0; for(float i = 0.0; i <= num; i++){ float percent = i * nFrag; float weight = percent - percent * percent; vec2 t = fc - fcc * percent * strength * nFrag; destColor += texture2D(texture, t * tFrag).rgb * weight; totalWeight += weight; } gl_FragColor = vec4(destColor / totalWeight, 1.0); } </script> <script type="text/javascript"> var c; onload = function(){ var strength = 2.0; //変更 c = document.getElementById('canvas'); c.width = 512; c.height = 512; // webglコンテキストを取得 var gl = c.getContext('webgl') || c.getContext('experimental-webgl'); // 正射影で板ポリゴンをレンダリングするシェーダ v_shader = create_shader('vs'); f_shader = create_shader('fs'); var oPrg = create_program(v_shader, f_shader); var oAttLocation = new Array(); oAttLocation[0] = gl.getAttribLocation(oPrg, 'position'); oAttLocation[1] = gl.getAttribLocation(oPrg, 'texCoord'); var oAttStride = new Array(); oAttStride[0] = 3; oAttStride[1] = 2; var oUniLocation = new Array(); oUniLocation[0] = gl.getUniformLocation(oPrg, 'mvpMatrix'); oUniLocation[1] = gl.getUniformLocation(oPrg, 'texture'); oUniLocation[2] = gl.getUniformLocation(oPrg, 'strength'); var position = [ -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 texCoord = [ 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0 ]; var index = [ 0, 2, 1, 2, 3, 1 ]; var vPosition = create_vbo(position); var vTexCoord = create_vbo(texCoord); var vVBOList = [vPosition, vTexCoord]; var vIndex = create_ibo(index); // 各種行列の生成と初期化 var m = new matIV(); var tmpMatrix = m.identity(m.create()); var mvpMatrix = m.identity(m.create()); // テクスチャ生成 var texture1 = null; create_texture('decode.jpg', 1); setTimeout(render, 100); function render(){ // バッファクリア gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); // プログラムオブジェクトの選択 gl.useProgram(oPrg); // canvas を初期化 gl.clearDepth(1.0); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); // テクスチャの適用 gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, texture1); // 板ポリゴンのレンダリング set_attribute(vVBOList, oAttLocation, oAttStride); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, vIndex); gl.uniformMatrix4fv(oUniLocation[0], false, tmpMatrix); gl.uniform1i(oUniLocation[1], 0); gl.uniform1f(oUniLocation[2], strength); gl.drawElements(gl.TRIANGLES, index.length, gl.UNSIGNED_SHORT, 0); // コンテキストの再描画 gl.flush(); } // シェーダを生成する関数 function create_shader(id){ // シェーダを格納する変数 var shader; // HTMLからscriptタグへの参照を取得 var scriptElement = document.getElementById(id); // scriptタグが存在しない場合は抜ける if(!scriptElement){return;} // scriptタグのtype属性をチェック switch(scriptElement.type){ // 頂点シェーダの場合 case 'x-shader/x-vertex': shader = gl.createShader(gl.VERTEX_SHADER); break; // フラグメントシェーダの場合 case 'x-shader/x-fragment': shader = gl.createShader(gl.FRAGMENT_SHADER); break; default : return; } // 生成されたシェーダにソースを割り当てる gl.shaderSource(shader, scriptElement.text); // シェーダをコンパイルする gl.compileShader(shader); // シェーダが正しくコンパイルされたかチェック if(gl.getShaderParameter(shader, gl.COMPILE_STATUS)){ // 成功していたらシェーダを返して終了 return shader; }else{ // 失敗していたらエラーログをアラートする alert(gl.getShaderInfoLog(shader)); } } // プログラムオブジェクトを生成しシェーダをリンクする関数 function create_program(vs, fs){ // プログラムオブジェクトの生成 var program = gl.createProgram(); // プログラムオブジェクトにシェーダを割り当てる gl.attachShader(program, vs); gl.attachShader(program, fs); // シェーダをリンク gl.linkProgram(program); // シェーダのリンクが正しく行なわれたかチェック if(gl.getProgramParameter(program, gl.LINK_STATUS)){ // 成功していたらプログラムオブジェクトを有効にする gl.useProgram(program); // プログラムオブジェクトを返して終了 return program; }else{ // 失敗していたらエラーログをアラートする alert(gl.getProgramInfoLog(program)); } } // VBOを生成する関数 function create_vbo(data){ // バッファオブジェクトの生成 var vbo = gl.createBuffer(); // バッファをバインドする gl.bindBuffer(gl.ARRAY_BUFFER, vbo); // バッファにデータをセット gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW); // バッファのバインドを無効化 gl.bindBuffer(gl.ARRAY_BUFFER, null); // 生成した VBO を返して終了 return vbo; } // VBOをバインドし登録する関数 function set_attribute(vbo, attL, attS){ // 引数として受け取った配列を処理する for(var i in vbo){ // バッファをバインドする gl.bindBuffer(gl.ARRAY_BUFFER, vbo[i]); // attributeLocationを有効にする gl.enableVertexAttribArray(attL[i]); // attributeLocationを通知し登録する gl.vertexAttribPointer(attL[i], attS[i], gl.FLOAT, false, 0, 0); } } // IBOを生成する関数 function create_ibo(data){ // バッファオブジェクトの生成 var ibo = gl.createBuffer(); // バッファをバインドする gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo); // バッファにデータをセット gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Int16Array(data), gl.STATIC_DRAW); // バッファのバインドを無効化 gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null); // 生成したIBOを返して終了 return ibo; } // テクスチャを生成する関数 function create_texture(source, number){ // イメージオブジェクトの生成 var img = new Image(); // データのオンロードをトリガーにする img.onload = function(){ // テクスチャオブジェクトの生成 var tex = gl.createTexture(); // テクスチャをバインドする gl.bindTexture(gl.TEXTURE_2D, tex); // テクスチャへイメージを適用 gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img); // ミップマップを生成 gl.generateMipmap(gl.TEXTURE_2D); // テクスチャパラメータの設定 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.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); // 生成したテクスチャを変数に代入 switch(number){ case 1: texture1 = tex; break; case 2: texture2 = tex; break; default : break; } // テクスチャのバインドを無効化 gl.bindTexture(gl.TEXTURE_2D, null); }; // イメージオブジェクトのソースを指定 img.src = source; } // フレームバッファをオブジェクトとして生成する関数 function create_framebuffer(width, height){ // フレームバッファの生成 var frameBuffer = gl.createFramebuffer(); // フレームバッファをWebGLにバインド gl.bindFramebuffer(gl.FRAMEBUFFER, frameBuffer); // 深度バッファ用レンダーバッファの生成とバインド var depthRenderBuffer = gl.createRenderbuffer(); gl.bindRenderbuffer(gl.RENDERBUFFER, depthRenderBuffer); // レンダーバッファを深度バッファとして設定 gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, width, height); // フレームバッファにレンダーバッファを関連付ける gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, depthRenderBuffer); // フレームバッファ用テクスチャの生成 var fTexture = gl.createTexture(); // フレームバッファ用のテクスチャをバインド gl.bindTexture(gl.TEXTURE_2D, fTexture); // フレームバッファ用のテクスチャにカラー用のメモリ領域を確保 gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); // テクスチャパラメータ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); // フレームバッファにテクスチャを関連付ける gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, fTexture, 0); // 各種オブジェクトのバインドを解除 gl.bindTexture(gl.TEXTURE_2D, null); gl.bindRenderbuffer(gl.RENDERBUFFER, null); gl.bindFramebuffer(gl.FRAMEBUFFER, null); // オブジェクトを返して終了 return {f : frameBuffer, d : depthRenderBuffer, t : fTexture}; } }; function matIV(){ this.create = function(){ return new Float32Array(16); }; this.identity = function(dest){ dest[0] = 1; dest[1] = 0; dest[2] = 0; dest[3] = 0; dest[4] = 0; dest[5] = 1; dest[6] = 0; dest[7] = 0; dest[8] = 0; dest[9] = 0; dest[10] = 1; dest[11] = 0; dest[12] = 0; dest[13] = 0; dest[14] = 0; dest[15] = 1; return dest; }; } </script> </head> <body> <canvas id="canvas" width="512" height="512"></canvas> </body> </html> |
フラグメントシェーダでは自ピクセルの色を周辺ピクセルの色から計算して決定しています。ここでは縦方向のピクセルを対象にしてブレを起こしています。
どのピクセルを選択するかによって、ブレ方や方向を変化させることができそうです。
今回は平面だけでしたが、また掘り下げていきたいです。