ここ数年のWebで、インタラクティブな表現として普及してきたWebGL。これまで制作に取り入れたことがなく、なんとなくでも理解したいと思い、今回色々と調べてみたので忘備録も兼ねてご紹介したいと思います。
以下、Wikipediaの参照です。
WebGL(ウェブジーエル)は、ウェブブラウザで3次元コンピュータグラフィックスを表示させるための標準仕様。非営利団体のKhronos Groupで管理されている。WebGL 1.0は、ブラウザ上で利用できるOpenGL ES 2.0の派生規格であるが、細部に違いがある[1]。WebGL 2.0は、ブラウザ上で利用できるOpenGL ES 3.0の派生規格であるが、細部に違いがある[2]。WebGLはHTML5のcanvas要素に描画する。
Webサイト上に3Dグラフィックの描画を表示するための仕様です。HTMLのコードに「canvas」要素を追加し、Javascriptで描画内容をコーディングします。
マウスの動きに追従した動きをつけたり、クリックやキーボードの入力イベントをアニメーションのトリガーに出来たりと、Webならではのインタラクティブな表現が可能でクリエイティブなWebサイトなどで広く使われています。
・画像が「Creative」の文字でトリミングしており、マウスカーソルの動きに合わせて画像が伸び縮みする
・リンクエリアで画像がマウスカーソルに追従。ホバー時の画像の切り替わりや見出し文字の変化もWebGLならではの独特なアニメーション。
上記のようなインタラクティブな動きはJavascriptやCSSだけでは表現が不可能なものですが、WebGLを使うことで実現可能となります。
WebGLのソースコードを自分なりに分析してみました。
はじめに、記載内容については正確な知識ではなく認識の誤りがあるかもしれませんので、それについてご了承の上お読みいただけると幸いです。
今回参考にさせていただいたデモは「Codrops」の「Creative WebGL Image Transitions」のDemo5を題材にしています。
CodropsはWebGLを始め、Webのインタラクティブな表現のデモを多く公開している素敵サイトです。嬉しいことにソースコードも一緒に公開してくれています。
また今回のデモは主に「three.js」のライブラリを利用して描画のソースコードを書いています。
「three.js」とは、WebGLで3Dコンテンツを描画するためのスタンダードなライブラリです。WebGLをライブラリなしでソースコードを書くのは相応の知識がないと難しく、上級者でない限り「three.js」で作っていくのが無難だと思われます。
Root
┣ css … ヘッダーやリンクメニューのレイアウト
┣ img … スライド画像
┣ js
┃ ┣ three.js … 3D描画のライブラリ
┃ ┣ dat-gui.js … Javascriptの変数をパラメーターとしてブラウザで操作できるGUIライブラリ
┃ ┣ gsap.js … トゥイーン(アンメーション)ライブラリ
┃ ┣ sketch.js … スライド画像のフェードクラス
┃ ┗ demo5.js … Demo5用のフェードクラスのインスタンス
┗ index5.html
今回メインとなるファイルは「sketch.js」と「demo5.js」です。
ちなみに「dat-gui.js」はデモページの右上に表示されている各種パラメータのコントローラUIを作成できるGUIライブラリです。sketchクラスのプロパティなどをパラメーターとして渡して値を変えることで、スライド画像のフェードに動きの変化をつけられます。
まず、「sketch.js」はスライド画像のフェードをWebGLでコンテンツ描画するためのプロパティやメソッドを用意しているクラスです。
このクラスでは以下のような機能が実装されています。
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 |
onstructor(opts) { // Three.jsのインスタンス作成 this.scene = new THREE.Scene(); // 頂点シェーダー this.vertex = `varying vec2 vUv;void main() {vUv = uv;gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );}`; // this.fragment = opts.fragment; // this.uniforms = opts.uniforms; // レンダーのインスタンス作成 this.renderer = new THREE.WebGLRenderer(); // ウィンドウサイズ this.width = window.innerWidth; this.height = window.innerHeight; // デバイスの解像度 this.renderer.setPixelRatio(window.devicePixelRatio); // キャンバスのサイズ this.renderer.setSize(this.width, this.height); // 1Frame毎の塗りつぶし色 this.renderer.setClearColor(0xeeeeee, 1); // アニメーションの時間 this.duration = opts.duration || 3; // パラメーターのGUIを表示する this.debug = opts.debug || false // イージング this.easing = opts.easing || 'easeInOut' // クリック範囲 this.clicker = document.getElementById("content"); // コンテンツサイズ this.container = document.getElementById("slider"); // スライド画像(文字列 -> 配列に変換) this.images = JSON.parse(this.container.getAttribute('data-images')); // コンテンツサイズ this.width = this.container.offsetWidth; this.height = this.container.offsetHeight; // Canvasを追加 this.container.appendChild(this.renderer.domElement); // カメラの設定(視野角,アスペクト比,near,far) this.camera = new THREE.PerspectiveCamera( 70, window.innerWidth / window.innerHeight, 0.001, 1000 ); // カメラの位置(X,Y,Z) this.camera.position.set(0, 0, 1); this.time = 0; this.current = 0; this.textures = []; // 一時停止フラグ this.paused = true; // 初期化 this.initiate(()=>{ // 以下コールバック console.log(this.textures); // コンテンツサイズ変更 this.setupResize(); // dat GUI セッティング this.settings(); this.addObjects(); this.resize(); this.clickEvent(); this.play(); }) } |
コンストラクタでは、WebGLのシーンやカメラ、レンダラーのインスタンスが作成されます。
また、キャンバス要素を「#slider」の要素に挿入します。
頂点シェーダー(this.vertex)には、3D空間の中で描画するオブジェクトの頂点の座標位置を格納しています。座標は(x,y,z)の3次元で保持されます。
フラグメントシェーダー(this.fragment)は頂点シェーダに格納された頂点位置の情報にピクセルの色情報を決定します。
コンストラクタの最後に「this.initial」メソッドが実行され、コールバックでスライドアニメーションのループ処理が実行されます。
this.initialメソッドでは、「#slider」要素の「data-images」属性で指定された画像をthis.texturesにthree.jsのTextureLoaderメソッドでテクスチャ画像としてロードした上で格納します。ここでは、画像のロードが完了される前に以降の処理が実行されないように、Promiseで同期処理にしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
initiate(cb){ const promises = []; let that = this; this.images.forEach((url,i)=>{ let promise = new Promise(resolve => { // 画像のロード(ファイルパス, onLoad時の処理) that.textures[i] = new THREE.TextureLoader().load( url, resolve ); }); promises.push(promise); }) // 全ての画像が読み込まれたらコールバックを実行 Promise.all(promises).then(() => { cb(); }); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// sketch.js settings() { let that = this; // パラメーターGUIのインスタンス生成 if(this.debug) this.gui = new dat.GUI(); this.settings = {progress:0.5}; // if(this.debug) this.gui.add(this.settings, "progress", 0, 1, 0.01); // 操作するパラメーターの設定 Object.keys(this.uniforms).forEach((item)=> { this.settings[item] = this.uniforms[item].value; if(this.debug) { // 操作するパラメータを追加 this.gui.add( this.settings, // GUIオブジェクト item, // キー this.uniforms[item].min, // レンジ(最小) this.uniforms[item].max, // レンジ(最大) 0.01) // 小数点何桁まで指定可能か ; } }) } |
settingsメソッドはdat-guiのパラメータを設定しページに追加します。
パラメーターのデータはdemo5.jsのSketchクラスのインスタンス作成時に[uniforms]オブジェクトで渡されます。オブジェクトのキーがパラメータ名で値の[min]と[max]が下限と上限になります。
addObjectsメソッドではスライドさせる画像のテクスチャをシェーダーマテリアル(THREE.ShaderMaterial)で描画し、シーンに追加します。
描画するためのパラメータはユニフォーム変数(uniforms)としてシェーダーに渡されます。
THREE.PlaneGeometryで平面の図形を描画し、そこにシェーダーマテリアルで作成されたテクスチャ画像を貼り付けるようなイメージです。
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 |
addObjects() { let that = this; // シェーダーマテリアル作成 this.material = new THREE.ShaderMaterial({ extensions: { derivatives: "#extension GL_OES_standard_derivatives : enable" }, // 描画面(両面に描画) side: THREE.DoubleSide, uniforms: { time: { type: "f", value: 0 }, progress: { type: "f", value: 0 }, border: { type: "f", value: 0 }, intensity: { type: "f", value: 0 }, scaleX: { type: "f", value: 40 }, scaleY: { type: "f", value: 40 }, transition: { type: "f", value: 40 }, swipe: { type: "f", value: 0 }, width: { type: "f", value: 0 }, radius: { type: "f", value: 0 }, texture1: { type: "f", value: this.textures[0] }, texture2: { type: "f", value: this.textures[1] }, displacement: { type: "f", value: new THREE.TextureLoader().load('img/disp1.jpg') }, resolution: { type: "v4", value: new THREE.Vector4() }, }, // ワイヤーの表示 wireframe: false, // 頂点シェーダー vertexShader: this.vertex, // フラグメントシェーダ(色の座標値) fragmentShader: this.fragment }); // 平面ジオメトリ生成(Width, Height, WidthSegment, HeightSegmant) this.geometry = new THREE.PlaneGeometry(1, 1, 2, 2); // メッシュ生成(ジオメトリ, マテリアル) this.plane = new THREE.Mesh(this.geometry, this.material); // シーンに追加 this.scene.add(this.plane); } |
フェードの具体的な描画処理はフラグメントシェーダーで実装されています。「demo5.js」ファイルのSketchインスタンス作成時に渡される[fragment]の処理部分です。
nextメソッドはスライド時のアニメーションの制御を実装しています。クリックイベントで発火し、フェードで切り替えるスライド画像をaddObjectメソッドで作成されたthis.materialプロパティのtexture1とtexture2にセットします。
アニメーションのイージングは「gsap.js」のTimelineMaxクラスが利用されています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
next(){ // スライド中の場合は処理を中断 if(this.isRunning) return; this.isRunning = true; // 次の画像をセット let len = this.textures.length; let nextTexture =this.textures[(this.current +1)%len]; this.material.uniforms.texture2.value = nextTexture; // トゥイーンライブラリ let tl = new TimelineMax(); tl.to(this.material.uniforms.progress,this.duration,{ value:1, ease: Power2[this.easing], // コールバック onComplete:()=>{ console.log('FINISH'); this.current = (this.current +1)%len; this.material.uniforms.texture1.value = nextTexture; this.material.uniforms.progress.value = 0; this.isRunning = false; }}) } |
レンダーメソッドはthree.jsで作成したシーンやカメラ、オブジェクトなど3D空間に描画された内容をTHREE.WebGLRenderer()でキャンバス上に描画します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
render() { if (this.paused) return; this.time += 0.05; this.material.uniforms.time.value = this.time; Object.keys(this.uniforms).forEach((item)=> { this.material.uniforms[item].value = this.settings[item]; }); this.camera.position.z = 1; // ループして1フレームごとに再描画する requestAnimationFrame(this.render.bind(this)); this.renderer.render(this.scene, this.camera); } |
demo5.jsではsketchクラスをインスタンスを作成し、フラグメントシェーダーでフェードの描画処理を記述しています。他のデモなど全て、インスタンスに渡すパラメータの違いや、フラグメントシェーダーの内容で描画内容のバリエーションを作っています。
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 |
let sketch = new Sketch({ debug: true, uniforms: { intensity: {value: 0.3, type:'f', min:0., max:2}, }, fragment: ` uniform float time; uniform float progress; uniform float width; uniform float scaleX; uniform float scaleY; uniform float transition; uniform float radius; uniform float intensity; uniform sampler2D texture1; uniform sampler2D texture2; uniform sampler2D displacement; uniform vec4 resolution; varying vec2 vUv; void main() { vec2 newUV = (vUv - vec2(0.5))*resolution.zw + vec2(0.5); vec4 d1 = texture2D(texture1, newUV); vec4 d2 = texture2D(texture2, newUV); float displace1 = (d1.r + d1.g + d1.b)*0.1; float displace2 = (d2.r + d2.g + d2.b)*0.1; vec4 t1 = texture2D( texture1, vec2( newUV.x + progress * (displace2 * intensity), newUV.y + progress * (displace2 * intensity) ) ); vec4 t2 = texture2D( texture2, vec2( newUV.x + (1.0 - progress) * (displace1 * intensity), newUV.y + (1.0 - progress) * (displace1 * intensity) ) ); // 線形補間 gl_FragColor = mix(t1, t2, progress); } ` }); |
今回はWebGLで作成されたコンテンツの処理の流れを分析してみました。
THREE.ShaderMaterialや頂点・フラグメントシェーダーの実際の処理の部分はもっとWebGLの知識がないので、もう少し勉強が必要そうでした。
WebGLについても、今後もブログでご紹介していけたらと思います。