babylon.dds.ts 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  1. module BABYLON.Internals {
  2. // Based on demo done by Brandon Jones - http://media.tojicode.com/webgl-samples/dds.html
  3. // All values and structures referenced from:
  4. // http://msdn.microsoft.com/en-us/library/bb943991.aspx/
  5. var DDS_MAGIC = 0x20534444;
  6. var DDSD_CAPS = 0x1,
  7. DDSD_HEIGHT = 0x2,
  8. DDSD_WIDTH = 0x4,
  9. DDSD_PITCH = 0x8,
  10. DDSD_PIXELFORMAT = 0x1000,
  11. DDSD_MIPMAPCOUNT = 0x20000,
  12. DDSD_LINEARSIZE = 0x80000,
  13. DDSD_DEPTH = 0x800000;
  14. var DDSCAPS_COMPLEX = 0x8,
  15. DDSCAPS_MIPMAP = 0x400000,
  16. DDSCAPS_TEXTURE = 0x1000;
  17. var DDSCAPS2_CUBEMAP = 0x200,
  18. DDSCAPS2_CUBEMAP_POSITIVEX = 0x400,
  19. DDSCAPS2_CUBEMAP_NEGATIVEX = 0x800,
  20. DDSCAPS2_CUBEMAP_POSITIVEY = 0x1000,
  21. DDSCAPS2_CUBEMAP_NEGATIVEY = 0x2000,
  22. DDSCAPS2_CUBEMAP_POSITIVEZ = 0x4000,
  23. DDSCAPS2_CUBEMAP_NEGATIVEZ = 0x8000,
  24. DDSCAPS2_VOLUME = 0x200000;
  25. var DDPF_ALPHAPIXELS = 0x1,
  26. DDPF_ALPHA = 0x2,
  27. DDPF_FOURCC = 0x4,
  28. DDPF_RGB = 0x40,
  29. DDPF_YUV = 0x200,
  30. DDPF_LUMINANCE = 0x20000;
  31. function FourCCToInt32(value) {
  32. return value.charCodeAt(0) +
  33. (value.charCodeAt(1) << 8) +
  34. (value.charCodeAt(2) << 16) +
  35. (value.charCodeAt(3) << 24);
  36. }
  37. function Int32ToFourCC(value) {
  38. return String.fromCharCode(
  39. value & 0xff,
  40. (value >> 8) & 0xff,
  41. (value >> 16) & 0xff,
  42. (value >> 24) & 0xff
  43. );
  44. }
  45. var FOURCC_DXT1 = FourCCToInt32("DXT1");
  46. var FOURCC_DXT3 = FourCCToInt32("DXT3");
  47. var FOURCC_DXT5 = FourCCToInt32("DXT5");
  48. var FOURCC_DX10 = FourCCToInt32("DX10");
  49. var FOURCC_D3DFMT_R16G16B16A16F = 113;
  50. var FOURCC_D3DFMT_R32G32B32A32F = 116;
  51. var DXGI_FORMAT_R16G16B16A16_FLOAT = 10;
  52. var DXGI_FORMAT_B8G8R8X8_UNORM = 88;
  53. var headerLengthInt = 31; // The header length in 32 bit ints
  54. // Offsets into the header array
  55. var off_magic = 0;
  56. var off_size = 1;
  57. var off_flags = 2;
  58. var off_height = 3;
  59. var off_width = 4;
  60. var off_mipmapCount = 7;
  61. var off_pfFlags = 20;
  62. var off_pfFourCC = 21;
  63. var off_RGBbpp = 22;
  64. var off_RMask = 23;
  65. var off_GMask = 24;
  66. var off_BMask = 25;
  67. var off_AMask = 26;
  68. var off_caps1 = 27;
  69. var off_caps2 = 28;
  70. var off_caps3 = 29;
  71. var off_caps4 = 30;
  72. var off_dxgiFormat = 32
  73. export interface DDSInfo {
  74. width: number;
  75. height: number;
  76. mipmapCount: number;
  77. isFourCC: boolean;
  78. isRGB: boolean;
  79. isLuminance: boolean;
  80. isCube: boolean;
  81. isCompressed: boolean;
  82. dxgiFormat: number;
  83. textureType: number;
  84. };
  85. export class DDSTools {
  86. public static StoreLODInAlphaChannel = false;
  87. public static GetDDSInfo(arrayBuffer: any): DDSInfo {
  88. var header = new Int32Array(arrayBuffer, 0, headerLengthInt);
  89. var extendedHeader = new Int32Array(arrayBuffer, 0, headerLengthInt + 4);
  90. var mipmapCount = 1;
  91. if (header[off_flags] & DDSD_MIPMAPCOUNT) {
  92. mipmapCount = Math.max(1, header[off_mipmapCount]);
  93. }
  94. var fourCC = header[off_pfFourCC];
  95. var dxgiFormat = (fourCC === FOURCC_DX10) ? extendedHeader[off_dxgiFormat] : 0;
  96. var textureType = Engine.TEXTURETYPE_UNSIGNED_INT;
  97. switch (fourCC) {
  98. case FOURCC_D3DFMT_R16G16B16A16F:
  99. textureType = Engine.TEXTURETYPE_HALF_FLOAT;
  100. break;
  101. case FOURCC_D3DFMT_R32G32B32A32F:
  102. textureType = Engine.TEXTURETYPE_FLOAT;
  103. break;
  104. case FOURCC_DX10:
  105. if (dxgiFormat === DXGI_FORMAT_R16G16B16A16_FLOAT) {
  106. textureType = Engine.TEXTURETYPE_HALF_FLOAT;
  107. break;
  108. }
  109. }
  110. return {
  111. width: header[off_width],
  112. height: header[off_height],
  113. mipmapCount: mipmapCount,
  114. isFourCC: (header[off_pfFlags] & DDPF_FOURCC) === DDPF_FOURCC,
  115. isRGB: (header[off_pfFlags] & DDPF_RGB) === DDPF_RGB,
  116. isLuminance: (header[off_pfFlags] & DDPF_LUMINANCE) === DDPF_LUMINANCE,
  117. isCube: (header[off_caps2] & DDSCAPS2_CUBEMAP) === DDSCAPS2_CUBEMAP,
  118. isCompressed: (fourCC === FOURCC_DXT1 || fourCC === FOURCC_DXT3 || FOURCC_DXT1 === FOURCC_DXT5),
  119. dxgiFormat: dxgiFormat,
  120. textureType: textureType
  121. };
  122. }
  123. // ref: http://stackoverflow.com/questions/32633585/how-do-you-convert-to-half-floats-in-javascript
  124. private static _FloatView: Float32Array;
  125. private static _Int32View: Int32Array;
  126. private static _ToHalfFloat(value: number): number {
  127. if (!DDSTools._FloatView) {
  128. DDSTools._FloatView = new Float32Array(1);
  129. DDSTools._Int32View = new Int32Array(DDSTools._FloatView.buffer);
  130. }
  131. DDSTools._FloatView[0] = value;
  132. var x = DDSTools._Int32View[0];
  133. var bits = (x >> 16) & 0x8000; /* Get the sign */
  134. var m = (x >> 12) & 0x07ff; /* Keep one extra bit for rounding */
  135. var e = (x >> 23) & 0xff; /* Using int is faster here */
  136. /* If zero, or denormal, or exponent underflows too much for a denormal
  137. * half, return signed zero. */
  138. if (e < 103) {
  139. return bits;
  140. }
  141. /* If NaN, return NaN. If Inf or exponent overflow, return Inf. */
  142. if (e > 142) {
  143. bits |= 0x7c00;
  144. /* If exponent was 0xff and one mantissa bit was set, it means NaN,
  145. * not Inf, so make sure we set one mantissa bit too. */
  146. bits |= ((e == 255) ? 0 : 1) && (x & 0x007fffff);
  147. return bits;
  148. }
  149. /* If exponent underflows but not too much, return a denormal */
  150. if (e < 113) {
  151. m |= 0x0800;
  152. /* Extra rounding may overflow and set mantissa to 0 and exponent
  153. * to 1, which is OK. */
  154. bits |= (m >> (114 - e)) + ((m >> (113 - e)) & 1);
  155. return bits;
  156. }
  157. bits |= ((e - 112) << 10) | (m >> 1);
  158. bits += m & 1;
  159. return bits;
  160. }
  161. private static _FromHalfFloat(value: number): number {
  162. var s = (value & 0x8000) >> 15;
  163. var e = (value & 0x7C00) >> 10;
  164. var f = value & 0x03FF;
  165. if(e === 0) {
  166. return (s ? -1 : 1) * Math.pow(2, -14) * (f / Math.pow(2, 10));
  167. } else if (e == 0x1F) {
  168. return f ? NaN : ((s ? -1 : 1) * Infinity);
  169. }
  170. return (s ? -1 : 1) * Math.pow(2, e-15) * (1 + (f / Math.pow(2, 10)));
  171. }
  172. private static _GetHalfFloatAsFloatRGBAArrayBuffer(width: number, height: number, dataOffset: number, dataLength: number, arrayBuffer: ArrayBuffer, lod: number): Float32Array {
  173. var destArray = new Float32Array(dataLength);
  174. var srcData = new Uint16Array(arrayBuffer, dataOffset);
  175. var index = 0;
  176. for (var y = 0; y < height; y++) {
  177. for (var x = 0; x < width; x++) {
  178. var srcPos = (x + y * width) * 4;
  179. destArray[index] = DDSTools._FromHalfFloat(srcData[srcPos]);
  180. destArray[index + 1] = DDSTools._FromHalfFloat(srcData[srcPos + 1]);
  181. destArray[index + 2] = DDSTools._FromHalfFloat(srcData[srcPos + 2]);
  182. if (DDSTools.StoreLODInAlphaChannel) {
  183. destArray[index + 3] = lod;
  184. } else {
  185. destArray[index + 3] = DDSTools._FromHalfFloat(srcData[srcPos + 3]);
  186. }
  187. index += 4;
  188. }
  189. }
  190. return destArray;
  191. }
  192. private static _GetHalfFloatRGBAArrayBuffer(width: number, height: number, dataOffset: number, dataLength: number, arrayBuffer: ArrayBuffer, lod: number): Uint16Array {
  193. if (DDSTools.StoreLODInAlphaChannel) {
  194. var destArray = new Uint16Array(dataLength);
  195. var srcData = new Uint16Array(arrayBuffer, dataOffset);
  196. var index = 0;
  197. for (var y = 0; y < height; y++) {
  198. for (var x = 0; x < width; x++) {
  199. var srcPos = (x + y * width) * 4;
  200. destArray[index] = srcData[srcPos];
  201. destArray[index + 1] = srcData[srcPos + 1];
  202. destArray[index + 2] = srcData[srcPos + 2];
  203. destArray[index + 3] = DDSTools._ToHalfFloat(lod)
  204. index += 4;
  205. }
  206. }
  207. return destArray;
  208. }
  209. return new Uint16Array(arrayBuffer, dataOffset, dataLength);
  210. }
  211. private static _GetFloatRGBAArrayBuffer(width: number, height: number, dataOffset: number, dataLength: number, arrayBuffer: ArrayBuffer, lod: number): Float32Array {
  212. if (DDSTools.StoreLODInAlphaChannel) {
  213. var destArray = new Float32Array(dataLength);
  214. var srcData = new Float32Array(arrayBuffer, dataOffset);
  215. var index = 0;
  216. for (var y = 0; y < height; y++) {
  217. for (var x = 0; x < width; x++) {
  218. var srcPos = (x + y * width) * 4;
  219. destArray[index] = srcData[srcPos];
  220. destArray[index + 1] = srcData[srcPos + 1];
  221. destArray[index + 2] = srcData[srcPos + 2];
  222. destArray[index + 3] = lod;
  223. index += 4;
  224. }
  225. }
  226. return destArray;
  227. }
  228. return new Float32Array(arrayBuffer, dataOffset, dataLength);
  229. }
  230. private static _GetFloatAsUIntRGBAArrayBuffer(width: number, height: number, dataOffset: number, dataLength: number, arrayBuffer: ArrayBuffer, lod: number): Float32Array {
  231. var destArray = new Uint8Array(dataLength);
  232. var srcData = new Float32Array(arrayBuffer, dataOffset);
  233. var index = 0;
  234. for (var y = 0; y < height; y++) {
  235. for (var x = 0; x < width; x++) {
  236. var srcPos = (x + y * width) * 4;
  237. destArray[index] = MathTools.Clamp(srcData[srcPos]) * 255;
  238. destArray[index + 1] = MathTools.Clamp(srcData[srcPos + 1]) * 255;
  239. destArray[index + 2] = MathTools.Clamp(srcData[srcPos + 2]) * 255;
  240. if (DDSTools.StoreLODInAlphaChannel) {
  241. destArray[index + 3] = lod;
  242. } else {
  243. destArray[index + 3] = MathTools.Clamp(srcData[srcPos + 3]) * 255;
  244. }
  245. index += 4;
  246. }
  247. }
  248. return destArray;
  249. }
  250. private static _GetHalfFloatAsUIntRGBAArrayBuffer(width: number, height: number, dataOffset: number, dataLength: number, arrayBuffer: ArrayBuffer, lod: number): Float32Array {
  251. var destArray = new Uint8Array(dataLength);
  252. var srcData = new Uint16Array(arrayBuffer, dataOffset);
  253. var index = 0;
  254. for (var y = 0; y < height; y++) {
  255. for (var x = 0; x < width; x++) {
  256. var srcPos = (x + y * width) * 4;
  257. destArray[index] = MathTools.Clamp(DDSTools._FromHalfFloat(srcData[srcPos])) * 255;
  258. destArray[index + 1] = MathTools.Clamp(DDSTools._FromHalfFloat(srcData[srcPos + 1])) * 255;
  259. destArray[index + 2] = MathTools.Clamp(DDSTools._FromHalfFloat(srcData[srcPos + 2])) * 255;
  260. if (DDSTools.StoreLODInAlphaChannel) {
  261. destArray[index + 3] = lod;
  262. } else {
  263. destArray[index + 3] = MathTools.Clamp(DDSTools._FromHalfFloat(srcData[srcPos + 3])) * 255;
  264. }
  265. index += 4;
  266. }
  267. }
  268. return destArray;
  269. }
  270. private static _GetRGBAArrayBuffer(width: number, height: number, dataOffset: number, dataLength: number, arrayBuffer: ArrayBuffer): Uint8Array {
  271. var byteArray = new Uint8Array(dataLength);
  272. var srcData = new Uint8Array(arrayBuffer, dataOffset);
  273. var index = 0;
  274. for (var y = 0; y < height; y++) {
  275. for (var x = 0; x < width; x++) {
  276. var srcPos = (x + y * width) * 4;
  277. byteArray[index] = srcData[srcPos + 2];
  278. byteArray[index + 1] = srcData[srcPos + 1];
  279. byteArray[index + 2] = srcData[srcPos];
  280. byteArray[index + 3] = srcData[srcPos + 3];
  281. index += 4;
  282. }
  283. }
  284. return byteArray;
  285. }
  286. private static _GetRGBArrayBuffer(width: number, height: number, dataOffset:number, dataLength: number, arrayBuffer: ArrayBuffer): Uint8Array {
  287. var byteArray = new Uint8Array(dataLength);
  288. var srcData = new Uint8Array(arrayBuffer, dataOffset);
  289. var index = 0;
  290. for (var y = 0; y < height; y++) {
  291. for (var x = 0; x < width; x++) {
  292. var srcPos = (x + y * width) * 3;
  293. byteArray[index] = srcData[srcPos + 2];
  294. byteArray[index + 1] = srcData[srcPos + 1];
  295. byteArray[index + 2] = srcData[srcPos];
  296. index += 3;
  297. }
  298. }
  299. return byteArray;
  300. }
  301. private static _GetLuminanceArrayBuffer(width: number, height: number, dataOffset: number, dataLength: number, arrayBuffer: ArrayBuffer): Uint8Array {
  302. var byteArray = new Uint8Array(dataLength);
  303. var srcData = new Uint8Array(arrayBuffer, dataOffset);
  304. var index = 0;
  305. for (var y = 0; y < height; y++) {
  306. for (var x = 0; x < width; x++) {
  307. var srcPos = (x + y * width);
  308. byteArray[index] = srcData[srcPos];
  309. index++;
  310. }
  311. }
  312. return byteArray;
  313. }
  314. public static UploadDDSLevels(engine: Engine, arrayBuffer: any, info: DDSInfo, loadMipmaps: boolean, faces: number, lodIndex = -1): void {
  315. var gl = engine._gl;
  316. var ext = engine.getCaps().s3tc;
  317. var header = new Int32Array(arrayBuffer, 0, headerLengthInt),
  318. fourCC, blockBytes, internalFormat, format,
  319. width, height, dataLength, dataOffset,
  320. byteArray, mipmapCount, mip;
  321. if (header[off_magic] != DDS_MAGIC) {
  322. Tools.Error("Invalid magic number in DDS header");
  323. return;
  324. }
  325. if (!info.isFourCC && !info.isRGB && !info.isLuminance) {
  326. Tools.Error("Unsupported format, must contain a FourCC, RGB or LUMINANCE code");
  327. return;
  328. }
  329. if (info.isCompressed && !ext) {
  330. Tools.Error("Compressed textures are not supported on this platform.");
  331. return;
  332. }
  333. var bpp = header[off_RGBbpp];
  334. dataOffset = header[off_size] + 4;
  335. let computeFormats = false;
  336. if (info.isFourCC) {
  337. fourCC = header[off_pfFourCC];
  338. switch (fourCC) {
  339. case FOURCC_DXT1:
  340. blockBytes = 8;
  341. internalFormat = ext.COMPRESSED_RGBA_S3TC_DXT1_EXT;
  342. break;
  343. case FOURCC_DXT3:
  344. blockBytes = 16;
  345. internalFormat = ext.COMPRESSED_RGBA_S3TC_DXT3_EXT;
  346. break;
  347. case FOURCC_DXT5:
  348. blockBytes = 16;
  349. internalFormat = ext.COMPRESSED_RGBA_S3TC_DXT5_EXT;
  350. break;
  351. case FOURCC_D3DFMT_R16G16B16A16F:
  352. computeFormats = true;
  353. break;
  354. case FOURCC_D3DFMT_R32G32B32A32F:
  355. computeFormats = true;
  356. break;
  357. case FOURCC_DX10:
  358. // There is an additionnal header so dataOffset need to be changed
  359. dataOffset += 5 * 4; // 5 uints
  360. let supported = false;
  361. switch (info.dxgiFormat) {
  362. case DXGI_FORMAT_R16G16B16A16_FLOAT:
  363. computeFormats = true;
  364. supported = true;
  365. break;
  366. case DXGI_FORMAT_B8G8R8X8_UNORM:
  367. info.isRGB = true;
  368. info.isFourCC = false;
  369. bpp = 32;
  370. supported = true;
  371. break;
  372. }
  373. if (supported) {
  374. break;
  375. }
  376. default:
  377. console.error("Unsupported FourCC code:", Int32ToFourCC(fourCC));
  378. return;
  379. }
  380. }
  381. if (computeFormats) {
  382. format = engine._getWebGLTextureType(info.textureType);
  383. internalFormat = engine._getRGBABufferInternalSizedFormat(info.textureType);
  384. }
  385. mipmapCount = 1;
  386. if (header[off_flags] & DDSD_MIPMAPCOUNT && loadMipmaps !== false) {
  387. mipmapCount = Math.max(1, header[off_mipmapCount]);
  388. }
  389. for (var face = 0; face < faces; face++) {
  390. var sampler = faces === 1 ? gl.TEXTURE_2D : (gl.TEXTURE_CUBE_MAP_POSITIVE_X + face);
  391. width = header[off_width];
  392. height = header[off_height];
  393. for (mip = 0; mip < mipmapCount; ++mip) {
  394. if (lodIndex === -1 || lodIndex === mip) {
  395. // In case of fixed LOD, if the lod has just been uploaded, early exit.
  396. const i = (lodIndex === -1) ? mip : 0;
  397. if (!info.isCompressed && info.isFourCC) {
  398. dataLength = width * height * 4;
  399. var floatArray: ArrayBufferView;
  400. if (engine.badOS) {
  401. if (bpp === 128) {
  402. floatArray = DDSTools._GetFloatAsUIntRGBAArrayBuffer(width, height, dataOffset, dataLength, arrayBuffer, i);
  403. }
  404. else if (bpp === 64) {
  405. floatArray = DDSTools._GetHalfFloatAsUIntRGBAArrayBuffer(width, height, dataOffset, dataLength, arrayBuffer, i);
  406. }
  407. info.textureType = Engine.TEXTURETYPE_UNSIGNED_INT;
  408. format = engine._getWebGLTextureType(info.textureType);
  409. internalFormat = engine._getRGBABufferInternalSizedFormat(info.textureType);
  410. }
  411. else {
  412. if (bpp === 128) {
  413. floatArray = DDSTools._GetFloatRGBAArrayBuffer(width, height, dataOffset, dataLength, arrayBuffer, i);
  414. } else if (bpp === 64 && !engine.getCaps().textureHalfFloat) { // Let's fallback to full float
  415. floatArray = DDSTools._GetHalfFloatAsFloatRGBAArrayBuffer(width, height, dataOffset, dataLength, arrayBuffer, i);
  416. info.textureType = Engine.TEXTURETYPE_FLOAT;
  417. format = engine._getWebGLTextureType(info.textureType);
  418. internalFormat = engine._getRGBABufferInternalSizedFormat(info.textureType);
  419. } else { // 64
  420. floatArray = DDSTools._GetHalfFloatRGBAArrayBuffer(width, height, dataOffset, dataLength, arrayBuffer, i);
  421. }
  422. }
  423. engine._uploadDataToTexture(sampler, i, internalFormat, width, height, gl.RGBA, format, floatArray);
  424. } else if (info.isRGB) {
  425. if (bpp === 24) {
  426. dataLength = width * height * 3;
  427. byteArray = DDSTools._GetRGBArrayBuffer(width, height, dataOffset, dataLength, arrayBuffer);
  428. engine._uploadDataToTexture(sampler, i, gl.RGB, width, height, gl.RGB, gl.UNSIGNED_BYTE, byteArray);
  429. } else { // 32
  430. dataLength = width * height * 4;
  431. byteArray = DDSTools._GetRGBAArrayBuffer(width, height, dataOffset, dataLength, arrayBuffer);
  432. engine._uploadDataToTexture(sampler, i, gl.RGBA, width, height, gl.RGBA, gl.UNSIGNED_BYTE, byteArray);
  433. }
  434. } else if (info.isLuminance) {
  435. var unpackAlignment = gl.getParameter(gl.UNPACK_ALIGNMENT);
  436. var unpaddedRowSize = width;
  437. var paddedRowSize = Math.floor((width + unpackAlignment - 1) / unpackAlignment) * unpackAlignment;
  438. dataLength = paddedRowSize * (height - 1) + unpaddedRowSize;
  439. byteArray = DDSTools._GetLuminanceArrayBuffer(width, height, dataOffset, dataLength, arrayBuffer);
  440. engine._uploadDataToTexture(sampler, i, gl.LUMINANCE, width, height, gl.LUMINANCE, gl.UNSIGNED_BYTE, byteArray);
  441. } else {
  442. dataLength = Math.max(4, width) / 4 * Math.max(4, height) / 4 * blockBytes;
  443. byteArray = new Uint8Array(arrayBuffer, dataOffset, dataLength);
  444. engine._uploadCompressedDataToTexture(sampler, i, internalFormat, width, height, byteArray);
  445. }
  446. }
  447. dataOffset += width * height * (bpp / 8);
  448. width *= 0.5;
  449. height *= 0.5;
  450. width = Math.max(1.0, width);
  451. height = Math.max(1.0, height);
  452. }
  453. }
  454. }
  455. }
  456. }