app
Adom 3D Viewer
Public Made by Adomby adom
The Babylon 9.5 engine behind every component page's 3D tab on wiki.adom.inc. Versioned ESM bundle with GLB loading, view cube, layers toolbar, ground shadows, and Z-up CAD framing.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
import { Engine } from '@babylonjs/core/Engines/engine';
import { Scene } from '@babylonjs/core/scene';
import { AdomCamera } from './Camera/AdomCamera';
import type { Nullable } from '@babylonjs/core/types';
import type { ShadowGenerator, ISceneLoaderAsyncResult } from '@babylonjs/core';
import type { SpotLight } from '@babylonjs/core/Lights/spotLight';
import type { HemisphericLight } from '@babylonjs/core/Lights/hemisphericLight';
import { RenderTargetTexture } from '@babylonjs/core';
export class AdomStatics {
public static engine: Nullable<Engine> = null;
public static scene: Nullable<Scene> = null;
public static camera: Nullable<AdomCamera> = null;
public static shadowGenerator: Nullable<ShadowGenerator> = null;
public static spotLight: Nullable<SpotLight> = null;
/** Bottom-fill hemispheric light — illuminates the underside of parts.
* Default OFF (intensity 0). Toggleable via ThreeDViewer.setBottomLight(). */
public static bottomLight: Nullable<HemisphericLight> = null;
static modelLoadingPromises = new Map<string, Promise<ISceneLoaderAsyncResult>>();
private static _windowResizeHandler: Nullable<EventListener> = null;
/**
* Initialize the engine and scene.
* @param canvas - The canvas element to render to
* @param zUp - If true, use Z-up coordinate system (default). If false, use Y-up (Babylon default).
*/
public static async init(canvas: HTMLCanvasElement, zUp: boolean = true): Promise<void> {
if (this.engine && this.scene && this.camera) {
return;
}
this.engine = new Engine(canvas, true, { useHighPrecisionFloats: true });
this.scene = new Scene(this.engine);
this.scene.useRightHandedSystem = zUp;
this.camera = new AdomCamera(this.scene, canvas, zUp);
this.camera.disableBounds();
this.camera.setClickToSetRotationPoint(false);
this.scene.activeCamera = this.camera;
}
public static dispose(): void {
if (this.scene) {
this.scene.dispose();
this.scene = null;
}
if (this.engine) {
this.engine.stopRenderLoop();
this.engine.dispose();
this.engine = null;
}
if (this.camera) {
this.camera.dispose();
this.camera = null;
}
if (this.shadowGenerator) {
this.shadowGenerator.dispose();
this.shadowGenerator = null;
}
if (this.spotLight) {
this.spotLight.dispose();
this.spotLight = null;
}
if (this.bottomLight) {
this.bottomLight.dispose();
this.bottomLight = null;
}
if (this.modelLoadingPromises) {
this.modelLoadingPromises.clear();
}
}
/**
* Re-renders the shadow map once. Defers by one frame so that any light
* frustum or caster-list changes from the current frame are applied first.
*/
private static _pcssTransparencyWarned = false;
/**
* PCSS combined with soft transparent shadows renders NO shadows at all,
* silently (pixel-diff proven: PCSS 0.0% change, PCF 26.7%). Auto-correct
* to PCF so consumers flipping useContactHardeningShadow on a scene with
* enableSoftTransparentShadow don't lose shadows. Called from
* refreshShadows and once per frame (two boolean reads, negligible).
*/
public static enforceShadowFilterCompat(): void {
const sg = this.shadowGenerator;
if (!sg) return;
if (sg.useContactHardeningShadow && sg.enableSoftTransparentShadow) {
if (!this._pcssTransparencyWarned) {
this._pcssTransparencyWarned = true;
console.warn(
'[adom-3d-viewer] PCSS (useContactHardeningShadow) + enableSoftTransparentShadow ' +
'silently renders no shadows. Falling back to PCF (usePercentageCloserFiltering). ' +
'Use PCF whenever soft transparent shadows are enabled.'
);
}
sg.useContactHardeningShadow = false;
sg.usePercentageCloserFiltering = true;
}
}
public static refreshShadows(): void {
if (!this.shadowGenerator || !this.scene) return;
const sg = this.shadowGenerator;
const shadowMap = sg.getShadowMap();
if (!shadowMap) return;
this.enforceShadowFilterCompat();
// Animated models (glTF animationGroups, e.g. a press-fit insertion or a
// spin) need the shadow map re-rendered EVERY frame so the cast shadows
// track the moving meshes. A one-shot render freezes the shadow at frame 0,
// which reads as broken (the pin moves, its shadow does not). Keep the
// cheap render-once only for fully static models.
const hasAnimation = (this.scene.animationGroups || []).length > 0;
if (hasAnimation) {
shadowMap.refreshRate = RenderTargetTexture.REFRESHRATE_RENDER_ONEVERYFRAME;
return;
}
this.scene.onAfterRenderObservable.addOnce(() => {
shadowMap.refreshRate = RenderTargetTexture.REFRESHRATE_RENDER_ONCE;
shadowMap.resetRefreshCounter();
});
}
}