【クソアプリ】部屋とテレビの大きさを比較するサービスをWebGLでごりごり作っていく

この記事は「[クソアプリ Advent Calendar 2022](https://qiita.com/advent-calendar/2022/kuso-app) ２」の１９日目の記事です。

https://qiita.com/advent-calendar/2022/kuso-app

# 概要

テレビを買うときって、何インチとか何型とか何Vとかいろいろあって良くわからないですよね。
インチをcmに直して横縦のサイズを一覧してくれているサイトもいろいろありますが、数値だけ書かれても実際どのくらいの大きさなのか良くわからないですよね。
何かと比べてどのくらいって言ってくれれば比較しやすいのになと思いますよね。
畳と比較してくれれば大体わかるのになって思いますよね。

そういうのを作っていきましょう。

# 出来たもの

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64098/eda60d9a-3c95-e3ae-2d06-6898d6c19b44.png)

https://resilient-puffpuff-6c1857.netlify.app/

部屋のサイズ（4～12畳）とテレビのサイズ（30～100型）を選ぶと比較が表示されます。
緑のところが畳で黒いところがテレビです。

意外に小さく見えちゃっていますが、家具があったりドアがあったり自分はこの部屋の中にいるんだというところを**脳内で補っていく**と、さすがに８畳に９０型は大きすぎるだろうということがひと目でわかりますね。よかったですね。

# WebGLで作る

壁２つと床１つとテレビを平面で書けばいいだけなので、モデリングツールは使わずに直接ごりごりとWebGLを書いていってみます。
Three.jsなんか使ってしまうと一瞬で終わってしまうので、勉強がてらイチから書いていってみます。

## 参考資料を探す

https://ja.tech.jar.jp/webgl/basics.html

このような記事を熟読しつつコードをコピーしてきて実行してみたりしていきます。
そしてまずはサンプル通りにキューブを出力して喜んでいきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64098/b777f66e-3736-871a-592c-944e4387f8df.png)


## 頂点を用意する

練習は済んだので早速とりかかっていきます。
頂点はこんな感じです。
あとで部屋の大きさは動的に変更したいので、予め変数化しておきます。

```js
function setGeometry(gl) {
  // size
  const roomHeight = 2.4 / 2;
  const roomUnit   = 6;
  const roomWidth  = 0.9 * 4 / 2;
  const roomLength = 0.9 * 3 / 2;
  var positions = transpose4(new Float32Array([
    // west wall
    -roomWidth, -roomHeight,   roomLength,
    -roomWidth,  roomHeight,   roomLength,
     roomWidth, -roomHeight,   roomLength,
    -roomWidth,  roomHeight,   roomLength,
     roomWidth,  roomHeight,   roomLength,
     roomWidth, -roomHeight,   roomLength,

    // south wall
    -roomWidth,  -roomHeight,  -roomLength,
    -roomWidth,   roomHeight,   roomLength,
    -roomWidth,  -roomHeight,   roomLength,
    -roomWidth,  -roomHeight,  -roomLength,
    -roomWidth,   roomHeight,  -roomLength,
    -roomWidth,   roomHeight,   roomLength,

     // floor
    -roomWidth,  roomHeight,  -roomLength,
     roomWidth,  roomHeight,  -roomLength,
     roomWidth,  roomHeight,   roomLength,
    -roomWidth,  roomHeight,  -roomLength,
     roomWidth,  roomHeight,   roomLength,
    -roomWidth,  roomHeight,   roomLength,
  ]));
  gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);
}
```

畳１枚は大体180cm×90cmで2:1になっているとのことだったので、90cm×90cmを何×何に並べていくかで計算できそうですね。
６畳間の場合は90cm×90cmが4×3、８畳間なら4×4といった感じです。

参考イメージ

> ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64098/856b5388-e1ba-3603-93a3-82ff0dda2665.png)

## とりあえず描画

これに法線と頂点カラーも付けて描画するとこんな感じです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64098/c84afbff-7cac-107e-716e-600aa19769e5.png)


形がよくわからないので・・・
さすがにライトは要りますね。

ライトを付けていきましょう。

## 参考資料を探す

https://dev.to/ndesmic/webgl-engine-from-scratch-part-7-diffuse-lighting-3in8
https://developer.mozilla.org/ja/docs/Web/API/WebGL_API/Tutorial/Lighting_in_WebGL
https://scrapbox.io/teamlab-frontend/Three.js%E3%80%803D%E5%85%A5%E9%96%80%E7%B7%A8_~%E3%83%A9%E3%82%A4%E3%83%88%E3%81%A8%E3%83%9E%E3%83%86%E3%83%AA%E3%82%A2%E3%83%AB~

よくわからないけど、面の法線と光源の内積（dot）を取ってあげると同じ面でも光源との距離に応じて違う値を得られるのでその値をハイライトに利用したり減衰に利用したりできるっていう感じですかね。
なるほどねー

> ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64098/7823d19c-19ed-915c-8ad7-7a9fdf50390b.png)
> https://webglfundamentals.org/webgl/lessons/ja/webgl-3d-lighting-point.html

一番シンプルなシェーダーだとこんな感じ。
なるほどねー

```glsl
//fragment shader
void main() {
    mediump float light = dot(vNormal, uLight1[1].xyz);
    gl_FragColor = vColor * vec4(light, light, light, 1);
}
```

## 描画してみる

いろいろ試してみて、
スポットライトのソースも借りてきて使ってみたらこんな感じ。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64098/5144a4fe-7a1b-3abf-e7f2-70911ad48c4a.png)

まあまあいけそうなので、これでよしとします。
（シェーダーはお借りしたものほぼそのままなので載せないでおきます。計算も意味わかってないですし。）

## テレビを置く

テレビを置いていきます。
奥側の壁と同じ頂点の並びと法線を使えばすぐできます。
サイズの方は、入力値がインチなので、cmに直してmに直して16:9三角形のsinΘとcosΘから縦横サイズに変換します。

```js
function setGeometry(gl) {
  // size
  const roomHeight = 2.4 / 2;
  const roomUnit   = 6;
  const roomWidth  = 0.9 * 4 / 2;
  const roomLength = 0.9 * 3 / 2;
  const tvUnit = 100;
  const tvWidth = tvUnit * 2.54 / 100 * 0.87 / 2;
  const tvHeight = tvUnit * 2.54 / 100 * 0.49 / 2;
  const tvLenght = roomLength - 5;
  var positions = transpose4(new Float32Array([
    // west wall
    -roomWidth, -roomHeight,   roomLength,
    -roomWidth,  roomHeight,   roomLength,
     roomWidth, -roomHeight,   roomLength,
    -roomWidth,  roomHeight,   roomLength,
     roomWidth,  roomHeight,   roomLength,
     roomWidth, -roomHeight,   roomLength,

    // south wall
    -roomWidth,  -roomHeight,  -roomLength,
    -roomWidth,   roomHeight,   roomLength,
    -roomWidth,  -roomHeight,   roomLength,
    -roomWidth,  -roomHeight,  -roomLength,
    -roomWidth,   roomHeight,  -roomLength,
    -roomWidth,   roomHeight,   roomLength,

    // tv
    -tvWidth, -tvHeight,   tvLenght,
    -tvWidth,  tvHeight,   tvLenght,
     tvWidth, -tvHeight,   tvLenght,
    -tvWidth,  tvHeight,   tvLenght,
     tvWidth,  tvHeight,   tvLenght,
     tvWidth, -tvHeight,   tvLenght,

     // floor
    -roomWidth,  roomHeight,  -roomLength,
     roomWidth,  roomHeight,  -roomLength,
     roomWidth,  roomHeight,   roomLength,
    -roomWidth,  roomHeight,  -roomLength,
     roomWidth,  roomHeight,   roomLength,
    -roomWidth,  roomHeight,   roomLength,
  ]);
  gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);
}
```

描画するとこんな感じ。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64098/381c6210-ab7c-bcc7-a5c1-798c5858e1bd.png)

だんだん出来てきましたね。
どんどんいきましょう。

## 畳を貼る

「畳の大きさと比較すればテレビがどのくらいの大きさなのか分かる」ということで始めたのだから、**畳のテクスチャは必須**ですよね。

でも、**めんどかった**んですよね。

畳１枚のテクスチャだけ用意すると、それを敷き詰めるには床の平面をもっと分割していかないといけないし、
畳を敷き詰めたテクスチャを用意して一枚の平面に貼るとすると、頂点はそのままでいいけど部屋のバリエーション分の畳を敷き詰めたテクスチャを用意しないといけないので、どっちにしても大変。

ということで閃きました。

「**グリッドを引けばいいじゃない。90cm×90cmで。**」

ついでに、テレビの横幅が畳のどこに当たっているかわかるようにテレビ側からも補助線を引いていきましょう。

## グリッド用のシェーダーを用意する

線を引く用のシンプルなシェーダー。
グリッドは光源の影響を受けなくてよいので何も計算しなくてよい。
線の太さも色も今回は固定とします。

```glsl
  //vertex-shader
  attribute vec4 a_position;
  uniform mat4 u_worldViewProjection;
  
  void main() {
    // Multiply the position by the matrix.
    gl_Position = u_worldViewProjection * a_position;

    gl_PointSize = 10.0;
  }
```

```glsl
  //fragment-shader
  void main() {
    gl_FragColor = vec4(vec3(1.0), 1);
  }
  </script>
```

## グリッド用の頂点を用意する

`gl.LINES`で線を引くため、始点と終点のペアを必要数分用意する。
TVの左右の補助線用の頂点ペアにも用意する。

```js
function setGrid(gl) {
  const gridSize = 0.9;

  var _axis = [];
  for (var ii = 0; ii < roomWidthUnit + 1; ii += 1) {
    _axis.push(-roomWidth + ( ii * gridSize ), roomHeight,  roomLength);
    _axis.push(-roomWidth + ( ii * gridSize ), roomHeight, -roomLength);
  }
  for (var jj = 0; jj < roomLengthUnit + 1; jj += 1) {
    _axis.push( roomWidth, roomHeight, -roomLength + ( jj * gridSize ));
    _axis.push(-roomWidth, roomHeight, -roomLength + ( jj * gridSize ));
  }

  var axis = transpose4(new Float32Array([
    ..._axis,

    // tv
    -tvWidth,  roomHeight, roomLength,
    -tvWidth, -roomHeight, roomLength,

     tvWidth,  roomHeight, roomLength,
     tvWidth, -roomHeight, roomLength,
  ]));
  gl.bufferData(gl.ARRAY_BUFFER, axis, gl.STATIC_DRAW);
}
```

## シェーダーを切り替えてグリッドを引く

単純に、前の描画処理の後に`useProgram`でシェーダーを切り替えて次の描画を開始すればよさそうでした。
worldViewProjectionMatrixは使いまわします。

```js
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  // 部屋とテレビ用のシェーダー
  gl.useProgram(program);
  // 部屋用の前処理
  ...省略
  // 部屋を描画
  gl.drawArrays(primitiveType, offset, count);

  // シェーダーを切り替える
  gl.useProgram(program_grid);
  gl.enableVertexAttribArray(positionLocation_grid);
  gl.bindBuffer(gl.ARRAY_BUFFER, axisBuffer);
  gl.vertexAttribPointer(
    positionLocation_grid, size, type, normalize, stride, offset);
  gl.uniformMatrix4fv(worldViewProjectionLocation_grid, false, worldViewProjectionMatrix);

  // 直線を描画
  var primitiveType = gl.LINES;
  var offset = 0;
  var count = ( roomWidthUnit + 1 + roomLengthUnit + 1 + 2) * 2;
  gl.drawArrays(primitiveType, offset, count);
```

## 描画する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64098/8487ba7c-dfd2-0620-b627-c931242b77e0.png)

いい感じですね。
補助線のおかげで大体の長さも分かりますし。

## コントロールをつけてスタイルを調整して完成

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64098/1dbb73c9-c5c6-7cdc-fcf3-f71308b5f3d1.png)

出来ました :clap: :clap: 

Netlifyにアップして完成です。

https://resilient-puffpuff-6c1857.netlify.app/

:clap: :clap: 

# 終わり

お疲れ様でした。
絶妙に役に立たないものが出来て満足です。