今回は、3Dモデルの描画ライブラリである「Babylon.js」を使って、鉄骨部材を描画してみます。
Babylon.jsとは
Babylon.jsは、Microsoftが開発したWebGLベースの3D描画ライブラリです。 https://www.babylonjs.com/ WebGLベースのため、ブラウザがあれば3Dモデルを描画でき、専用アプリケーションのインストールなどが必要ありません。実装は、JavaScript または TypeScript で行います。
押出しでH形とBOX形部材を作ってみる
今回は、押出し(ExtrudeShape)を使ってみました。以下のドキュメントを参考にしています。
https://doc.babylonjs.com/features/featuresDeepDive/mesh/creation/param/extrude_shape 押出しは、平面として断面を一筆書きで定義した後、始点から終点に対してその断面を積分するように伸ばすような形でオブジェクトを生成できます。始点、終点の座標がわかっていて、断面として形状を定義する考え方が今回の用途には親和性が良さそうです。
実際に書いてみた結果です。
const createHShape = function (h, b, tw, tf, rotation = 0.0) {
let sin = Math.sin(rotation / 180.0 * Math.PI);
let cos = Math.cos(rotation / 180.0 * Math.PI);
let xys = [
{ x: - 0.5 * h , y: 0.5 * b },
{ x: - 0.5 * h + tf, y: 0.5 * b },
{ x: - 0.5 * h + tf, y: 0.5 * tw },
{ x: 0.5 * h - tf, y: 0.5 * tw },
{ x: 0.5 * h - tf, y: 0.5 * b },
{ x: 0.5 * h , y: 0.5 * b },
{ x: 0.5 * h , y: - 0.5 * b },
{ x: 0.5 * h - tf, y: - 0.5 * b },
{ x: 0.5 * h - tf, y: - 0.5 * tw },
{ x: - 0.5 * h + tf, y: - 0.5 * tw },
{ x: - 0.5 * h + tf, y: - 0.5 * b },
{ x: - 0.5 * h , y: - 0.5 * b },
];
return xys.map(point => {
let x = point.x;
let y = point.y;
return new BABYLON.Vector3( x * cos - y * sin, x * sin + y * cos , 0.0 );
});
};
const createBox = function (h, b, tw, tf, rotation = 0.0) {
let sin = Math.sin(rotation / 180.0 * Math.PI);
let cos = Math.cos(rotation / 180.0 * Math.PI);
let xys = [
{ x: - 0.5 * h , y: 0.5 * b },
{ x: 0.5 * h , y: 0.5 * b },
{ x: 0.5 * h , y: - 0.5 * b },
{ x: - 0.5 * h , y: - 0.5 * b },
{ x: - 0.5 * h , y: 0.5 * b - tw },
{ x: - 0.5 * h + tf, y: 0.5 * b - tw },
{ x: - 0.5 * h + tf, y: - 0.5 * b + tw },
{ x: 0.5 * h - tf, y: - 0.5 * b + tw },
{ x: 0.5 * h - tf, y: 0.5 * b - tw },
{ x: - 0.5 * h , y: 0.5 * b - tw },
];
return xys.map(point => {
let x = point.x;
let y = point.y;
return new BABYLON.Vector3( x * cos - y * sin, x * sin + y * cos , 0.0 );
});
};
var createScene = function() {
const scene = new BABYLON.Scene(engine);
const camera = new BABYLON.ArcRotateCamera("Camera", 3 * Math.PI / 2, Math.PI / 2, 30, BABYLON.Vector3.Zero());
camera.attachControl(canvas, true);
const light = new BABYLON.HemisphericLight("hemi", new BABYLON.Vector3(50, 50, 0));
const hShape = createHShape(1.0, 0.8, 0.06, 0.08, rotation = 90.0);
const boxShape = createBox(1.2, 1.2, 0.1, 0.1);
const girderXPath = [
new BABYLON.Vector3(0, 3, 3),
new BABYLON.Vector3(6, 3, 3)
];
const girderYPath = [
new BABYLON.Vector3(3, 3, 0),
new BABYLON.Vector3(3, 3, 6)
];
const columnPath = [
new BABYLON.Vector3(3, 0, 3),
new BABYLON.Vector3(3, 6, 3)
];
const g1 = BABYLON.MeshBuilder.ExtrudeShape("g1", {shape: hShape, closeShape: true, path: girderXPath, sideOrientation: BABYLON.Mesh.DOUBLESIDE}, scene);
const g2 = BABYLON.MeshBuilder.ExtrudeShape("g2", {shape: hShape, closeShape: true, path: girderYPath, sideOrientation: BABYLON.Mesh.DOUBLESIDE}, scene);
const c1 = BABYLON.MeshBuilder.ExtrudeShape("c1", {shape: boxShape, closeShape: true, path: columnPath, sideOrientation: BABYLON.Mesh.DOUBLESIDE }, scene);
return scene;
};
https://playground.babylonjs.com/#U6N2EV
押出しの問題点
先程の結果はうまくいっているように見えますが、一つ問題があります。今回使った押出しによる方法だと、中身が空洞になってしまいます。
When you need the appearance of a solid shape then there is an option to cap the ends. The caps are drawn by creating triangles from the Barycenter of the shape profile to the profile vertices, so that there are profile shapes that cause caps to not correctly fit the profile shape. ソリッド シェイプの外観が必要な場合は、端をキャップするオプションがあります。キャップは、形状プロファイルの重心からプロファイル頂点までの三角形を作成することによって描画されるため、キャップがプロファイル形状に正しく適合しないプロファイル形状が存在します。 (前述のドキュメントより抜粋)実際にcapしてみるとこのようになってしまいました。
これについて、Babylon.js勉強会のDiscordコミュニティで質問したところ、「CSGを使えばいいのでは?」と教えてもらいました。
CSGとは、"Constructive Solid Geometry"の略で、要は図形を合体させたりくり抜いたりして形状を作る手法のようです。
https://ja.wikipedia.org/wiki/Constructive_Solid_Geometry
CSGによる実装
CSGによる実装も行ってみました。CSGだと、きちんと空洞にならずに描画できました。一旦図形としてオブジェクトを座標原点に生成した後、そのオブジェクトの中心座標を所定の座標に移動したり回転したりするようなイメージです。
この方法だと、部材を正しい向きにするために全体座標系のどの軸に対してどのくらい回転するのか指示してあげる必要があります。最初はH形鋼が強軸を向いたり弱軸を向いたりしてしまってなやんでしまったので、どちらかというと押出しのほうが扱いやすいと感じました。
const createHShapeMember = function (scene, h, b, tw, tf, point0, point1, rotation = 0.0) {
let x0 = point0[0];
let y0 = point0[1];
let z0 = point0[2];
let x1 = point1[0];
let y1 = point1[1];
let z1 = point1[2];
let length = Math.sqrt((x0 - x1) ** 2.0 + (y0 - y1) ** 2.0 + (z0 - z1) ** 2.0);
// 一旦、X軸上に配置する
const baseBox = BABYLON.MeshBuilder.CreateBox("box", {width: length, height: h, depth: b});
const subBox1 = BABYLON.MeshBuilder.CreateBox("box", {width: length, height: h - tf * 2.0, depth: 0.5 * (b - tw) });
const subBox2 = BABYLON.MeshBuilder.CreateBox("box", {width: length, height: h - tf * 2.0, depth: 0.5 * (b - tw) });
subBox1.position.z += 0.25 * (b + tw);
subBox2.position.z -= 0.25 * (b + tw);
const hMesh =
BABYLON.CSG.FromMesh(baseBox).subtract(BABYLON.CSG.FromMesh(subBox1))
.subtract(BABYLON.CSG.FromMesh(subBox2))
.toMesh("csg plate8", null, scene);
// 回転・座標移動
let alpha = rotation / 180.0 * Math.PI;
let beta = Math.asin( (z1 - z0) / length );
let gamma = Math.asin( (y1 - y0) / length );
hMesh.position = new BABYLON.Vector3(0.5 * (x0 + x1), 0.5 * (y0 + y1), 0.5 * (z0 + z1));
hMesh.rotation = new BABYLON.Vector3(alpha, beta, gamma);
baseBox.dispose();
subBox1.dispose();
subBox2.dispose();
}
const createBoxShapeMember = function (scene, h, b, tw, tf, point0, point1, rotation = 0.0) {
let x0 = point0[0];
let y0 = point0[1];
let z0 = point0[2];
let x1 = point1[0];
let y1 = point1[1];
let z1 = point1[2];
// 一旦、Y軸(鉛直軸)方向に配置
let length = Math.sqrt((x0 - x1) ** 2.0 + (y0 - y1) ** 2.0 + (z0 - z1) ** 2.0);
const baseBox = BABYLON.MeshBuilder.CreateBox("box", {height: length, width: h, depth: b});
const subBox1 = BABYLON.MeshBuilder.CreateBox("box", {height: length, width: h - tf * 2.0, depth: b - tw * 0.5 });
const hMesh =
BABYLON.CSG.FromMesh(baseBox).subtract(BABYLON.CSG.FromMesh(subBox1))
.toMesh("csg plate8", null, scene);
// 回転・座標移動
let alpha = Math.asin( (z1 - z0) / length );
let beta = rotation / 180.0 * Math.PI;
let gamma = Math.asin( (x1 - x0) / length );
hMesh.position = new BABYLON.Vector3(0.5 * (x0 + x1), 0.5 * (y0 + y1), 0.5 * (z0 + z1));
hMesh.rotation = new BABYLON.Vector3(alpha, beta, gamma);
baseBox.dispose();
subBox1.dispose();
}
https://playground.babylonjs.com/#FDS7B1
まとめ
Babylon.jsでを使ってみました。思いの外簡単で楽しかったです。Babylon.js はゲームエンジンとしてのユースケースが想定されていますので、物理演算機能などもあります。
今後、もう少しいろいろな機能を使い込んでいきたいと思います。
採用情報
構造計画研究所 RESPチームでは、いっしょに働いていただけるエンジニアを募集しています。
構造設計・構造解析だけでなく、プログラミング技術を活かして新しいものを生み出したいと思っている方、ぜひご応募ください。
採用HPはこちら→https://www.kke.co.jp/recruit/