こんにちは。
株式会社Oh my teethでテックリードをやっているTerukiです。
テック企業を自称しているのにQiitaなどの技術系記事が無いのは如何なものかということで今月から不定期で記事を上げていきたいと思います
TL;DR
- この記事を読めばミニマムな構成でThree.jsが動かせるようになる
- Oh my teethの無料歯型スキャン予約時にプロモーションコード「QiitaSTL」を入力すると契約時にスキャンしたSTLファイルが貰える
概要
(Three.jsの話に入る前に)Oh my teethでは矯正が可能か調べるために無料歯型スキャンを行っています。
その時にスキャンした歯型データを以下のようなビューアでユーザーが触れるようになっています。
もちろん、ユーザー操作でカメラ移動や拡大などもできます。
このビューアの実装にThree.jsが使われています。
この記事では最小構成でThree.jsを使ったSTLデータのレンダリングまで説明できればと思います。
準備するもの
- NodeJS
- 適当なエディタ
基本的にTypeScriptで書いていこうと思います。
やってみる
この記事ではStanford Bunnyのstlファイルをレンダリングしてドラッグでカメラ移動出来るところまでやってみようと思います。
最終形のサンプルのリポジトリはこちら。git cloneしてお使いいただければ。
プロジェクト作成
今風にサクッとviteでやってみます。
npm create vite@latest
ここではプロジェクト名はデフォルトのまま、フレームワークはVanilla
、VariantはTypeScriptを選びました。
ここで一度起動してみます。
cd vite-project
npm install
npm run dev
Three.jsインストール
さっそく本題に入っていきます。
npm install --save-dev @types/three
npm install --save three
Three.jsとその型定義をインストールします。
今回使うSTLファイルもダウンロードしてpackage.jsonがあるディレクトリに置いておきます。
viteを起動しておきましょう。
npm run dev
STLファイルをレンダリングしてみる
main.tsをThree.jsの公式ドキュメントを参考に書き換えてみます。
import * as THREE from 'three';
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader';
const wrapper = document.getElementById('app') as HTMLDivElement;
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
wrapper.appendChild(renderer.domElement);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 2000);
camera.position.z = 300;
// STLファイルをロードしてレンダリング
const stlLoader = new STLLoader();
stlLoader.load('./Stanford_Bunny.stl', (geometry) => {
const material = new THREE.MeshBasicMaterial({
color: 0xFFFFFF
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
renderer.render(scene, camera);
});
謎の白いシルエットが出てきました。これだとどの辺がうさぎなのかよく分かりませんね
光を当ててみる
STLファイルをレンダリングすること自体はできたようですが、これでは3Dモデル感がないので適当に光源を設定して立体感が出るようにしてみます。
import * as THREE from 'three';
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader';
const wrapper = document.getElementById('app') as HTMLDivElement;
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
wrapper.appendChild(renderer.domElement);
const scene = new THREE.Scene();
// ライティングの設定
const light1 = new THREE.DirectionalLight(0xFFFFFF);
light1.position.set(0, 0, -1);
const light2 = new THREE.DirectionalLight(0xFFFFFF);
light2.position.set(0, 0, 1);
const light3 = new THREE.AmbientLight(0x404040);
scene.add(light1);
scene.add(light2);
scene.add(light3);
const camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 2000);
camera.position.z = 300;
// STLファイルをロードしてレンダリング
const stlLoader = new STLLoader();
stlLoader.load('./Stanford_Bunny.stl', (geometry) => {
const material = new THREE.MeshPhongMaterial({
color: 0xFFFFFF
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
renderer.render(scene, camera);
});
どうでしょう。さっきよりはよっぽど3Dモデル感が出たのではないでしょうか。
操作できるようにしてみる
これだけだと面白みが足りないのでマウスで操作できるようにしてみます。
import * as THREE from 'three';
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
const wrapper = document.getElementById('app') as HTMLDivElement;
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
wrapper.appendChild(renderer.domElement);
const scene = new THREE.Scene();
// ライティングの設定
const light1 = new THREE.DirectionalLight(0xFFFFFF);
light1.position.set(0, 0, -1);
const light2 = new THREE.DirectionalLight(0xFFFFFF);
light2.position.set(0, 0, 1);
const light3 = new THREE.AmbientLight(0x404040);
scene.add(light1);
scene.add(light2);
scene.add(light3);
const camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 2000);
camera.position.z = 300;
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableZoom = true;
controls.enableDamping = true;
controls.dampingFactor = 0.1;
// STLファイルをロードしてレンダリング
const stlLoader = new STLLoader();
stlLoader.load('./Stanford_Bunny.stl', (geometry) => {
const material = new THREE.MeshPhongMaterial({
color: 0xFFFFFF
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
// レンダリングループ
const tick = () => {
controls.update();
renderer.render(scene, camera);
requestAnimationFrame(tick);
}
tick();
});
回転するようになりましたが横方向の回転の様子がおかしいです。
左から右にドラッグしたら時計回りに回転してしまいます。
これはSTLの回転軸とシーンの軸がズレているためなのでシーンを回転してあげれば良さそうです。
Oh my teethでも使用する3Dスキャナーの種類によって回転軸が異なり、ビューア側で個別対応する必要があったため苦労しました。
const scene = new THREE.Scene();
// 回転軸をオブジェクトと合わせて直感的な操作になるようにしておく
scene.rotateX(-90 * (Math.PI / 180));
良い感じです。微妙に右上に寄ってるのが気になるので中央に配置されるようにしてみます。
const mesh = new THREE.Mesh(geometry, material);
mesh.geometry.center(); // オブジェクトを中央に配置
scene.add(mesh);
かなり良さそうです!
Three.jsを使えばほんの50行程度のコードでモデルを触れるようになります。
今回はSTLファイルでしたがThree.jsにはSTL以外のローダーもあるのでローダーを差し替えれば他の形式のファイルでも使えるかもしれません。
最終形のコードはこちら:
https://github.com/ohmyteeth/threejs-stl-sample/blob/main/src/main.ts
歯型STLプレゼントキャンペーン
Oh my teethはマウスピース矯正ブランドの1つです。
詳細なサービスの説明はQiitaでやることでもないと思うので公式サイトにお任せしますが、tech系の内容については以前Techableに寄稿したこちらの記事が面白いかもしれません。
また、マウスピース矯正に興味がある方に無料歯型スキャンでスキャンしたSTLデータをご契約後にお渡しするキャンペーンをやりたいと思います
ご契約後に私から上顎と下顎のSTLファイルをお渡しします!
※不適合(Oh my teethのマウスピース矯正では直せない重度のがたつきなど)になり契約できなかった場合もお渡しします
おわりに
歯科業界は電話での予約や紙の問診票など、かなりアナログなイメージがある方が多いと思いますがそれと比較してOh my teethはかなりぶっ飛んだことをやっているつもりなので興味がある方は是非ストアに来ていただければと思います。
Oh my teethは未来の歯科体験を実現していくミッションがありますので、テクノロジーに力を入れているところをどんどん発信していけたらと思っています。
ではでは!