📌

【Three.js】ShaderMaterialを使ってテクスチャを画面にフィットさせる

2024/01/18に公開

Three.jsでShaderMaterialを使ってテクスチャをブラウザの画面にフィットさせる方法について紹介します。この記事では、開発環境としてViteを採用していますが、紹介する考え方は他の開発環境でも同様に適用可能です。

全体のコードの概要

ここでは、Planeメッシュを作成し、ShaderMaterialを使用してテクスチャを適用する方法を紹介します。詳細なコードは以下のリンクから確認できます。

https://github.com/yend724/yend-playground/blob/main/src/three-fit-texure/assets/ts/index.ts

HTMLはシェーダー用のscript要素とcanvas要素を含む以下の構成を想定しています。

HTML
<!-- 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は画面サイズにぴったり合うようになります。

参考:WebGL のモデル、ビュー、投影

PlaneGeometry
const geometry = new THREE.PlaneGeometry(2, 2);
vertexShader
varying vec2 vUv;
void main() {
  vUv = uv;
  // MVPの変換を行わないので、そのままのpositionを渡す
  gl_Position = vec4(position, 1.0);
}

カメラに関しては、PerspectiveCameraOrthographicCameraの代わりに、基本クラスのCameraを使用します何も設定していないOrthographicCameraを使用します。

これは、vertexShaderでMVP変換を行わないので、カメラの設定の影響を受けないためです。ただし、描画するにあたりrendererにはカメラオブジェクトが必須なため、カメラ自体は作成する必要があります。

camera
const camera = new THREE.OrthographicCamera();
// パフォーマンスを考慮する場合、matrixAutoUpdateをfalseにする
camera.matrixAutoUpdate = false;

リサイズ処理

ブラウザのリサイズ時は、rendererを更新して適切な表示を維持します。またuniformで渡しているuScreenAspectShaderMaterialの設定を参照)の更新も忘れないようにしてください。

Planeの大きさを変更する
// 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変数として、uTextureuTextureAspectuScreenAspectを設定しています。uTextureAspectはテクスチャのアスペクト比、uScreenAspectは画面のアスペクト比です。

ShaderMaterialの設定
// 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はフラグメントシェーダーに渡すための変数です。

vertexShader
varying vec2 vUv;
void main() {
  vUv = uv;
  gl_Position = vec4(position, 1.0);
}

幅・高さともに画面と同じ大きさにする

この方法はテクスチャのアスペクト比を無視し、画面に完全にフィットさせます。CSSのwidth:100%;height:100%;のような効果を得られます。

fragmentShader
uniform sampler2D uTexture;
varying vec2 vUv;
void main() {
  vec4 color = texture2D(uTexture, vUv);
  gl_FragColor = color;
}

幅・高さともに画面と同じ大きさにするDEMO

テクスチャのアスペクト比を保ったまま幅を画面にフィットさせる

この方法はテクスチャのアスペクト比を保った状態で、幅のみを画面にフィットさせます。CSSのwidth:100%;height:auto;のような効果を得られます。

fragmentShader
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%;のような効果を得られます。

fragmentShader
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;のような効果を得られます。

fragmentShader
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;
}

画面を覆うように表示するDEMO

画面に収まるように表示する

この方法はテクスチャを画面に収まるように表示させます。テクスチャのアスペクト比は保持され、CSSのbackground-size:contain;のような効果を得られます。

fragmentShader
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);
}

画面に収まるように表示するDEMO

画面に収まるように表示しつつリピートする

この方法はテクスチャを画面に収めつつ、空いたスペースにテクスチャをリピートされるように表示させます。CSSのbackground-size:contain;background-repeat:repeat;とのような効果を得られます。

fragmentShader
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;
}

画面に収まるように表示しつつリピートするDEMO

おわりに

ShaderMaterialを使用してテクスチャをブラウザの画面にフィットさせる方法についてご紹介しました。本記事の内容は非常に基本的なものですが、細かい箇所で忘れがちな点も多いため、自分用の備忘録も兼ねてまとめてみました。改めて思考が整理されたので良かったです。

参考

https://threejs.org/
https://zenn.dev/bokoko33/articles/bd6744879af0d5

Discussion