WebGL で色々な表現に挑戦する - マウスの座標による変化

## 記事の概要

[前回の記事](https://qiita.com/xrxoxcxox/items/7e21802bab3ab6f11614)では時間経過による変化を実現しました。

今回はマウスの座標にあわせて変化させます。

## 画面解像度を取得する

[1 色のグラデーションを作ったときの記事](https://qiita.com/xrxoxcxox/items/146d744bf7f124924eff)で以下のようなシェーダーを書いていました。

```diff_glsl:plane.vert
  #version 300 es

  in vec2 aPosition;
  out vec2 vTextureCoord;

  void main() {
    gl_Position = vec4(aPosition, 0.0, 1.0);
   
    vTextureCoord = aPosition * 0.5 + 0.5;
  }
```

```diff_glsl:plane.frag
  #version 300 es

  precision mediump float;

  in vec2 vTextureCoord;
  out vec4 outColor;

  void main() {
    float r = vTextureCoord.x;
    outColor = vec4(r, 0.0, 0.0, 1.0);
  }
```

マウスの座標にあわせて背景色を変化させる際など、今のままだと形状を制御しづらいです。
そのため、少し書き方を変えます。

```diff_typescript:main.ts
  // シェーダーに渡すデータ
  const positionAttributeLocation = gl.getAttribLocation(
    shaderProgram,
    "aPosition"
  );
  gl.enableVertexAttribArray(positionAttributeLocation);
  gl.vertexAttribPointer(positionAttributeLocation, 2, gl.FLOAT, false, 0, 0);

+ const resolutionUniformLocation = gl.getUniformLocation(shaderProgram, 'uResolution');

  // 描画の実行
- function drawScene() {
+ function drawScene(width: number, height: number) {
+ gl.uniform2f(resolutionUniformLocation, width, height);
  gl.clearColor(0.0, 0.0, 0.0, 1.0);
  gl.clear(gl.COLOR_BUFFER_BIT);
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
+ requestAnimationFrame(() => drawScene(width, height));
  }

  // canvas のリサイズ
  function resizeCanvasToDisplaySize() {
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
    gl.viewport(0, 0, canvas.width, canvas.height);
-   drawScene();
+   drawScene(canvas.width, canvas.height);
  }
```

```diff_glsl:plane.vert
  #version 300 es

  in vec2 aPosition;
- out vec2 vTextureCoord;

  void main() {
    gl_Position = vec4(aPosition, 0.0, 1.0);
-  
-   vTextureCoord = aPosition * 0.5 + 0.5;
  }
```

```diff_glsl:plane.frag
  #version 300 es

  precision mediump float;

- in vec2 vTextureCoord;
+ uniform vec2 uResolution;
  out vec4 outColor;

  void main() {
-   float r = vTextureCoord.x;
+   vec2 uv = gl_FragCoord.xy / uResolution;
-   outColor = vec4(r, 0.0, 0.0, 1.0);
+   outColor = vec4(uv.x, 0.0, 0.0, 1.0);
  }
```

ややこしくなっただけに見えるかもしれませんが、後々役立ちます。

最終的には座標を正規化するタイミングを、頂点シェーダーからフラグメントシェーダーに移しただけです。
ただ、どちらかと言えば重要なのは `uResolution` という名前で画面の解像度を取得したことです。

## マウスの座標を取得し、シェーダーへ送る

まずは TypeScript の変更です。

```diff_typescript:main.ts
  // canvas要素の取得とWebGL2コンテキストの取得
  const canvas = document.getElementById("webgl-canvas") as HTMLCanvasElement;
  const gl = canvas.getContext("webgl2") as WebGL2RenderingContext;

  // 中略

  // シェーダーに渡すデータ
  const positionAttributeLocation = gl.getAttribLocation(
    shaderProgram,
    "aPosition"
  );
  gl.enableVertexAttribArray(positionAttributeLocation);
  gl.vertexAttribPointer(positionAttributeLocation, 2, gl.FLOAT, false, 0, 0);

  const resolutionUniformLocation = gl.getUniformLocation(shaderProgram, 'uResolution');

+ const mousePositionUniformLocation = gl.getUniformLocation(shaderProgram, "uMousePosition");
+
+ let mousePosition = { x: 0, y: 0 };
+
+ function getMousePosition(event: MouseEvent) {
+   mousePosition.x = event.clientX / canvas.width;
+   mousePosition.y = 1.0 - (event.clientY / canvas.height); // Y軸を反転
+ }
+
+ canvas.addEventListener('mousemove', getMousePosition);

  // 描画の実行
  function drawScene(width: number, height: number) {
    gl.uniform2f(resolutionUniformLocation, width, height);
+   gl.uniform2f(mousePositionUniformLocation, mousePosition.x, mousePosition.y);
    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    gl.clear(gl.COLOR_BUFFER_BIT);
    gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
    requestAnimationFrame(() => drawScene(width, height));
  }
```

次にユニフォーム変数として受け取り、`uv.x` を `uMousePosition.x` に変えてみます。

```diff_glsl:plane.frag
  #version 300 es

  precision mediump float;

  uniform vec2 uResolution;
+ uniform vec2 uMousePosition;
  out vec4 outColor;

  void main() {
    vec2 uv = gl_FragCoord.xy / uResolution;
-   outColor = vec4(uv.x, 0.0, 0.0, 1.0);
+   outColor = vec4(uMousePosition.x, 0.0, 0.0, 1.0);
  }
```

これで、マウスの位置にあわせて色が変わりました。

| `uv.x` | `uMousePosition.x` |
| --- | --- |
| ![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214677/77c11a03-6bc5-ecb1-a2c9-3702394c9ce7.png) | ![movie.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214677/2f4c3252-2da7-da4b-1aed-ba4ec6ada6c9.gif) |

グリーン成分などに `uMousePosition.y` を入れれば、マウスを縦横に動かした際背景色が変わります。

## マウスの位置にあわせて色を変える

組み込み関数である `distance()` と `smoothstep()` を組み合わせ、マウスから 200px の範囲だけ色がつくようにしました。

```diff_glsl:plane.frag
  void main() {
    vec2 uv = gl_FragCoord.xy / uResolution;

+   // マウス位置との距離を計算
+   float mouseDistnace = distance(uMousePosition, uv);
+   float mouseColorIntensity = 1.0 - smoothstep(0.0, 200.0 / uResolution.x, mouseDistnace);

-   outColor = vec4(uMousePosition.x, 0.0, 0.0, 1.0);
+   outColor = vec4(mouseColorIntensity, 0.0, 0.0, 1.0);
  }
```

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214677/fb37977f-8b16-6baf-a86a-5ca85470385c.gif)

しかし、赤い範囲が楕円形になっています。
これが先ほど書いていた「形状を制御しづらい」話です。

修正し、画面のアスペクト比によらず円形となるように変えます。

```diff_glsl:plane.frag
  void main() {
    vec2 uv = gl_FragCoord.xy / uResolution;

+   // アスペクト比を考慮したUV座標の計算
+   vec2 aspectCorrectedUV = uv;
+   aspectCorrectedUV.x *= uResolution.x / uResolution.y;

+   // マウス位置のアスペクト比を補正
+   vec2 aspectCorrectedMouse = uMousePosition;
+   aspectCorrectedMouse.x *= uResolution.x / uResolution.y;

    // マウス位置との距離を計算
    float mouseDistnace = distance(uMousePosition, uv);
    float mouseColorIntensity = 1.0 - smoothstep(0.0, 200.0 / uResolution.x, mouseDistnace);

    outColor = vec4(mouseColorIntensity, 0.0, 0.0, 1.0);
  }
```

`uResolution.x / uResolution.y` をかけることで、1:1 の比率となるようにしました。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214677/1adc0e8e-3a4e-f3f1-7900-2fd18038689a.gif)

## 最後に

マウスの位置にあわせた変化ができたので、ユーザーの操作にあわせてインタラクティブに画面を変えられそうです。

次回はスクロール関連の記事を書きたいと思います。