threejs-towers について
threejs-towersは、Three.jsで建築物をプロシージャルにモデリングし、建設アニメーションを作るためのガイドです。建築シーンを作る開発者向けに、再利用可能なジオメトリビルダーとパラメーター化した屋根や様式を扱います。
主な機能
- 01共有配列を使ったジオメトリ構築
- 02パラメーターによる複数様式の定義
- 03関数による曲面屋根の生成
- 04クリッピング平面を使った建設アニメーション
- 05マテリアル単位の統合による描画呼び出し削減
活用シーン
- 01塔や屋根のプロシージャルモデリング
- 02多様な様式を持つ建築シーンの構築
- 03足場と段階表示を備えた建設アニメーションの作成
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ mengto/ skills --skill threejs-towersインストール後、Claude Code で/threejs-towersを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
agents/openai.yamldemo/index.htmlPROMPT.md詳細
Three.js Towers
プロシージャル生成の効果がすぐに現れる分野は、建築です。建物はパラメーター化された部材の繰り返しでできているため、ひと通りの語彙をそろえれば、別の様式を作るコストはモデルではなくパラメーターセットで済みます。
threejs-landscape に建物を配置し、threejs-weather で風化させましょう。
建物を作る前に語彙をそろえる
共有の頂点/法線/UV/インデックス配列にすべて追加する小さなビルダーを10個書き、その後は生のジオメトリを二度と書かないようにします。
face(P, uv) quad with a computed normal
box(cx,cy,cz, sx,sy,sz, ry, uvScale, rz)
prism(y0,y1, R0,R1, uv, capTop, capBot) square on plan
prismN(y0,y1, R0,R1, n, uv, capTop, capBot) any n-gon
sweepPlan(plan, y0,y1, steps, profile) an outline scaled up a curve
lathe(cx,cy,cz, y0,y1, profile, steps, seg) surface of revolution
ring / arc annulus on a face plane
fan(points, z, ry, cx, cy) filled polygon on a face
tube(points, r, sides) swept tube
このセットで、日本の城、中国の仏塔、オスマン様式のドーム、クメールのプラサート、ベトナムのタップを表現できます。それぞれの様式は、組み立て関数ひとつと、階層のテーブルで実現できます。
部位ごとではなく、マテリアルごとにマージします。建物を6棟、11~18回のドローコールで描画できるかどうかが、120fpsと30fpsの差になります。
最もよく遭遇するバグは、頂点の巻き順です。 側面の頂点が内向きの順に並んだプリズムは、バックフェースカリングで描画されず、奥の壁の内側を見ることになります。まずプリズムひとつを正しく作り、その頂点順をどこでも使い回しましょう。面が消えたら、何より先に巻き順を疑ってください。
屋根は軒に沿った位置の関数です
東アジアの屋根の特徴――棟の近くでは平らで、そこから勾配が急になり、角が反り上がる形――は、(パネル、横方向の位置、下方向の位置) から点への変換を行う関数ひとつで表現できます。
function roofPoint(o, p, u, t) {
const tc = Math.min(1, t);
const g = 1 - Math.pow(1 - tc, o.pow); // the sag of the slope
const corner = Math.pow(Math.abs(u), 2.0); // 0 mid-eave, 1 at hips
const flare = 1 + o.flare * corner * Math.pow(tc, 3.2);
const y = o.yT - (o.yT - o.yE) * g
+ o.lift * corner * Math.pow(tc, 2.6); // corners lift
return [ /* lerp inner edge → outer edge × flare */ ];
}
6つの値で、このセットのすべての屋根を表現できます。lift、tip、flare、pow、trunc、ridge です。見落としやすいのは trunc です。これによって、下の屋根が上の壁の背後で自然に途切れ、壁を突き抜けなくなります。
Math.pow(negative, 2.35) は NaN になります。 軒を越えて伸びた瓦によって t が1を超えると、屋根全体が何事もなく消えてしまいます。tc = Math.min(1, t) でクランプし、はみ出した部分は別の線形項として処理します。
正方形ではない平面形状
prismN は八角形や十六角形の胴部に対応します。クメールのプラサートのように、隅が入り組んだ形状――正方形を各軸方向に張り出させ、角の手前で二度段状に後退させた形――には、輪郭を一度だけ作り、そこから押し出します。
const oct = [[1+P,0],[1+P,0.30],[1.0,0.30],[1.0,0.45],[0.90,0.45], ... ];
const half = oct.concat(oct.slice(0,-1).reverse().map(q => [q[1], q[0]]));
// mirror across the diagonal, then rotate four times
これで、各層は同じ輪郭を縮小した形になり、シルエットも自然に統一されます。
張り出し部分を作りましょう。 ある層が上の層より後退すると、その2つの輪郭の間に隙間ができます。平面上に平らな環状面を追加して埋めないと、塔は棚が浮いたように見え、隙間から空が見えてしまいます。
ディテールの大きさは、配置する面に合わせる
最もよくあるモデリングの間違いは、ジオメトリではなく比率にあります。幅が顔の3分の1しかない面に合わせて扉を作ると、脇の面にはみ出し、雑然として見えます。まず面の寸法を測り、その面に対する比率で扉、柱形、ペディメントの大きさを決めましょう。そして、ペディメントがコーニスを突き抜けず、その下で収まっていることを確認してください。
組み立てアニメーションは、ひとつのクリッピング平面で実現できます
すべての要素は最初のフレームから存在しています。下向きの平面を上昇させ、すべての構造マテリアルに対してクリッピングを適用します。
renderer.localClippingEnabled = true;
const CLIP = new THREE.Plane(new THREE.Vector3(0, -1, 0), 0);
['stone','plaster','tile','timber', ...].forEach(k => {
MAT[k].clippingPlanes = [CLIP];
MAT[k].clipShadows = true; // or the shadow builds early
});
CLIP.constant = heightAt(t);
クリッピングされた外殻は中空なので、平面の高さに、建物の平面形状に合わせてスケールしたキャップメッシュを追加します。これによって、透けて見える断面が、石積みのひとつの層として認識されるようになります。
キャップは、ふさぐ平面形状と一致させる必要があります。 八角形の塔に正方形のキャップを置くと、4つの隙間から空が見えてしまいます。それぞれの様式で断面の平面形状を宣言できるようにしましょう。ホールは4辺、胴部は8辺、ドームは16辺です。そして、平面がその断面から次の断面へ移るときに、キャップのジオメトリを作り直します。
caps: [[0, 4.74, 2.56], [4.74, 5.42, 2.02, 8], [5.42, 6.46, 1.86, 16]]
// y0 y1 radius sides
内装には、DoubleSide の暗いマテリアルを使ったキャップ付きの内部ボリュームを使うと、どの角度から見ても石造りのように見え、処理コストもほとんどかかりません。
枠組みはプレーンより先に現れる
枠組みはプレーンを無視するため、完成した部分より常に一段先に立ちます。この関係だけで、アニメーションはワイプではなく、建築中の様子として伝わります。
BoxGeometry はサイズにかかわらず、すべての面に 0..1 の UV を割り当てます。 そのため、3 メートルの柱も 15 センチの筋交いも同じ木目になり、どちらもプラスチックのように見えます。アップロード前に、UV をワールド単位で書き換えます。
const dims = [[sz,sy],[sz,sy],[sx,sz],[sx,sz],[sx,sy],[sx,sy]];
for (let f = 0; f < 6; f++) {
const du = dims[f][0], dv = dims[f][1], swap = dv > du;
for (let i = 0; i < 4; i++) { /* scale by real size, offset randomly */ }
}
各段階を短いイージング付きスケールで伸ばしていきます。垂直材は Y 軸方向に、水平材は X 軸方向にスケールし、次の段階が現れる直前に消えるようにします。
段階でタイムラインを構成する
各スタイルに [ローカルキャプション、ENGLISH、目標の高さ] のリストを用意し、目標間でプレーンをイージングします。キャプションは何が起きているかを示すもので、それが建築の過程を理解しやすくする要素の大部分を占めます。
stages: [['準備','READY',0.00], ['石垣普請','STONEWORK',3.36],
['柱梁組立','TIMBER',6.24], ['白壁塗籠','PLASTER',8.80], ...]
段階が変わるたびに単発の効果音を鳴らし、最後にはベルを鳴らします。制作全体は短く、4〜5 秒に収め、画面の落ち着いた場所に大きなパーセンテージを表示します。テンポが速ければ、何度でも作り直してもらえます。
発光マテリアルから光が漏れる場合
夜間の窓を発光マテリアルで照らすと、床を張っていないすべての開口部から光が漏れます。軒下、切妻屋根の断面、空洞になった階から光が出てしまいます。切妻屋根の断面には床を張り、囲まれたボリュームは非発光マテリアルに分けます。建物の輪郭がランタンのように光るのが、その症状です。
完了とする前に確認すること
- 地面の高さで一周回転させます。壁が欠けている場合は巻き順が逆で、面に取り付けたボックスから長い棒が飛び出す場合は回転の符号が間違っています。
- タイムラインをゆっくり動かし、すべての段階を確認します。天板はどの高さでも壁の内側に収まり、角から突き出ないようにします。
- 屋根を真上から一度確認します。タイルのはみ出しや NaN のパネルは、この角度からしか見えません。
- スタイルを 10 回切り替えます。メモリ使用量が増える場合は、前のグループのジオメトリを破棄できていません。
- スタイルごとのドローコール数を数えます。あるスタイルだけ他の 3 倍になっている場合は、マテリアルが共有されていません。