Angular で Three.js 

# Angularプロジェクトを作成する

## プロジェクトを新規作成する

### プロジェクトを作るフォルダを決める

shift + 右クリック
PowerShell ウインドウをここで開く

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142847/3b075740-5933-cd7c-6ae0-7ec157500a57.png)

### PowerShell でコマンドを打つ

```powershell
ng new プロジェクト名
```

今回は、プロジェクト名を `threeTest` としました

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142847/79fe9c96-f805-b4fa-f701-d0447a7a4684.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142847/546f198f-99de-d469-f7cc-d624aa2f9d29.png)

>？ ワークスペースでより厳密なタイプチェックとより厳密なバンドルバジェットを適用しますか？
   この設定は、保守性を向上させ、バグを事前にキャッチするのに役立ちます。
   詳細については、https：//angular.io/strict（y / N）を参照してください。

→ y `Yes`

>？ Angularルーティングを追加しますか？ （y / N）

→ y `Yes`

>？ どのスタイルシート形式を使用しますか？

→ `SCSS`

新規作成できました
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142847/1e018720-ab75-389c-55fa-446b70468eb0.png)

## プロジェクトを起動してみる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142847/198bada7-710b-8b83-f32e-e681e028d806.png)
新しいターミナルを開く
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142847/4dcf74e9-017d-8058-2fd5-3c94de3d41ed.png)

必要なコンポーネントをインストールするコマンドを打つ

```powershell
npm install
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142847/6f409899-6a4a-5cef-4335-030d502e87e0.png)

プロジェクトを起動するコマンドを打つ

```powershell
ng serve --open
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142847/c7b2591f-36c4-37fa-fa5d-c87cf43eaf51.png)
デフォルトのアプリが起動できた
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142847/3479c633-20bb-24c4-6db1-708a5df77712.png)

# Three.js をプロジェクトに追加する

## 準備

### 外部のモジュールを追加

```powershell
npm install three
```

```powershell
npm install @types/three
```

```powershell
npm install troika-three-text
```

```powershell
npm install  @three-ts/orbit-controls
```

package.json がこうなる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142847/d974142a-63a1-f471-0a96-7aaebd3c6563.png)


### プロジェクトにコンポーネントを追加

**Three.js を表示するコンポーネント** を追加する コマンドを打つ

```powershell
ng g c three
```

**Three.js をコントロールするサービス** を追加する コマンドを打つ

```powershell
ng g s three/scene
```

## ソースコードを編集

```html:a

```

```typescript:scene.service.ts
import { Injectable } from '@angular/core';
import * as THREE from 'three';
import { OrbitControls } from '@three-ts/orbit-controls';

@Injectable({
  providedIn: 'root'
})
export class SceneService {

  // シーン
  private scene: THREE.Scene;

  // レンダラー
  private renderer!: THREE.WebGLRenderer;

  // カメラ
  private camera!: THREE.PerspectiveCamera;
  private aspectRatio: number = 0;
  private Width: number = 0;
  private Height: number = 0;

  private GridHelper!: THREE.GridHelper;

  // 初期化
  public constructor() {
    // シーンを作成
    this.scene = new THREE.Scene();
    // シーンの背景を白に設定
    // this.scene.background = new THREE.Color(0xf0f0f0);
    this.scene.background = new THREE.Color( 0xffffff );
    // レンダラーをバインド
    this.render = this.render.bind(this);

  }

  public OnInit(aspectRatio: number,
                canvasElement: HTMLCanvasElement,
                deviceRatio: number,
                Width: number,
                Height: number): void {
    // カメラ
    this.aspectRatio = aspectRatio;
    this.Width = Width;
    this.Height = Height;
    this.createCamera(aspectRatio, Width, Height);
    // 環境光源
    this.add(new THREE.AmbientLight(0xf0f0f0));
    // レンダラー
    this.createRender(canvasElement,
                      deviceRatio,
                      Width,
                      Height);
    // コントロール
    this.addControls();

    // 床面を生成する
    this.createHelper();

  }


  // 床面を生成する
  private createHelper() {
    this.GridHelper = new THREE.GridHelper(200, 20);
    this.GridHelper.geometry.rotateX(Math.PI / 2);
    this.scene.add(this.GridHelper);                      
  }

  // コントロール
  public addControls() {
    const controls = new OrbitControls(this.camera, this.renderer.domElement);
    controls.addEventListener('change', this.render);
  }

  // カメラの初期化
  public createCamera(aspectRatio: number,
                      Width: number, Height: number ) {

    aspectRatio = (aspectRatio === null) ? this.aspectRatio : aspectRatio;
    Width = (Width === null) ? this.Width : Width;
    Height = (Height === null) ? this.Height : Height;

    const target = this.scene.getObjectByName('camera');
    if (target !== undefined) {
      this.scene.remove(this.camera);
    }
    this.camera = new THREE.PerspectiveCamera(
      70,
      aspectRatio,
      0.1,
      1000
    );
    this.camera.position.set(0, -50, 20);
    this.camera.name = 'camera';
    this.scene.add(this.camera);

  }

  // レンダラーを初期化する
  public createRender(canvasElement: HTMLCanvasElement,
                      deviceRatio: number,
                      Width: number,
                      Height: number): void {
    this.renderer = new THREE.WebGLRenderer({
      preserveDrawingBuffer: true,
      canvas: canvasElement,
      alpha: true,    // transparent background
      antialias: true // smooth edges
    });
    this.renderer.setPixelRatio(deviceRatio);
    this.renderer.setSize(Width, Height);
    this.renderer.shadowMap.enabled = true;
  }


  // リサイズ
  public onResize(deviceRatio: number,
                  Width: number,
                  Height: number): void {

    this.camera.updateProjectionMatrix();
    this.renderer.setSize(Width, Height);
    this.render();
  }

  // レンダリングする
  public render() {
    this.renderer.render(this.scene, this.camera);
  }

  // レンダリングのサイズを取得する
  public getBoundingClientRect(): ClientRect | DOMRect  {
    return this.renderer.domElement.getBoundingClientRect();
  }

  // シーンにオブジェクトを追加する
  public add(...threeObject: THREE.Object3D[]): void {
    for (const obj of threeObject) {
      this.scene.add(obj);
    }
  }

  // シーンのオブジェクトを削除する
  public remove(...threeObject: THREE.Object3D[]): void {
    for (const obj of threeObject) {
      this.scene.remove(obj);
    }
  }

  // シーンにオブジェクトを削除する
  public removeByName(...threeName: string[]): void {
    for (const name of threeName) {
      const target = this.scene.getObjectByName(name);
      if (target === undefined) {
        continue;
      }
      this.scene.remove(target);
    }
  }

  // ファイルに視点を保存する
  public getSettingJson(): any {
    return {
      camera: {
        x: this.camera.position.x,
        y: this.camera.position.y,
        z: this.camera.position.z,
      }
    };
  }


}
```

```html:three.component.html
<canvas #myCanvas id="myCanvas" style="overflow:hidden; position: absolute;" (mousedown)="onMouseDown($event)"
(mouseup)="onMouseUp($event)" (mousemove)="onMouseMove($event)">
</canvas>
```

```typescript:three.component.ts
import { AfterViewInit, Component, ElementRef, ViewChild, HostListener, NgZone, OnDestroy } from '@angular/core';
import * as THREE from 'three';

import { SceneService } from './scene.service';

@Component({
  selector: 'app-three',
  templateUrl: './three.component.html',
  styleUrls: ['./three.component.scss'],
})
export class ThreeComponent implements AfterViewInit, OnDestroy {

  @ViewChild('myCanvas', { static: true }) private canvasRef!: ElementRef;


  constructor(private ngZone: NgZone,
              private scene: SceneService) {

    THREE.Object3D.DefaultUp.set(0, 0, 1);
  }

  private get canvas(): HTMLCanvasElement {
    return this.canvasRef.nativeElement;
  }

  ngAfterViewInit() {
    this.scene.OnInit(this.getAspectRatio(),
                      this.canvas,
                      devicePixelRatio,
                      window.innerWidth,
                      window.innerHeight);
    // レンダリングする
    this.animate();
  }

  ngOnDestroy() {
  }

  animate(): void {
    // We have to run this outside angular zones,
    // because it could trigger heavy changeDetection cycles.
    this.ngZone.runOutsideAngular(() => {
      window.addEventListener('DOMContentLoaded', () => {
      this.scene.render();
      });
    });
  }

  // マウスクリック時のイベント
  @HostListener('mousedown', ['$event'])
  public onMouseDown(event: MouseEvent) {
  }

  // マウスクリック時のイベント
  @HostListener('mouseup', ['$event'])
  public onMouseUp(event: MouseEvent) {
  }

  // マウス移動時のイベント
  @HostListener('mousemove', ['$event'])
  public onMouseMove(event: MouseEvent) {
  }

  // ウインドウがリサイズした時のイベント処理
  @HostListener('window:resize', ['$event'])
  public onResize(event: Event) {
    this.scene.onResize(this.getAspectRatio(),
                        window.innerWidth,
                        window.innerHeight - 120);
  }

  private getAspectRatio(): number {
    if (this.canvas.clientHeight === 0) {
      return 0;
    }
    return this.canvas.clientWidth / this.canvas.clientHeight;
  }

}

```

# サンプルファイル

完成形
https://github.com/sasaco/threeTest/archive/refs/heads/master.zip