KHR_materials_transmission.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. import { PBRMaterial } from "@babylonjs/core/Materials/PBR/pbrMaterial.js";
  2. import { GLTFLoader } from "../glTFLoader.js";
  3. import { RenderTargetTexture } from "@babylonjs/core/Materials/Textures/renderTargetTexture.js";
  4. import { Observable } from "@babylonjs/core/Misc/observable.js";
  5. import { Constants } from "@babylonjs/core/Engines/constants.js";
  6. import { Tools } from "@babylonjs/core/Misc/tools.js";
  7. /**
  8. * A class to handle setting up the rendering of opaque objects to be shown through transmissive objects.
  9. */
  10. class TransmissionHelper {
  11. /**
  12. * Creates the default options for the helper.
  13. * @returns the default options
  14. */
  15. static _GetDefaultOptions() {
  16. return {
  17. renderSize: 1024,
  18. samples: 4,
  19. lodGenerationScale: 1,
  20. lodGenerationOffset: -4,
  21. renderTargetTextureType: Constants.TEXTURETYPE_HALF_FLOAT,
  22. generateMipmaps: true,
  23. };
  24. }
  25. /**
  26. * constructor
  27. * @param options Defines the options we want to customize the helper
  28. * @param scene The scene to add the material to
  29. */
  30. constructor(options, scene) {
  31. this._opaqueRenderTarget = null;
  32. this._opaqueMeshesCache = [];
  33. this._transparentMeshesCache = [];
  34. this._materialObservers = {};
  35. this._options = {
  36. ...TransmissionHelper._GetDefaultOptions(),
  37. ...options,
  38. };
  39. this._scene = scene;
  40. this._scene._transmissionHelper = this;
  41. this.onErrorObservable = new Observable();
  42. this._scene.onDisposeObservable.addOnce(() => {
  43. this.dispose();
  44. });
  45. this._parseScene();
  46. this._setupRenderTargets();
  47. }
  48. /**
  49. * Updates the background according to the new options
  50. * @param options
  51. */
  52. updateOptions(options) {
  53. // First check if any options are actually being changed. If not, exit.
  54. const newValues = Object.keys(options).filter((key) => this._options[key] !== options[key]);
  55. if (!newValues.length) {
  56. return;
  57. }
  58. const newOptions = {
  59. ...this._options,
  60. ...options,
  61. };
  62. const oldOptions = this._options;
  63. this._options = newOptions;
  64. // If size changes, recreate everything
  65. if (newOptions.renderSize !== oldOptions.renderSize ||
  66. newOptions.renderTargetTextureType !== oldOptions.renderTargetTextureType ||
  67. newOptions.generateMipmaps !== oldOptions.generateMipmaps ||
  68. !this._opaqueRenderTarget) {
  69. this._setupRenderTargets();
  70. }
  71. else {
  72. this._opaqueRenderTarget.samples = newOptions.samples;
  73. this._opaqueRenderTarget.lodGenerationScale = newOptions.lodGenerationScale;
  74. this._opaqueRenderTarget.lodGenerationOffset = newOptions.lodGenerationOffset;
  75. }
  76. }
  77. /**
  78. * @returns the opaque render target texture or null if not available.
  79. */
  80. getOpaqueTarget() {
  81. return this._opaqueRenderTarget;
  82. }
  83. _shouldRenderAsTransmission(material) {
  84. if (!material) {
  85. return false;
  86. }
  87. if (material instanceof PBRMaterial && material.subSurface.isRefractionEnabled) {
  88. return true;
  89. }
  90. return false;
  91. }
  92. _addMesh(mesh) {
  93. this._materialObservers[mesh.uniqueId] = mesh.onMaterialChangedObservable.add(this._onMeshMaterialChanged.bind(this));
  94. // we need to defer the processing because _addMesh may be called as part as an instance mesh creation, in which case some
  95. // internal properties are not setup yet, like _sourceMesh (needed when doing mesh.material below)
  96. Tools.SetImmediate(() => {
  97. if (this._shouldRenderAsTransmission(mesh.material)) {
  98. mesh.material.refractionTexture = this._opaqueRenderTarget;
  99. if (this._transparentMeshesCache.indexOf(mesh) === -1) {
  100. this._transparentMeshesCache.push(mesh);
  101. }
  102. }
  103. else {
  104. if (this._opaqueMeshesCache.indexOf(mesh) === -1) {
  105. this._opaqueMeshesCache.push(mesh);
  106. }
  107. }
  108. });
  109. }
  110. _removeMesh(mesh) {
  111. mesh.onMaterialChangedObservable.remove(this._materialObservers[mesh.uniqueId]);
  112. delete this._materialObservers[mesh.uniqueId];
  113. let idx = this._transparentMeshesCache.indexOf(mesh);
  114. if (idx !== -1) {
  115. this._transparentMeshesCache.splice(idx, 1);
  116. }
  117. idx = this._opaqueMeshesCache.indexOf(mesh);
  118. if (idx !== -1) {
  119. this._opaqueMeshesCache.splice(idx, 1);
  120. }
  121. }
  122. _parseScene() {
  123. this._scene.meshes.forEach(this._addMesh.bind(this));
  124. // Listen for when a mesh is added to the scene and add it to our cache lists.
  125. this._scene.onNewMeshAddedObservable.add(this._addMesh.bind(this));
  126. // Listen for when a mesh is removed from to the scene and remove it from our cache lists.
  127. this._scene.onMeshRemovedObservable.add(this._removeMesh.bind(this));
  128. }
  129. // When one of the meshes in the scene has its material changed, make sure that it's in the correct cache list.
  130. _onMeshMaterialChanged(mesh) {
  131. const transparentIdx = this._transparentMeshesCache.indexOf(mesh);
  132. const opaqueIdx = this._opaqueMeshesCache.indexOf(mesh);
  133. // If the material is transparent, make sure that it's added to the transparent list and removed from the opaque list
  134. const useTransmission = this._shouldRenderAsTransmission(mesh.material);
  135. if (useTransmission) {
  136. if (mesh.material instanceof PBRMaterial) {
  137. mesh.material.subSurface.refractionTexture = this._opaqueRenderTarget;
  138. }
  139. if (opaqueIdx !== -1) {
  140. this._opaqueMeshesCache.splice(opaqueIdx, 1);
  141. this._transparentMeshesCache.push(mesh);
  142. }
  143. else if (transparentIdx === -1) {
  144. this._transparentMeshesCache.push(mesh);
  145. }
  146. // If the material is opaque, make sure that it's added to the opaque list and removed from the transparent list
  147. }
  148. else {
  149. if (transparentIdx !== -1) {
  150. this._transparentMeshesCache.splice(transparentIdx, 1);
  151. this._opaqueMeshesCache.push(mesh);
  152. }
  153. else if (opaqueIdx === -1) {
  154. this._opaqueMeshesCache.push(mesh);
  155. }
  156. }
  157. }
  158. /**
  159. * @internal
  160. * Check if the opaque render target has not been disposed and can still be used.
  161. * @returns
  162. */
  163. _isRenderTargetValid() {
  164. return this._opaqueRenderTarget?.getInternalTexture() !== null;
  165. }
  166. /**
  167. * @internal
  168. * Setup the render targets according to the specified options.
  169. */
  170. _setupRenderTargets() {
  171. if (this._opaqueRenderTarget) {
  172. this._opaqueRenderTarget.dispose();
  173. }
  174. this._opaqueRenderTarget = new RenderTargetTexture("opaqueSceneTexture", this._options.renderSize, this._scene, this._options.generateMipmaps, undefined, this._options.renderTargetTextureType);
  175. this._opaqueRenderTarget.ignoreCameraViewport = true;
  176. this._opaqueRenderTarget.renderList = this._opaqueMeshesCache;
  177. this._opaqueRenderTarget.clearColor = this._options.clearColor?.clone() ?? this._scene.clearColor.clone();
  178. this._opaqueRenderTarget.gammaSpace = false;
  179. this._opaqueRenderTarget.lodGenerationScale = this._options.lodGenerationScale;
  180. this._opaqueRenderTarget.lodGenerationOffset = this._options.lodGenerationOffset;
  181. this._opaqueRenderTarget.samples = this._options.samples;
  182. this._opaqueRenderTarget.renderSprites = true;
  183. this._opaqueRenderTarget.renderParticles = true;
  184. let sceneImageProcessingapplyByPostProcess;
  185. let saveSceneEnvIntensity;
  186. this._opaqueRenderTarget.onBeforeBindObservable.add((opaqueRenderTarget) => {
  187. saveSceneEnvIntensity = this._scene.environmentIntensity;
  188. this._scene.environmentIntensity = 1.0;
  189. sceneImageProcessingapplyByPostProcess = this._scene.imageProcessingConfiguration.applyByPostProcess;
  190. if (!this._options.clearColor) {
  191. this._scene.clearColor.toLinearSpaceToRef(opaqueRenderTarget.clearColor, this._scene.getEngine().useExactSrgbConversions);
  192. }
  193. else {
  194. opaqueRenderTarget.clearColor.copyFrom(this._options.clearColor);
  195. }
  196. // we do not use the applyByPostProcess setter to avoid flagging all the materials as "image processing dirty"!
  197. this._scene.imageProcessingConfiguration._applyByPostProcess = true;
  198. });
  199. this._opaqueRenderTarget.onAfterUnbindObservable.add(() => {
  200. this._scene.environmentIntensity = saveSceneEnvIntensity;
  201. this._scene.imageProcessingConfiguration._applyByPostProcess = sceneImageProcessingapplyByPostProcess;
  202. });
  203. this._transparentMeshesCache.forEach((mesh) => {
  204. if (this._shouldRenderAsTransmission(mesh.material)) {
  205. mesh.material.refractionTexture = this._opaqueRenderTarget;
  206. }
  207. });
  208. }
  209. /**
  210. * Dispose all the elements created by the Helper.
  211. */
  212. dispose() {
  213. this._scene._transmissionHelper = undefined;
  214. if (this._opaqueRenderTarget) {
  215. this._opaqueRenderTarget.dispose();
  216. this._opaqueRenderTarget = null;
  217. }
  218. this._transparentMeshesCache = [];
  219. this._opaqueMeshesCache = [];
  220. }
  221. }
  222. const NAME = "KHR_materials_transmission";
  223. /**
  224. * [Specification](https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_transmission/README.md)
  225. */
  226. // eslint-disable-next-line @typescript-eslint/naming-convention
  227. export class KHR_materials_transmission {
  228. /**
  229. * @internal
  230. */
  231. constructor(loader) {
  232. /**
  233. * The name of this extension.
  234. */
  235. this.name = NAME;
  236. /**
  237. * Defines a number that determines the order the extensions are applied.
  238. */
  239. this.order = 175;
  240. this._loader = loader;
  241. this.enabled = this._loader.isExtensionUsed(NAME);
  242. if (this.enabled) {
  243. loader.parent.transparencyAsCoverage = true;
  244. }
  245. }
  246. /** @internal */
  247. dispose() {
  248. this._loader = null;
  249. }
  250. /**
  251. * @internal
  252. */
  253. loadMaterialPropertiesAsync(context, material, babylonMaterial) {
  254. return GLTFLoader.LoadExtensionAsync(context, material, this.name, (extensionContext, extension) => {
  255. const promises = new Array();
  256. promises.push(this._loader.loadMaterialBasePropertiesAsync(context, material, babylonMaterial));
  257. promises.push(this._loader.loadMaterialPropertiesAsync(context, material, babylonMaterial));
  258. promises.push(this._loadTransparentPropertiesAsync(extensionContext, material, babylonMaterial, extension));
  259. return Promise.all(promises).then(() => { });
  260. });
  261. }
  262. _loadTransparentPropertiesAsync(context, material, babylonMaterial, extension) {
  263. if (!(babylonMaterial instanceof PBRMaterial)) {
  264. throw new Error(`${context}: Material type not supported`);
  265. }
  266. const pbrMaterial = babylonMaterial;
  267. // Enables "refraction" texture which represents transmitted light.
  268. pbrMaterial.subSurface.isRefractionEnabled = true;
  269. // Since this extension models thin-surface transmission only, we must make IOR = 1.0
  270. pbrMaterial.subSurface.volumeIndexOfRefraction = 1.0;
  271. // Albedo colour will tint transmission.
  272. pbrMaterial.subSurface.useAlbedoToTintRefraction = true;
  273. if (extension.transmissionFactor !== undefined) {
  274. pbrMaterial.subSurface.refractionIntensity = extension.transmissionFactor;
  275. const scene = pbrMaterial.getScene();
  276. if (pbrMaterial.subSurface.refractionIntensity && !scene._transmissionHelper) {
  277. new TransmissionHelper({}, pbrMaterial.getScene());
  278. }
  279. else if (pbrMaterial.subSurface.refractionIntensity && !scene._transmissionHelper?._isRenderTargetValid()) {
  280. // If the render target is not valid, recreate it.
  281. scene._transmissionHelper?._setupRenderTargets();
  282. }
  283. }
  284. else {
  285. pbrMaterial.subSurface.refractionIntensity = 0.0;
  286. pbrMaterial.subSurface.isRefractionEnabled = false;
  287. return Promise.resolve();
  288. }
  289. pbrMaterial.subSurface.minimumThickness = 0.0;
  290. pbrMaterial.subSurface.maximumThickness = 0.0;
  291. if (extension.transmissionTexture) {
  292. extension.transmissionTexture.nonColorData = true;
  293. return this._loader.loadTextureInfoAsync(`${context}/transmissionTexture`, extension.transmissionTexture, undefined).then((texture) => {
  294. pbrMaterial.subSurface.refractionIntensityTexture = texture;
  295. pbrMaterial.subSurface.useGltfStyleTextures = true;
  296. });
  297. }
  298. else {
  299. return Promise.resolve();
  300. }
  301. }
  302. }
  303. GLTFLoader.RegisterExtension(NAME, (loader) => new KHR_materials_transmission(loader));
  304. //# sourceMappingURL=KHR_materials_transmission.js.map