cubemapToSphericalPolynomial.ts 8.4 KB

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