|
| 1 | +/* eslint-disable @typescript-eslint/naming-convention */ |
| 2 | +import type { Nullable } from "core/types"; |
| 3 | +import type { Material } from "core/Materials/material"; |
| 4 | +import { Color3 } from "core/Maths/math.color"; |
| 5 | +import { Vector3 } from "core/Maths/math.vector"; |
| 6 | +import type { IMaterial } from "../glTFLoaderInterfaces"; |
| 7 | +import type { IGLTFLoaderExtension } from "../glTFLoaderExtension"; |
| 8 | +import { GLTFLoader } from "../glTFLoader"; |
| 9 | +import type { IKHRMaterialsVolumeScatter } from "babylonjs-gltf2interface"; |
| 10 | +import { registerGLTFExtension, unregisterGLTFExtension } from "../glTFLoaderExtensionRegistry"; |
| 11 | + |
| 12 | +const NAME = "KHR_materials_volume_scatter"; |
| 13 | + |
| 14 | +declare module "../../glTFFileLoader" { |
| 15 | + // eslint-disable-next-line jsdoc/require-jsdoc, @typescript-eslint/naming-convention |
| 16 | + export interface GLTFLoaderExtensionOptions { |
| 17 | + /** |
| 18 | + * Defines options for the KHR_materials_volume_scatter extension. |
| 19 | + */ |
| 20 | + // NOTE: Don't use NAME here as it will break the UMD type declarations. |
| 21 | + ["KHR_materials_volume_scatter"]: {}; |
| 22 | + } |
| 23 | +} |
| 24 | + |
| 25 | +function multiScatterToSingleScatterAlbedo(multiScatter: Color3): Vector3 { |
| 26 | + const multiScatterAlbedo = new Vector3(multiScatter.r, multiScatter.g, multiScatter.b); |
| 27 | + const s: Vector3 = new Vector3(4.09712, 4.09712, 4.09712); |
| 28 | + s.multiplyInPlace(new Vector3(4.20863, 4.20863, 4.20863).multiplyInPlace(multiScatterAlbedo)); |
| 29 | + |
| 30 | + const p: Vector3 = new Vector3(9.59217, 9.59217, 9.59217); |
| 31 | + p.addInPlace(new Vector3(41.6808, 41.6808, 41.6808).multiplyInPlace(multiScatterAlbedo)); |
| 32 | + p.addInPlace(new Vector3(17.7126, 17.7126, 17.7126).multiplyInPlace(multiScatterAlbedo.multiply(multiScatterAlbedo))); |
| 33 | + s.subtractInPlace(new Vector3(Math.sqrt(p.x), Math.sqrt(p.y), Math.sqrt(p.z))); |
| 34 | + return new Vector3(1.0, 1.0, 1.0).subtract(s.multiply(s)); |
| 35 | +} |
| 36 | + |
| 37 | +/** |
| 38 | + * [Specification](https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_volume_scatter/README.md) |
| 39 | + * @since 5.0.0 |
| 40 | + */ |
| 41 | +// eslint-disable-next-line @typescript-eslint/naming-convention |
| 42 | +export class KHR_materials_volume_scatter implements IGLTFLoaderExtension { |
| 43 | + /** |
| 44 | + * The name of this extension. |
| 45 | + */ |
| 46 | + public readonly name = NAME; |
| 47 | + |
| 48 | + /** |
| 49 | + * Defines whether this extension is enabled. |
| 50 | + */ |
| 51 | + public enabled: boolean; |
| 52 | + |
| 53 | + /** |
| 54 | + * Defines a number that determines the order the extensions are applied. |
| 55 | + */ |
| 56 | + public order = 174; |
| 57 | + |
| 58 | + private _loader: GLTFLoader; |
| 59 | + |
| 60 | + /** |
| 61 | + * @internal |
| 62 | + */ |
| 63 | + constructor(loader: GLTFLoader) { |
| 64 | + this._loader = loader; |
| 65 | + this.enabled = this._loader.isExtensionUsed(NAME); |
| 66 | + if (this.enabled) { |
| 67 | + // We need to disable instance usage because the attenuation factor depends on the node scale of each individual mesh |
| 68 | + this._loader._disableInstancedMesh++; |
| 69 | + } |
| 70 | + } |
| 71 | + |
| 72 | + /** @internal */ |
| 73 | + public dispose() { |
| 74 | + if (this.enabled) { |
| 75 | + this._loader._disableInstancedMesh--; |
| 76 | + } |
| 77 | + (this._loader as any) = null; |
| 78 | + } |
| 79 | + |
| 80 | + /** |
| 81 | + * @internal |
| 82 | + */ |
| 83 | + // eslint-disable-next-line no-restricted-syntax |
| 84 | + public loadMaterialPropertiesAsync(context: string, material: IMaterial, babylonMaterial: Material): Nullable<Promise<void>> { |
| 85 | + return GLTFLoader.LoadExtensionAsync<IKHRMaterialsVolumeScatter>(context, material, this.name, async (extensionContext, extension) => { |
| 86 | + const promises = new Array<Promise<any>>(); |
| 87 | + promises.push(this._loader.loadMaterialPropertiesAsync(context, material, babylonMaterial)); |
| 88 | + promises.push(this._loadVolumePropertiesAsync(extensionContext, material, babylonMaterial, extension)); |
| 89 | + // eslint-disable-next-line github/no-then |
| 90 | + return await Promise.all(promises).then(() => {}); |
| 91 | + }); |
| 92 | + } |
| 93 | + |
| 94 | + // eslint-disable-next-line @typescript-eslint/promise-function-async, no-restricted-syntax |
| 95 | + private _loadVolumePropertiesAsync(context: string, material: IMaterial, babylonMaterial: Material, extension: IKHRMaterialsVolumeScatter): Promise<void> { |
| 96 | + const adapter = this._loader._getOrCreateMaterialAdapter(babylonMaterial); |
| 97 | + |
| 98 | + // If transparency isn't enabled already, this extension shouldn't do anything. |
| 99 | + // i.e. it requires either the KHR_materials_transmission or KHR_materials_diffuse_transmission extensions. |
| 100 | + if (adapter.transmissionWeight === 0 && adapter.subsurfaceWeight === 0) { |
| 101 | + return Promise.resolve(); |
| 102 | + } |
| 103 | + |
| 104 | + const scatterColor = extension.multiscatterColor !== undefined && extension.multiscatterColor.length == 3 ? Color3.FromArray(extension.multiscatterColor) : Color3.Black(); |
| 105 | + const scatterAnisotropy = extension.scatterAnisotropy !== undefined ? extension.scatterAnisotropy : 0; |
| 106 | + |
| 107 | + // In glTF, both the translucency volume and subsurface volume use the same input parameters. |
| 108 | + // We'll apply them to both, as appropriate. |
| 109 | + if (adapter.transmissionWeight > 0) { |
| 110 | + const singleScatterAlbedo = multiScatterToSingleScatterAlbedo(scatterColor); |
| 111 | + const absorptionCoefficient = adapter.extinctionCoefficient.multiplyByFloats(1.0 - singleScatterAlbedo.x, 1.0 - singleScatterAlbedo.y, 1.0 - singleScatterAlbedo.z); |
| 112 | + const scatteringCoefficient = adapter.extinctionCoefficient.multiply(singleScatterAlbedo); |
| 113 | + |
| 114 | + const maxVal = Math.max(absorptionCoefficient.x, absorptionCoefficient.y, absorptionCoefficient.z); |
| 115 | + const absorptionDistance = maxVal !== 0.0 ? 1.0 / maxVal : 1.0; |
| 116 | + |
| 117 | + adapter.transmissionColor = new Color3( |
| 118 | + Math.exp(-absorptionCoefficient.x * absorptionDistance), |
| 119 | + Math.exp(-absorptionCoefficient.y * absorptionDistance), |
| 120 | + Math.exp(-absorptionCoefficient.z * absorptionDistance) |
| 121 | + ); |
| 122 | + adapter.transmissionDepth = absorptionDistance; |
| 123 | + adapter.transmissionScatter = scatteringCoefficient; |
| 124 | + adapter.transmissionScatterAnisotropy = scatterAnisotropy; |
| 125 | + } |
| 126 | + // Subsurface volume |
| 127 | + if (adapter.subsurfaceWeight > 0) { |
| 128 | + adapter.subsurfaceScatterAnisotropy = scatterAnisotropy; |
| 129 | + adapter.subsurfaceColor = scatterColor; |
| 130 | + |
| 131 | + const mfp = new Vector3( |
| 132 | + adapter.extinctionCoefficient.x !== 0 ? 1.0 / adapter.extinctionCoefficient.x : 1.0, |
| 133 | + adapter.extinctionCoefficient.y !== 0 ? 1.0 / adapter.extinctionCoefficient.y : 1.0, |
| 134 | + adapter.extinctionCoefficient.z !== 0 ? 1.0 / adapter.extinctionCoefficient.z : 1.0 |
| 135 | + ); |
| 136 | + |
| 137 | + adapter.subsurfaceRadius = Math.max(mfp.x, mfp.y, mfp.z); |
| 138 | + adapter.subsurfaceRadiusScale = new Color3(mfp.x / adapter.subsurfaceRadius, mfp.y / adapter.subsurfaceRadius, mfp.z / adapter.subsurfaceRadius); |
| 139 | + } |
| 140 | + |
| 141 | + return Promise.resolve(); |
| 142 | + } |
| 143 | +} |
| 144 | + |
| 145 | +unregisterGLTFExtension(NAME); |
| 146 | +registerGLTFExtension(NAME, true, (loader) => new KHR_materials_volume_scatter(loader)); |
0 commit comments