どうも!かけちまるです!
Three.jsで画像をテクスチャとして使うときにGLSLでテクスチャをbackground-size: cover;
のような見た目にする方法を解説します。
背景画像をテクスチャにしたい時などに使えそうですね。
これでテクスチャをレスポンシブに対応させることができます。
この記事では、
がわかります。
次のコードを元に実装していきます。
ディレクトリ構成index.html css/ -style.css js/ -main.js
index.html<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" type="text/css" href="./css/style.css"> <script async src="https://unpkg.com/es-module-shims@1.5.8/dist/es-module-shims.js"></script> <script type="importmap"> { "imports": { "three": "https://unpkg.com/three@0.142.0/build/three.module.js" } } </script> <script src="./js/main.js" type="module"></script> </head> <body> <script id="v-shader" type="x-shader/x-vertex"> </script> <script id="f-shader" type="x-shader/x-fragment"> </script> </body> </html>
style.css*{ margin: 0; padding: 0; box-sizing: border-box; } html, body{ height: 100%; } canvas{ position: fixed; top: 0; left: 0; z-index: -1; }
main.jsimport * as THREE from 'three'; /** * シーン・カメラ・レンダラーを用意する */ const fov = 50; const fovRad = (fov / 2) * (Math.PI / 180); // 視野角をラジアンに変換 let distance = (window.innerHeight / 2) / Math.tan(fovRad); // カメラ距離を求める const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera( fov, window.innerWidth / window.innerHeight, 0.1, 2000 ); camera.position.z = distance; const renderer = new THREE.WebGLRenderer(); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setPixelRatio(window.devicePixelRatio); document.body.appendChild(renderer.domElement); /** * ジオメトリ+マテリアル=メッシュを用意する */ const planeGeometry = new THREE.PlaneGeometry(100, 100); const shaderMaterial = new THREE.ShaderMaterial(); let mesh = new THREE.Mesh(planeGeometry, shaderMaterial); scene.add(mesh); // アニメーション function animate() { renderer.render(scene, camera); requestAnimationFrame(animate); } animate(); // ブラウザのリサイズに対応させる window.addEventListener('resize', onWindowResize); function onWindowResize() { renderer.setSize(window.innerWidth, window.innerHeight); camera.aspect = window.innerWidth / window.innerHeight; distance = (window.innerHeight / 2) / Math.tan(fovRad); camera.position.z = distance; camera.updateProjectionMatrix(); }
ここまで次のようになっていればオッケーです。
今回はShaderを使うのでindex.htmlでは、Shaderを記述する準備をしています。(19、23行目)
main.jsでは、ジオメトリはライトがいらないPlaneGeometry
、マテリアルはShaderMaterial
を使用しています。(27、28行目)
ここまで何をやっているかよくわからない人はこちらの記事が参考になると思います。
6〜8行目はwindowサイズとカメラの画角を合わせメッシュ位置の指定などをpx
で指定できるようにカメラの距離を求めています。
詳細は次の記事で解説しています。
今回は、Shaderを使うので少し複雑になります。
なので、順序だてて説明していきます。
vertexShader
とfragmentShader
を読み込む
まずは、Shaderを読み込んでみます。
index.htmlでShaderを用意します。(3~5、9~11行目)
main.jsのShaderMaterial
の引数でShaderを読み込みます。(3、4行目)
ここまでで次のようになっていればOKです。
これでShaderで読み込み確認ができました。
index.html・・・ <script id="v-shader" type="x-shader/x-vertex"> void main() { gl_Position = vec4( position, 1.0 ); } </script> <script id="f-shader" type="x-shader/x-fragment"> void main() { gl_FragColor = vec4( 1.0, 0.0, 0.0, 1.0 ); } </script> ・・・
main.js・・・ const shaderMaterial = new THREE.ShaderMaterial({ vertexShader: document.querySelector('#v-shader').textContent, fragmentShader: document.querySelector('#f-shader').textContent, }); let mesh = new THREE.Mesh(planeGeometry, shaderMaterial); scene.add(mesh); ・・・
テクスチャを表示させる
まずは、main.jsでTextureLoader
を用意し、任意の画像を読み込みます。(5、6行目)
※画像はunsplashから拝借しています。
そして、Shaderにtexture
を渡します。(10、12行目)
index.htmlでは、texture
を受け取り画像の座標をマッピングします。
ここまでで次のようになっていればOKですが、画像比率がおかしなことになっています。
main.js・・・ /** * ジオメトリ+マテリアル=メッシュを用意する */ const loader = new THREE.TextureLoader(); const texture = loader.load('https://source.unsplash.com/9orSC4PLJr4/'); const planeGeometry = new THREE.PlaneGeometry(2, 2); const shaderMaterial = new THREE.ShaderMaterial({ uniforms: { uTexture: { value: texture } }, vertexShader: document.querySelector('#v-shader').textContent, fragmentShader: document.querySelector('#f-shader').textContent, }); let mesh = new THREE.Mesh(planeGeometry, shaderMaterial); scene.add(mesh); ・・・
index.html・・・ <script id="v-shader" type="x-shader/x-vertex"> varying vec2 vUv; void main() { vUv = uv; gl_Position = vec4( position, 1.0 ); } </script> <script id="f-shader" type="x-shader/x-fragment"> varying vec2 vUv; uniform sampler2D uTexture; void main() { vec3 color = texture2D( uTexture, vUv ).rgb; gl_FragColor = vec4( color, 1.0 ); } </script> ・・・
background-size: cover;
のようにする
まずは、main.jsでY座標用のアスペクト比とX座標用のアスペクト比をShaderに渡します。(11、12行目)
index.htmlでは、Y座標用のアスペクト比とX座標用のアスペクト比を受け取ります。(4、5行目)
14行目のvUv = uv;
をコメントアウトし、アスペクト比を補正する処理を書きます。(9~12行目)
ここまででbackground-size: cover;
のようになりましたがリサイズに対応していません。
main.js・・・ /** * ジオメトリ+マテリアル=メッシュを用意する */ const loader = new THREE.TextureLoader(); const texture = loader.load('https://source.unsplash.com/9orSC4PLJr4/'); const planeGeometry = new THREE.PlaneGeometry(2, 2); const shaderMaterial = new THREE.ShaderMaterial({ uniforms: { uYAspect: { value: window.innerHeight / window.innerWidth }, uXAspect: { value: window.innerWidth / window.innerHeight }, uTexture: { value: texture } }, vertexShader: document.querySelector('#v-shader').textContent, fragmentShader: document.querySelector('#f-shader').textContent, }); let mesh = new THREE.Mesh(planeGeometry, shaderMaterial); scene.add(mesh); ・・・
index.html・・・ <script id="v-shader" type="x-shader/x-vertex"> varying vec2 vUv; uniform float uYAspect; uniform float uXAspect; void main() { vUv = uv - .5; vUv.y *= min(uYAspect, 1.0); vUv.x *= min(uXAspect, 1.0); vUv += .5; //vUv = uv; gl_Position = vec4( position, 1.0 ); } </script> ・・・
リサイズ処理を追加する
リサイズ処理はY座標用のアスペクト比とX座標用のアスペクト比を計算しなおすだけでOKです。(5、6行目)
これで完成です。
main.js// ブラウザのリサイズに対応させる window.addEventListener('resize', onWindowResize); function onWindowResize() { renderer.setSize(window.innerWidth, window.innerHeight); shaderMaterial.uniforms.uYAspect.value = window.innerHeight / window.innerWidth; shaderMaterial.uniforms.uXAspect.value = window.innerWidth / window.innerHeight; camera.aspect = window.innerWidth / window.innerHeight; distance = (window.innerHeight / 2) / Math.tan(fovRad); camera.position.z = distance; camera.updateProjectionMatrix(); }
おわり
フィードバックを送信
記事についてのフィードバックはTwitterかお問い合わせフォームから受け付けております。