equirect-cubemap-faces-js
Version:
Convert an equirectangular image (aka lat-long map) into cubemap faces you can feed to WebGL
186 lines (159 loc) • 6.35 kB
JavaScript
var equirectToCubemapFaces = (function() {
'use strict';
var min = Math.min, max = Math.max;
var pow = Math.pow, atan2 = Math.atan2, sqrt = Math.sqrt, log = Math.log;
var floor = Math.floor, round = Math.round;
var PI = +Math.PI;
function clamp(v, lo, hi) {
return min(hi, max(lo, v));
}
// These are approximations that assume gamma is 2.0. Not ideal, but close enough.
function srgbToLinear(v) {
var component = (+v * (1.0 / 255.0));
return component * component;
}
function linearToSRGB(v) {
return (sqrt(v) * 255.0) | 0;
}
function nearestPowerOfTwo(n) {
return 1 << round(log(n)/log(2))
}
var DEFAULT_OPTIONS = {
flipTheta: false,
interpolation: "bilinear"
};
function transformSingleFace(inPixels, faceIdx, facePixels, opts) {
if (!opts) {
opts = DEFAULT_OPTIONS;
}
var thetaFlip = opts.flipTheta ? -1 : 1;
var edge = facePixels.width|0;
var inWidth = inPixels.width|0;
var inHeight = inPixels.height|0;
var inData = inPixels.data;
var smoothNearest = (opts.interpolation === "nearest");
var faceData = facePixels.data;
var faceWidth = facePixels.width|0;
var faceHeight = facePixels.height|0;
var face = faceIdx | 0;
var iFaceWidth2 = 2.0 / faceWidth;
var iFaceHeight2 = 2.0 / faceHeight;
for (var j = 0; j < faceHeight; ++j) {
for (var i = 0; i < faceWidth; ++i) {
var a = iFaceWidth2 * i;
var b = iFaceHeight2 * j;
var outPos = (i + j * edge) << 2;
var x = 0.0, y = 0.0, z = 0.0;
// @@NOTE: Tried using explicit matrices for this and didn't see any
// speedup over the (IMO more understandable) switch. (Probably because these
// branches should be correctly predicted almost every time).
switch (face) {
case 0: x = 1.0 - a; y = 1.0; z = 1.0 - b; break; // right (+x)
case 1: x = a - 1.0; y = -1.0; z = 1.0 - b; break; // left (-x)
case 2: x = b - 1.0; y = a - 1.0; z = 1.0; break; // top (+y)
case 3: x = 1.0 - b; y = a - 1.0; z = -1.0; break; // bottom (-y)
case 4: x = 1.0; y = a - 1.0; z = 1.0 - b; break; // front (+z)
case 5: x = -1.0; y = 1.0 - a; z = 1.0 - b; break; // back (-z)
}
var theta = thetaFlip * atan2(y, x);
var rad = sqrt(x*x+y*y);
var phi = atan2(z, rad);
var uf = 2.0 * (inWidth / 4) * (theta + PI) / PI;
var vf = 2.0 * (inWidth / 4) * (PI/2 - phi) / PI;
var ui = floor(uf)|0, vi = floor(vf)|0;
if (smoothNearest) {
var inPos = ((ui % inWidth) + inWidth * clamp(vi, 0, inHeight-1)) << 2;
faceData[outPos + 0] = inData[inPos + 0] | 0;
faceData[outPos + 1] = inData[inPos + 1] | 0;
faceData[outPos + 2] = inData[inPos + 2] | 0;
faceData[outPos + 3] = inData[inPos + 3] | 0;
} else {
// bilinear blend
var u2 = ui+1, v2 = vi+1;
var mu = uf-ui, nu = vf-vi;
var pA = ((ui % inWidth) + inWidth * clamp(vi, 0, inHeight-1)) << 2;
var pB = ((u2 % inWidth) + inWidth * clamp(vi, 0, inHeight-1)) << 2;
var pC = ((ui % inWidth) + inWidth * clamp(v2, 0, inHeight-1)) << 2;
var pD = ((u2 % inWidth) + inWidth * clamp(v2, 0, inHeight-1)) << 2;
var aA = (inData[pA+3]|0)*(1.0 / 255.0)
var aB = (inData[pB+3]|0)*(1.0 / 255.0)
var aC = (inData[pC+3]|0)*(1.0 / 255.0)
var aD = (inData[pD+3]|0)*(1.0 / 255.0)
// Do the bilinear blend in linear space.
var rA = srgbToLinear(inData[pA+0]|0) * aA, gA = srgbToLinear(inData[pA+1]|0) * aA, bA = srgbToLinear(inData[pA+2]|0) * aA;
var rB = srgbToLinear(inData[pB+0]|0) * aB, gB = srgbToLinear(inData[pB+1]|0) * aB, bB = srgbToLinear(inData[pB+2]|0) * aB;
var rC = srgbToLinear(inData[pC+0]|0) * aC, gC = srgbToLinear(inData[pC+1]|0) * aC, bC = srgbToLinear(inData[pC+2]|0) * aC;
var rD = srgbToLinear(inData[pD+0]|0) * aD, gD = srgbToLinear(inData[pD+1]|0) * aD, bD = srgbToLinear(inData[pD+2]|0) * aD;
var r = (rA*(1.0-mu)*(1.0-nu) + rB*mu*(1.0-nu) + rC*(1.0-mu)*nu + rD*mu*nu);
var g = (gA*(1.0-mu)*(1.0-nu) + gB*mu*(1.0-nu) + gC*(1.0-mu)*nu + gD*mu*nu);
var b = (bA*(1.0-mu)*(1.0-nu) + bB*mu*(1.0-nu) + bC*(1.0-mu)*nu + bD*mu*nu);
var a = (aA*(1.0-mu)*(1.0-nu) + aB*mu*(1.0-nu) + aC*(1.0-mu)*nu + aD*mu*nu);
var ia = 1.0 / a;
faceData[outPos+0] = linearToSRGB(r * ia)|0;
faceData[outPos+1] = linearToSRGB(g * ia)|0;
faceData[outPos+2] = linearToSRGB(b * ia)|0;
faceData[outPos+3] = (a * 255.0)|0;
}
}
}
return facePixels;
}
function transformToCubeFaces(inPixels, facePixArray, options) {
if (facePixArray.length !== 6) {
throw new Error("facePixArray length must be 6!");
}
if (!options) {
options = DEFAULT_OPTIONS;
}
for (var face = 0; face < 6; ++face) {
transformSingleFace(inPixels, face, facePixArray[face], options);
}
return facePixArray;
}
function imageGetPixels(image) {
if (image.data) {
return image;
}
var canvas = image, ctx = null;
if (canvas.tagName !== 'CANVAS') {
canvas = document.createElement('canvas');
canvas.width = image.naturalWidth || image.width;
canvas.height = image.naturalHeight || image.height;
ctx = canvas.getContext('2d');
ctx.drawImage(image, 0, 0, canvas.width, canvas.height, 0, 0, canvas.width, canvas.height);
}
else {
ctx = canvas.getContext('2d');
}
return ctx.getImageData(0, 0, canvas.width, canvas.height);
}
function equirectToCubemapFaces(image, faceSize, options) {
var inPixels = imageGetPixels(image);
if (!faceSize) {
faceSize = nearestPowerOfTwo(image.width/4)|0;
}
if (typeof faceSize !== 'number') {
throw new Error("faceSize needed to be a number or missing");
}
var faces = [];
for (var i = 0; i < 6; ++i) {
var c = document.createElement('canvas');
c.width = faceSize;
c.height = faceSize;
faces.push(c);
}
transformToCubeFaces(inPixels, faces.map(function(canv) {
return canv.getContext('2d').createImageData(canv.width, canv.height);
}), options)
.forEach(function(imageData, i) {
faces[i].getContext('2d').putImageData(imageData, 0, 0);
});
return faces;
}
equirectToCubemapFaces.transformSingleFace = transformSingleFace;
equirectToCubemapFaces.transformToCubeFaces = transformToCubeFaces;
return equirectToCubemapFaces;
}());
if (typeof module !== 'undefined' && module.exports) {
module.exports = equirectToCubemapFaces;
}