【Three.js】ShaderMaterialを使ってテクスチャを画面にフィットさせる
Three.jsでShaderMaterial
を使ってテクスチャをブラウザの画面にフィットさせる方法について紹介します。この記事では、開発環境としてViteを採用していますが、紹介する考え方は他の開発環境でも同様に適用可能です。
全体のコードの概要
ここでは、Plane
メッシュを作成し、ShaderMaterial
を使用してテクスチャを適用する方法を紹介します。詳細なコードは以下のリンクから確認できます。
HTMLはシェーダー用のscript
要素とcanvas
要素を含む以下の構成を想定しています。
<!-- script要素 -->
<script id="vertexShader" type="x-shader/x-vertex">
//...頂点シェーダー
</script>
<script id="fragmentShader" type="x-shader/x-fragment">
//...フラグメントシェーダー
</script>
<!-- canvas要素 -->
<canvas id="canvas"></canvas>
Planeを画面サイズと同じ大きさにする
PlaneGeometry
のサイズを2
に設定しています。vertexShader
で、MVPの変換を行わず、頂点のposition
をそのままgl_Position
に渡しています。結果として、描画される頂点座標は何も変換がされていない(-1,-1,-1)
から(1,1,1)
までの範囲(正規化デバイス座標)に収まります。PlaneGeometry
のサイズを2
に設定することで、Plane
は画面サイズにぴったり合うようになります。
const geometry = new THREE.PlaneGeometry(2, 2);
varying vec2 vUv;
void main() {
vUv = uv;
// MVPの変換を行わないので、そのままのpositionを渡す
gl_Position = vec4(position, 1.0);
}
カメラに関しては、何も設定していないPerspectiveCamera
やOrthographicCamera
の代わりに、基本クラスのCamera
を使用しますOrthographicCamera
を使用します。
これは、vertexShader
でMVP変換を行わないので、カメラの設定の影響を受けないためです。ただし、描画するにあたりrenderer
にはカメラオブジェクトが必須なため、カメラ自体は作成する必要があります。
const camera = new THREE.OrthographicCamera();
// パフォーマンスを考慮する場合、matrixAutoUpdateをfalseにする
camera.matrixAutoUpdate = false;
リサイズ処理
ブラウザのリサイズ時は、renderer
を更新して適切な表示を維持します。またuniform
で渡しているuScreenAspect
(ShaderMaterialの設定を参照)の更新も忘れないようにしてください。
// windowのリサイズ処理
const onResize = () => {
const windowSize = getWindowSize();
// uniformで渡しているwindowのアスペクト比を更新
material.uniforms.uScreenAspect.value = windowSize.aspect;
// rendererを更新
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(windowSize.width, windowSize.height);
};
window.addEventListener('resize', onResize);
ShaderMaterialの設定
ShaderMaterial
に渡すuniform
変数は以下のように設定します。
uniform
変数として、uTexture
、uTextureAspect
、uScreenAspect
を設定しています。uTextureAspect
はテクスチャのアスペクト比、uScreenAspect
は画面のアスペクト比です。
// uniform変数の一覧
const uniforms = {
uTexture: {
value: texture,
},
uTextureAspect: {
value: textureAspect,
},
uScreenAspect: {
value: screenAspect,
},
};
// ShaderMaterialの設定
const material = new THREE.ShaderMaterial({
uniforms,
vertexShader: VertexShader,
fragmentShader: FragmentShader,
});
テクスチャをブラウザの画面にフィットさせる
ブラウザの画面に合わせてテクスチャをフィットさせるには、ブラウザとテクスチャのアスペクト比に基づいてUV座標を計算します。ここでは画面にフィットさせるいくつかのパターンを紹介します。
「Planeを画面サイズと同じ大きさにする」の箇所で、すでにコードを見てしまいましたが、頂点シェーダーは次の共通のコードを使用しています。頂点シェーダーではvarying
を用いたUV座標の受け渡しを行なっています。vUv
はフラグメントシェーダーに渡すための変数です。
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = vec4(position, 1.0);
}
幅・高さともに画面と同じ大きさにする
この方法はテクスチャのアスペクト比を無視し、画面に完全にフィットさせます。CSSのwidth:100%;height:100%;
のような効果を得られます。
uniform sampler2D uTexture;
varying vec2 vUv;
void main() {
vec4 color = texture2D(uTexture, vUv);
gl_FragColor = color;
}
テクスチャのアスペクト比を保ったまま幅を画面にフィットさせる
この方法はテクスチャのアスペクト比を保った状態で、幅のみを画面にフィットさせます。CSSのwidth:100%;height:auto;
のような効果を得られます。
uniform sampler2D uTexture;
uniform float uTextureAspect;
uniform float uScreenAspect;
varying vec2 vUv;
void main() {
// アスペクト比からテクスチャの比率を計算
// 幅は常に1.0にする
vec2 ratio = vec2(
1.0,
uTextureAspect / uScreenAspect
);
// 中央に配置するための計算
vec2 textureUv = vec2(
(vUv.x - 0.5) * ratio.x + 0.5,
(vUv.y - 0.5) * ratio.y + 0.5
);
vec4 color = texture2D(uTexture, textureUv);
// テクスチャの範囲外は黒にする
vec4 black = vec4(0.0, 0.0, 0.0, 1.0);
float outOfBounds = float(textureUv.x < 0.0 || textureUv.x > 1.0 || textureUv.y < 0.0 || textureUv.y > 1.0);
gl_FragColor = mix(color, black, outOfBounds);
}
テクスチャのアスペクト比を保ったまま幅を画面にフィットさせるDEMO
テクスチャのアスペクト比を保ったまま高さを画面にフィットさせる
この方法はテクスチャのアスペクト比を保った状態で、高さのみを画面にフィットさせます。CSSのwidth:auto;height:100%;
のような効果を得られます。
uniform sampler2D uTexture;
uniform float uTextureAspect;
uniform float uScreenAspect;
varying vec2 vUv;
void main() {
// アスペクト比からテクスチャの比率を計算
// 高さは常に1.0にする
vec2 ratio = vec2(
uScreenAspect / uTextureAspect,
1.0
);
// 中央に配置するための計算
vec2 textureUv = vec2(
(vUv.x - 0.5) * ratio.x + 0.5,
(vUv.y - 0.5) * ratio.y + 0.5
);
vec4 color = texture2D(uTexture, textureUv);
// テクスチャの範囲外は黒にする
vec4 black = vec4(0.0, 0.0, 0.0, 1.0);
float outOfBounds = float(textureUv.x < 0.0 || textureUv.x > 1.0 || textureUv.y < 0.0 || textureUv.y > 1.0);
gl_FragColor = mix(color, black, outOfBounds);
}
テクスチャのアスペクト比を保ったまま高さを画面にフィットさせるDEMO
画面を覆うように表示する
この方法はテクスチャを画面に覆うように表示させます。テクスチャのアスペクト比は保持され、CSSのbackground-size:cover;
のような効果を得られます。
uniform sampler2D uTexture;
uniform float uTextureAspect;
uniform float uScreenAspect;
varying vec2 vUv;
void main() {
// アスペクト比からテクスチャの比率を計算
vec2 ratio = vec2(
min(uScreenAspect / uTextureAspect, 1.0),
min(uTextureAspect / uScreenAspect, 1.0)
);
// 中央に配置するための計算
vec2 textureUv = vec2(
(vUv.x - 0.5) * ratio.x + 0.5,
(vUv.y - 0.5) * ratio.y + 0.5
);
vec4 color = texture2D(uTexture, textureUv);
gl_FragColor = color;
}
画面に収まるように表示する
この方法はテクスチャを画面に収まるように表示させます。テクスチャのアスペクト比は保持され、CSSのbackground-size:contain;
のような効果を得られます。
uniform sampler2D uTexture;
uniform float uTextureAspect;
uniform float uScreenAspect;
varying vec2 vUv;
void main() {
// アスペクト比からテクスチャの比率を計算
vec2 ratio = vec2(
max(uScreenAspect / uTextureAspect, 1.0),
max(uTextureAspect / uScreenAspect, 1.0)
);
// 中央に配置するための計算
vec2 textureUv = vec2(
(vUv.x - 0.5) * ratio.x + 0.5,
(vUv.y - 0.5) * ratio.y + 0.5
);
vec4 color = texture2D(uTexture, textureUv);
// テクスチャの範囲外は黒にする
vec4 black = vec4(0.0, 0.0, 0.0, 1.0);
float outOfBounds = float(textureUv.x < 0.0 || textureUv.x > 1.0 || textureUv.y < 0.0 || textureUv.y > 1.0);
gl_FragColor = mix(color, black, outOfBounds);
}
画面に収まるように表示しつつリピートする
この方法はテクスチャを画面に収めつつ、空いたスペースにテクスチャをリピートされるように表示させます。CSSのbackground-size:contain;background-repeat:repeat;
とのような効果を得られます。
uniform sampler2D uTexture;
uniform float uTextureAspect;
uniform float uScreenAspect;
varying vec2 vUv;
void main() {
// アスペクト比からテクスチャの比率を計算
vec2 ratio = vec2(
max(uScreenAspect / uTextureAspect, 1.0),
max(uTextureAspect / uScreenAspect, 1.0)
);
// 中央に配置するための計算
vec2 textureUv = vec2(
(vUv.x - 0.5) * ratio.x + 0.5,
(vUv.y - 0.5) * ratio.y + 0.5
);
// fractでリピートする
vec4 color = texture2D(uTexture, fract(textureUv));
gl_FragColor = color;
}
おわりに
ShaderMaterial
を使用してテクスチャをブラウザの画面にフィットさせる方法についてご紹介しました。本記事の内容は非常に基本的なものですが、細かい箇所で忘れがちな点も多いため、自分用の備忘録も兼ねてまとめてみました。改めて思考が整理されたので良かったです。
参考
Discussion