【GLSL】ウィンドウいっぱいに画像を表示したい

GLSL

-ナカタの記憶 vol.1-
幼稚園の頃、カニの絵を描いて市のコンクールに入選したことがある。

いきさつ

Three.jsのShaderMaterialを使っていました。で、シェーダーを書いてました。そしたら「画像をウィンドウいっぱいに表示させたいなぁ」と思いました。表示させたい画像がこちら↓です。ぱくたそさんのものを使わせていただきました。ありがとうございます。

こちら↓がフラグメントシェーダー。

<script id="fs" type="x-shader/x-fragment">
  precision mediump float;

  uniform sampler2D uTexture0;
  in vec2 vUv;
  out vec4 fragColor;

  void main(void) {
    fragColor = texture(uTexture0, vUv);
  }
</script>

これで表示はされるんですけど、ウィンドウサイズを変更すると画像の縦横比が崩れてしまうんですよね↓。

これではイカンという事でインターネッツで調べていたらありました。そのものズバリが。神。
windowサイズいっぱいに広げたPlaneのテクスチャにbackground-size:coverのような挙動をさせる。
こちらのサイトを参考にしながら書き直したシェーダーがこちら↓です。

<script id="fs" type="x-shader/x-fragment">
  precision mediump float;
  
  const vec2 IMAGE_RESOLUTION = vec2(1024.0, 512.0); // 画像の縦横
  uniform vec2 uResolution; // ウィンドウの縦横
  uniform sampler2D uTexture0;
  in vec2 vUv;
  out vec4 fragColor;

  /*
   * ウィンドウのサイズに合わせたテクスチャ座標に変換する
   * background-size: cover と同じ効果
   */
  vec2 getConvUv(vec2 windowRes, vec2 imageRes) {

    vec2 ratio = vec2(
      min((windowRes.x / windowRes.y) / (imageRes.x / imageRes.y), 1.0),
      min((windowRes.y / windowRes.x) / (imageRes.y / imageRes.x), 1.0)
    );

    vec2 convUv = vec2(
      vUv.x * ratio.x + (1.0 - ratio.x) * 0.5,
      vUv.y * ratio.y + (1.0 - ratio.y) * 0.5
    );

    return convUv;
  }

  void main(void) {
    vec2 convUv = getConvUv(uResolution, IMAGE_RESOLUTION);
    fragColor = texture(uTexture0, convUv);
  }
</script>

そしたらこうなりました↓。

はみ出す部分はカットされて縦横比が崩れていませんね。「できたできた」と喜んでいたのですが、ワタクシ全く理解できなかったんですねぇ。ここの所が↓。

vec2 ratio = vec2(
  min((windowRes.x / windowRes.y) / (imageRes.x / imageRes.y), 1.0),
  min((windowRes.y / windowRes.x) / (imageRes.y / imageRes.x), 1.0)
);
vec2 convUv = vec2(
  vUv.x * ratio.x + (1.0 - ratio.x) * 0.5,
  vUv.y * ratio.y + (1.0 - ratio.y) * 0.5
);

これは一体なにをしているのでしょうか…。
というワケで、前置きが長くなりましたが、今回はこのコードを読み解いてみましょうというお話です。頭の中のイメージをうまく言葉にできないせいで、かなりくどい言い回しになります。よろしくお願いします。

2行目

min((windowRes.x / windowRes.y) / (imageRes.x / imageRes.y), 1.0)

先程のコードの2行目だけ抜き出しました。まずはこれの前半部分を見てみましょう。
“windowRes.x / windowRes.y"でウィンドウの縦に対する横の比率が分かります。つまり「このウィンドウはどのぐらいの横長具合なんだい!?」という事を問うているのです。仮に計算結果が1より小さいとなると、このウィンドウは横長ではなく縦長ということになりますが、その「このウィンドウは縦長」という結果に心を引っ張られてはいけません。「縦長だ」と意識してしまうと、この後の計算で何をやっているのか分からなくなってしまうでしょう。重要なのは「横長具合」です。計算の過程においても「私はこのウィンドウがどのぐらい横長なのか知りたいからこの割り算をやっているのだ」と強く意識しましょう。計算結果が1より小さくても「このウィンドウは縦長である」と思わずに「このウィンドウは横長具合が小さい」と思うようにする事が、この問題を解くコツです。

その後の"imageRes.x / imageRes.y"ですが、これも先程と同じです。「この画像はどのぐらいの横長具合なんだい!?」という事を調べたいのです。何度でも言いましょう。「縦長なのか、それとも横長なのか」を調べるために計算しているわけではないのです。あくまでも「横長具合」を調べている。それを忘れないでください。

今度はこの2つの結果を使って割り算をします。「ウィンドウの横長具合」÷「画像の横長具合」です。これは一体なにをしているのか。そうです。「画像の横長具合はウィンドウの横長具合と比べて、どれ程の横長具合なんだい?」という事を問うておるのです。横長具合が同じなら1ぴったり、ウィンドウの横長具合の方が大きければ1より大きくなり、画像の横長具合の方が大きければ1より小さくなるのです。

そしてmin関数を使って、その割り算の結果と1.0を比べて小さい方を抜き取っているのです。ここまでが2行目でやっていること。

3行目

min((windowRes.y / windowRes.x) / (imageRes.y / imageRes.x), 1.0)

2行目とほぼ同じですね。違う所はxとyが逆になっているところ。つまり"windowRes.y / windowRes.x"はウィンドウの横に対する縦の比率を求めています。分かりやすいように2行目と同じ言い方をするならば「このウィンドウはどのぐらいの縦長具合なんだい!?」という問いを投げかけていると言えましょう。何度でも言いたい言葉がある。縦長なのか横長なのか」を調べているのではありません。「縦長具合」を調べているのです。

同じく"imageRes.y / imageRes.x"は画像の横に対する縦の比率を求めています。もう言わなくても分かるでしょう。「この画像はどのぐらいの縦長具合なんだい!?」とおっしゃっているのです。

「ウィンドウの縦長具合」÷「画像の縦長具合」の計算をし、その結果と1.0をmin関数に渡してあげて小さい方を抜き出す。これも2行目と同じですね。3行目は以上です。

実際に計算してみましょう

例えばこんなサイズのウィンドウと画像があったとします↓。

この画像をイイ感じにウィンドウいっぱいに表示させたいのです。早速さっきのコードに数字をあてはめてみましょう。

さっき説明したところまでを再掲します↓。

vec2 ratio = vec2(
  min((windowRes.x / windowRes.y) / (imageRes.x / imageRes.y), 1.0),
  min((windowRes.y / windowRes.x) / (imageRes.y / imageRes.x), 1.0)
);

これに数字を入れていくと…。

vec2 ratio = vec2(
  min((40.0 / 30.0) / (10.0 / 6.0), 1.0),
  min((30.0 / 40.0) / (6.0 / 10.0), 1.0)
);

こうなりますね↑。これを計算していくと…。

vec2 ratio = vec2(
  min(1.33 / 1.66, 1.0),
  min(0.75 / 0.60, 1.0)
);

こうなって…↑。

vec2 ratio = vec2(
  min(0.80, 1.0),
  min(1.25, 1.0)
);

こうなって…↑。

vec2 ratio = vec2(0.8, 1.0);

こうですね!

さて、この0.8と1.0は何を表しているのでしょうか?まだピンときませんね。ピンとこないのでいったん横に置いておいて、もう一度さっきの絵を見てみましょう。

この画像をウィンドウいっぱいに表示したいんでしたよね。しかも画像の縦横比は崩さずに。画像の方が小さいので、画像を何倍かにすれば良さそうですよね。まずは、ウィンドウと画像の横の長さが同じになるように、画像を4倍にして重ねてみましょう。

なんかダメみたいですね。縦がたりていないので、ウィンドウいっぱいにはなりません。今度はウィンドウと画像の縦の長さが同じになるように、画像を5倍にして重ねてみます。

ウィンドウいっぱいになりましたね!横にはみ出していますが、画像の縦横比は崩したくないので、横方向にギュッとするワケにはいきません。はみ出した部分は切り落とすしかないですね。10切り落とせばぴったりです。逆に言うと、画像の横幅50のうち40は残せるワケですね。40っていうと50に0.8を掛けた数ですね。0.8…。

0.8

さっき見ましたよね。

vec2 ratio = vec2(0.8, 1.0);

この0.8は「画像の横幅をどのぐらいまで使えるか」を表していたのです。そして、お隣の1.0は「画像の縦幅をどのぐらいまで使えるか」を表しています。要するに「元々の画像の縦と横それぞれにこの数字を掛けあげると、画像の縦横比がウィンドウの縦横比と同じになりますよ」という事です。分かりますね。

まだ終わりじゃない

vec2 ratio = vec2(
  min((windowRes.x / windowRes.y) / (imageRes.x / imageRes.y), 1.0),
  min((windowRes.y / windowRes.x) / (imageRes.y / imageRes.x), 1.0)
);
vec2 convUv = vec2(
  vUv.x * ratio.x + (1.0 - ratio.x) * 0.5,
  vUv.y * ratio.y + (1.0 - ratio.y) * 0.5
);

今のでやっと4行目までが終わってratioを求めることができました。今度は5行目以降を見ていきましょう。

vUv.x * ratio.x + (1.0 - ratio.x) * 0.5

6行目だけを抜き出しました↑。まずはこれの前半部分だけを考えましょう。

vUv.x * ratio.x

前半部分だけを抜き出しました↑。ここでvUvというのはテクスチャ座標の事です。「画像のこの位置を読み取ってよー」ってヤツですね。そいつにさっき計算したratioのxを掛けています。さっきの例でいうとratio.xは0.8でしたね。絵を見てみましょう↓。

dav

はみ出した部分をカットする事で、縦横比を崩さずウィンドウいっぱいに画像を表示する事ができました。

まだ終わっていない

vUv.x * ratio.x + (1.0 - ratio.x) * 0.5

まだコレ↑の前半部分しか終わっていません。後半部分は何をしているのでしょうか?実はコレ、表示位置をずらしているのです。テクスチャ座標は左下が原点になっているので、先程のようにはみ出した部分をカットするとなんかイヤな感じになります。そこで、この後半部分の計算をしてあげる事でなんかイイ感じになります。絵で説明しましょう↓。

これで原点が画像の中央になったので、はみ出した部分をカットした時の違和感が軽減されたのではないでしょうか。ちなみに7行目は同じ事を縦方向でも行っているだけです。

おわりに

どうでしょうか?分かりましたでしょうか?説明するのが苦手なので分かりにくい所もあるかと思いますが、なんとなくイメージを掴めたのであれば、あとは頭の中で色々考えてみると良いと思います。僕なんか1週間ぐらいこの事を考えていました。ずっと同じ事を考え続けていると分かるようになるものなのですね。今回は以上です。ありがとうございました。

おしゃれ度

★☆☆☆☆

Posted by ナカタ