HDRI spherical map to cubemap converter

@mrdoob Thank you :smile: , I don’t plan to come back to this project any time soon, but I’ll keep your suggestion in mind!

@fancyrainbow Yes I meant “-Y” :grin: , thanks!


Agree this is a fantastic implementation, I hope you do keep it going.

  • for separate layout I could be wrong but the name convention I’ve seen is/was nx, ny, nz etc instead of xn, yn ,zn, for me this would save renaming all my files after adjusting the output.

I’ve switched the seperate names :grinning:
Now they save as nx, ny, nz, px, py, pz


Just like to add another vote for RGBM16 support! That would make this really useful :grin:

1 Like

@mrdoob I’ve checked the links once again and I think that it may be easier than I initially thought. I could probably render the output to a canvas and simply save the canvas as png.
I thought that 16 stands for 16 bit, but its not, right? The example RGBM16 maps are 32bit (RGBA_8888).

I’ll do some tests on the weekend :slight_smile:


Yes, this should be easy. Just use a render target and set the target’s encoding.

Hmm… Interesting… How much smaller can you get a HDR/EXR by converting to RGBM16 PNG?.. Let’s say you had a HDRHaven 2k hdr that is normally 7.5MB, roughly what size would this be as RGBM16 PNG?

Probably quite a bit smaller. The RGBM16 Pisa cubemap example on the three.js repo is about 30% smaller than the RGBE (.hdr) version. But I think the main advantage is that you can use the browser’s built-in PNG decoder which is likely to be much faster than the three.js RGBELoader.


I think i need some help with the convertion shader

I’m using functions from encodings_pars_fragment.glsl.js

void main() {

  vec4 texelColor = texture2D( tDiffuse, vUv );

  texelColor = RGBEToLinear(texelColor);

  texelColor = LinearToGamma(texelColor, 2.2);

  texelColor = LinearToRGBM(texelColor, 6.0);

  // color test
  // texelColor = RGBMToLinear(texelColor, 6.0);

  gl_FragColor = texelColor;

My fragment shader

the output of this shader is saved to .png

Result: https://codepen.io/matheowis/pen/XOGKzz

My guess is that values that i use for gammaFactor(2.2) and maxRange(6.0) are wrong.

Any suggestions?

// initial .hdr texture, texelColor is in RGBE space

// RGBE -> linear
texelColor = RGBEToLinear(texelColor);

// texelColor now in linear space 

// linear -> gamma 2.2
texelColor = LinearToGamma(texelColor, 2.2);

// texelColor is now in gamma 2.2 space

// linear -> RGBM (!!! expects texelColor to be in linear space !!! )
texelColor = LinearToRGBM(texelColor, 6.0);

So it looks like the LinearToGamma(texelColor, 2.2) transform is not needed.

Without LinearToGamma the result is full of artefacts, also 2nd link from @mrdoob suggested to convert space to gamma before RGBMEncoding function

2nd link from @mrdoob suggested to convert space to gamma before RGBMEncoding function

From the link:

I should also note that it is best to convert the colors from linear to gamma space before encoding. If you plan to use them again in linear a simple additional sqrt and square will work fine for encoding and decoding respectively.

Hmm, well the fact that the three.js function is called LinearToRGBM suggests that we are using them in linear space, so that would mean we need that additional sqrt for encoding.

You could try to use the function from that article rather than the three.js function?

float4 RGBMEncode( float3 color ) {
  float4 rgbm;
  color *= 1.0 / 6.0;
  rgbm.a = saturate( max( max( color.r, color.g ), max( color.b, 1e-6 ) ) );
  rgbm.a = ceil( rgbm.a * 255.0 ) / 255.0;
  rgbm.rgb = color / rgbm.a;
  return rgbm;

rather than

vec4 LinearToRGBM( in vec4 value, in float maxRange ) {
	float maxRGB = max( value.r, max( value.g, value.b ) );
	float M = clamp( maxRGB / maxRange, 0.0, 1.0 );
	M = ceil( M * 255.0 ) / 255.0;
	return vec4( value.rgb / ( M * maxRange ), M );

Or try modifying either function to put in the “additional sqrt”, wherever that is meant to go.

Awesome tool indeed!
A little bit off-topic but why should one prefer a cubemap over a spherical map? As far as I know cubemaps distort a view at certain angles.

Very useful and cool! :sunglasses: :muscle:
Thank you very much for building and sharing it! :raised_hands:

1 Like

Huge thanks! :smile:

1 Like

Wow, amazing! Thanks for your efforts!

1 Like

suppperrr gread work

1 Like

Some may be interested that kubi supports hdr files as well. It supports multiple output layouts and verious resampling methods.

Sorry, I don’t get the excitement, why convert a spherical map to a cube map and not use it the way it was meant for, while retaining its simplicity and compactness?

Cubemaps have a more even pixel density. So you can often get more detail and less distortion with lower resolution. Also cubemaps are not stored as a “cross” in vram but just the 6 faces. So I would say it is more compact.