cubemapToSphericalPolynomial.js 9.3 KB

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