3d-arena / src /routes /viewers /BabylonViewer.ts
dylanebert's picture
dylanebert HF staff
support topology-only outputs
9c554be
import type { IViewer } from "./IViewer";
import * as BABYLON from "@babylonjs/core";
import "@babylonjs/loaders/glTF";
import "@babylonjs/loaders/OBJ";
export class BabylonViewer implements IViewer {
canvas: HTMLCanvasElement;
engine: BABYLON.Engine;
scene: BABYLON.Scene;
camera: BABYLON.ArcRotateCamera;
vertexCount: number = 0;
topoOnly: boolean = false;
private _originalMaterials: Map<BABYLON.AbstractMesh, BABYLON.Material> = new Map();
private _originalVertexColors: Map<BABYLON.AbstractMesh, BABYLON.Nullable<BABYLON.FloatArray>> = new Map();
private _wireframes: Array<BABYLON.Mesh> = [];
private _solidColor = new BABYLON.Color4(1, 1, 1, 1);
private _wireframeMaterial: BABYLON.StandardMaterial;
private _solidMaterial: BABYLON.StandardMaterial;
constructor(canvas: HTMLCanvasElement) {
this.canvas = canvas;
this.engine = new BABYLON.Engine(canvas, true);
this.scene = new BABYLON.Scene(this.engine);
this.scene.clearColor = BABYLON.Color4.FromHexString("#000000FF");
this.scene.imageProcessingConfiguration.exposure = 1.3;
this.camera = new BABYLON.ArcRotateCamera(
"camera",
Math.PI / 3,
Math.PI / 3,
30,
BABYLON.Vector3.Zero(),
this.scene
);
this.camera.angularSensibilityY = 1000;
this.camera.panningSensibility = 500;
this.camera.wheelPrecision = 5;
this.camera.inertia = 0.9;
this.camera.panningInertia = 0.9;
this.camera.lowerRadiusLimit = 3;
this.camera.upperRadiusLimit = 100;
this.camera.setTarget(BABYLON.Vector3.Zero());
this.camera.attachControl(this.canvas, true);
this.camera.onAfterCheckInputsObservable.add(() => {
this.camera.wheelPrecision = 150 / this.camera.radius;
this.camera.panningSensibility = 10000 / this.camera.radius;
});
this._wireframeMaterial = new BABYLON.StandardMaterial("wireframe", this.scene);
this._wireframeMaterial.diffuseColor = new BABYLON.Color3(0, 0, 0);
this._wireframeMaterial.emissiveColor = new BABYLON.Color3(0.7, 0.7, 0.7);
this._wireframeMaterial.disableLighting = true;
this._wireframeMaterial.wireframe = true;
this._solidMaterial = new BABYLON.StandardMaterial("solid", this.scene);
this._solidMaterial.diffuseColor = new BABYLON.Color3(0.8, 0.8, 0.8);
this._solidMaterial.alphaMode = 0;
this.handleResize = this.handleResize.bind(this);
window.addEventListener("resize", this.handleResize);
this.canvas.addEventListener("wheel", (e) => e.preventDefault());
}
handleResize() {
this.engine.resize();
}
async loadScene(url: string, loadingBarCallback?: (progress: number) => void, topoOnly?: boolean) {
this.topoOnly = topoOnly ?? false;
// Load scene
await BABYLON.SceneLoader.AppendAsync("", url, this.scene, (event) => {
const progress = event.loaded / event.total;
loadingBarCallback?.(progress);
});
// Dispose of all cameras and lights
this.scene.cameras.forEach((camera) => {
if (camera !== this.camera) {
camera.dispose();
}
});
this.scene.lights.forEach((light) => {
light.dispose();
});
// Add lights
const hemi = new BABYLON.HemisphericLight("hemi", new BABYLON.Vector3(0, 1, 0), this.scene);
hemi.intensity = 0.5;
hemi.diffuse = new BABYLON.Color3(1, 1, 1);
hemi.groundColor = new BABYLON.Color3(1, 1, 1);
const sun = new BABYLON.DirectionalLight("sun", new BABYLON.Vector3(-0.5, -1, -0.5), this.scene);
sun.intensity = 1;
sun.diffuse = new BABYLON.Color3(1, 1, 1);
// Center and scale model
const parentNode = new BABYLON.TransformNode("parent", this.scene);
const standardSize = 10;
let scaleFactor = 1;
let center = BABYLON.Vector3.Zero();
if (this.scene.meshes.length > 0) {
let bounds = this.scene.meshes[0].getBoundingInfo().boundingBox;
let min = bounds.minimumWorld;
let max = bounds.maximumWorld;
for (let i = 1; i < this.scene.meshes.length; i++) {
bounds = this.scene.meshes[i].getBoundingInfo().boundingBox;
min = BABYLON.Vector3.Minimize(min, bounds.minimumWorld);
max = BABYLON.Vector3.Maximize(max, bounds.maximumWorld);
}
const extent = max.subtract(min).scale(0.5);
const size = extent.length();
center = BABYLON.Vector3.Center(min, max);
scaleFactor = standardSize / size;
}
this.vertexCount = 0;
this.scene.meshes.forEach((mesh) => {
mesh.setParent(parentNode);
if (mesh.getTotalVertices() > 0) {
this.vertexCount += mesh.getTotalVertices();
}
});
parentNode.position = center.scale(-1 * scaleFactor);
parentNode.scaling.scaleInPlace(scaleFactor);
if (this.topoOnly) {
this.setRenderMode("wireframe");
}
// Run render loop
this.engine.runRenderLoop(() => {
this.scene.render();
});
}
dispose() {
if (this.scene) {
this.scene.dispose();
}
if (this.engine) {
this.engine.dispose();
}
this._originalMaterials.clear();
window.removeEventListener("resize", this.handleResize);
this.canvas.removeEventListener("wheel", (e) => e.preventDefault());
}
async capture(): Promise<string | null> {
if (!this.engine || !this.camera) return null;
const cachedColor = this.scene.clearColor;
this.scene.clearColor = BABYLON.Color4.FromHexString("#00000000");
let data = await new Promise<string>((resolve) => {
BABYLON.Tools.CreateScreenshotUsingRenderTarget(this.engine, this.camera, 512, (result) => {
resolve(result);
});
});
this.scene.clearColor = cachedColor;
return data;
}
setRenderMode(mode: string) {
if (mode === "wireframe") {
this.scene.meshes.forEach((mesh) => {
const vertexData = mesh.getVerticesData(BABYLON.VertexBuffer.ColorKind);
if (vertexData) {
this._originalVertexColors.set(mesh, vertexData);
const newVertexData = new Array<number>(vertexData.length);
for (let i = 0; i < vertexData.length; i += 4) {
newVertexData[i] = this._solidColor.r;
newVertexData[i + 1] = this._solidColor.g;
newVertexData[i + 2] = this._solidColor.b;
newVertexData[i + 3] = this._solidColor.a;
}
mesh.setVerticesData(BABYLON.VertexBuffer.ColorKind, newVertexData);
}
const material = mesh.material as BABYLON.StandardMaterial;
if (material) {
this._originalMaterials.set(mesh, material);
mesh.material = this._solidMaterial;
}
const wireframeMesh = mesh.clone(mesh.name + "_wireframe", null) as BABYLON.Mesh;
wireframeMesh.material = this._wireframeMaterial;
this._wireframes.push(wireframeMesh);
});
} else {
this._wireframes.forEach((mesh) => {
mesh.dispose();
});
this._wireframes = [];
this.scene.meshes.forEach((mesh) => {
const vertexData = this._originalVertexColors.get(mesh);
if (vertexData) {
mesh.setVerticesData(BABYLON.VertexBuffer.ColorKind, vertexData);
}
const material = this._originalMaterials.get(mesh);
if (material) {
mesh.material = material;
}
});
}
}
}