Babylon.js と IFCで遊ぶ
自分用作業メモです。
IFCをBabylon.jsで表示させて遊ぼうという思いつき。
web-ifc-three使えって?自分でもそう思います。
IFCのパーサーにifcopenshellとweb-ifcのどちらを使おうか迷ったけど、今回はブラウザ上で処理を完結させたいのでweb-ifcを使います。
...ChatGPTに全部おまかせしようと思ったらweb-ifcがマイナーなせいか、おまかせできないようになので気合で作ります。
とりあえず web-ifc
はこれでimport できた。ググってもサンプル少なすぎてこれを見つけるだけで時間がかかるという。
import * as WebIFC from 'web-ifc';
さて、次はIFCファイルからモデルを読み込むわけですが...
これで読めた...かな?
IFCファイルを選択したらファイルを読み込む。
次は3Dモデルを読み込んで表示させます。
function loadIFCFile(file: File){
const reader = new FileReader();
reader.onload = async (event) => {
const buffer = new Uint8Array(event.target.result)
await ifcapi.Init();
const modelID = ifcapi.OpenModel(buffer)
};
reader.readAsArrayBuffer(file);
}
function handleFileChange(event: React.ChangeEvent<HTMLInputElement>){
const file = event.target.files[0];
if (file) {
loadIFCFile(file);
}
}
<input type="file" onChange={handleFileChange} className="fileInput"/>
web-ifc
で全メッシュ取得はおそらくこれでいいんだけど、取得したメッシュをBabylon.jsで表示させるのどうやるんだ...?
ifcapi.StreamAllMeshes(modelID, (mesh) => {
const placedGeometries = mesh.geometries;
const size = placedGeometries.size();
for (let i = 0; i < size; i++) {
const placedGeometry = placedGeometries.get(i)
const geometry = ifcapi.GetGeometry(modelID, placedGeometry.geometryExpressID);
// 6つで一組のデータ: x, y, z, normalx, normaly, normalz
const verts = ifcapi.GetVertexArray(geometry.GetVertexData(), geometry.GetVertexDataSize());
// 3つで一組のデータ:頂点index 1, 2, 3
const indices = ifcapi.GetIndexArray(geometry.GetIndexData(), geometry.GetIndexDataSize());
}
});
Babylon.jsでメッシュ作成。ChatGPT先生がつよつよでした。
ただ、web-ifcから取得したメッシュは位置情報と形状情報が別なので、次は位置情報を適応して正しい位置に移動させれば見た目は整うはず。
function createMeshFromData(scene: BABYLON.Scene, vertexData: { positions: number[], normals: number[], indices: number[] }) {
const mesh = new BABYLON.Mesh("mesh", scene);
const vertexDataForBabylon = new BABYLON.VertexData();
vertexDataForBabylon.positions = vertexData.positions;
vertexDataForBabylon.normals = vertexData.normals;
vertexDataForBabylon.indices = vertexData.indices;
vertexDataForBabylon.applyToMesh(mesh);
return mesh;
}
公式ドキュメントだとこれかな
Babylon.jsで変形行列を掛けるのはこれだそうで。さすがChatGPT先生です。
const matrix = Matrix.FromArray(matrixData);
mesh.setPivotMatrix(matrix, false);
ただ、見えてる面がおかしいので、次はそこを修正します。
面の表と裏が逆になっているのと、右手系と左手系の違いでZ方向が逆転していたっぽい。
作成したメッシュのZ軸を反転して、面も反転させると正しくなった。
mesh.scaling.z *= -1;
mesh.flipFaces(true);
次は色を付けます。
色の指定はこれで。
const {x, y, z, w} = placedGeometry.color
const color = new BABYLON.Color3(x, y, z);
const material = new BABYLON.StandardMaterial("material", scene);
material.diffuseColor = color;
material.alpha = w
mesh.material = material;
カラフルにしようと思ったら...
ちなみにコード。頂点カラーをアニメーションさせてる。とてつもなく重いです。
function vertexColorAnimation(mesh: BABYLON.Mesh, positions: number[], flatTransformation: number[]){
if(scene === null) return
const px = flatTransformation[12]
const pz = flatTransformation[13]
const py = flatTransformation[14]
let time = 0
scene.registerBeforeRender(function () {
// Update vertex colors over time
const colors = []
time += 0.05; // Increment the time
for(let i = 0; i < positions.length; i += 3){
const pix = positions[i + 0]
const piy = positions[i + 1]
const piz = positions[i + 2]
const r = (Math.sin(time + px + pix) + 1) / 2
const g = (Math.sin(time + py + piy + Math.PI/3) + 1) / 2
const b = (Math.sin(time + pz + piz + Math.PI*2/3) + 1) / 2
colors.push(r, g, b, 0)
}
mesh.setVerticesData(BABYLON.VertexBuffer.ColorKind, colors);
});
}
建築アニメーションっぽいこと
おおよそのコード
function createMesh(modelID : number){
let meshData = []
// アニメーションしたいのでメッシュを同期取得
const meshes = ifcapi.LoadAllGeometry(modelID)
for(let i = 0; i < meshes.size(); i++){
const mesh = meshes.get(i)
meshData = meshData.concat(getIfcMesh(modelID, mesh))
}
// 10秒くらいのアニメーションに
const msec = 10000 / meshData.length
// 低い位置にあるメッシュから表示されるようにソート
meshData.sort((a, b) => a.ymax - b.ymax)
meshData.forEach((v, i)=> {
// アニメーションするように遅延させてメッシュ生成
setTimeout(() => {
ifc2babylonMesh(scene, v.vertexData, v.flatTransformation, v.color)
}, msec * i);
})
}
function getIfcMesh(modelID, mesh){
if(scene === null) return
const meshData = []
const placedGeometries = mesh.geometries;
const size = placedGeometries.size();
for (let i = 0; i < size; i++) {
const placedGeometry = placedGeometries.get(i)
const geometry = ifcapi.GetGeometry(modelID, placedGeometry.geometryExpressID);
// 6つで一組のデータ: x, y, z, normalx, normaly, normalz
const verts = ifcapi.GetVertexArray(geometry.GetVertexData(), geometry.GetVertexDataSize());
// 3つで一組のデータ:頂点index 1, 2, 3
const indices = ifcapi.GetIndexArray(geometry.GetIndexData(), geometry.GetIndexDataSize());
let ymax = 100000
const positions = [];
const normals = [];
for(let i = 0; i < verts.length; i += 6) {
positions.push(verts[i], verts[i + 1], verts[i + 2]);
normals.push(verts[i + 3], verts[i + 4], verts[i + 5]);
// アニメーション用に高さ最小値計算(変数名おかしいけど気にしない)
ymax = Math.min(ymax, verts[i + 2])
}
const vertexData = {
positions: positions,
normals: normals,
indices: Array.from(indices),
}
meshData.push({
vertexData: vertexData,
flatTransformation: placedGeometry.flatTransformation,
color: placedGeometry.color,
ymax: placedGeometry.flatTransformation[13] + ymax,
})
}
return meshData
}
IFCファイルをBabylon.jsで表示できたし、アニメーションっぽいこともして遊べたのでこれで閉じます。
どこかでちゃんとまとめるかも?