@babylonjs/core
Version:
Getting started? Play directly with the Babylon.js API using our [playground](https://playground.babylonjs.com/). It also contains a lot of samples to learn how to use it.
196 lines (195 loc) • 9.67 kB
JavaScript
import { Vector3 } from "../../Maths/math.vector.js";
import { Clamp } from "../../Maths/math.scalar.functions.js";
import { SphericalPolynomial, SphericalHarmonics } from "../../Maths/sphericalPolynomial.js";
import { ToLinearSpace } from "../../Maths/math.constants.js";
import { Color3 } from "../../Maths/math.color.js";
class FileFaceOrientation {
constructor(name, worldAxisForNormal, worldAxisForFileX, worldAxisForFileY) {
this.name = name;
this.worldAxisForNormal = worldAxisForNormal;
this.worldAxisForFileX = worldAxisForFileX;
this.worldAxisForFileY = worldAxisForFileY;
}
}
/**
* Helper class dealing with the extraction of spherical polynomial dataArray
* from a cube map.
*/
export class CubeMapToSphericalPolynomialTools {
/**
* Converts a texture to the according Spherical Polynomial data.
* This extracts the first 3 orders only as they are the only one used in the lighting.
*
* @param texture The texture to extract the information from.
* @returns The Spherical Polynomial data.
*/
static ConvertCubeMapTextureToSphericalPolynomial(texture) {
if (!texture.isCube) {
// Only supports cube Textures currently.
return null;
}
texture.getScene()?.getEngine().flushFramebuffer();
const size = texture.getSize().width;
const rightPromise = texture.readPixels(0, undefined, undefined, false);
const leftPromise = texture.readPixels(1, undefined, undefined, false);
let upPromise;
let downPromise;
if (texture.isRenderTarget) {
upPromise = texture.readPixels(3, undefined, undefined, false);
downPromise = texture.readPixels(2, undefined, undefined, false);
}
else {
upPromise = texture.readPixels(2, undefined, undefined, false);
downPromise = texture.readPixels(3, undefined, undefined, false);
}
const frontPromise = texture.readPixels(4, undefined, undefined, false);
const backPromise = texture.readPixels(5, undefined, undefined, false);
const gammaSpace = texture.gammaSpace;
// Always read as RGBA.
const format = 5;
let type = 0;
if (texture.textureType == 1 || texture.textureType == 2) {
type = 1;
}
return new Promise((resolve) => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises, github/no-then
Promise.all([leftPromise, rightPromise, upPromise, downPromise, frontPromise, backPromise]).then(([left, right, up, down, front, back]) => {
const cubeInfo = {
size,
right,
left,
up,
down,
front,
back,
format,
type,
gammaSpace,
};
resolve(this.ConvertCubeMapToSphericalPolynomial(cubeInfo));
});
});
}
/**
* Compute the area on the unit sphere of the rectangle defined by (x,y) and the origin
* See https://www.rorydriscoll.com/2012/01/15/cubemap-texel-solid-angle/
* @param x
* @param y
* @returns the area
*/
static _AreaElement(x, y) {
return Math.atan2(x * y, Math.sqrt(x * x + y * y + 1));
}
/**
* Converts a cubemap to the according Spherical Polynomial data.
* This extracts the first 3 orders only as they are the only one used in the lighting.
*
* @param cubeInfo The Cube map to extract the information from.
* @returns The Spherical Polynomial data.
*/
static ConvertCubeMapToSphericalPolynomial(cubeInfo) {
const sphericalHarmonics = new SphericalHarmonics();
let totalSolidAngle = 0.0;
// The (u,v) range is [-1,+1], so the distance between each texel is 2/Size.
const du = 2.0 / cubeInfo.size;
const dv = du;
const halfTexel = 0.5 * du;
// The (u,v) of the first texel is half a texel from the corner (-1,-1).
const minUV = halfTexel - 1.0;
for (let faceIndex = 0; faceIndex < 6; faceIndex++) {
const fileFace = this._FileFaces[faceIndex];
const dataArray = cubeInfo[fileFace.name];
let v = minUV;
// TODO: we could perform the summation directly into a SphericalPolynomial (SP), which is more efficient than SphericalHarmonic (SH).
// This is possible because during the summation we do not need the SH-specific properties, e.g. orthogonality.
// Because SP is still linear, so summation is fine in that basis.
const stride = cubeInfo.format === 5 ? 4 : 3;
for (let y = 0; y < cubeInfo.size; y++) {
let u = minUV;
for (let x = 0; x < cubeInfo.size; x++) {
// World direction (not normalised)
const worldDirection = fileFace.worldAxisForFileX.scale(u).add(fileFace.worldAxisForFileY.scale(v)).add(fileFace.worldAxisForNormal);
worldDirection.normalize();
const deltaSolidAngle = this._AreaElement(u - halfTexel, v - halfTexel) -
this._AreaElement(u - halfTexel, v + halfTexel) -
this._AreaElement(u + halfTexel, v - halfTexel) +
this._AreaElement(u + halfTexel, v + halfTexel);
let r = dataArray[y * cubeInfo.size * stride + x * stride + 0];
let g = dataArray[y * cubeInfo.size * stride + x * stride + 1];
let b = dataArray[y * cubeInfo.size * stride + x * stride + 2];
// Prevent NaN harmonics with extreme HDRI data.
if (isNaN(r)) {
r = 0;
}
if (isNaN(g)) {
g = 0;
}
if (isNaN(b)) {
b = 0;
}
// Handle Integer types.
if (cubeInfo.type === 0) {
r /= 255;
g /= 255;
b /= 255;
}
// Handle Gamma space textures.
if (cubeInfo.gammaSpace) {
r = Math.pow(Clamp(r), ToLinearSpace);
g = Math.pow(Clamp(g), ToLinearSpace);
b = Math.pow(Clamp(b), ToLinearSpace);
}
// Prevent to explode in case of really high dynamic ranges.
// sh 3 would not be enough to accurately represent it.
const max = this.MAX_HDRI_VALUE;
if (this.PRESERVE_CLAMPED_COLORS) {
const currentMax = Math.max(r, g, b);
if (currentMax > max) {
const factor = max / currentMax;
r *= factor;
g *= factor;
b *= factor;
}
}
else {
r = Clamp(r, 0, max);
g = Clamp(g, 0, max);
b = Clamp(b, 0, max);
}
const color = new Color3(r, g, b);
sphericalHarmonics.addLight(worldDirection, color, deltaSolidAngle);
totalSolidAngle += deltaSolidAngle;
u += du;
}
v += dv;
}
}
// Solid angle for entire sphere is 4*pi
const sphereSolidAngle = 4.0 * Math.PI;
// Adjust the solid angle to allow for how many faces we processed.
const facesProcessed = 6.0;
const expectedSolidAngle = (sphereSolidAngle * facesProcessed) / 6.0;
// Adjust the harmonics so that the accumulated solid angle matches the expected solid angle.
// This is needed because the numerical integration over the cube uses a
// small angle approximation of solid angle for each texel (see deltaSolidAngle),
// and also to compensate for accumulative error due to float precision in the summation.
const correctionFactor = expectedSolidAngle / totalSolidAngle;
sphericalHarmonics.scaleInPlace(correctionFactor);
sphericalHarmonics.convertIncidentRadianceToIrradiance();
sphericalHarmonics.convertIrradianceToLambertianRadiance();
return SphericalPolynomial.FromHarmonics(sphericalHarmonics);
}
}
CubeMapToSphericalPolynomialTools._FileFaces = [
new FileFaceOrientation("right", new Vector3(1, 0, 0), new Vector3(0, 0, -1), new Vector3(0, -1, 0)), // +X east
new FileFaceOrientation("left", new Vector3(-1, 0, 0), new Vector3(0, 0, 1), new Vector3(0, -1, 0)), // -X west
new FileFaceOrientation("up", new Vector3(0, 1, 0), new Vector3(1, 0, 0), new Vector3(0, 0, 1)), // +Y north
new FileFaceOrientation("down", new Vector3(0, -1, 0), new Vector3(1, 0, 0), new Vector3(0, 0, -1)), // -Y south
new FileFaceOrientation("front", new Vector3(0, 0, 1), new Vector3(1, 0, 0), new Vector3(0, -1, 0)), // +Z top
new FileFaceOrientation("back", new Vector3(0, 0, -1), new Vector3(-1, 0, 0), new Vector3(0, -1, 0)), // -Z bottom
];
/** @internal */
CubeMapToSphericalPolynomialTools.MAX_HDRI_VALUE = 4096;
/** @internal */
CubeMapToSphericalPolynomialTools.PRESERVE_CLAMPED_COLORS = false;
//# sourceMappingURL=cubemapToSphericalPolynomial.js.map