threejs + typescript + webpackの環境構築

threejsで大規模なモデル操作とか始めるとTypescriptの型サポートはとても助かる。

以前にも書いていたことがあったが、古かったので消して、代わりに2020年度時点の環境設定を残しておく。

package.json
{
  "name": "threejs-tutorial",
  "version": "0.0.1",
  "description": "threejs-tutorial",
  "main": "src/index.ts",
  "scripts": {
    "build": "webpack --mode production",
    "dev": "webpack-dev-server",
    "start": "webpack-dev-server --open --mode development"
  },
  "devDependencies": {
    "@types/dat.gui": "^0.7.5",
    "@types/three": "^0.103.2",
    "css-loader": "^3.5.3",
    "html-webpack-plugin": "^4.3.0",
    "style-loader": "^1.2.1",
    "ts-loader": "^7.0.4",
    "typescript": "^3.9.3",
    "webpack": "^4.43.0",
    "webpack-cli": "^3.3.11",
    "webpack-dev-server": "^3.11.0",
    "worker-plugin": "^4.0.3"
  },
  "dependencies": {
    "dat.gui": "^0.7.7",
    "three": "^0.116.1"
  }
}
tsconfig.json

Typescriptの設定。

{
    "compilerOptions": {
        "target": "es2019",
        "module": "esnext",
        "moduleResolution": "node",
        "strict": true,
        "sourceMap": true,
        "esModuleInterop": true,
        "forceConsistentCasingInFileNames": true
    }
}
webpack.config.js

webpackの設定。babelは使っていない。

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: {
    app: './src/index.ts',
  },
  devtool: 'inline-source-map',
  devServer: {
    contentBase: './dist'
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: 'threejs tutorial',
      meta: [
        {viewport: 'width=device-width, initial-scale=1'},
      ],
    })
  ],
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: 'ts-loader',
        exclude: /node_modules/
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      }
    ]
  },
  resolve: {
    extensions: ['.tsx', '.ts', '.js']
  },
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
};
src/index.ts

GLTFLoaderでglbのモデルを読み込んで、OrbitControlsでマウスでぐるぐるできるだけの簡単なサンプル。 このくらいだと型定義はほとんどないけど。

import * as THREE from 'three'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import './style.css';

function createRenderer() {
    const scene = new THREE.Scene();
    const renderer = new THREE.WebGLRenderer({
        antialias: true
    });
    renderer.setClearColor(0x111111);
    renderer.setSize(window.innerWidth, window.innerHeight);

    const planeGeometory = new THREE.PlaneGeometry(60, 20);
    const planeMaterial = new THREE.MeshBasicMaterial({
        color: 0xcccccc
    });
    const planeMesh = new THREE.Mesh(planeGeometory, planeMaterial);
    planeMesh.rotation.x = -0.5 * Math.PI;
    planeMesh.position.x = 15;
    planeMesh.position.y = 0;
    planeMesh.position.z = 0;
    scene.add(planeMesh);

    const camera = new THREE.PerspectiveCamera(
        45,
        window.innerWidth/window.innerHeight,
        0.1,
        1000
    );
    camera.position.x = -30;
    camera.position.y =  40;
    camera.position.z = 30;
    camera.lookAt(scene.position);

    const light = new THREE.DirectionalLight(0xffffff);
    light.position.set(1, 1, 1).normalize();
    scene.add(light);

    const loader = new GLTFLoader();
    loader.load('./torus.glb', (glb) => {
        glb.scene.scale.set(10, 10, 10);
        scene.add(glb.scene);
        console.log('glb model loaded');
    });

    document.body.appendChild(renderer.domElement);

    const controls = new OrbitControls(camera, renderer.domElement);
    controls.update();

    return () => renderer.render(scene, camera);
}

function startAnimation(updateView: Function) {
    const update = () => {
        requestAnimationFrame(update);
        updateView();
    }
    update();
}

window.addEventListener("DOMContentLoaded", () => {
    const updateView = createRenderer();
    startAnimation(updateView);
});
src/style.css

全画面表示のためのスタイル

body {
    margin: 0;
    overflow: hidden;
}

あと、dist/torus.glbを適当につくって置いておけばOK。