ドキュメント / 使い方
ファイルをセルにして表示する
点群や 3D モデルのファイルを、ブラウザの中でセルの集まりに変換して、WebGL2 で表示できます。ファイルをサーバに送る必要はありません。この文書では、VoxelizerJs でファイルをセルにする方法と、@mirgrid/web-sdk/render.mjs で画面に描く方法を説明します。
大まかな流れは次のとおりです。
- レベルを決めて
VoxelizerJsを作ります。 setInputで、入力の座標系を指定します。pushFileでファイルの中身を渡します。点や三角形を直接渡すこともできます。- 結果を読むか、
faces()で表に出ている面を作って描画器に渡します。 - 使い終わったら
free()で解放します。
点群をセルにする
例として、静岡県が公開している「駿府城点群データ(令和7年度)」(CC BY 4.0)から、東御門橋のあたりを切り出した LAZ ファイルを使います。点は 217,869 個です。座標は平面直角座標系の VIII 系(EPSG:6676)で、高さは標高です。この場所のジオイド高は 40.53 m です。
次の例は Node.js で動かすときの書き方です。点群を一辺 0.25 m のセル(レベル 26)にしています。
import { readFileSync } from 'node:fs';
import init, { VoxelizerJs } from '@mirgrid/web-sdk';
const wasmUrl = new URL(import.meta.resolve('@mirgrid/web-sdk/voxel_core_bg.wasm'));
await init({ module_or_path: readFileSync(wasmUrl) });
const voxelizer = new VoxelizerJs(26);
voxelizer.setInput({ coordSystem: '6676', geoidHeightM: 40.53 });
const summary = voxelizer.pushFile(
readFileSync('sunpu-higashigomon-bridge.laz'),
'sunpu-higashigomon-bridge.laz',
);
console.log(summary);
console.log(voxelizer.cellCount);
{
format: 'laz',
points: 217869,
triangles: 0,
samples: 0,
declaredEpsg: 6676
}
8712
21 万あまりの点が、8,712 個のセルにまとまりました。
ブラウザでは、ファイル選択やドラッグ & ドロップで受け取った File の中身を Uint8Array にして渡します。
const bytes = new Uint8Array(await file.arrayBuffer());
voxelizer.pushFile(bytes, file.name);
対応しているファイル形式
pushFile が読めるのは、PLY・LAS・LAZ・glTF・GLB・OBJ・STL の 7 つの形式です。2 つ目の引数には、'laz' のような形式名か、拡張子の付いたファイル名を渡します。ほかの形式を渡すと Error になります。
glTF は、外部のファイルを参照していないもの(GLB か、データを data: URI で埋め込んだ glTF)だけを読めます。
pushFile の戻り値には、読み込んだ内容がまとめられています。
| 項目 | 内容 |
|---|---|
format |
読み込んだ形式 |
points |
点の数 |
triangles |
三角形の数 |
samples |
三角形の表面から取り出した点の数 |
declaredEpsg |
LAS・LAZ のヘッダに書かれている EPSG(無ければ null) |
3D モデルの三角形は、表面に点を散らしてからセルにします。点の密度(1 m² あたりの点の数)は setSurfaceDensity で変えられます。
declaredEpsg はファイルに書かれている情報を示すだけで、入力の座標系は自動では切り替わりません。座標系は setInput で必ず指定してください。
入力の座標系を指定する
setInput には、ファイルの座標がどの座標系で書かれているかを指定します。指定は、そのあとに渡す点や三角形から効きます。指定しなかったときは ECEF として扱います。
coordSystem には次のどれかを書きます。
coordSystem |
座標の意味 |
|---|---|
'ecef' |
ECEF の x・y・z(m) |
'latlon' |
経度・緯度(度)と楕円体高(m) |
'local' |
origin に指定した地点から見た東・北・上(m) |
'utm' |
UTM の東・北(m)と楕円体高(m) |
'6669'〜'6687' |
平面直角座標系の東・北(m)と楕円体高(m) |
'utm' のときは、utmZone(ゾーンの番号)と utmHemisphere('north' か 'south')も指定します。平面直角座標系は、'6676' のように EPSG の番号を文字列で書きます。
ほかに、次の項目を指定できます。
| 項目 | 内容 |
|---|---|
origin |
'local' の原点 [経度, 緯度, 楕円体高] |
unitScale |
入力の 1 単位が何 m か(mm で書かれたファイルなら 0.001、既定は 1) |
geoidHeightM |
高さが標高のときに、標高に足すジオイド高(m) |
rotationDeg |
鉛直の軸まわりに回す角度(度、正の値で反時計回り) |
translation |
東・北・上への平行移動 [東, 北, 上](m) |
pivot |
回転の中心(入力と同じ単位) |
知らない項目を書くと Error になります。たとえば geoidHeightM を geoidHeight と書き間違えると、すぐに気づけます。
ジオイド高については、位置と空間アドレスでも説明しています。
点や三角形を直接渡す
ファイルではなく、アプリが持っている点や三角形をセルにすることもできます。座標は setInput で指定した座標系で書きます。
pushPoints(xyz, rgb): 点を[x, y, z, x, y, z, …]と並べたFloat64Arrayを渡します。rgbは 1 点 3 バイトのUint8Arrayです。rgbを省くと、灰色(180, 180, 180)になります。pushTriangles(xyz, indices, rgb): 三角形の頂点を渡します。indicesをnullにすると、頂点を 3 つずつ 1 つの三角形とみなします。rgbは 1 三角形 3 バイトです。戻り値は、表面から取り出した点の数です。
const shapes = new VoxelizerJs(24);
shapes.setInput({ coordSystem: 'local', origin: [139.767125, 35.681236, 40] });
shapes.pushPoints(new Float64Array([0, 0, 0, 3, 0, 0]), new Uint8Array([255, 0, 0, 0, 0, 255]));
const samples = shapes.pushTriangles(
new Float64Array([0, 0, 0, 4, 0, 0, 0, 3, 0]),
null,
new Uint8Array([200, 200, 200]),
);
console.log(samples, shapes.cellCount);
113 16
セルの数の上限を決める
細かいレベルで大きなファイルをセルにすると、セルが増えてメモリを使い切ることがあります。setMaxCells で上限を決めておくと、上限を超えたところで pushFile などが Error になります。null か 0 を渡すと上限はなくなります。
const limited = new VoxelizerJs(26);
limited.setInput({ coordSystem: '6676', geoidHeightM: 40.53 });
limited.setMaxCells(1000);
try {
limited.pushFile(readFileSync('sunpu-higashigomon-bridge.laz'), 'sunpu-higashigomon-bridge.laz');
} catch (error) {
console.log(error.message);
}
voxelize の一意セル数が上限 1000 セルを超えた
Error になった VoxelizerJs には、途中までの結果が残っています。free() で解放してから、レベルを粗くして作り直してください。
現場に揃えた格子と地球基準の格子
セルの並べ方(格子)には 2 種類あります。
| 地球基準の格子 | 現場に揃えた格子 | |
|---|---|---|
| 作り方 | new VoxelizerJs(level) |
VoxelizerJs.siteAuto(level) |
| セルの向き | ECEF の軸に沿った向き(地上からは傾いて見える) | その場所の東・北・上に沿った向き(地面に水平) |
| セルの ID | 世界で共通の空間アドレス | 現場の格子用の ID(32 桁の 16 進) |
| 向いている使い方 | ほかのデータとの、空間アドレスでの突き合わせ | 建物や構造物の形の確認や計測 |
地球基準の格子は、位置と空間アドレスで説明したセルそのものです。どこで作っても同じ空間アドレスになるので、ほかのデータと重ね合わせやすくなります。ただし、セルの面は地面と平行になりません。
現場に揃えた格子は、最初に渡した点の近くを基準点(anchor)にして、その場所の東・北・上に沿ってセルを並べます。基準点は、最初の点を含むレベル 16 のセル(一辺 256 m)です。データは基準点から 2,048 m 以内に収まっている必要があります。
const site = VoxelizerJs.siteAuto(26);
site.setInput({ coordSystem: '6676', geoidHeightM: 40.53 });
site.pushFile(readFileSync('sunpu-higashigomon-bridge.laz'), 'sunpu-higashigomon-bridge.laz');
console.log(site.cellCount);
console.log(site.frameDescriptor());
8095
{
kind: 'site',
datum: 'JGD2011',
epoch: 2011.4,
anchor: '0000000002e94f721e16540000000000',
yaw: 0
}
frameDescriptor() は、格子の決め方を表すオブジェクト(格子の記述子)を返します。基準点は最初の点で決まるので、点を渡す前は null です。
格子の向きを変える
siteAuto の 3 つ目の引数で、格子を水平に回せます。0 なら格子が東西南北に揃います。角度は度ではなく、1 周を 65536 に分けた整数で渡します。度からは spatial_yaw_from_deg で変換してください。角度は真北から東回りに測ります。
import { spatial_yaw_from_deg } from '@mirgrid/web-sdk';
console.log(spatial_yaw_from_deg(30), spatial_yaw_from_deg(90));
5461 16384
2 つ目の引数は測地系の名前で、省くと 'JGD2011' になります。向きだけを変えるときは、VoxelizerJs.siteAuto(26, 'JGD2011', spatial_yaw_from_deg(30)) のように書きます。
同じ格子にもう 1 つのファイルを重ねる
2 つのファイルを同じ格子でセルにしたいときは、1 つ目の frameDescriptor() を 2 つ目の VoxelizerJs に渡します。
const second = new VoxelizerJs(26, site.frameDescriptor());
console.log(second.frameDescriptor().anchor);
0000000002e94f721e16540000000000
FrameJs で格子を扱う
FrameJs は、格子をあらわすオブジェクトです。FrameJs.ecef() は地球基準の格子、FrameJs.fromDescriptor(descriptor) は記述子から作った格子です。cellAt(ix, iy, iz, level) で、その格子のセルの ID と中心の ECEF を求められます。
import { FrameJs } from '@mirgrid/web-sdk';
const frame = FrameJs.fromDescriptor(site.frameDescriptor());
const firstCell = site.cellInfo(0);
const at = frame.cellAt(firstCell.index[0], firstCell.index[1], firstCell.index[2], 26);
console.log(frame.kind(), at.cellId === firstCell.cellId);
site true
matrix() は、現場の格子の座標を ECEF に移す 4 × 4 の行列を、列優先の Float64Array で返します。three.js の Matrix4.fromArray にそのまま渡せます。FrameJs も WebAssembly のメモリを使うので、使い終わったら free() してください。
結果を読む
セルにした結果は、セルの番号の順(x・y・z の辞書順)に並んでいます。
| メソッド・プロパティ | 返すもの |
|---|---|
cellCount |
セルの数 |
level |
レベル |
indices() |
セルの番号を [x, y, z, …] と並べた Float64Array |
colors() |
セルの色を 1 セル 3 バイト(r, g, b)で並べた Uint8Array |
counts() |
セルに入った点(3D モデルでは表面から取り出した点)の数の Float64Array |
cellInfo(i) |
i 番目のセルの情報 |
toCellSet() |
同じセルを入れた CellSetJs |
セルの色は、セルに入った点の色の平均です。cellInfo(i) は次のような値を返します。
console.log(voxelizer.cellInfo(0));
{
index: [ 17908406, 47452843, 48097938 ],
cellId: '0000000002e94f721e16575d09dbf000',
rgb: [ 126, 121, 116 ],
count: 2,
centerEcef: [ -3911506.3828125, 3474602.8671875, 3635876.6171875 ],
sourceType: 2
}
cellId は、地球基準の格子なら空間アドレス、現場に揃えた格子なら現場の格子用の ID です。centerEcef はセルの中心の ECEF です。sourceType はセルの由来で、3D モデルから作ったセルは 1、点群から作ったセルは 2、両方が入ったセルは 3 です。
toCellSet() で作った集合を使うと、和・積・差や距離の計算ができます(セルの集合を参照)。
表に出ている面を作る
画面に描くときは、まず faces() で、セルの面のうち表に出ているものを集めます。隣のセルと接している面は、外から見えないので除かれます。
const faces = voxelizer.faces();
console.log(faces.cellCount, faces.faceCount, faces.cellSizeM);
console.log(faces.originLlh);
8712 14552 0.25
[ 138.3851385356091, 34.9777374735036, 62.81689990963787 ]
faces() の結果は、JavaScript の型付き配列だけでできています。free() は要りません。Worker から送るときは、配列の ArrayBuffer を transfer で渡せます。
描画の空間は、originLlh(経度・緯度・楕円体高)を原点にした ENU(m)です。原点を省くと、地球基準の格子ではセル全体を囲む箱の中心、現場に揃えた格子では基準点の真上になります。原点を決めたいときは、faces([経度, 緯度, 楕円体高]) のように渡します。
隠れた面も残したいときは、faces(null, { occlusion: false }) とします。
画面に描く
@mirgrid/web-sdk/render.mjs の createVoxelRenderer で描画器を作り、setLayer で面を渡します。描画器は WebGL2 を使います。WebGL2 が使えないときは、createVoxelRenderer が Error を投げます。
import init, { VoxelizerJs } from '@mirgrid/web-sdk';
import { createVoxelRenderer } from '@mirgrid/web-sdk/render.mjs';
await init();
const canvas = document.querySelector('canvas');
const renderer = createVoxelRenderer(canvas);
let voxelizer = null;
document.querySelector('input[type=file]').addEventListener('change', async (event) => {
const file = event.target.files[0];
const next = new VoxelizerJs(26);
next.setInput({ coordSystem: '6676', geoidHeightM: 40.53 });
next.pushFile(new Uint8Array(await file.arrayBuffer()), file.name);
renderer.setLayer('scan', next.faces());
voxelizer?.free();
voxelizer = next;
});
描画器は canvas の大きさの変化に合わせて、自動で描き直します。canvas の大きさは CSS で決めてください。
マウスでは、左ドラッグで回転、右ドラッグか Shift を押しながらのドラッグで移動、ホイールで拡大・縮小ができます。タッチでは、1 本指で回転、2 本指でつまんで拡大・縮小と移動ができます。
レイヤを重ねる
setLayer(id, faces) は、id ごとにレイヤを置きます。同じ id を渡すと置き換えます。setVisible(id, false) で隠し、removeLayer(id) で取り除けます。
1 つの描画器に重ねるレイヤは、すべて同じ原点で作る必要があります。2 つ目からは、faces(renderer.originLlh) のように描画器の原点を渡してください。原点が違うと、setLayer が Error を投げます。
視点を決める
視点は、最初のレイヤを置いたときに自動で合わせます。fitView() を呼ぶと、見えているレイヤ全体が収まるように合わせ直します。
setView で視点を指定することもできます。方位角 azimuthDeg は、東から反時計回りに測った角度です。
renderer.setView({ azimuthDeg: -90, elevationDeg: 90 }); // 真上から見る
renderer.setView({ azimuthDeg: -90, elevationDeg: 0 }); // 南から北を見る
renderer.setView({ targetEnu: [0, 0, 0], distanceM: 30 }); // 原点を中心に 30 m 離れて見る
今の視点は renderer.view で読めます。描画器を作るときに onRender を渡すと、描くたびに視点を受け取れます。view.axesOnScreen には東・北・上が画面のどちらを向いているかが入っているので、方位の印を描くのに使えます。
クリックしたセルを調べる
pick(clientX, clientY) は、画面の座標の下にある面を返します。何も無ければ null です。戻り値の cell を cellInfo に渡すと、そのセルの情報が分かります。
回転のためのドラッグをクリックと取り違えないように、押した位置と離した位置が近いときだけ調べます。
let down = null;
canvas.addEventListener('pointerdown', (event) => {
down = { x: event.clientX, y: event.clientY };
});
canvas.addEventListener('pointerup', (event) => {
if (!down || Math.hypot(event.clientX - down.x, event.clientY - down.y) > 4) return;
const hit = renderer.pick(event.clientX, event.clientY);
if (hit && hit.layerId === 'scan') {
console.log(voxelizer.cellInfo(hit.cell));
}
});
hit には、レイヤの id(layerId)、面の番号(face)、セルの番号(cell)、面の向き(direction。0〜5 が +X・−X・+Y・−Y・+Z・−Z)が入っています。
描画器が要らなくなったら、renderer.dispose() でイベントの登録と GPU の資源を外します。
重い処理を Worker に回す
pushFile は、ファイルの中身を一度に WebAssembly のメモリに写してセルにします。大きなファイルでは時間がかかり、その間は画面が止まります。セル化は module Worker で行い、結果の面だけを画面のスレッドに送るようにしましょう。
Worker の側では、VoxelizerJs を Worker の中に持ち続けます。面の型付き配列は transfer で送るので、コピーは起きません。
// voxelize.worker.js
import init, { VoxelizerJs } from '@mirgrid/web-sdk';
const ready = init();
let voxelizer = null;
const handlers = {
voxelize({ bytes, fileName, level, input }) {
const next = new VoxelizerJs(level);
try {
next.setInput(input);
next.pushFile(new Uint8Array(bytes), fileName);
const faces = next.faces();
voxelizer?.free();
voxelizer = next;
const transfer = [faces.start, faces.coords, faces.colors, faces.cells, faces.gridToRender]
.map((array) => array.buffer);
return [{ faces, cellCount: next.cellCount }, transfer];
} catch (error) {
next.free();
throw error;
}
},
cellInfo({ cell }) {
return [{ info: voxelizer.cellInfo(cell) }, []];
},
};
self.onmessage = async ({ data }) => {
try {
await ready;
const [result, transfer] = handlers[data.kind](data);
self.postMessage({ id: data.id, ok: true, ...result }, transfer);
} catch (error) {
self.postMessage({ id: data.id, ok: false, error: error.message });
}
};
画面の側では、要求ごとに id を振って、返事を Promise で受け取ります。
const worker = new Worker(new URL('./voxelize.worker.js', import.meta.url), { type: 'module' });
const pending = new Map();
let nextId = 1;
worker.onmessage = ({ data }) => {
const { resolve, reject } = pending.get(data.id);
pending.delete(data.id);
if (data.ok) resolve(data);
else reject(new Error(data.error));
};
function call(kind, payload, transfer = []) {
return new Promise((resolve, reject) => {
const id = nextId++;
pending.set(id, { resolve, reject });
worker.postMessage({ id, kind, ...payload }, transfer);
});
}
const bytes = await file.arrayBuffer();
const result = await call('voxelize', {
bytes,
fileName: file.name,
level: 26,
input: { coordSystem: '6676', geoidHeightM: 40.53 },
}, [bytes]);
renderer.setLayer('scan', result.faces);
ファイルの ArrayBuffer も transfer で渡しているので、送ったあとは画面の側では使えなくなります。設定を変えて何度もセル化し直すときは、bytes.slice(0) でコピーを作ってから送ってください。
クリックしたセルを調べるときは、call('cellInfo', { cell: hit.cell }) のように Worker に問い合わせます。
MGVX に保存する
toPacket() は、セル化した結果を MGVX packet(MIRGRID の配信形式)のバイト列にします。セルの色も保たれます。
const packet = voxelizer.toPacket();
console.log(packet.length);
148148
packet のバイト列には、格子の情報が入っていません。現場に揃えた格子で作った packet を保存するときは、frameDescriptor() を JSON にして一緒に保存してください。
const sitePacket = site.toPacket();
const siteFrame = JSON.stringify(site.frameDescriptor());
console.log(sitePacket.length, siteFrame);
137659 {"kind":"site","datum":"JGD2011","epoch":2011.4,"anchor":"0000000002e94f721e16540000000000","yaw":0}
記述子を付けずに読むと、地球基準の格子の番号として読まれ、まったく別の場所に出てしまいます。保存した packet を読み込んで描く方法は、配信データを読むで説明しています。
使い終わったら解放する
VoxelizerJs と FrameJs は WebAssembly のメモリを使います。使い終わったら free() で解放してください。
for (const object of [voxelizer, shapes, limited, site, second, frame]) {
object.free();
}
faces() の結果と pushFile の戻り値は普通の JavaScript の値なので、解放は要りません。
関連する文書
セルの番号や ENU の考え方は位置と空間アドレスで、セルの集合の計算はセルの集合で説明しています。保存した packet や配信データを読むときは配信データを読むへ進んでください。関数の引数の一覧はAPI リファレンスにあります。