three.jsでカードゲーム盤面構築

# 概要
[ドグマブレード](https://tsd0313.github.io/ygo-DogmaBlade/dist/)作ったはいいけど、如何にも素人丸出しの貧相なビジュアルやなぁ…ということで
three.jsで奥行ある盤面描写に挑戦していきます。
バージョンは執筆時点で最新の[r119]を使用。
typescriptで書いてますがjsでもほぼ変わりません。

# 下準備
①レンダリングを乗せるcanvasを用意。

```index.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8"/>
  <meta name="3dboard" content="width=device-width, initial-scale=1"/>
  <script src="main.js"></script>
  <style>
    body {
      margin: 0;
      overflow: hidden;
    }
  </style>
</head>
<body>
  <canvas id="myCanvas"></canvas>
</body>
</html>
```

②`npm install three`でインストール。

③WebGL処理を書くmain.tsを用意、threejsをインポート。

```main.ts
import * as THREE from 'three';

window.onload = function() {
  // ここにWebGL処理を書く
}
```

# 基本となる5つのクラス
`Scene` `Light` `Scene` `Camera` `Mesh` `Renderer`について

## `Scene`
3D空間を定義するクラス。
`Scene`インスタンスにカメラ、ライト、オブジェクトをaddしていきます。

```scene.ts
    // シーンを作成
    const scene = new THREE.Scene();
    scene.background = new THREE.Color("#BBFFFF");
```

## `Camera`
3D空間を撮影する視点を定義するクラス。
遠近感が適用される`THREE.PerspectiveCamera` を使用。

```camera.ts
    // カメラを作成
    // (角度,アスペクト比)
    const camera = new THREE.PerspectiveCamera(70, width / height);
    camera.position.set( 0, 150, 180 ); // カメラを置く座標
    camera.lookAt(new THREE.Vector3(0, 0, 0));  // 原点座標を向かせる
    scene.add(camera); // sceneに加える
```

## `Light`
光源を定義するクラス。

```light.ts
    // 光源を作成
    // (光色,光量,照射距離,照射角,ぼかし,減衰率)
    const light = new THREE.SpotLight(0xFFFFFF, 1, 1000, Math.PI / 4, 10, 0.1);
    light.castShadow = true; // 光源側で影を有効に
    light.position.set( 0, 150, 150 ); // 光源を置く座標
    light.lookAt(0,0,0);  // 原点座標を向かせる
    light.shadow.mapSize.width = 2048;  // 影の解像度、デフォルトの512では粗い
    light.shadow.mapSize.height = 2048; // 〃
    light.shadow.radius = 10; // 影の拡張半径
    scene.add(light); // sceneに加える
```

## `Mesh`
3D空間に浮かべるオブジェクトを定義するクラス。
形状`THREE.Geometry`、質感`THREE.Material`を設定する必要があります。

```mesh.ts
    // キューブを作成
    const cubeGeometry = new THREE.BoxGeometry(10, 10, 10); // 形状
    const cubeMaterial = new THREE.MeshBasicMaterial({color:"red"}); // 質感
    const cube = new THREE.Mesh(cubeGeometry, cubeMaterial); // 形状、質感設定
    cube.position.set( 0, 20, 0 ); // キューブを置く座標
    scene.add(cube); // sceneに加える
```

## `Renderer`
カメラ視点から撮影した3D空間をcanvasに描写するクラス。

```renderer.ts
    // サイズを指定
    const width = window.innerWidth;
    const height = window.innerHeight;

    // レンダラーを作成
    const mainCanv = <HTMLCanvasElement>document.querySelector('#myCanvas');
    const renderer = new THREE.WebGLRenderer({
      canvas: mainCanv,
      antialias: true //描画を滑らかにするやつ
    });
    renderer.setPixelRatio(window.devicePixelRatio);
    renderer.setSize(width, height);
    // レンダラー側で影を有効に
    renderer.shadowMap.enabled = true;

    tick();
    // 毎フレーム時に実行されるループイベント
    function tick() {
      // レンダリング
      renderer.render(scene, camera);
      requestAnimationFrame(tick);
      controls.update();
    };
```

# 実践
いよいよ盤面を作っていきます。
## ボード
カードを置く平面。
`THREE.PlaneGeometry`のMeshを寝かせるだけ。

```genBoardBack.ts
const boardBack =  new THREE.Mesh(                                      
    new THREE.PlaneGeometry(200, 200),
    new THREE.MeshStandardMaterial({ 
    color: "palegreen",
    })
);
boardBack.name = "boardBack";
boardBack.rotation.set(-Math.PI/2, 0, 0); // x軸に対して回転
boardBack.receiveShadow = true;
boardBack.castShadow = true;
```
次にボードのゾーン枠を描写します。
`THREE.Line`でシコシコ書いてますが、画像を用意してBoardBackに貼り付けてもOK。

```genZone.ts
const genZone = (mode:"MONSTER"|"OTHER",x:number)=>{
    const points = [
        new THREE.Vector3( -7.5, 0, -10 ),
        new THREE.Vector3( 7.5, 0, -10 ),
        new THREE.Vector3( 7.5, 0, 10 ),
        new THREE.Vector3( -7.5, 0, 10 ),
        new THREE.Vector3( -7.5, 0, -10 )
    ];
    const zoneFrame = new THREE.Line(
        new THREE.BufferGeometry().setFromPoints(points),
        new THREE.LineBasicMaterial({color: "red"})
    );
    zoneFrame.receiveShadow = true;
    const zoneMaterial = new THREE.Mesh(
        new THREE.PlaneBufferGeometry(15, 20),
        new THREE.MeshBasicMaterial({color:"pink"})
    );
    zoneMaterial.receiveShadow = true;
    zoneMaterial.rotation.set(-Math.PI/2, 0, 0);

    const result = new THREE.Group()
    if(mode=="MONSTER"){
        const atk = new THREE.Group().add(zoneFrame,zoneMaterial);
        const def = atk.clone();
        def.rotation.set(0, -Math.PI/2, 0);
        result.add(atk,def);  
    }else if(mode=="OTHER"){
        result.add(zoneFrame,zoneMaterial);  
    };
    result.position.set(x, 0, 0);
    result.receiveShadow = true;
    return result
};
const ZoneGroup = ()=>{
    const ZoneHorizon = (vertical:"front" | "back")=>{
        const zoneHorizonLine = new THREE.Group();
        if(vertical=="back"){
            for (let H = 0; H < 7; H++) {
                zoneHorizonLine.add(
                    genZone("OTHER",-75 + H*25)
                )
            };
            zoneHorizonLine.position.set(0, 0, -12.5)
        }else if(vertical=="front"){
            for (let H = 0; H < 7; H++) {
                if( 1<=H && H<=5){
                    zoneHorizonLine.add(genZone("MONSTER",-75 + H*25))
                }else{
                    zoneHorizonLine.add(
                        genZone("OTHER",-75 + H*25)
                    )
                };
            };
            zoneHorizonLine.position.set(0, 0, 12.5)
        };
        zoneHorizonLine.receiveShadow = true;
        return zoneHorizonLine;
    };
    const playerGroup = new THREE.Group().add(ZoneHorizon("front"),ZoneHorizon("back"));
    playerGroup.position.set(0, 0, -37.5);
    playerGroup.receiveShadow = true;
    const enemyGroup = new THREE.Group().add(ZoneHorizon("front"),ZoneHorizon("back"));
    enemyGroup.position.set(0, 0, 37.5);
    enemyGroup.rotation.set(0, Math.PI, 0);
    enemyGroup.receiveShadow = true;

    const result = new THREE.Group().add(playerGroup,enemyGroup);
    result.position.set(0, 0.1, 0);
    result.receiveShadow = true;
    return result;
};

const boardGroup : THREE.Group = new THREE.Group().add(boardBack,ZoneGroup())
boardGroup.receiveShadow = true;
scene.add(boardGroup);
```

## カード
カードも平面だから`THREE.PlaneGeometry`で…と行きたいところですが

- 表裏に別のテクスチャを貼ることができない
- ボードに影を落とせない

ので`THREE.BoxGeometry`で極薄直方体として描写します。

```genCard.ts
    const genCardImgObj = (openImg:string)=>{
        const loader = new THREE.TextureLoader();
        //6面それぞれにmaterialを設定。表裏以外は黒
        const materialArray = [
            new THREE.MeshBasicMaterial({color:"black"}),
            new THREE.MeshBasicMaterial({color:"black"}),
            new THREE.MeshBasicMaterial({color:"black"}),
            new THREE.MeshBasicMaterial({color:"black"}),
            new THREE.MeshBasicMaterial({map: loader.load(openImg)}),
            new THREE.MeshBasicMaterial({map: loader.load("cardback.jpg" )}),
        ];
        const cardGeometry = new THREE.BoxGeometry(13.5, 18, 0.01); // 厚さ0.01
        const cardMesh = new THREE.Mesh(cardGeometry,materialArray);

        cardMesh.rotation.x = -Math.PI/2;
        cardMesh.castShadow = true;
        cardMesh.receiveShadow = true;
        return cardMesh;
    };

    // 1枚配置
    const AirmanA = genCardImgObj("Airman.jpg");
    AirmanA.position.set(25,1,25);
    scene.add(AirmanA);
    // 40枚配置してデッキに
    for (let i = 0; i < 40; i++) {
        const card = AirmanA.clone();
        card.position.set(75, 1+0.2*(i), 50);
        card.rotation.y = Math.PI;
        scene.add(card);
    };
```

## カメラコントロール
盤面はできましたが、折角作ったオブジェクトを定点カメラで眺めるだけでは寂しいので
ドラッグで視点をグリグリ動かして色んな角度から鑑賞できるようにします。
`OrbitControls`をインポートして呼び出すだけ！

```OrbitControls.ts
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
const controls = new OrbitControls( camera, renderer.domElement );
controls.enableDamping = true; // 動きを滑らかにするやつ
```

## 完成！
![3dss1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/657379/f800d143-a7d0-c958-580e-7334e4902374.png)
![3dss2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/657379/59fb0a64-05b5-40ec-778c-12522eae2223.png)

# 以上
最近触り始めたところなので有識者からのツッコミ、及び便利なライブラリ・ヘルパー情報お待ちしております。
次回はマウスイベントとアニメーションについて書きます。