babylon.hdrCubeTexture.ts 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523
  1. module BABYLON {
  2. /**
  3. * This represents a texture coming from an HDR input.
  4. *
  5. * The only supported format is currently panorama picture stored in RGBE format.
  6. * Example of such files can be found on HDRLib: http://hdrlib.com/
  7. */
  8. export class HDRCubeTexture extends BaseTexture {
  9. private static _facesMapping = [
  10. "right",
  11. "left",
  12. "up",
  13. "down",
  14. "front",
  15. "back"
  16. ];
  17. private _useInGammaSpace = false;
  18. private _generateHarmonics = true;
  19. private _noMipmap: boolean;
  20. private _textureMatrix: Matrix;
  21. private _size: number;
  22. private _usePMREMGenerator: boolean;
  23. private _isBABYLONPreprocessed = false;
  24. private _onLoad: Nullable<() => void> = null;
  25. private _onError: Nullable<() => void> = null;
  26. /**
  27. * The texture URL.
  28. */
  29. public url: string;
  30. /**
  31. * The texture coordinates mode. As this texture is stored in a cube format, please modify carefully.
  32. */
  33. public coordinatesMode = Texture.CUBIC_MODE;
  34. /**
  35. * Specifies wether the texture has been generated through the PMREMGenerator tool.
  36. * This is usefull at run time to apply the good shader.
  37. */
  38. public isPMREM = false;
  39. protected _isBlocking: boolean = true;
  40. /**
  41. * Sets wether or not the texture is blocking during loading.
  42. */
  43. public set isBlocking(value: boolean) {
  44. this._isBlocking = value;
  45. }
  46. /**
  47. * Gets wether or not the texture is blocking during loading.
  48. */
  49. public get isBlocking(): boolean {
  50. return this._isBlocking;
  51. }
  52. /**
  53. * Instantiates an HDRTexture from the following parameters.
  54. *
  55. * @param url The location of the HDR raw data (Panorama stored in RGBE format)
  56. * @param scene The scene the texture will be used in
  57. * @param size The cubemap desired size (the more it increases the longer the generation will be) If the size is omitted this implies you are using a preprocessed cubemap.
  58. * @param noMipmap Forces to not generate the mipmap if true
  59. * @param generateHarmonics Specifies wether you want to extract the polynomial harmonics during the generation process
  60. * @param useInGammaSpace Specifies if the texture will be use in gamma or linear space (the PBR material requires those texture in linear space, but the standard material would require them in Gamma space)
  61. * @param usePMREMGenerator Specifies wether or not to generate the CubeMap through CubeMapGen to avoid seams issue at run time.
  62. */
  63. constructor(url: string, scene: Scene, size?: number, noMipmap = false, generateHarmonics = true, useInGammaSpace = false, usePMREMGenerator = false, onLoad: Nullable<() => void> = null, onError: Nullable<(message?: string, exception?: any) => void> = null) {
  64. super(scene);
  65. if (!url) {
  66. return;
  67. }
  68. this.name = url;
  69. this.url = url;
  70. this.hasAlpha = false;
  71. this.isCube = true;
  72. this._textureMatrix = Matrix.Identity();
  73. this._onLoad = onLoad;
  74. this._onError = onError;
  75. this.gammaSpace = false;
  76. let caps = scene.getEngine().getCaps();
  77. if (size) {
  78. this._isBABYLONPreprocessed = false;
  79. this._noMipmap = noMipmap;
  80. this._size = size;
  81. this._useInGammaSpace = useInGammaSpace;
  82. this._usePMREMGenerator = usePMREMGenerator &&
  83. caps.textureLOD &&
  84. caps.textureFloat &&
  85. !this._useInGammaSpace;
  86. }
  87. else {
  88. this._isBABYLONPreprocessed = true;
  89. this._noMipmap = false;
  90. this._useInGammaSpace = false;
  91. this._usePMREMGenerator = caps.textureLOD && caps.textureFloat &&
  92. !this._useInGammaSpace;
  93. }
  94. this.isPMREM = this._usePMREMGenerator;
  95. this._texture = this._getFromCache(url, this._noMipmap);
  96. if (!this._texture) {
  97. if (!scene.useDelayedTextureLoading) {
  98. this.loadTexture();
  99. } else {
  100. this.delayLoadState = Engine.DELAYLOADSTATE_NOTLOADED;
  101. }
  102. }
  103. }
  104. /**
  105. * Occurs when the file is a preprocessed .babylon.hdr file.
  106. */
  107. private loadBabylonTexture() {
  108. var mipLevels = 0;
  109. var floatArrayView: Nullable<Float32Array> = null;
  110. let scene = this.getScene();
  111. var mipmapGenerator = (!this._useInGammaSpace && scene && scene.getEngine().getCaps().textureFloat) ? (data: ArrayBufferView[]): Array<Array<Float32Array>> => {
  112. var mips = new Array<Array<Float32Array>>();
  113. if (!floatArrayView) {
  114. return mips;
  115. }
  116. var startIndex = 30;
  117. for (var level = 0; level < mipLevels; level++) {
  118. mips.push([]);
  119. // Fill each pixel of the mip level.
  120. var faceSize = Math.pow(this._size >> level, 2) * 3;
  121. for (var faceIndex = 0; faceIndex < 6; faceIndex++) {
  122. var faceData = floatArrayView.subarray(startIndex, startIndex + faceSize);
  123. mips[level].push(faceData);
  124. startIndex += faceSize;
  125. }
  126. }
  127. return mips;
  128. } : null;
  129. var callback = (buffer: ArrayBuffer) => {
  130. let scene = this.getScene();
  131. if (!scene) {
  132. return null;
  133. }
  134. // Create Native Array Views
  135. var intArrayView = new Int32Array(buffer);
  136. floatArrayView = new Float32Array(buffer);
  137. // Fill header.
  138. var version = intArrayView[0]; // Version 1. (MAy be use in case of format changes for backward compaibility)
  139. this._size = intArrayView[1]; // CubeMap max mip face size.
  140. // Update Texture Information.
  141. if (!this._texture) {
  142. return null;
  143. }
  144. this._texture.updateSize(this._size, this._size);
  145. // Fill polynomial information.
  146. var sphericalPolynomial = new SphericalPolynomial();
  147. sphericalPolynomial.x.copyFromFloats(floatArrayView[2], floatArrayView[3], floatArrayView[4]);
  148. sphericalPolynomial.y.copyFromFloats(floatArrayView[5], floatArrayView[6], floatArrayView[7]);
  149. sphericalPolynomial.z.copyFromFloats(floatArrayView[8], floatArrayView[9], floatArrayView[10]);
  150. sphericalPolynomial.xx.copyFromFloats(floatArrayView[11], floatArrayView[12], floatArrayView[13]);
  151. sphericalPolynomial.yy.copyFromFloats(floatArrayView[14], floatArrayView[15], floatArrayView[16]);
  152. sphericalPolynomial.zz.copyFromFloats(floatArrayView[17], floatArrayView[18], floatArrayView[19]);
  153. sphericalPolynomial.xy.copyFromFloats(floatArrayView[20], floatArrayView[21], floatArrayView[22]);
  154. sphericalPolynomial.yz.copyFromFloats(floatArrayView[23], floatArrayView[24], floatArrayView[25]);
  155. sphericalPolynomial.zx.copyFromFloats(floatArrayView[26], floatArrayView[27], floatArrayView[28]);
  156. this.sphericalPolynomial = sphericalPolynomial;
  157. // Fill pixel data.
  158. mipLevels = intArrayView[29]; // Number of mip levels.
  159. var startIndex = 30;
  160. var data = [];
  161. var faceSize = Math.pow(this._size, 2) * 3;
  162. for (var faceIndex = 0; faceIndex < 6; faceIndex++) {
  163. data.push(floatArrayView.subarray(startIndex, startIndex + faceSize));
  164. startIndex += faceSize;
  165. }
  166. var results = [];
  167. var byteArray: Nullable<Uint8Array> = null;
  168. // Push each faces.
  169. for (var k = 0; k < 6; k++) {
  170. var dataFace = null;
  171. // To be deprecated.
  172. if (version === 1) {
  173. var j = ([0, 2, 4, 1, 3, 5])[k]; // Transforms +X+Y+Z... to +X-X+Y-Y...
  174. dataFace = data[j];
  175. }
  176. // If special cases.
  177. if (!mipmapGenerator && dataFace) {
  178. if (!scene.getEngine().getCaps().textureFloat) {
  179. // 3 channels of 1 bytes per pixel in bytes.
  180. var byteBuffer = new ArrayBuffer(faceSize);
  181. byteArray = new Uint8Array(byteBuffer);
  182. }
  183. for (var i = 0; i < this._size * this._size; i++) {
  184. // Put in gamma space if requested.
  185. if (this._useInGammaSpace) {
  186. dataFace[(i * 3) + 0] = Math.pow(dataFace[(i * 3) + 0], ToGammaSpace);
  187. dataFace[(i * 3) + 1] = Math.pow(dataFace[(i * 3) + 1], ToGammaSpace);
  188. dataFace[(i * 3) + 2] = Math.pow(dataFace[(i * 3) + 2], ToGammaSpace);
  189. }
  190. // Convert to int texture for fallback.
  191. if (byteArray) {
  192. var r = Math.max(dataFace[(i * 3) + 0] * 255, 0);
  193. var g = Math.max(dataFace[(i * 3) + 1] * 255, 0);
  194. var b = Math.max(dataFace[(i * 3) + 2] * 255, 0);
  195. // May use luminance instead if the result is not accurate.
  196. var max = Math.max(Math.max(r, g), b);
  197. if (max > 255) {
  198. var scale = 255 / max;
  199. r *= scale;
  200. g *= scale;
  201. b *= scale;
  202. }
  203. byteArray[(i * 3) + 0] = r;
  204. byteArray[(i * 3) + 1] = g;
  205. byteArray[(i * 3) + 2] = b;
  206. }
  207. }
  208. }
  209. // Fill the array accordingly.
  210. if (byteArray) {
  211. results.push(byteArray);
  212. }
  213. else {
  214. results.push(dataFace);
  215. }
  216. }
  217. return results;
  218. }
  219. if (scene) {
  220. this._texture = (<any>scene.getEngine()).createRawCubeTextureFromUrl(this.url, scene, this._size,
  221. Engine.TEXTUREFORMAT_RGB,
  222. scene.getEngine().getCaps().textureFloat ? Engine.TEXTURETYPE_FLOAT : Engine.TEXTURETYPE_UNSIGNED_INT,
  223. this._noMipmap,
  224. callback,
  225. mipmapGenerator, this._onLoad, this._onError);
  226. }
  227. }
  228. /**
  229. * Occurs when the file is raw .hdr file.
  230. */
  231. private loadHDRTexture() {
  232. var callback = (buffer: ArrayBuffer): Nullable<ArrayBufferView[]> => {
  233. let scene = this.getScene();
  234. if (!scene) {
  235. return null;
  236. }
  237. // Extract the raw linear data.
  238. var data = Internals.HDRTools.GetCubeMapTextureData(buffer, this._size);
  239. // Generate harmonics if needed.
  240. if (this._generateHarmonics) {
  241. var sphericalPolynomial = Internals.CubeMapToSphericalPolynomialTools.ConvertCubeMapToSphericalPolynomial(data);
  242. this.sphericalPolynomial = sphericalPolynomial;
  243. }
  244. var results = [];
  245. var byteArray: Nullable<Uint8Array> = null;
  246. // Push each faces.
  247. for (var j = 0; j < 6; j++) {
  248. // Create uintarray fallback.
  249. if (!scene.getEngine().getCaps().textureFloat) {
  250. // 3 channels of 1 bytes per pixel in bytes.
  251. var byteBuffer = new ArrayBuffer(this._size * this._size * 3);
  252. byteArray = new Uint8Array(byteBuffer);
  253. }
  254. var dataFace = <Float32Array>((<any>data)[HDRCubeTexture._facesMapping[j]]);
  255. // If special cases.
  256. if (this._useInGammaSpace || byteArray) {
  257. for (var i = 0; i < this._size * this._size; i++) {
  258. // Put in gamma space if requested.
  259. if (this._useInGammaSpace) {
  260. dataFace[(i * 3) + 0] = Math.pow(dataFace[(i * 3) + 0], ToGammaSpace);
  261. dataFace[(i * 3) + 1] = Math.pow(dataFace[(i * 3) + 1], ToGammaSpace);
  262. dataFace[(i * 3) + 2] = Math.pow(dataFace[(i * 3) + 2], ToGammaSpace);
  263. }
  264. // Convert to int texture for fallback.
  265. if (byteArray) {
  266. var r = Math.max(dataFace[(i * 3) + 0] * 255, 0);
  267. var g = Math.max(dataFace[(i * 3) + 1] * 255, 0);
  268. var b = Math.max(dataFace[(i * 3) + 2] * 255, 0);
  269. // May use luminance instead if the result is not accurate.
  270. var max = Math.max(Math.max(r, g), b);
  271. if (max > 255) {
  272. var scale = 255 / max;
  273. r *= scale;
  274. g *= scale;
  275. b *= scale;
  276. }
  277. byteArray[(i * 3) + 0] = r;
  278. byteArray[(i * 3) + 1] = g;
  279. byteArray[(i * 3) + 2] = b;
  280. }
  281. }
  282. }
  283. if (byteArray) {
  284. results.push(byteArray);
  285. }
  286. else {
  287. results.push(dataFace);
  288. }
  289. }
  290. return results;
  291. }
  292. var mipmapGenerator = null;
  293. // TODO. Implement In code PMREM Generator following the LYS toolset generation.
  294. // if (!this._noMipmap &&
  295. // this._usePMREMGenerator) {
  296. // mipmapGenerator = (data: ArrayBufferView[]) => {
  297. // // Custom setup of the generator matching with the PBR shader values.
  298. // var generator = new BABYLON.Internals.PMREMGenerator(data,
  299. // this._size,
  300. // this._size,
  301. // 0,
  302. // 3,
  303. // this.getScene().getEngine().getCaps().textureFloat,
  304. // 2048,
  305. // 0.25,
  306. // false,
  307. // true);
  308. // return generator.filterCubeMap();
  309. // };
  310. // }
  311. let scene = this.getScene();
  312. if (scene) {
  313. this._texture = scene.getEngine().createRawCubeTextureFromUrl(this.url, scene, this._size,
  314. Engine.TEXTUREFORMAT_RGB,
  315. scene.getEngine().getCaps().textureFloat ? Engine.TEXTURETYPE_FLOAT : Engine.TEXTURETYPE_UNSIGNED_INT,
  316. this._noMipmap,
  317. callback,
  318. mipmapGenerator, this._onLoad, this._onError);
  319. }
  320. }
  321. /**
  322. * Starts the loading process of the texture.
  323. */
  324. private loadTexture() {
  325. if (this._isBABYLONPreprocessed) {
  326. this.loadBabylonTexture();
  327. }
  328. else {
  329. this.loadHDRTexture();
  330. }
  331. }
  332. public clone(): HDRCubeTexture {
  333. let scene = this.getScene();
  334. if (!scene) {
  335. return this;
  336. }
  337. var size = <number>(this._isBABYLONPreprocessed ? null : this._size);
  338. var newTexture = new HDRCubeTexture(this.url, scene, size, this._noMipmap,
  339. this._generateHarmonics, this._useInGammaSpace, this._usePMREMGenerator);
  340. // Base texture
  341. newTexture.level = this.level;
  342. newTexture.wrapU = this.wrapU;
  343. newTexture.wrapV = this.wrapV;
  344. newTexture.coordinatesIndex = this.coordinatesIndex;
  345. newTexture.coordinatesMode = this.coordinatesMode;
  346. return newTexture;
  347. }
  348. // Methods
  349. public delayLoad(): void {
  350. if (this.delayLoadState !== Engine.DELAYLOADSTATE_NOTLOADED) {
  351. return;
  352. }
  353. this.delayLoadState = Engine.DELAYLOADSTATE_LOADED;
  354. this._texture = this._getFromCache(this.url, this._noMipmap);
  355. if (!this._texture) {
  356. this.loadTexture();
  357. }
  358. }
  359. public getReflectionTextureMatrix(): Matrix {
  360. return this._textureMatrix;
  361. }
  362. public setReflectionTextureMatrix(value: Matrix): void {
  363. this._textureMatrix = value;
  364. }
  365. public static Parse(parsedTexture: any, scene: Scene, rootUrl: string): Nullable<HDRCubeTexture> {
  366. var texture = null;
  367. if (parsedTexture.name && !parsedTexture.isRenderTarget) {
  368. var size = parsedTexture.isBABYLONPreprocessed ? null : parsedTexture.size;
  369. texture = new HDRCubeTexture(rootUrl + parsedTexture.name, scene, size, parsedTexture.noMipmap,
  370. parsedTexture.generateHarmonics, parsedTexture.useInGammaSpace, parsedTexture.usePMREMGenerator);
  371. texture.name = parsedTexture.name;
  372. texture.hasAlpha = parsedTexture.hasAlpha;
  373. texture.level = parsedTexture.level;
  374. texture.coordinatesMode = parsedTexture.coordinatesMode;
  375. texture.isBlocking = parsedTexture.isBlocking;
  376. }
  377. return texture;
  378. }
  379. public serialize(): any {
  380. if (!this.name) {
  381. return null;
  382. }
  383. var serializationObject: any = {};
  384. serializationObject.name = this.name;
  385. serializationObject.hasAlpha = this.hasAlpha;
  386. serializationObject.isCube = true;
  387. serializationObject.level = this.level;
  388. serializationObject.size = this._size;
  389. serializationObject.coordinatesMode = this.coordinatesMode;
  390. serializationObject.useInGammaSpace = this._useInGammaSpace;
  391. serializationObject.generateHarmonics = this._generateHarmonics;
  392. serializationObject.usePMREMGenerator = this._usePMREMGenerator;
  393. serializationObject.isBABYLONPreprocessed = this._isBABYLONPreprocessed;
  394. serializationObject.customType = "BABYLON.HDRCubeTexture";
  395. serializationObject.noMipmap = this._noMipmap;
  396. serializationObject.isBlocking = this._isBlocking;
  397. return serializationObject;
  398. }
  399. /**
  400. * Saves as a file the data contained in the texture in a binary format.
  401. * This can be used to prevent the long loading tie associated with creating the seamless texture as well
  402. * as the spherical used in the lighting.
  403. * @param url The HDR file url.
  404. * @param size The size of the texture data to generate (one of the cubemap face desired width).
  405. * @param onError Method called if any error happens during download.
  406. * @return The packed binary data.
  407. */
  408. public static generateBabylonHDROnDisk(url: string, size: number, onError: Nullable<(() => void)> = null): void {
  409. var callback = function (buffer: ArrayBuffer) {
  410. var data = new Blob([buffer], { type: 'application/octet-stream' });
  411. // Returns a URL you can use as a href.
  412. var objUrl = window.URL.createObjectURL(data);
  413. // Simulates a link to it and click to dowload.
  414. var a = document.createElement("a");
  415. document.body.appendChild(a);
  416. a.style.display = "none";
  417. a.href = objUrl;
  418. (<any>a).download = "envmap.babylon.hdr";
  419. a.click();
  420. };
  421. HDRCubeTexture.generateBabylonHDR(url, size, callback, onError);
  422. }
  423. /**
  424. * Serializes the data contained in the texture in a binary format.
  425. * This can be used to prevent the long loading tie associated with creating the seamless texture as well
  426. * as the spherical used in the lighting.
  427. * @param url The HDR file url.
  428. * @param size The size of the texture data to generate (one of the cubemap face desired width).
  429. * @param onError Method called if any error happens during download.
  430. * @return The packed binary data.
  431. */
  432. public static generateBabylonHDR(url: string, size: number, callback: ((ArrayBuffer: ArrayBuffer) => void), onError: Nullable<(() => void)> = null): void {
  433. // Needs the url tho create the texture.
  434. if (!url) {
  435. return;
  436. }
  437. // Check Power of two size.
  438. if (!Tools.IsExponentOfTwo(size)) { // Need to check engine.needPOTTextures
  439. return;
  440. }
  441. // Coming Back in 3.x.
  442. Tools.Error("Generation of Babylon HDR is coming back in 3.2.");
  443. }
  444. }
  445. }