[Astro] #110 Point Cloud Viewer v5 — ボックス選択・断面ポリライン・Octree LOD・座標系変換
概要
前回[#109]でIntensity/Classification表示とLAS/PCDエクスポートを追加してPoint Cloud Viewerの基本機能が完成した。今回のv5では「閲覧」から「編集」へ踏み込む4機能を一気に追加する。
[Astro] #109 Point Cloud Viewer — Intensity/Classification表示&LAS/PCDエクスポート // PROTOCOL.LAIN
Point Cloud ViewerにIntensity(反射強度)とClassification(ASPRS分類コード)のカラーモード、LAS/PCDエクスポート、スクリーンショット保存を追加。
lain-lab.comv5 新機能
| 機能 | 概要 |
|---|---|
| ボックス選択+削除 | 矩形ドラッグで範囲選択→不要な点群を削除 |
| 断面ポリライン | スライス断面のプロファイル生成+DXFエクスポート |
| Octree LOD | 八分木による距離ベースレベルオブディテール |
| 座標系選択 | proj4によるCRS変換(JGD2011全19系/UTM/WGS84) |
スクリーンショット
ボックス選択+削除
Octree LOD
座標系選択
動画 (GIF)
ボックス選択+削除
仕組み
TOOLセクションに「Box Select」モードを追加。マウスドラッグで2D矩形を描き、矩形内に投影される3D点群を選択する。
選択の核心はNDC(Normalized Device Coordinates)投影。全点をカメラの投影行列でスクリーン座標に変換し、矩形内に収まるかを判定する。
// カメラの投影行列(projection × view)
const projMatrix = new THREE.Matrix4().multiplyMatrices(
camera.projectionMatrix,
camera.matrixWorldInverse
);
const v = new THREE.Vector4();
for (let i = 0; i < count; i++) {
v.set(positions.getX(i), positions.getY(i), positions.getZ(i), 1);
v.applyMatrix4(projMatrix);
// カメラの背面にある点はスキップ
if (v.w <= 0) continue;
// NDC座標(-1〜1)に変換
const nx = v.x / v.w;
const ny = v.y / v.w;
if (nx >= ndcX1 && nx <= ndcX2 && ny >= ndcY1 && ny <= ndcY2) {
selected.add(i);
}
}
200万点の全点投影でも実用的な速度で動く。THREE.Vector4と行列乗算1回で済むため、Raycasterより高速。
削除
選択した点はマゼンタ(#ff0066)でハイライト表示される。「Delete Selected」で選択点をジオメトリから除外。position/color/intensity/classificationの全属性を保持したまま新しいBufferGeometryを構築する。
for (let i = 0; i < count; i++) {
if (selectedIndices.has(i)) continue;
newPos.push(positions.getX(i), positions.getY(i), positions.getZ(i));
if (hasColor) {
newCol.push(
geo.attributes.color.getX(i),
geo.attributes.color.getY(i),
geo.attributes.color.getZ(i)
);
}
if (hasIntensity) newInt.push(geo.attributes.intensity.getX(i));
if (hasClassification) newCls.push(geo.attributes.classification.getX(i));
}
削除後のデータはそのままエクスポート可能。SOR(統計的外れ値除去)が自動ノイズ除去なら、ボックス選択は手動の精密除去。
断面ポリライン
プロファイル生成
既存のSlice機能(軸平行の矩形断面)の上に、断面のプロファイルライン生成を追加した。
スライスで薄い断面を取ると、その断面に含まれる点群が得られる。これを水平軸でソートし、ビン分割して各ビンの最高点(地表面)を抽出することで、断面のプロファイルラインが生成される。
// ビン分割 — 各ビンの最高Y(地表面)を取得
const bins = Array(BINS).fill(null);
for (const p of sliced) {
const b = Math.min(BINS - 1, Math.floor((p.sort - minS) / binW));
if (!bins[b] || p.val > bins[b].val) {
bins[b] = { xyz: p.xyz, val: p.val };
}
}
生成されたプロファイルは緑のポリラインとして3Dビュー上に表示される。頂点マーカーも20個程度間引いて表示。
DXFエクスポート
プロファイルラインはDXF形式でエクスポートできる。3D POLYLINEエンティティとして出力し、CADソフト(AutoCAD、QGIS等)で開ける。
let dxf = '0\nSECTION\n2\nHEADER\n0\nENDSEC\n';
dxf += '0\nSECTION\n2\nENTITIES\n';
dxf += '0\nPOLYLINE\n8\nPROFILE\n66\n1\n70\n8\n';
for (const p of profilePoints) {
dxf += '0\nVERTEX\n8\nPROFILE\n';
// Three.js → CAD座標変換
dxf += `10\n${p.x.toFixed(6)}\n20\n${(-p.z).toFixed(6)}\n30\n${p.y.toFixed(6)}\n`;
dxf += '70\n32\n';
}
dxf += '0\nSEQEND\n8\nPROFILE\n';
dxf += '0\nENDSEC\n0\nEOF\n';
Octree LOD
八分木の構築
点群を空間的に八分木(Octree)に分割し、カメラからの距離に応じて表示する点の密度を動的に変える。近い部分は高密度、遠い部分は間引いて表示する。
export interface OctreeNode {
min: [number, number, number];
max: [number, number, number];
center: [number, number, number];
halfSize: number;
children: (OctreeNode | null)[];
indices: number[]; // リーフの全点インデックス
lodSamples: number[]; // 遠距離用サンプル(64点)
pointCount: number;
}
構築は再帰的に行う。最大深度5、最小256点でリーフノードとする。各ノードにはLOD用のサンプル(64点を等間隔抽出)を保持する。
// 8つのオクタントに分割
const buckets: number[][] = [[], [], [], [], [], [], [], []];
for (const idx of indices) {
const x = pos[idx * 3], y = pos[idx * 3 + 1], z = pos[idx * 3 + 2];
const oct = (x >= center[0] ? 1 : 0)
| (y >= center[1] ? 2 : 0)
| (z >= center[2] ? 4 : 0);
buckets[oct].push(idx);
}
200万点の構築時間は約250ms。
LOD選択
useFrameで15フレームごとにLOD選択を実行。各ノードについて:
- Frustum Culling — カメラの視錐台外のノードを丸ごとスキップ
- スクリーンサイズ判定 — ノードの投影サイズが小さければLODサンプルを使用、大きければ子ノードに再帰
const dist = Math.max(0.001, camPos.distanceTo(nodeCenter));
const screenSize = (node.halfSize * 2 / dist) * screenHeight;
if (isLeaf || screenSize < 80) {
// 遠い or リーフ → LODサンプルまたは全点を返す
const pts = (isLeaf && screenSize >= 80) ? node.indices : node.lodSamples;
// ...
}
子ノードは近い順にソートして処理し、予算(budget)を近い領域に優先配分する。
バッファ直接更新
LOD選択で得られたインデックスに基づき、pre-allocatedバッファに直接コピーする。メモリ確保なし、drawRangeで描画範囲を制御。
for (let i = 0; i < indices.length; i++) {
const s = indices[i] * 3;
const d = i * 3;
dstPos[d] = srcPos[s];
dstPos[d + 1] = srcPos[s + 1];
dstPos[d + 2] = srcPos[s + 2];
// color も同様
}
lodGeo.setDrawRange(0, indices.length);
lodGeo.attributes.position.needsUpdate = true;
ハッシュ比較で変更がなければバッファ更新をスキップし、不要な描画コストを回避。
座標系選択 + CRS変換
coordSystems.ts
proj4を動的ロードし、主要な測地系の定義を一括登録する。
export const CRS_LIST: CRSDef[] = [
{ code: 'EPSG:4326', name: 'WGS84 (Lat/Lon)', proj4def: '...' },
{ code: 'EPSG:3857', name: 'Web Mercator', proj4def: '...' },
// UTM 51N〜56N(日本周辺)
{ code: 'EPSG:32654', name: 'UTM 54N', proj4def: '...' },
// JGD2011 平面直角座標系 I〜XIX
{ code: 'EPSG:6669', name: 'JGD2011 / CS I (長崎)', proj4def: '...' },
{ code: 'EPSG:6677', name: 'JGD2011 / CS IX (東京)', proj4def: '...' },
// ... 全19系
{ code: 'LOCAL', name: 'ローカル座標 (変換なし)', proj4def: '' },
];
対応CRS:
- WGS84 (EPSG:4326)
- Web Mercator (EPSG:3857)
- UTM 51N〜56N (EPSG:32651〜32656)
- JGD2011 平面直角座標系 I〜XIX (EPSG:6669〜6687)
- ローカル座標(変換なし)
proj4の動的ロード
Viteの静的解析を回避するため、new Functionで動的importを行う。proj4が未インストールでもアプリは正常に動作し、変換だけスキップされる。
async function loadProj4(): Promise<any> {
try {
const modName = 'proj4';
const mod = await (new Function('m', 'return import(m)'))(modName);
_proj4 = mod.default || mod;
// CRS定義を一括登録
for (const def of CRS_LIST) {
if (def.proj4def) _proj4.defs(def.code, def.proj4def);
}
return _proj4;
} catch {
console.warn('proj4 not installed — CRS transform disabled');
return null;
}
}
エクスポート時の座標変換
Source CRSとExport CRSが異なる場合、LASエクスポート時に各点の座標を変換する。
// Three.js → LAS座標に逆変換
let lasX = positions.getX(i);
let lasY = -positions.getZ(i);
let lasZ = positions.getY(i);
// CRS変換(水平座標のみ、標高はそのまま)
if (sourceCRS !== exportCRS) {
[lasX, lasY, lasZ] = transformPointSync(lasX, lasY, lasZ, sourceCRS, exportCRS);
}
新ファイル構成
| ファイル | 役割 |
|---|---|
| octree.ts | 八分木構築 + LOD点選択 |
| coordSystems.ts | CRS定義 + proj4ラッパー |
既存ファイルの変更:
| ファイル | 変更内容 |
|---|---|
| PointCloudApp.tsx | ToolMode追加、選択/プロファイル/CRS状態管理、削除ハンドラ |
| PointCloudScene.tsx | ボックス選択イベント、LOD useFrame、プロファイル描画 |
| ControlPanel.tsx | Box Select/LOD/CRS UI追加 |
まとめ
Point Cloud Viewer v5で閲覧ツールから編集ツールへと進化した。
開発経緯:
- v1: PLY読み込み、ヒートマップ、スライス、計測、SOR
- v2: LAS/PCD読み込み、200万点サンプリング、正規化
- v3: LAZ読み込み、XYZ/CSV入出力、ローディング
- v4: Intensity/Classification表示、LAS/PCD/スクショエクスポート
- v5: ボックス選択+削除、断面ポリライン+DXF、Octree LOD、座標系変換
v5の実装ポイント:
- ボックス選択はNDC投影で全点をスクリーン座標に変換。Raycasterより高速
- Octree LODはfustum culling+スクリーンサイズ判定。pre-allocatedバッファで毎フレームのGCを回避
- proj4のVite動的インポートは
new Functionでバンドラーの静的解析を回避 - DXFエクスポートはThree.js→CAD座標の変換が必要(Y↔Z、Z符号反転)
今後の課題
- 元座標オフセットの保持(正規化で失われる地理座標の復元)
- Octreeに全点(サンプリング前)を格納して本当の大規模対応
- 任意角度の断面ポリライン
- Web Worker分離(パース・SORをメインスレッドから分離)
- メッシュ化(Delaunay三角形分割 / Poisson Reconstruction)