いいね、その選び方すごく筋が通ってる。
「Three.js側をOSとして設計する」前提で、最小構成だけど拡張しやすい構造を描いてみる。
全体アーキテクチャ概要
-
Layer 0:Core
Three.jsのScene / Camera / Renderer / Controlsなどの土台。 -
Layer 1:Primitive Engine
球・円柱・箱などのプリミティブ生成+マテリアル管理。 -
Layer 2:Structure Loader
AIが返すJSONレシピを読み込み、プリミティブを組み立てる層。 -
Layer 3:Edit/Selection Layer
面・頂点・オブジェクト選択、Transform操作。 -
Layer 4:AI I/O Layer
2D情報やユーザ指示 → AI呼び出し → JSONレシピ受信。
あなたのDelphiアプリでやっていたことを、このレイヤーに素直にマッピングするイメージ。
Layer 0:Core(三種の神器)
構成モジュール例:
core/scene.ts
Scene生成・ライト追加だけを担当。core/camera.ts
PerspectiveCamera生成+初期位置。core/renderer.ts
WebGLRenderer生成+resize対応。core/controls.ts
OrbitControls設定。
// core/init.ts
export function initCore(canvas: HTMLCanvasElement) {
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(60, canvas.clientWidth / canvas.clientHeight, 0.1, 1000);
camera.position.set(0, 20, 40);
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setSize(canvas.clientWidth, canvas.clientHeight);
const controls = new OrbitControls(camera, renderer.domElement);
// ライト
scene.add(new THREE.AmbientLight(0xffffff, 0.6));
const dir = new THREE.DirectionalLight(0xffffff, 0.8);
dir.position.set(10, 20, 10);
scene.add(dir);
return { scene, camera, renderer, controls };
}
Layer 1:Primitive Engine(プリミティブOS)
役割:
- プリミティブ生成(sphere / cylinder / box)
- マテリアルの共通管理
- 後でメッシュ結合したいなら、
BufferGeometryを返すAPIも用意
// primitives/factory.ts
export type PrimitiveType = 'sphere' | 'cylinder' | 'box';
export function createPrimitive(type: PrimitiveType, params: {
radius?: number;
width?: number;
height?: number;
depth?: number;
radialSegments?: number;
}) {
let geometry: THREE.BufferGeometry;
switch (type) {
case 'sphere':
geometry = new THREE.SphereGeometry(params.radius ?? 1, 32, 16);
break;
case 'cylinder':
geometry = new THREE.CylinderGeometry(params.radius ?? 1, params.radius ?? 1, params.height ?? 1, 24);
break;
case 'box':
geometry = new THREE.BoxGeometry(params.width ?? 1, params.height ?? 1, params.depth ?? 1);
break;
}
const material = new THREE.MeshStandardMaterial({ color: 0xcccccc, metalness: 0.1, roughness: 0.8 });
return new THREE.Mesh(geometry, material);
}
ここは「Delphiでやっていたプリミティブ生成」をそのままWebに移した層。
Layer 2:Structure Loader(AIレシピ→シーン)
AIが返すJSONレシピを受け取り、
プリミティブを生成し、位置・回転・スケールを適用してSceneに追加する層。
JSONレシピ例(再掲)
{
"parts": [
{
"id": "head",
"primitive": "sphere",
"position": [0, 12, 0],
"scale": [4, 4, 4],
"rotation": [0, 0, 0]
}
]
}
ローダ実装イメージ
// structure/loader.ts
import { createPrimitive, PrimitiveType } from '../primitives/factory';
export type PartSpec = {
id: string;
primitive: PrimitiveType;
position: [number, number, number];
scale: [number, number, number];
rotation: [number, number, number]; // degree
};
export type StructureSpec = {
parts: PartSpec[];
};
export function loadStructure(scene: THREE.Scene, spec: StructureSpec) {
const group = new THREE.Group();
group.name = 'structure-root';
spec.parts.forEach(part => {
const mesh = createPrimitive(part.primitive, {});
mesh.position.set(...part.position);
mesh.scale.set(...part.scale);
const [rx, ry, rz] = part.rotation.map(v => THREE.MathUtils.degToRad(v));
mesh.rotation.set(rx, ry, rz);
mesh.userData.partId = part.id;
group.add(mesh);
});
scene.add(group);
return group;
}
ここで「構造=StructureSpec」というOS的単位を定義しておくと、
AIとのインターフェースも安定する。
Layer 3:Edit/Selection Layer(編集OS)
あなたの既存アプリの「面・頂点・オブジェクト選択+変換」をWebに持ってくる層。
最初の段階では:
- オブジェクト単位選択(Raycaster)
- TransformControlsで移動・回転・スケール
から始めるのが現実的。
// edit/selection.ts
export function setupSelection(scene: THREE.Scene, camera: THREE.Camera, renderer: THREE.WebGLRenderer) {
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
const transform = new TransformControls(camera, renderer.domElement);
scene.add(transform);
let selected: THREE.Object3D | null = null;
function onClick(event: MouseEvent) {
const rect = renderer.domElement.getBoundingClientRect();
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(scene.children, true);
if (intersects.length > 0) {
selected = intersects[0].object;
transform.attach(selected);
} else {
transform.detach();
selected = null;
}
}
renderer.domElement.addEventListener('click', onClick);
return { transform, getSelected: () => selected };
}
頂点・面レベルの編集は、
BufferGeometryのposition属性を直接いじる- 選択対象を「頂点インデックス」「面インデックス」として管理
という、もう一段下のレイヤーとして後から足せる。
Layer 4:AI I/O Layer(構造OSとの境界)
ここは「AIは構造だけ返す」というあなたの思想をそのまま形にする層。
- 入力:
- 2Dスケッチ情報(点群・ラベル)
- ユーザのテキスト指示
- 出力:
StructureSpec(JSON)
// ai/client.ts
export async function requestStructure(prompt: string): Promise<StructureSpec> {
const res = await fetch('/api/ai-structure', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt })
});
const json = await res.json();
return json as StructureSpec;
}
フロント側では:
const spec = await requestStructure(userPrompt);
const group = loadStructure(scene, spec);
という形で、AI層とThree.js層を「StructureSpec」でだけ接続する。
進め方のミニロードマップ
- Core + Primitive Engine
- Three.jsでシーン表示+プリミティブ生成まで。
- Structure Loader
- 手書きのJSONを読み込んで、昆虫っぽい構造を組む。
- Selection + TransformControls
- 部品単位で動かせるようにする。
- AI I/O Layer
- 手動JSON → AI生成JSONに差し替える。
もしよければ次は、
「StructureSpecの最小仕様を一緒に固定する」ところまでやろうか。
昆虫・人型・ロボットなど、どのカテゴリを基準に設計したい?