KHR_materials_transmission.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. import { Nullable } from "babylonjs/types";
  2. import { PBRMaterial } from "babylonjs/Materials/PBR/pbrMaterial";
  3. import { Material } from "babylonjs/Materials/material";
  4. import { BaseTexture } from "babylonjs/Materials/Textures/baseTexture";
  5. import { IMaterial, ITextureInfo } from "../glTFLoaderInterfaces";
  6. import { IGLTFLoaderExtension } from "../glTFLoaderExtension";
  7. import { GLTFLoader } from "../glTFLoader";
  8. import { IKHRMaterialsTransmission } from 'babylonjs-gltf2interface';
  9. import { Scene } from "babylonjs/scene";
  10. import { AbstractMesh } from "babylonjs/Meshes/abstractMesh";
  11. import { Mesh } from "babylonjs/Meshes/mesh";
  12. import { Texture } from "babylonjs/Materials/Textures/texture";
  13. import { RenderTargetTexture } from "babylonjs/Materials/Textures/renderTargetTexture";
  14. import { Observable } from "babylonjs/Misc/observable";
  15. interface ITransmissionHelperHolder {
  16. /**
  17. * @hidden
  18. */
  19. _transmissionHelper: TransmissionHelper | undefined;
  20. }
  21. interface ITransmissionHelperOptions {
  22. /**
  23. * The size of the render buffers
  24. */
  25. renderSize: number;
  26. }
  27. /**
  28. * A class to handle setting up the rendering of opaque objects to be shown through transmissive objects.
  29. */
  30. class TransmissionHelper {
  31. /**
  32. * Creates the default options for the helper.
  33. */
  34. private static _getDefaultOptions(): ITransmissionHelperOptions {
  35. return {
  36. renderSize: 512
  37. };
  38. }
  39. /**
  40. * Stores the creation options.
  41. */
  42. private readonly _scene: Scene & ITransmissionHelperHolder;
  43. private _options: ITransmissionHelperOptions;
  44. private _opaqueRenderTarget: Nullable<RenderTargetTexture> = null;
  45. private _opaqueMeshesCache: Mesh[] = [];
  46. private _transparentMeshesCache: Mesh[] = [];
  47. /**
  48. * This observable will be notified with any error during the creation of the environment,
  49. * mainly texture creation errors.
  50. */
  51. public onErrorObservable: Observable<{ message?: string, exception?: any }>;
  52. /**
  53. * constructor
  54. * @param options Defines the options we want to customize the helper
  55. * @param scene The scene to add the material to
  56. */
  57. constructor(options: Partial<ITransmissionHelperOptions>, scene: Scene) {
  58. this._options = {
  59. ...TransmissionHelper._getDefaultOptions(),
  60. ...options
  61. };
  62. this._scene = scene as any;
  63. this._scene._transmissionHelper = this;
  64. this.onErrorObservable = new Observable();
  65. this._scene.onDisposeObservable.addOnce((scene) => {
  66. this.dispose();
  67. });
  68. this._parseScene();
  69. this._setupRenderTargets();
  70. }
  71. /**
  72. * Updates the background according to the new options
  73. * @param options
  74. */
  75. public updateOptions(options: Partial<ITransmissionHelperOptions>) {
  76. // First check if any options are actually being changed. If not, exit.
  77. const newValues = Object.keys(options).filter((key: string) => (this._options as any)[key] !== (options as any)[key]);
  78. if (!newValues.length) {
  79. return;
  80. }
  81. const newOptions = {
  82. ...this._options,
  83. ...options
  84. };
  85. const oldOptions = this._options;
  86. this._options = newOptions;
  87. // If size changes, recreate everything
  88. if (newOptions.renderSize !== oldOptions.renderSize) {
  89. this._setupRenderTargets();
  90. }
  91. }
  92. public getOpaqueTarget(): Nullable<Texture> {
  93. return this._opaqueRenderTarget;
  94. }
  95. private shouldRenderAsTransmission(material: Nullable<Material>): boolean {
  96. if (!material) {
  97. return false;
  98. }
  99. if (material instanceof PBRMaterial && (material.subSurface.isRefractionEnabled)) {
  100. return true;
  101. }
  102. return false;
  103. }
  104. private _addMesh(mesh: AbstractMesh): void {
  105. if (mesh instanceof Mesh) {
  106. mesh.onMaterialChangedObservable.add(this.onMeshMaterialChanged.bind(this));
  107. if (this.shouldRenderAsTransmission(mesh.material)) {
  108. this._transparentMeshesCache.push(mesh);
  109. } else {
  110. this._opaqueMeshesCache.push(mesh);
  111. }
  112. }
  113. }
  114. private _removeMesh(mesh: AbstractMesh): void {
  115. if (mesh instanceof Mesh) {
  116. mesh.onMaterialChangedObservable.remove(this.onMeshMaterialChanged.bind(this));
  117. let idx = this._transparentMeshesCache.indexOf(mesh);
  118. if (idx !== -1) {
  119. this._transparentMeshesCache.splice(idx, 1);
  120. }
  121. idx = this._opaqueMeshesCache.indexOf(mesh);
  122. if (idx !== -1) {
  123. this._opaqueMeshesCache.splice(idx, 1);
  124. }
  125. }
  126. }
  127. private _parseScene(): void {
  128. this._scene.meshes.forEach(this._addMesh.bind(this));
  129. // Listen for when a mesh is added to the scene and add it to our cache lists.
  130. this._scene.onNewMeshAddedObservable.add(this._addMesh.bind(this));
  131. // Listen for when a mesh is removed from to the scene and remove it from our cache lists.
  132. this._scene.onMeshRemovedObservable.add(this._removeMesh.bind(this));
  133. }
  134. // When one of the meshes in the scene has its material changed, make sure that it's in the correct cache list.
  135. private onMeshMaterialChanged(mesh: AbstractMesh) {
  136. if (mesh instanceof Mesh) {
  137. const transparentIdx = this._transparentMeshesCache.indexOf(mesh);
  138. const opaqueIdx = this._opaqueMeshesCache.indexOf(mesh);
  139. // If the material is transparent, make sure that it's added to the transparent list and removed from the opaque list
  140. const useTransmission = this.shouldRenderAsTransmission(mesh.material);
  141. if (useTransmission) {
  142. if (mesh.material instanceof PBRMaterial) {
  143. mesh.material.subSurface.refractionTexture = this._opaqueRenderTarget;
  144. }
  145. if (opaqueIdx !== -1) {
  146. this._opaqueMeshesCache.splice(opaqueIdx, 1);
  147. this._transparentMeshesCache.push(mesh);
  148. } else if (transparentIdx === -1) {
  149. this._transparentMeshesCache.push(mesh);
  150. }
  151. // If the material is opaque, make sure that it's added to the opaque list and removed from the transparent list
  152. } else {
  153. if (transparentIdx !== -1) {
  154. this._transparentMeshesCache.splice(transparentIdx, 1);
  155. this._opaqueMeshesCache.push(mesh);
  156. } else if (opaqueIdx === -1) {
  157. this._opaqueMeshesCache.push(mesh);
  158. }
  159. }
  160. }
  161. }
  162. /**
  163. * Setup the render targets according to the specified options.
  164. */
  165. private _setupRenderTargets(): void {
  166. let opaqueRTIndex = -1;
  167. // Remove any layers rendering to the opaque scene.
  168. if (this._scene.layers && this._opaqueRenderTarget) {
  169. for (let layer of this._scene.layers) {
  170. const idx = layer.renderTargetTextures.indexOf(this._opaqueRenderTarget);
  171. if (idx >= 0) {
  172. layer.renderTargetTextures.splice(idx, 1);
  173. }
  174. }
  175. }
  176. // Remove opaque render target
  177. if (this._opaqueRenderTarget) {
  178. opaqueRTIndex = this._scene.customRenderTargets.indexOf(this._opaqueRenderTarget);
  179. this._opaqueRenderTarget.dispose();
  180. }
  181. this._opaqueRenderTarget = new RenderTargetTexture("opaqueSceneTexture", this._options.renderSize, this._scene, true);
  182. this._opaqueRenderTarget.renderList = this._opaqueMeshesCache;
  183. // this._opaqueRenderTarget.clearColor = new Color4(0.0, 0.0, 0.0, 0.0);
  184. this._opaqueRenderTarget.gammaSpace = true;
  185. this._opaqueRenderTarget.lodGenerationScale = 1;
  186. this._opaqueRenderTarget.lodGenerationOffset = -4;
  187. if (opaqueRTIndex >= 0) {
  188. this._scene.customRenderTargets.splice(opaqueRTIndex, 0, this._opaqueRenderTarget);
  189. } else {
  190. opaqueRTIndex = this._scene.customRenderTargets.length;
  191. this._scene.customRenderTargets.push(this._opaqueRenderTarget);
  192. }
  193. // If there are other layers, they should be included in the render of the opaque background.
  194. if (this._scene.layers && this._opaqueRenderTarget) {
  195. for (let layer of this._scene.layers) {
  196. layer.renderTargetTextures.push(this._opaqueRenderTarget);
  197. }
  198. }
  199. this._transparentMeshesCache.forEach((mesh: AbstractMesh) => {
  200. if (this.shouldRenderAsTransmission(mesh.material) && mesh.material instanceof PBRMaterial) {
  201. mesh.material.refractionTexture = this._opaqueRenderTarget;
  202. }
  203. });
  204. }
  205. /**
  206. * Dispose all the elements created by the Helper.
  207. */
  208. public dispose(): void {
  209. this._scene._transmissionHelper = undefined;
  210. if (this._opaqueRenderTarget) {
  211. this._opaqueRenderTarget.dispose();
  212. this._opaqueRenderTarget = null;
  213. }
  214. this._transparentMeshesCache = [];
  215. this._opaqueMeshesCache = [];
  216. }
  217. }
  218. const NAME = "KHR_materials_transmission";
  219. /**
  220. * [Proposed Specification](https://github.com/KhronosGroup/glTF/pull/1698)
  221. * !!! Experimental Extension Subject to Changes !!!
  222. */
  223. export class KHR_materials_transmission implements IGLTFLoaderExtension {
  224. /**
  225. * The name of this extension.
  226. */
  227. public readonly name = NAME;
  228. /**
  229. * Defines whether this extension is enabled.
  230. */
  231. public enabled: boolean;
  232. /**
  233. * Defines a number that determines the order the extensions are applied.
  234. */
  235. public order = 175;
  236. private _loader: GLTFLoader;
  237. /** @hidden */
  238. constructor(loader: GLTFLoader) {
  239. this._loader = loader;
  240. this.enabled = this._loader.isExtensionUsed(NAME);
  241. if (this.enabled) {
  242. (loader as any)._parent.transparencyAsCoverage = true;
  243. }
  244. }
  245. /** @hidden */
  246. public dispose() {
  247. (this._loader as any) = null;
  248. }
  249. /** @hidden */
  250. public loadMaterialPropertiesAsync(context: string, material: IMaterial, babylonMaterial: Material): Nullable<Promise<void>> {
  251. return GLTFLoader.LoadExtensionAsync<IKHRMaterialsTransmission>(context, material, this.name, (extensionContext, extension) => {
  252. const promises = new Array<Promise<any>>();
  253. promises.push(this._loader.loadMaterialBasePropertiesAsync(context, material, babylonMaterial));
  254. promises.push(this._loader.loadMaterialPropertiesAsync(context, material, babylonMaterial));
  255. promises.push(this._loadTransparentPropertiesAsync(extensionContext, material, babylonMaterial, extension));
  256. return Promise.all(promises).then(() => { });
  257. });
  258. }
  259. private _loadTransparentPropertiesAsync(context: string, material: IMaterial, babylonMaterial: Material, extension: IKHRMaterialsTransmission): Promise<void> {
  260. if (!(babylonMaterial instanceof PBRMaterial)) {
  261. throw new Error(`${context}: Material type not supported`);
  262. }
  263. let pbrMaterial = babylonMaterial as PBRMaterial;
  264. // Enables "refraction" texture which represents transmitted light.
  265. pbrMaterial.subSurface.isRefractionEnabled = true;
  266. // Since this extension models thin-surface transmission only, we must make IOR = 1.0
  267. pbrMaterial.subSurface.volumeIndexOfRefraction = 1.0;
  268. // Albedo colour will tint transmission.
  269. pbrMaterial.subSurface.useAlbedoToTintRefraction = true;
  270. if (extension.transmissionFactor !== undefined) {
  271. pbrMaterial.subSurface.refractionIntensity = extension.transmissionFactor;
  272. const scene = pbrMaterial.getScene() as unknown as ITransmissionHelperHolder;
  273. if (pbrMaterial.subSurface.refractionIntensity && !scene._transmissionHelper) {
  274. new TransmissionHelper({}, pbrMaterial.getScene());
  275. }
  276. } else {
  277. pbrMaterial.subSurface.refractionIntensity = 0.0;
  278. pbrMaterial.subSurface.isRefractionEnabled = false;
  279. return Promise.resolve();
  280. }
  281. if (extension.transmissionTexture) {
  282. (extension.transmissionTexture as ITextureInfo).isNotColorData = true;
  283. return this._loader.loadTextureInfoAsync(`${context}/transmissionTexture`, extension.transmissionTexture, undefined)
  284. .then((texture: BaseTexture) => {
  285. pbrMaterial.subSurface.thicknessTexture = texture;
  286. pbrMaterial.subSurface.useMaskFromThicknessTexture = true;
  287. });
  288. } else {
  289. return Promise.resolve();
  290. }
  291. }
  292. }
  293. GLTFLoader.RegisterExtension(NAME, (loader) => new KHR_materials_transmission(loader));