2019/12/06
Three.jsを使ったWebGLでアウトラインだけの表示
WebGLを利用したサイトは導入のハードルの高さから決して多いとは言えませんが、
ECにおいても少しずつ利用されるサイトが増えているようです。
RIMOWA UNIQUE | Custom Luggage | RIMOWA
Konfiguriere dir jetzt deinen Backforce
WebGLによる3Dのコンテンツは臨場感が高く、
写真と比べた場合、いろいろな角度から見ることができ、
動画と比べた場合、ユーザーの意思で見る角度やアレンジができることから、
うまく利用すれば、ECサイトにおいても高い効果が得られることは間違いありません。
ただ、利用どころが難しいというのは、否めません。
今回、ECサイトでもっと手軽な感じでWebGLを利用した方法はないか考えていたところ、
予約商品などでシルエットだけ表示されていたりすることがありますが、
それを3Dでやってみたら楽しみ感が出ていいかもしれないと思いいたりました。
ただ、普通のシルエットだと、いまいち特別感がないので、輪郭だけが浮かび上がったような表現ができたら面白いかもしれないと思い、試しに作ってみることにしました。
↓作ってみたもの
DEMO
方法
基本的にThree.jsを使いますが、重要なポイントはWebGLを直接的に操作します。
- カメラと、ステンシル用および通常用のシーンを生成し、Three.jsのローダーで3Dオブジェクトを読み込む
- 読み込んだオブジェクトをクローンして、クローンしたオブジェクトをTHREE.ShaderMaterialのvertexShaderで一回り大きくする
- THREE.WebGLRendererでレンダラーを用意し、.setClearColor()で背景を透過にして、.autoClearをfalseに設定して、任意のタイミングで.clear()を実行するようにする
- 大きくしていない方のオブジェクトでステンシルバッファーを設定し、その設定をもとに一回り大きくしたオブジェクトを型抜きしたような状態で描画
今回の方法では、一回り大きくしたオブジェクトをステンシルバッファーを利用して型抜きにします。
基本的な方法は以下のサイトを参考にさせていただきました。
wgld.org | WebGL: ステンシルバッファでアウトライン
1. カメラと、ステンシル用および通常用のシーンを生成し、Three.jsのローダーで3Dオブジェクトを読み込む
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 | // グローバル変数の定義 var container, controls; var camera, scene, sceneStencil, renderer, gl; var renderingOutline = false; var clock = new THREE.Clock(); var mixer; init(); // 初期設定 animate(); // 描画 function init() { // Canvasのコンテナを準備 container = document.createElement( 'div' ); document.body.appendChild( container ); // カメラを生成 camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 2000 ); camera.position.set( 100, 200, 300 ); // シーンを通常・ステンシル用の2つ生成 scene = new THREE.Scene(); sceneStencil = new THREE.Scene(); // modelをローダーで読み込む var loader = new THREE.FBXLoader(); loader.load( 'models/boeing787lp.fbx', function ( object ) { ︙ |
2. 読み込んだオブジェクトをクローンして、クローンしたオブジェクトをTHREE.ShaderMaterialのvertexShaderで一回り大きくする
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 | loader.load( 'models/boeing787lp.fbx', function ( object ) { //ステンシル用にオブジェクトをクローン var objectStencil = object.clone(); // ステンシル用のオブジェクトは真っ黒なシルエットだけのマテリアルを設定 objectStencil.traverse(function (child) { if ( child.isMesh ) { child.material = new THREE.MeshBasicMaterial({ color: 0x000000 }); } }); // ステンシル用のシーンにオブジェクトを追加 sceneStencil.add( objectStencil ); // アウトラインのオブジェクトはTHREE.ShaderMateriaで直接シェーダーを指定して一回り大きくする object.traverse(function (child) { if ( child.isMesh ) { child.material = new THREE.ShaderMaterial({ vertexShader: document.getElementById('vshader').textContent, fragmentShader: document.getElementById('fshader').textContent, }); } }); // オブジェクトを通常のシーンに追加 scene.add( object ); }); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <!-- vertex shader --> <script type="x-shader/x-vertex" id="vshader"> void main () { vec3 pos = position; pos += normal * 0.1; gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0); } </script> <!-- fragment shader --> <script type="x-shader/x-fragment" id="fshader"> void main () { gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0); } </script> |
3. THREE.WebGLRendererでレンダラーを用意し、.setClearColor()で背景を透過にして、.autoClearをfalseに設定して、任意のタイミングで.clear()を実行するようにする
1 2 3 4 5 6 7 8 9 10 11 12 13 | // 背景透過を有効化してレンダラーを生成 renderer = new THREE.WebGLRenderer( { antialias: true, alpha: true } ); // glにコンテクストを代入 gl = renderer.getContext(); renderer.setPixelRatio( window.devicePixelRatio ); renderer.setSize( window.innerWidth, window.innerHeight ); // 背景に透明を指定 renderer.setClearColor( 0x000000, 0 ); // レンダラーの auto clear をfalseにする renderer.autoClear = false; container.appendChild( renderer.domElement ); } |
レンダラーの.autoClearをfalseにするところが実は重要で、私はここでハマりました。
この部分は以下のサイトを参考にさせていただきました。
[Three.js][WebGL] DepthBufferを共有して選択オブジェクト以外にブラーをかけてみる – Qiita
4. 大きくしていない方のオブジェクトでステンシルバッファーを設定し、その設定をもとに一回り大きくしたオブジェクトを型抜きしたような状態で描画
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 | function animate() { requestAnimationFrame( animate ); var delta = clock.getDelta(); if ( mixer ) mixer.update( delta ); // レンダーラーのクリアを実行 renderer.clear(); // ステンシルバッファーの設定 gl.enable(gl.STENCIL_TEST); gl.colorMask(false, false, false, false); gl.depthMask(false); gl.stencilFunc(gl.ALWAYS, 1, ~0); gl.stencilOp(gl.KEEP, gl.REPLACE, gl.REPLACE); // ステンシル用のシーンをもとにステンシルを設定 renderer.render(sceneStencil, camera); // ステンシルバッファーの設定 gl.colorMask(true, true, true, true); gl.depthMask(true); gl.stencilFunc(gl.EQUAL, 0, ~0); gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP); // ステンシルバッファーの設定をもとに、一回り大きくしたオブジェクトを描画する renderer.render(scene, camera); gl.disable(gl.STENCIL_TEST); } |
これで、アウトラインだけを描画することができました。
ステンシルバッファーを利用すると、様々な表現を混在させることができるので、
表現の幅が広がります。
いろいろな効果を試してみたいです。
Script部分全文
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 | <script src="three.js/build/three.min.js"></script> <script src="three.js/examples/js/libs/inflate.min.js"></script> <script src="three.js/examples/js/controls/OrbitControls.js"></script> <script src="three.js/examples/js/loaders/FBXLoader.js"></script> <script type="x-shader/x-vertex" id="vshader"> void main () { vec3 pos = position; pos += normal * 0.1; gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0); } </script> <script type="x-shader/x-fragment" id="fshader"> void main () { gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0); } </script> <script> var loading = document.querySelector('.loading'); (() => { var container, controls; var camera, scene, sceneStencil, renderer, gl; var renderingOutline = false; var clock = new THREE.Clock(); var mixer; init(); animate(); function init() { container = document.createElement( 'div' ); document.body.appendChild( container ); camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 2000 ); camera.position.set( 100, 200, 300 ); scene = new THREE.Scene(); sceneStencil = new THREE.Scene(); // model var loader = new THREE.FBXLoader(); loader.load( 'models/boeing787lp.fbx', function ( object ) { var objectStencil = object.clone(); objectStencil.traverse(function (child) { if ( child.isMesh ) { child.material = new THREE.MeshBasicMaterial({ color: 0x000000 }); } }); objectStencil.scale.set(0.15, 0.15, 0.15); sceneStencil.add( objectStencil ); object.traverse(function (child) { if ( child.isMesh ) { child.material = new THREE.ShaderMaterial({ vertexShader: document.getElementById('vshader').textContent, fragmentShader: document.getElementById('fshader').textContent, }); } }); object.scale.set(0.15, 0.15, 0.15); scene.add( object ); loading.classList.remove('is-active'); }); renderer = new THREE.WebGLRenderer( { antialias: true, alpha: true } ); gl = renderer.getContext(); renderer.setPixelRatio( window.devicePixelRatio ); renderer.setSize( window.innerWidth, window.innerHeight ); renderer.setClearColor( 0x000000, 0 ); renderer.autoClear = false; container.appendChild( renderer.domElement ); controls = new THREE.OrbitControls( camera, renderer.domElement ); controls.target.set( 0, 0, 0 ); controls.update(); window.addEventListener( 'resize', onWindowResize, false ); } function onWindowResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize( window.innerWidth, window.innerHeight ); } function animate() { requestAnimationFrame( animate ); var delta = clock.getDelta(); if ( mixer ) mixer.update( delta ); renderer.clear(); gl.enable(gl.STENCIL_TEST); gl.colorMask(false, false, false, false); gl.depthMask(false); gl.stencilFunc(gl.ALWAYS, 1, ~0); gl.stencilOp(gl.KEEP, gl.REPLACE, gl.REPLACE); renderer.render(sceneStencil, camera); gl.colorMask(true, true, true, true); gl.depthMask(true); gl.stencilFunc(gl.EQUAL, 0, ~0); gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP); renderer.render(scene, camera); gl.disable(gl.STENCIL_TEST); } })(); </script> |
余談ですが、最近はWebGLを使ったサイトで、3Dではなく、GPUの特性を生かした水面のようなリアルな効果を生み出す手段としても、使われる機会が増えているようです。
Bruno Arizio — Interactive Designer
Author Profile
NINOMIYA
Webデザイナー兼コーダー出身のフロントエンド開発者です。 UXデザインやチーム開発の効率化など、勉強中です。
SHARE