cubemapToSphericalPolynomial.ts 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. import { Vector3 } from "../../Maths/math.vector";
  2. import { Scalar } from "../../Maths/math.scalar";
  3. import { SphericalPolynomial, SphericalHarmonics } from "../../Maths/sphericalPolynomial";
  4. import { BaseTexture } from "../../Materials/Textures/baseTexture";
  5. import { Nullable } from "../../types";
  6. import { Constants } from "../../Engines/constants";
  7. import { CubeMapInfo } from "./panoramaToCubemap";
  8. import { ToLinearSpace } from '../../Maths/math.constants';
  9. import { Color3 } from '../../Maths/math.color';
  10. class FileFaceOrientation {
  11. public name: string;
  12. public worldAxisForNormal: Vector3; // the world axis corresponding to the normal to the face
  13. public worldAxisForFileX: Vector3; // the world axis corresponding to texture right x-axis in file
  14. public worldAxisForFileY: Vector3; // the world axis corresponding to texture down y-axis in file
  15. public constructor(name: string, worldAxisForNormal: Vector3, worldAxisForFileX: Vector3, worldAxisForFileY: Vector3) {
  16. this.name = name;
  17. this.worldAxisForNormal = worldAxisForNormal;
  18. this.worldAxisForFileX = worldAxisForFileX;
  19. this.worldAxisForFileY = worldAxisForFileY;
  20. }
  21. }
  22. /**
  23. * Helper class dealing with the extraction of spherical polynomial dataArray
  24. * from a cube map.
  25. */
  26. export class CubeMapToSphericalPolynomialTools {
  27. private static FileFaces: FileFaceOrientation[] = [
  28. new FileFaceOrientation("right", new Vector3(1, 0, 0), new Vector3(0, 0, -1), new Vector3(0, -1, 0)), // +X east
  29. new FileFaceOrientation("left", new Vector3(-1, 0, 0), new Vector3(0, 0, 1), new Vector3(0, -1, 0)), // -X west
  30. new FileFaceOrientation("up", new Vector3(0, 1, 0), new Vector3(1, 0, 0), new Vector3(0, 0, 1)), // +Y north
  31. new FileFaceOrientation("down", new Vector3(0, -1, 0), new Vector3(1, 0, 0), new Vector3(0, 0, -1)), // -Y south
  32. new FileFaceOrientation("front", new Vector3(0, 0, 1), new Vector3(1, 0, 0), new Vector3(0, -1, 0)), // +Z top
  33. new FileFaceOrientation("back", new Vector3(0, 0, -1), new Vector3(-1, 0, 0), new Vector3(0, -1, 0))// -Z bottom
  34. ];
  35. /**
  36. * Converts a texture to the according Spherical Polynomial data.
  37. * This extracts the first 3 orders only as they are the only one used in the lighting.
  38. *
  39. * @param texture The texture to extract the information from.
  40. * @return The Spherical Polynomial data.
  41. */
  42. public static ConvertCubeMapTextureToSphericalPolynomial(texture: BaseTexture): Nullable<Promise<SphericalPolynomial>> {
  43. if (!texture.isCube) {
  44. // Only supports cube Textures currently.
  45. return null;
  46. }
  47. texture.getScene()?.getEngine().flushFramebuffer();
  48. var size = texture.getSize().width;
  49. var rightPromise = texture.readPixels(0, undefined, undefined, false);
  50. var leftPromise = texture.readPixels(1, undefined, undefined, false);
  51. var upPromise: Nullable<Promise<ArrayBufferView>>;
  52. var downPromise: Nullable<Promise<ArrayBufferView>>;
  53. if (texture.isRenderTarget) {
  54. upPromise = texture.readPixels(3, undefined, undefined, false);
  55. downPromise = texture.readPixels(2, undefined, undefined, false);
  56. }
  57. else {
  58. upPromise = texture.readPixels(2, undefined, undefined, false);
  59. downPromise = texture.readPixels(3, undefined, undefined, false);
  60. }
  61. var frontPromise = texture.readPixels(4, undefined, undefined, false);
  62. var backPromise = texture.readPixels(5, undefined, undefined, false);
  63. var gammaSpace = texture.gammaSpace;
  64. // Always read as RGBA.
  65. var format = Constants.TEXTUREFORMAT_RGBA;
  66. var type = Constants.TEXTURETYPE_UNSIGNED_INT;
  67. if (texture.textureType == Constants.TEXTURETYPE_FLOAT || texture.textureType == Constants.TEXTURETYPE_HALF_FLOAT) {
  68. type = Constants.TEXTURETYPE_FLOAT;
  69. }
  70. return new Promise((resolve, reject) => {
  71. Promise.all([leftPromise, rightPromise, upPromise, downPromise, frontPromise, backPromise]).then(([left, right, up, down, front, back]) => {
  72. var cubeInfo: CubeMapInfo = {
  73. size,
  74. right,
  75. left,
  76. up,
  77. down,
  78. front,
  79. back,
  80. format,
  81. type,
  82. gammaSpace,
  83. };
  84. resolve(this.ConvertCubeMapToSphericalPolynomial(cubeInfo));
  85. });
  86. });
  87. }
  88. /**
  89. * Converts a cubemap to the according Spherical Polynomial data.
  90. * This extracts the first 3 orders only as they are the only one used in the lighting.
  91. *
  92. * @param cubeInfo The Cube map to extract the information from.
  93. * @return The Spherical Polynomial data.
  94. */
  95. public static ConvertCubeMapToSphericalPolynomial(cubeInfo: CubeMapInfo): SphericalPolynomial {
  96. var sphericalHarmonics = new SphericalHarmonics();
  97. var totalSolidAngle = 0.0;
  98. // The (u,v) range is [-1,+1], so the distance between each texel is 2/Size.
  99. var du = 2.0 / cubeInfo.size;
  100. var dv = du;
  101. // The (u,v) of the first texel is half a texel from the corner (-1,-1).
  102. var minUV = du * 0.5 - 1.0;
  103. for (var faceIndex = 0; faceIndex < 6; faceIndex++) {
  104. var fileFace = this.FileFaces[faceIndex];
  105. var dataArray = (<any>cubeInfo)[fileFace.name];
  106. var v = minUV;
  107. // TODO: we could perform the summation directly into a SphericalPolynomial (SP), which is more efficient than SphericalHarmonic (SH).
  108. // This is possible because during the summation we do not need the SH-specific properties, e.g. orthogonality.
  109. // Because SP is still linear, so summation is fine in that basis.
  110. const stride = cubeInfo.format === Constants.TEXTUREFORMAT_RGBA ? 4 : 3;
  111. for (var y = 0; y < cubeInfo.size; y++) {
  112. var u = minUV;
  113. for (var x = 0; x < cubeInfo.size; x++) {
  114. // World direction (not normalised)
  115. var worldDirection =
  116. fileFace.worldAxisForFileX.scale(u).add(
  117. fileFace.worldAxisForFileY.scale(v)).add(
  118. fileFace.worldAxisForNormal);
  119. worldDirection.normalize();
  120. var deltaSolidAngle = Math.pow(1.0 + u * u + v * v, -3.0 / 2.0);
  121. var r = dataArray[(y * cubeInfo.size * stride) + (x * stride) + 0];
  122. var g = dataArray[(y * cubeInfo.size * stride) + (x * stride) + 1];
  123. var b = dataArray[(y * cubeInfo.size * stride) + (x * stride) + 2];
  124. // Prevent NaN harmonics with extreme HDRI data.
  125. if (isNaN(r)) { r = 0; }
  126. if (isNaN(g)) { g = 0; }
  127. if (isNaN(b)) { b = 0; }
  128. // Handle Integer types.
  129. if (cubeInfo.type === Constants.TEXTURETYPE_UNSIGNED_INT) {
  130. r /= 255;
  131. g /= 255;
  132. b /= 255;
  133. }
  134. // Handle Gamma space textures.
  135. if (cubeInfo.gammaSpace) {
  136. r = Math.pow(Scalar.Clamp(r), ToLinearSpace);
  137. g = Math.pow(Scalar.Clamp(g), ToLinearSpace);
  138. b = Math.pow(Scalar.Clamp(b), ToLinearSpace);
  139. }
  140. // Prevent to explode in case of really high dynamic ranges.
  141. // sh 3 would not be enough to accurately represent it.
  142. const max = 4096;
  143. r = Scalar.Clamp(r, 0, max);
  144. g = Scalar.Clamp(g, 0, max);
  145. b = Scalar.Clamp(b, 0, max);
  146. var color = new Color3(r, g, b);
  147. sphericalHarmonics.addLight(worldDirection, color, deltaSolidAngle);
  148. totalSolidAngle += deltaSolidAngle;
  149. u += du;
  150. }
  151. v += dv;
  152. }
  153. }
  154. // Solid angle for entire sphere is 4*pi
  155. var sphereSolidAngle = 4.0 * Math.PI;
  156. // Adjust the solid angle to allow for how many faces we processed.
  157. var facesProcessed = 6.0;
  158. var expectedSolidAngle = sphereSolidAngle * facesProcessed / 6.0;
  159. // Adjust the harmonics so that the accumulated solid angle matches the expected solid angle.
  160. // This is needed because the numerical integration over the cube uses a
  161. // small angle approximation of solid angle for each texel (see deltaSolidAngle),
  162. // and also to compensate for accumulative error due to float precision in the summation.
  163. var correctionFactor = expectedSolidAngle / totalSolidAngle;
  164. sphericalHarmonics.scaleInPlace(correctionFactor);
  165. sphericalHarmonics.convertIncidentRadianceToIrradiance();
  166. sphericalHarmonics.convertIrradianceToLambertianRadiance();
  167. return SphericalPolynomial.FromHarmonics(sphericalHarmonics);
  168. }
  169. }