@babylonjs/loaders
Version:
For usage documentation please visit https://doc.babylonjs.com/features/featuresDeepDive/importers/loadingFileTypes/.
373 lines • 16.5 kB
JavaScript
import { MultiMaterial } from "@babylonjs/core/Materials/multiMaterial.js";
import { RenderTargetTexture } from "@babylonjs/core/Materials/Textures/renderTargetTexture.js";
import { Observable } from "@babylonjs/core/Misc/observable.js";
import { Constants } from "@babylonjs/core/Engines/constants.js";
import { Tools } from "@babylonjs/core/Misc/tools.js";
/**
* A class to handle setting up the rendering of opaque objects to be shown through transmissive objects.
* @internal
*/
export class TransmissionHelper {
/**
* Creates the default options for the helper.
* @returns the default options
*/
static _GetDefaultOptions() {
return {
renderSize: 1024,
samples: 4,
lodGenerationScale: 1,
lodGenerationOffset: -4,
renderTargetTextureType: Constants.TEXTURETYPE_HALF_FLOAT,
generateMipmaps: true,
};
}
/**
* constructor
* @param options Defines the options we want to customize the helper
* @param scene The scene to add the material to
*/
constructor(options, scene) {
this._opaqueRenderTarget = null;
this._opaqueMeshesCache = [];
this._transparentMeshesCache = [];
this._materialObservers = {};
// Material implementations registered by loaders. Each entry maps a material class
// to its adapter class so the helper can classify and interact with materials
// independently of whichever loader originally created them.
this._materialImpls = [];
this._adapterCache = new WeakMap();
// For MultiMaterial meshes with mixed opaque/translucent sub-materials:
// maps mesh → set of materialIndex values that are translucent.
this._translucentMaterialIndices = new Map();
// Precomputed opaque-only submesh arrays for mixed meshes, swapped in
// during the opaque RT render to avoid per-frame allocations.
this._opaqueOnlySubMeshes = new Map();
this._savedSubMeshes = new Map();
this._options = {
...TransmissionHelper._GetDefaultOptions(),
...options,
};
this._scene = scene;
this._scene._transmissionHelper = this;
this.onErrorObservable = new Observable();
this._scene.onDisposeObservable.addOnce(() => {
this.dispose();
});
this._parseScene();
this._setupRenderTargets();
}
/**
* Registers a material implementation with the helper so it can classify and create
* adapters for materials of that type. Safe to call multiple times with the same
* implementation — duplicates are ignored.
* @param impl The material implementation to register
*/
addMaterialImpl(impl) {
if (!this._materialImpls.some((i) => i.materialClass === impl.materialClass)) {
this._materialImpls.push(impl);
}
}
/**
* Updates the helper options.
* @param options the options to update
*/
updateOptions(options) {
// First check if any options are actually being changed. If not, exit.
const newValues = Object.keys(options).filter((key) => this._options[key] !== options[key]);
if (!newValues.length) {
return;
}
const newOptions = {
...this._options,
...options,
};
const oldOptions = this._options;
this._options = newOptions;
// If size changes, recreate everything
if (newOptions.renderSize !== oldOptions.renderSize ||
newOptions.renderTargetTextureType !== oldOptions.renderTargetTextureType ||
newOptions.generateMipmaps !== oldOptions.generateMipmaps ||
!this._opaqueRenderTarget) {
this._setupRenderTargets();
}
else {
this._opaqueRenderTarget.samples = newOptions.samples;
this._opaqueRenderTarget.lodGenerationScale = newOptions.lodGenerationScale;
this._opaqueRenderTarget.lodGenerationOffset = newOptions.lodGenerationOffset;
}
}
/**
* @returns the opaque render target texture or null if not available.
*/
getOpaqueTarget() {
return this._opaqueRenderTarget;
}
_getOrCreateAdapter(material) {
let adapter = this._adapterCache.get(material);
if (!adapter) {
for (const impl of this._materialImpls) {
if (material instanceof impl.materialClass) {
adapter = new impl.adapterClass(material);
this._adapterCache.set(material, adapter);
break;
}
}
}
return adapter;
}
/**
* Classify a mesh's materials as transparent, opaque, or mixed.
* Sets the refraction background texture on any translucent materials found.
* For mixed MultiMaterial meshes, populates _translucentMaterialIndices so
* their translucent submeshes can be excluded from the opaque render target.
* @param mesh - The mesh to classify
* @returns 'transparent' if all materials are translucent, 'opaque' if none are, 'mixed' if both
*/
_classifyMeshMaterials(mesh) {
const material = mesh.material;
if (!material) {
return "opaque";
}
// Single material case
if (!(material instanceof MultiMaterial)) {
const adapter = this._getOrCreateAdapter(material);
if (!adapter) {
return "opaque";
}
if (adapter.isTranslucent()) {
adapter.refractionBackgroundTexture = this._opaqueRenderTarget;
return "transparent";
}
return "opaque";
}
// MultiMaterial case: check each sub-material individually
let hasTranslucent = false;
let hasOpaque = false;
const translucentIndices = new Set();
for (let i = 0; i < material.subMaterials.length; i++) {
const subMat = material.subMaterials[i];
if (!subMat) {
hasOpaque = true;
continue;
}
const adapter = this._getOrCreateAdapter(subMat);
if (adapter) {
if (adapter.isTranslucent()) {
adapter.refractionBackgroundTexture = this._opaqueRenderTarget;
hasTranslucent = true;
translucentIndices.add(i);
}
else {
hasOpaque = true;
}
}
else {
hasOpaque = true;
}
}
if (hasTranslucent && hasOpaque) {
this._translucentMaterialIndices.set(mesh, translucentIndices);
this._rebuildOpaqueOnlySubMeshes(mesh, translucentIndices);
return "mixed";
}
this._translucentMaterialIndices.delete(mesh);
this._opaqueOnlySubMeshes.delete(mesh);
return hasTranslucent ? "transparent" : "opaque";
}
/**
* Rebuild the cached opaque-only submesh array for a mixed mesh.
* Called when classification changes so the per-frame swap is allocation-free.
* @param mesh - The mesh to rebuild for
* @param translucentIndices - Set of materialIndex values that are translucent
*/
_rebuildOpaqueOnlySubMeshes(mesh, translucentIndices) {
if (mesh.subMeshes) {
this._opaqueOnlySubMeshes.set(mesh, mesh.subMeshes.filter((sm) => !translucentIndices.has(sm.materialIndex)));
}
}
_addMesh(mesh) {
this._materialObservers[mesh.uniqueId] = mesh.onMaterialChangedObservable.add(this._onMeshMaterialChanged.bind(this));
// we need to defer the processing because _addMesh may be called as part as an instance mesh creation, in which case some
// internal properties are not setup yet, like _sourceMesh (needed when doing mesh.material below)
Tools.SetImmediate(() => {
if (mesh.material) {
const classification = this._classifyMeshMaterials(mesh);
if (classification === "transparent") {
if (this._transparentMeshesCache.indexOf(mesh) === -1) {
this._transparentMeshesCache.push(mesh);
}
}
else {
// Both 'opaque' and 'mixed' go in the opaque cache.
// For 'mixed', the translucent submeshes are temporarily
// excluded during the opaque render target render.
if (this._opaqueMeshesCache.indexOf(mesh) === -1) {
this._opaqueMeshesCache.push(mesh);
}
}
}
});
}
_removeMesh(mesh) {
mesh.onMaterialChangedObservable.remove(this._materialObservers[mesh.uniqueId]);
delete this._materialObservers[mesh.uniqueId];
let idx = this._transparentMeshesCache.indexOf(mesh);
if (idx !== -1) {
this._transparentMeshesCache.splice(idx, 1);
}
idx = this._opaqueMeshesCache.indexOf(mesh);
if (idx !== -1) {
this._opaqueMeshesCache.splice(idx, 1);
}
this._translucentMaterialIndices.delete(mesh);
this._opaqueOnlySubMeshes.delete(mesh);
}
_parseScene() {
this._scene.meshes.forEach(this._addMesh.bind(this));
// Listen for when a mesh is added to the scene and add it to our cache lists.
this._scene.onNewMeshAddedObservable.add(this._addMesh.bind(this));
// Listen for when a mesh is removed from to the scene and remove it from our cache lists.
this._scene.onMeshRemovedObservable.add(this._removeMesh.bind(this));
}
// When one of the meshes in the scene has its material changed, make sure that it's in the correct cache list.
_onMeshMaterialChanged(mesh) {
const transparentIdx = this._transparentMeshesCache.indexOf(mesh);
const opaqueIdx = this._opaqueMeshesCache.indexOf(mesh);
const classification = this._classifyMeshMaterials(mesh);
if (classification === "transparent") {
// Fully translucent: move to transparent cache
if (opaqueIdx !== -1) {
this._opaqueMeshesCache.splice(opaqueIdx, 1);
this._transparentMeshesCache.push(mesh);
}
else if (transparentIdx === -1) {
this._transparentMeshesCache.push(mesh);
}
}
else {
// Opaque or mixed: move to opaque cache (mixed meshes have their
// translucent submeshes excluded during opaque RT render)
if (transparentIdx !== -1) {
this._transparentMeshesCache.splice(transparentIdx, 1);
this._opaqueMeshesCache.push(mesh);
}
else if (opaqueIdx === -1) {
this._opaqueMeshesCache.push(mesh);
}
}
}
/**
* @internal
* Check if the opaque render target has not been disposed and can still be used.
* @returns
*/
_isRenderTargetValid() {
return this._opaqueRenderTarget?.getInternalTexture() !== null;
}
/**
* @internal
* Setup the render targets according to the specified options.
*/
_setupRenderTargets() {
if (this._opaqueRenderTarget) {
this._opaqueRenderTarget.dispose();
}
this._opaqueRenderTarget = new RenderTargetTexture("opaqueSceneTexture", this._options.renderSize, this._scene, this._options.generateMipmaps, undefined, this._options.renderTargetTextureType);
this._opaqueRenderTarget.ignoreCameraViewport = true;
this._opaqueRenderTarget.renderList = this._opaqueMeshesCache;
this._opaqueRenderTarget.clearColor = this._options.clearColor?.clone() ?? this._scene.clearColor.clone();
this._opaqueRenderTarget.clearColor.a = 0.0;
this._opaqueRenderTarget.gammaSpace = false;
this._opaqueRenderTarget.lodGenerationScale = this._options.lodGenerationScale;
this._opaqueRenderTarget.lodGenerationOffset = this._options.lodGenerationOffset;
this._opaqueRenderTarget.samples = this._options.samples;
this._opaqueRenderTarget.renderSprites = true;
this._opaqueRenderTarget.renderParticles = true;
this._opaqueRenderTarget.disableImageProcessing = true;
let saveSceneEnvIntensity;
this._opaqueRenderTarget.onBeforeBindObservable.add((opaqueRenderTarget) => {
saveSceneEnvIntensity = this._scene.environmentIntensity;
this._scene.environmentIntensity = 1.0;
if (!this._options.clearColor) {
this._scene.clearColor.toLinearSpaceToRef(opaqueRenderTarget.clearColor, this._scene.getEngine().useExactSrgbConversions);
}
else {
opaqueRenderTarget.clearColor.copyFrom(this._options.clearColor);
}
opaqueRenderTarget.clearColor.a = 0.0;
// For mixed MultiMaterial meshes, swap in the precomputed opaque-only
// submesh array so translucent submeshes don't render into the opaque texture.
const tlEntries = this._opaqueOnlySubMeshes.entries();
for (let tlEntry = tlEntries.next(); !tlEntry.done; tlEntry = tlEntries.next()) {
const mesh = tlEntry.value[0];
const opaqueOnly = tlEntry.value[1];
if (mesh.subMeshes) {
this._savedSubMeshes.set(mesh, mesh.subMeshes);
mesh.subMeshes = opaqueOnly;
}
}
});
this._opaqueRenderTarget.onAfterUnbindObservable.add(() => {
this._scene.environmentIntensity = saveSceneEnvIntensity;
// Restore the full submesh list after the opaque RT render
const savedEntries = this._savedSubMeshes.entries();
for (let savedEntry = savedEntries.next(); !savedEntry.done; savedEntry = savedEntries.next()) {
savedEntry.value[0].subMeshes = savedEntry.value[1];
}
this._savedSubMeshes.clear();
});
// Update refraction textures on transparent and mixed meshes
for (const mesh of this._transparentMeshesCache) {
if (mesh.material) {
this._classifyMeshMaterials(mesh);
}
}
const mixedEntries = this._translucentMaterialIndices.entries();
for (let mixedEntry = mixedEntries.next(); !mixedEntry.done; mixedEntry = mixedEntries.next()) {
const mesh = mixedEntry.value[0];
if (mesh.material) {
this._classifyMeshMaterials(mesh);
}
}
}
/**
* Dispose all the elements created by the Helper.
*/
dispose() {
this._scene._transmissionHelper = undefined;
if (this._opaqueRenderTarget) {
this._opaqueRenderTarget.dispose();
this._opaqueRenderTarget = null;
}
this._transparentMeshesCache = [];
this._opaqueMeshesCache = [];
this._translucentMaterialIndices.clear();
this._opaqueOnlySubMeshes.clear();
this._savedSubMeshes.clear();
}
}
/**
* Ensures a TransmissionHelper exists on the scene and has all of the loader's material
* implementations registered with it. Creates the helper if one does not yet exist on the
* scene, and recreates its render target if it has been disposed. Does nothing when the
* loader's parent has `dontUseTransmissionHelper` set.
* @param loader The glTF loader whose material implementations should be registered
* @param babylonMaterial A material belonging to the scene where the helper should live
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
export function ensureTransmissionHelper(loader, babylonMaterial) {
if (loader.parent.dontUseTransmissionHelper) {
return;
}
const scene = babylonMaterial.getScene();
const existingHelper = scene._transmissionHelper;
const helper = existingHelper ?? new TransmissionHelper({}, babylonMaterial.getScene());
for (const impl of Array.from(loader._pbrMaterialImpls.values())) {
helper.addMaterialImpl(impl);
}
if (existingHelper && !existingHelper._isRenderTargetValid()) {
existingHelper._setupRenderTargets();
}
}
//# sourceMappingURL=transmissionHelper.js.map