24

There is a special way of mapping a cube to a sphere described here: http://mathproofs.blogspot.com/2005/07/mapping-cube-to-sphere.html

It is not your basic "normalize the point and you're done" approach and gives a much more evenly spaced mapping.

I've tried to do the inverse of the mapping going from sphere coords to cube coords and have been unable to come up the working equations. It's a rather complex system of equations with lots of square roots.

Any math geniuses want to take a crack at it?

Here's the equations in c++ code:

sx = x * sqrtf(1.0f - y * y * 0.5f - z * z * 0.5f + y * y * z * z / 3.0f);

sy = y * sqrtf(1.0f - z * z * 0.5f - x * x * 0.5f + z * z * x * x / 3.0f);

sz = z * sqrtf(1.0f - x * x * 0.5f - y * y * 0.5f + x * x * y * y / 3.0f);

sx,sy,sz are the sphere coords and x,y,z are the cube coords.

user1118321
  • 25,567
  • 4
  • 55
  • 86
petrocket
  • 1,458
  • 2
  • 14
  • 17
  • 2
    Possibly a question for MathOverflow (http://mathoverflow.net/)? – James B Apr 17 '10 at 01:48
  • 9
    @James: Probably not. Given the community, I bet they would find this both trivial and off-topic. – Stefan Kendall Apr 17 '10 at 01:49
  • Just in case, I posted there too. This is really important to my planetary program: http://petrocket.blogspot.com I need it to generate light maps and do basic collision detection with the terrain. – petrocket Apr 17 '10 at 01:55
  • I hate math without pictures! Dude you must draw geometrically the simple example first to see qualitatively what is going on there. Try a few sample coordinates like (1,0), (0, 1), (1,1), (0,0). Those might not work for the square might be centered at (0,0). It is good to think about that stuff. Perhaps you can spot an easier way ... because I feel like sphere to cube is just a projection of some sort. In case of a circle there are 4 if statements, and 4 simple operations. In case of the sphere I guess there would be 6 ... one must draw it out! Don't do math blindly. – Hamish Grubijan Apr 17 '10 at 03:33
  • Hamish: This is perfectly doable without drawing a picture! That said, thinking about it as a physical operation might help. – Andy Mikula Apr 17 '10 at 03:38
  • yeah, i kinda thought they might close it after I read the other questions posted there. – petrocket Apr 17 '10 at 13:17
  • 1
    If you look at the web page describing the procedure there are plenty of pictures: http://mathproofs.blogspot.com/2005/07/mapping-cube-to-sphere.html – petrocket Apr 17 '10 at 13:57

5 Answers5

15

I want to give gmatt credit for this because he's done a lot of the work. The only difference in our answers is the equation for x.

To do the inverse mapping from sphere to cube first determine the cube face the sphere point projects to. This step is simple - just find the component of the sphere vector with the greatest length like so:

// map the given unit sphere position to a unit cube position
void cubizePoint(Vector3& position) {
    double x,y,z;
    x = position.x;
    y = position.y;
    z = position.z;

    double fx, fy, fz;
    fx = fabsf(x);
    fy = fabsf(y);
    fz = fabsf(z);

    if (fy >= fx && fy >= fz) {
        if (y > 0) {
            // top face
            position.y = 1.0;
        }
        else {
            // bottom face
            position.y = -1.0;
        }
    }
    else if (fx >= fy && fx >= fz) {
        if (x > 0) {
            // right face
            position.x = 1.0;
        }
        else {
            // left face
            position.x = -1.0;
        }
    }
    else {
        if (z > 0) {
            // front face
            position.z = 1.0;
        }
        else {
            // back face
            position.z = -1.0;
        }
    }
}

For each face - take the remaining cube vector components denoted as s and t and solve for them using these equations, which are based on the remaining sphere vector components denoted as a and b:

s = sqrt(-sqrt((2 a^2-2 b^2-3)^2-24 a^2)+2 a^2-2 b^2+3)/sqrt(2)
t = sqrt(-sqrt((2 a^2-2 b^2-3)^2-24 a^2)-2 a^2+2 b^2+3)/sqrt(2)

You should see that the inner square root is used in both equations so only do that part once.

Here's the final function with the equations thrown in and checks for 0.0 and -0.0 and the code to properly set the sign of the cube component - it should be equal to the sign of the sphere component.

void cubizePoint2(Vector3& position)
{
    double x,y,z;
    x = position.x;
    y = position.y;
    z = position.z;

    double fx, fy, fz;
    fx = fabsf(x);
    fy = fabsf(y);
    fz = fabsf(z);

    const double inverseSqrt2 = 0.70710676908493042;

    if (fy >= fx && fy >= fz) {
        double a2 = x * x * 2.0;
        double b2 = z * z * 2.0;
        double inner = -a2 + b2 -3;
        double innersqrt = -sqrtf((inner * inner) - 12.0 * a2);

        if(x == 0.0 || x == -0.0) { 
            position.x = 0.0; 
        }
        else {
            position.x = sqrtf(innersqrt + a2 - b2 + 3.0) * inverseSqrt2;
        }

        if(z == 0.0 || z == -0.0) {
            position.z = 0.0;
        }
        else {
            position.z = sqrtf(innersqrt - a2 + b2 + 3.0) * inverseSqrt2;
        }

        if(position.x > 1.0) position.x = 1.0;
        if(position.z > 1.0) position.z = 1.0;

        if(x < 0) position.x = -position.x;
        if(z < 0) position.z = -position.z;

        if (y > 0) {
            // top face
            position.y = 1.0;
        }
        else {
            // bottom face
            position.y = -1.0;
        }
    }
    else if (fx >= fy && fx >= fz) {
        double a2 = y * y * 2.0;
        double b2 = z * z * 2.0;
        double inner = -a2 + b2 -3;
        double innersqrt = -sqrtf((inner * inner) - 12.0 * a2);

        if(y == 0.0 || y == -0.0) { 
            position.y = 0.0; 
        }
        else {
            position.y = sqrtf(innersqrt + a2 - b2 + 3.0) * inverseSqrt2;
        }

        if(z == 0.0 || z == -0.0) {
            position.z = 0.0;
        }
        else {
            position.z = sqrtf(innersqrt - a2 + b2 + 3.0) * inverseSqrt2;
        }

        if(position.y > 1.0) position.y = 1.0;
        if(position.z > 1.0) position.z = 1.0;

        if(y < 0) position.y = -position.y;
        if(z < 0) position.z = -position.z;

        if (x > 0) {
            // right face
            position.x = 1.0;
        }
        else {
            // left face
            position.x = -1.0;
        }
    }
    else {
        double a2 = x * x * 2.0;
        double b2 = y * y * 2.0;
        double inner = -a2 + b2 -3;
        double innersqrt = -sqrtf((inner * inner) - 12.0 * a2);

        if(x == 0.0 || x == -0.0) { 
            position.x = 0.0; 
        }
        else {
            position.x = sqrtf(innersqrt + a2 - b2 + 3.0) * inverseSqrt2;
        }

        if(y == 0.0 || y == -0.0) {
            position.y = 0.0;
        }
        else {
            position.y = sqrtf(innersqrt - a2 + b2 + 3.0) * inverseSqrt2;
        }

        if(position.x > 1.0) position.x = 1.0;
        if(position.y > 1.0) position.y = 1.0;

        if(x < 0) position.x = -position.x;
        if(y < 0) position.y = -position.y;

        if (z > 0) {
            // front face
            position.z = 1.0;
        }
        else {
            // back face
            position.z = -1.0;
        }
    }

So, this solution isn't nearly as pretty as the cube to sphere mapping, but it gets the job done!

Any suggestions to improve the efficiency or read ability of the code above are appreciated!

--- edit --- I should mention that I have tested this and so far in my tests the code appears correct with the results being accurate to at least the 7th decimal place. And that was from when I was using floats, it's probably more accurate now with doubles.

--- edit --- Here's an optimized glsl fragment shader version by Daniel to show that it doesn't have to be such a big scary function. Daniel uses this to filter sampling on cube maps! Great idea!

const float isqrt2 = 0.70710676908493042;

vec3 cubify(const in vec3 s)
{
float xx2 = s.x * s.x * 2.0;
float yy2 = s.y * s.y * 2.0;

vec2 v = vec2(xx2 – yy2, yy2 – xx2);

float ii = v.y – 3.0;
ii *= ii;

float isqrt = -sqrt(ii – 12.0 * xx2) + 3.0;

v = sqrt(v + isqrt);
v *= isqrt2;

return sign(s) * vec3(v, 1.0);
}

vec3 sphere2cube(const in vec3 sphere)
{
vec3 f = abs(sphere);

bool a = f.y >= f.x && f.y >= f.z;
bool b = f.x >= f.z;

return a ? cubify(sphere.xzy).xzy : b ? cubify(sphere.yzx).zxy : cubify(sphere);
}
petrocket
  • 1,458
  • 2
  • 14
  • 17
  • btw, the formula for `x` did you guess that it works and try it? Because Wolfram alpha spits out that ugly thing that I posted above. – ldog Apr 23 '10 at 16:57
  • actually, when I entered the system of 2 equations into wolfram alpha it gave me the two solutions for x and y that i posted. i also could recognize that the formula looked correct because i first solved the two dimensional version of the mapping so i had an idea for what to look for in the 3d version. thanks again for your help! i'm using it to generate light maps for my planet and do collision detection (vids here: http://www.youtube.com/user/petrocket) – petrocket Apr 28 '10 at 04:28
  • 1
    The C++ as written is performing double to single to double conversions. Slows it down and increases the error. fabsf and sqrtf – MB Reynolds May 23 '17 at 18:13
8

After some rearranging you can get the "nice" forms

(1)   1/2 z^2 = (alpha) / ( y^2 - x^2) + 1
(2)   1/2 y^2 = (beta)  / ( z^2 - x^2) + 1
(3)   1/2 x^2 = (gamma) / ( y^2 - z^2) + 1

where alpha = sx^2-sy^2 , beta = sx^2 - sz^2 and gamma = sz^2 - sy^2. Verify this yourself.

Now I neither have the motivation nor the time but from this point on its pretty straightforward to solve:

  1. Substitute (1) into (2). Rearrange (2) until you get a polynomial (root) equation of the form

    (4)    a(x) * y^4  + b(x) * y^2 + c(x) = 0
    

    this can be solved using the quadratic formula for y^2. Note that a(x),b(x),c(x) are some functions of x. The quadratic formula yields 2 roots for (4) which you will have to keep in mind.

  2. Using (1),(2),(4) figure out an expression for z^2 in terms of only x^2.

  3. Using (3) write out a polynomial root equation of the form:

    (5)    a * x^4  + b * x^2 + c = 0
    

    where a,b,c are not functions but constants. Solve this using the quadratic formula. In total you will have 2*2=4 possible solutions for x^2,y^2,z^2 pair meaning you will have 4*2=8 total solutions for possible x,y,z pairs satisfying these equations. Check conditions on each x,y,z pair and (hopefully) eliminate all but one (otherwise an inverse mapping does not exist.)

Good luck.

PS. It very well may be that the inverse mapping does not exist, think about the geometry: the sphere has surface area 4*pi*r^2 while the cube has surface area 6*d^2=6*(2r)^2=24r^2 so intuitively you have many more points on the cube that get mapped to the sphere. This means a many to one mapping, and any such mapping is not injective and hence is not bijective (i.e. the mapping has no inverse.) Sorry but I think you are out of luck.

----- edit --------------

if you follow the advice from MO, setting z=1 means you are looking at the solid square in the plane z=1.

Use your first two equations to solve for x,y, wolfram alpha gives the result:

x = (sqrt(6) s^2 sqrt(1/2 (sqrt((2 s^2-2 t^2-3)^2-24 t^2)+2 s^2-2 t^2-3)+3)-sqrt(6) t^2 sqrt(1/2 (sqrt((2 s^2-2 t^2-3)^2-24 t^2)+2 s^2-2 t^2-3)+3)-sqrt(3/2) sqrt((2 s^2-2 t^2-3)^2-24 t^2) sqrt(1/2 (sqrt((2 s^2-2 t^2-3)^2-24 t^2)+2 s^2-2 t^2-3)+3)+3 sqrt(3/2) sqrt(1/2 (sqrt((2 s^2-2 t^2-3)^2-24 t^2)+2 s^2-2 t^2-3)+3))/(6 s)

and

y = sqrt(-sqrt((2 s^2-2 t^2-3)^2-24 t^2)-2 s^2+2 t^2+3)/sqrt(2)

where above I use s=sx and t=sy, and I will use u=sz. Then you can use the third equation you have for u=sz. That is lets say that you want to map the top part of the sphere to the cube. Then for any 0 <= s,t <= 1 (where s,t are in the sphere's coordinate frame ) then the tuple (s,t,u) maps to (x,y,1) (here x,y are in the cubes coordinate frame.) The only thing left is for you to figure out what u is. You can figure this out by using s,t to solve for x,y then using x,y to solve for u.

Note that this will only map the top part of the cube to only the top plane of the cube z=1. You will have to do this for all 6 sides (x=1, y=1, z=0 ... etc ). I suggest using wolfram alpha to solve the resulting equations you get for each sub-case, because they will be as ugly or uglier as those above.

ldog
  • 11,707
  • 10
  • 54
  • 70
  • 1
    I don't know if the inverse mapping exists or not, but the idea that there are more points on the surface of a sphere than there are on the surface of a cube is bizarrely incorrect. – High Performance Mark Apr 17 '10 at 08:12
  • Thanks for the input gmatt - when I tried entering these equations into wolframalpha.com it did return several solutions (incorrect it seemed) and your answer gives me hope! – petrocket Apr 17 '10 at 13:30
  • @HPM: not sure you understood me correctly, in the response I never say sphere surface has more points, I say the opposite. I think OP wants to find the mapping from sphere to cube. – ldog Apr 17 '10 at 13:51
  • 1
    @petrorocket: I put some more though into solving this in the morning today. I realized there may be a snag in step 2. The solution to eq (4) will give you y^2 in an "ugly" form with a square root. Some miracle would have to happen to get rid of that square root haha. – ldog Apr 17 '10 at 13:53
  • 2
    @gmatt: The very notion of more or fewer points doesn't work with infinites. Consider that there exist bijections between the integers and the rationals. With uncountable infinites, like the points in a surface area, the situation is even trickier. e.g., f(x) = 2x gives you a bijection between [0, 1] and [0, 2], even though the intervals have different lengths. – Rob Lachlan Apr 17 '10 at 17:59
  • @gmatt: I verified your initial equations and have been trying to work out the polynomial equation in step 1 but haven't gotten it in the form you suggest yet. Say, even if there are several possible solutions for the inverse mapping, shouldn't I be able to intuitively rule out the undesirable ones? – petrocket Apr 17 '10 at 18:20
  • one of the math gurus at mathoverflow mentioned this approach: "As for the inverse, it's just a mess of radicals. Fix z=1 and solve for x and y squared if you insist on such a formula" – Leonid Kovalev – petrocket Apr 17 '10 at 18:49
  • @Rob Lachlan: indeed I am not unaware of arguments such as those (my undergrad was math :) ). I was trying to construct an intuitive argument, but you rightly shot it down. Very well may be a bijection, something along the lines of the banach paradox http://en.wikipedia.org/wiki/Banach%E2%80%93Tarski_paradox – ldog Apr 18 '10 at 02:34
  • @petrocket: if you fix z = 1 then solving for x and y squared becomes much easier. what it means is that you are calulating the inverse mapping of the level set (x,y,1) for x,y reals and z = 1, that is to say (more concretely) a slice of your sphere where it intersects the plane z = 1 (i.e. a circle.) So then if you want to construct the full inverse you may be able to look to see what happnes as you move z around (instead of z=1, let z be any number between 0 and 1.) That may be an excellent starting point for solving for the inverse. – ldog Apr 18 '10 at 02:45
  • @petrocket: also pertaining your question about if there exists more than one viable solution, the problem that arises is that then you can not define a mapping, because a mapping requires 1 input to map to 1 output, whereas you would have 1 input mapping to N outputs. You wouldn't have a mapping per se, but you may still be able to deal with it on a point by point basis. – ldog Apr 18 '10 at 02:50
  • @gmatt: skiing!? I'm so jealous. Thanks for your invaluable input again! I did sub 1 in for z and then substituted equation 1 into equation 2 and took the resulting equation - which no longer had x or z terms - and put that into wolfram alpha and it gave me this: http://www.wolframalpha.com/input/?i=solve+y^4+%2B+y^2+-+2g+-+2a+%2B+2ay^2+-2+%3D+0+for+y There are 4 solutions there to check which I haven't had time to check yet, but that's my next step. – petrocket Apr 18 '10 at 03:38
  • @petrocket: I just realized I said that your level sets are circles, but in fact it should be the intersection of the cube with the plane z=1, i.e. a solid square. Sorry about that. – ldog Apr 19 '10 at 01:16
  • @gmatt: yeah i figured that's what you meant. unfortunately, the solution for even just x and y for one cube face seems pretty intense. I'm still working on it though! – petrocket Apr 19 '10 at 02:51
  • @gmatt: I made some more progress. I went back and noticed that for the 2 dimensional mapping of a circle to a square for sx values from -1/sqrt(2) to 1/sqrt(2) - or basically 45 degrees left or right of the y axis the following equation maps from the circle to the square: x = sqrt(2) * sx for the bottom square the mapping is the same and for the left and right sides of the square you use y = sqrt(2) * sy since you know x is -1 or 1. For the cube/sphere mapping I've determined x = sqrt(2) * sx only works when y = 0 and z = 1 or -1. I must next determine how to work y into the 3d version. – petrocket Apr 19 '10 at 04:54
  • @gmatt Think I got a solution finally. Will try to post it as a separate answer tomorrow. It's not pretty. – petrocket Apr 22 '10 at 04:09
  • @gmatt Wow. Just finished testing a possible solution and saw your edit. My version is similar only my equation for x is different assuming face is +z or -z x = sqrt(-sqrt(-2s^2 + 2t^2 - 3)^2 - 24s^2) + 2s^2 - 2t^2 +3)/sqrt(2) and y = sqrt(-sqrt(-2s^2 + 2t^2 - 3)^2 - 24s^2) - 2s^2 + 2t^2 +3)/sqrt(2) Be sure to apply the sign of s and t b to x and y after the equations. So far in my testing this solution works - and it is encouraging to see that only one of our equations differs. It is late at night so my second equation might have a hole in it (I got used wolfram too) – petrocket Apr 22 '10 at 04:19
  • This is what i plugged into wolfram to get the equations for the solution I tested: a^2 = x^2(1/2 - y^2/2 + y^2/3), b^2 = y^2(1/2 - x^2/2+x^2/3) Naturally, not all the solutions are valid. – petrocket Apr 22 '10 at 04:22
3

This answer contains the cube2sphere and sphere2cube without the restriction of a = 1. So the cube has side 2a from -a to a and the radius of the sphere is a.

I know it's been 10 years since this question was asked. Nevertheless, I am giving the answer in case someone needs it. The implementation is in Python,

I am using (x, y, z) for the cube coordinates, (p, q, r) for the sphere coordinates and the relevant underscore variables (x_, y_, z_) meaning they have been produced by using the inverse function.

import math
from random import randint # for testing

def sign_aux(x):
    return lambda y: math.copysign(x, y)

sign = sign_aux(1) # no built-in sign function in python, I know...

def cube2sphere(x, y, z):
    if (all([x == 0, y == 0, z == 0])):
        return 0, 0, 0

    def aux(x, y_2, z_2, a, a_2):
        return x * math.sqrt(a_2 - y_2/2 - z_2/2 + y_2*z_2/(3*a_2))/a
    
    x_2 = x*x
    y_2 = y*y
    z_2 = z*z
    a = max(abs(x), abs(y), abs(z))
    a_2 = a*a

    return aux(x, y_2, z_2, a, a_2), aux(y, x_2, z_2, a, a_2), aux(z, x_2, y_2, a, a_2)

def sphere2cube(p, q, r):
    if (all([p == 0, q == 0, r == 0])):
        return 0, 0, 0
    def aux(s, t, radius):
        A = 3*radius*radius
        R = 2*(s*s - t*t)
        S = math.sqrt( max(0, (A+R)*(A+R) - 8*A*s*s) )  # use max 0 for accuraccy error
        iot = math.sqrt(2)/2
        s_ = sign(s) * iot * math.sqrt(max(0, A + R - S)) # use max 0 for accuraccy error
        t_ = sign(t) * iot * math.sqrt(max(0, A - R - S)) # use max 0 for accuraccy error
        return s_, t_
    
    norm_p, norm_q, norm_r = abs(p), abs(q), abs(r)
    norm_max = max(norm_p, norm_q, norm_r)
    radius = math.sqrt(p*p + q*q + r*r)
    if (norm_max == norm_p):
        y, z = aux(q, r, radius)
        x = sign(p) * radius
        return x, y, z
    if (norm_max == norm_q):
        z, x = aux(r, p, radius)
        y = sign(q) * radius
        return x, y, z
    x, y = aux(p, q, radius)
    z = sign(r) * radius
    return x, y, z

# measuring accuracy

max_mse = 0
for i in range(100000):
    x = randint(-20, 20)
    y = randint(-20, 20)
    z = randint(-20, 20)
    p, q, r = cube2sphere(x, y, z)
    x_, y_, z_ = sphere2cube(p, q, r)
    max_mse = max(max_mse, math.sqrt(((x-x_)**2 + (y-y_)**2 + (z-z_)**2))/3)
print(max_mse)

# 1.1239159602905078e-07

max_mse = 0
for i in range(100000):
    p = randint(-20, 20)
    q = randint(-20, 20)
    r = randint(-20, 20)
    x, y, z = sphere2cube(p, q, r)
    p_, q_, r_ = cube2sphere(x, y, z)
    max_mse = max(max_mse, math.sqrt(((p-p_)**2 + (q-q_)**2 + (r-r_)**2))/3)
print(max_mse)

# 9.832883321715792e-08

Also, I mapped some points to check the function visually and these are the results.

enter image description here

enter image description here

entropyfeverone
  • 1,052
  • 2
  • 8
  • 20
1

Here's one way you can think about it: for a given point P in the sphere, take the segment that starts at the origin, passes through P, and ends at the surface of the cube. Let L be the length of this segment. Now all you need to do is multiply P by L; this is equivalent to mapping ||P|| from the interval [0, 1] to the interval [0, L]. This mapping should be one-to-one - every point in the sphere goes to a unique point in the cube (and points on the surface stay on the surface). Note that this is assuming a unit sphere and cube; the idea should hold elsewhere, you'll just have a few scale factors involved.

I've glossed over the hard part (finding the segment), but this is a standard raycasting problem. There are some links here that explain how to compute this for an arbitrary ray versus axis-aligned bounding box; you can probably simplify things since your ray starts at the origin and goes to the unit cube. If you need help simplify the equations, let me know and I'll take a stab at it.

celion
  • 3,864
  • 25
  • 19
0

It looks like there is a much cleaner solution if you're not afraid of trig and pi, not sure if it's faster/comparable though.

Just take the remaining components after determining the face and do:

u = asin ( x ) / half_pi
v = asin ( y ) / half_pi

This is an intuitive leap, but this seems to back it up ( though not exactly the same topic ), so please correct me if I'm wrong.

I'm too lazy to post an illustration that explains why. :D

Nolo
  • 846
  • 9
  • 19