1

This is haunting me since several weeks, I didn’t end my researches on it because I’m currently overloaded and it’s keeping me in behind the first-year CS (opengl) college lessons that first made me research this: how to draw all the faces of a cube with only one for loop.

The exercise said us to list the coordinates of all the vertices of a square, then of a cube, then to draw one. I quickly noticed since it was a cube and all lengths were equal this mapped to binary. I noticed the square vertices coordinates naturally mapped to gray code which I studied formerly in digital electronics in first semester. I wondered if it was doing something for the cube and it in fact was going through all contiguuous vertices, what I then found to be called a hamiltonian path or an eulerian cycle (I’m not sure, I saw that linked to the snake in a box problem on these pages, including gray code), but it didn’t allow me to compute each (preferably contiguous, so I can use GL_QUADS or even GL_QUAD_STRIP) face in order to factorize my drawing of the cube (though it should allow me to compute a — I guess — specially-ordered array of all lines, maybe then from there I can compute each face?).

I also tried to do it with opengl transformations, as we kept going in the lesson learning them, as I guess it may defer more calculations to the gpu instead of the cpu, and almost succeeded, yet my for loop takes 8 iterations so there are 2 useless hidden faces somewhere:

void
face (void)
{
  glPushMatrix();
  {
    glTranslatef(0, 0, 0.5);
    glRectf(-0.5, -0.5, 0.5, 0.5);
  }
  glPopMatrix();
}

void
cube (void)
{
  for (unsigned int i=0; i < 8 ; ++i)
    {
      glColor3f(!(i&4), !(i&2), !(i&1));
      face();
      glRotatef(180, !!(i&4), !!(i&2), !!(i&1));
    }
}

I’m somewhat trying to continue my researches, though I’m lacking time and usually-finally-failing ideas keep popping in my mind regularly about this. And I now use to loose more than 1 hour on continuing this each time I’m being ask to draw a cube for something.

Something that thrilled me also is that if I succeed to factorize the procedurale computation of a cube, that might be abstracted from the notion of dimension and then generalizing the same algorithm to the nth dimension should be easy, thus giving an easy, natural and simple way to draw a tesseract or any hypercube… That match a long way of experiment I regularly go through of trying to generalize everything that’s got to be given to me at the nth-dimension (first did that when they asked to draw a spiral in 2D).

PS: for everything related to maths of dimensional space, n-dimensions, distribution of vertices, lines, faces in an ordered way, and relation to gray code, should I also ask on math.stackexchange.com?

Rabbid76
  • 202,892
  • 27
  • 131
  • 174
galex-713
  • 165
  • 6
  • in 4D and ND it is much harder as there are different simplexes and more degrees of freedom see [4D rendering techniques](https://stackoverflow.com/a/44970550/2521214) – Spektre Apr 06 '18 at 07:00
  • but isn’t there a method of drawing a cube that would, with, for instance, a slider or any input, naturally generalize to a way to draw a projection or rather a slice of an hypercube? or that would as well, changing the appropriate primitives, draw 0, 1 and 2-cubes (vertex, line, and square)? – galex-713 Apr 06 '18 at 14:48
  • 1
    @galex-713 you can still use triangle faces for 4D and ND so the Rabbid76's answer still applies you just need to figure out the rotation planes in higher dimensions (yes planes instead of axises) but once you start slicing (cross sections) you need proper simplex which is tetrahedron (4 points, 4 faces) in 4D and IIRC you add one point to simplex per dimension in ND. You still might generalize but it is really hard to imagine it... so easier is to generate the points and use convex hull instead. Btw Add `@nick` to comment to notify user `nick` ... – Spektre Apr 07 '18 at 05:55
  • @Spektre: actually I prefer the transformation-less method for ND, since I didn’t know how it would look to use ND transformations, and these are 3D anyway. For ND I did more look at a solution directly drawing the faces/edges/vertices/etc. at each correct coordinate without translations. That way Isuppose it’ll just consist in generating a set of arrays 4 coordinates… right? – galex-713 Apr 07 '18 at 14:28
  • Anyway I’m always interested in a solution without transformations. Not only because it could be unrolled and then use constant-propagation for more static (and maybe more fast?) execution. Also would it be relevant, after a few days, if no other answers did arise, to accept @Spektre's answer and make another question, more specific, asking for a solution without transformations (even if I would still be going to use Spektre's method, practically)? – galex-713 Apr 07 '18 at 14:35
  • @galex-713 The rotations in 4D are like this: [How to use 4d rotors](https://stackoverflow.com/a/45116079/2521214) and can be easily ported to any higher dimensions. Solution without rotation would most likely be extremly complicated and very hard to debug. On the other hand `90deg` rotation matrices are just ones and zeros so it can be hardcoded even without using any multiplication ... – Spektre Apr 09 '18 at 06:27
  • @Spektre: are you sure using grey code and iterating through dimensions would be that hard to calculate the direct coordinate of each vertex? I mean I can get the coordinates of each vertex and a hamiltonian path between all them with just grey code, I should find a way to figure out how to draw edges this way then, and thereafter generalizing to all successive dimensions wouldn’t be that hard if grey code works for all dimensions: it does for dimension 1, 2 and 3, why wouldn’t it for the others? – galex-713 Apr 10 '18 at 08:31
  • @galex-713 I managed to make 3D cube without rotations now struggling on 4D – Spektre Apr 10 '18 at 09:27
  • @Spektre really? :D how? I’d like to continue my research on this too but I’m a bit short of time so I’d like some resources like your method so I can explore too, or at least understand&research&learn :) – galex-713 Apr 10 '18 at 13:59
  • @galex-713 I got also ND figured out but my projections are not working correctly and can not verify it visually. Weird the same stuff works on my 4D engine it must be some silly bug I do not see now (maybe tomorrow I will spot it). Once I figure it out will post update code in my answer I am creating right now. – Spektre Apr 10 '18 at 14:46

2 Answers2

3

The 6 sides of the cube can be generated by alternating 90 degree rotations around the x- and y-axis:

+---+
| 1 |
+---+---+
| 2 | 3 |
+---+---+---+
    | 4 | 5 |
    +---+---+
        | 6 |
        +---+

This can be coded like this:

for( int i=0; i<6; ++i)
{ 
    glColor3f(!(i&4), !(i&2), !(i&1));
    face();
    glRotatef(90.0f, (float)(i%2), (float)(1-(i%2)), 0.0f);
}

Preview:

void face (void)
{
    glPushMatrix();
    glTranslatef(0, 0, 0.5);
    glRectf(-0.3, -0.3, 0.3, 0.3);
    glPopMatrix();
}

cube



The following gives a similar result. It is not the same as the above one, because the sides of the cube are twisted around their normal vectors, too.

glRotatef(180.0f, (float)(i%2), (float)(1-(i%2)), 1.0f);
Rabbid76
  • 202,892
  • 27
  • 131
  • 174
  • wow, amazing, it works (you just missed a space between int and i)! :D but why (also why mine isn’t working? why is it nonetheless partially working? why the two useless faces)? why is z-rotation always equal to 0 and you rotate around only two alternate axis? is this generalizable to nth dimension? how about without transforms? anyway I’m going to use your solution pretty everywhere now ^^ unless something more beautiful pops out – galex-713 Apr 05 '18 at 22:53
  • Noticing this match the net of a cube, would that work for any cube net (where you replace the translate operation with a rotate one in order to draw the thing you want to draw rather than its net)? would that work for other dimensions? – galex-713 Apr 06 '18 at 14:51
  • never mind, according @Spektre ND with transformations is just going to be more complex to make or at least to imagine, involving rotations around planes, etc. For ND I’d prefer without transforms, but if you only have you transformation method, I’m still curious about if z-axis being always 0 means something, the net thing, and the problems (and non-problems) of my initial try ^^ – galex-713 Apr 07 '18 at 14:31
  • @galex-713 A rotation around the x-axis and y-axis, tilts the object horizontal respectively vertical, but a rotation around the z-axis rolls the object. So why rotating around the z-axis? – Rabbid76 Apr 07 '18 at 16:27
  • For instance so that direction of x and y axis are changed in current or next transformation. – galex-713 Apr 07 '18 at 17:38
  • @galex-713 But then you have to do 2 rotation operations in a row. First around one axis and then around the other axis. You cannot setup a rotation around 2 axis at once by the function signature of `glRotatef`. – Rabbid76 Apr 07 '18 at 17:45
  • except when you do one rotation around an non-pure axis like I did in my partially-failed first attempt, and if that rotation is 180 degrees instead of 90 you don’t get weird issues. – galex-713 Apr 07 '18 at 19:56
  • @galex-713 I have no clue what you want to know further, or how I can help you. But `glRotatef(180.0f, (float)(i%2), (float)(1-(i%2)), 1.0f);` gives a similar resut. Note, it's not the same, because the sides rotated around its normal vectors, too. – Rabbid76 Apr 07 '18 at 20:22
1

This is what I come up with in C++:

//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
#include "list.h"
//---------------------------------------------------------------------------
//--- ND Camera -------------------------------------------------------------
//---------------------------------------------------------------------------
const int ND = 4;           // dimensions
double focal_length=2.5;    // camera focal length (camera ais axis aligned)
double focus[ND];           // camera position (viewing in -Z direction)
double* perspective3D(double *_p)   // camera perspective projection of ND point p to 3D <-1,+1> position on xy plane (Z=0)
    {
    int i,j;
    double p[ND],w,dw;
    static double q[3]={0.0,0.0,0.0};
    // relative to camera
    for (i=0;i<ND;i++) p[i]=_p[i]-focus[i];
    // perspective projection
    for (w=1.0,i=ND-1;i>=2;i--)
        {
        if (p[i]<=1e-10)
         { for (i=0;i<3;i++) q[i]=0; return q; } // behind camera
        w*=focal_length/p[i];
        }
    for (j=0;j<2;j++) p[j]*=w;

    // conversion to double[3]=(x,y,0.0)
    for (i=0;(i<2)&&(i<ND);i++) q[i]=p[i]; q[2]=0.0;
    return q;
    }
//---------------------------------------------------------------------------
//--- ND Quad mesh ----------------------------------------------------------
//---------------------------------------------------------------------------
struct _quad        // Quad face
    {
    double p[4][ND];    // points
    _quad(){}
    _quad(_quad& a) { *this=a; }
    ~_quad(){}
    _quad* operator = (const _quad *a) { *this=*a; return this; }
    //_quad* operator = (const _quad &a) { ...copy... return this; }
    };
//---------------------------------------------------------------------------
void quad_hypercube(List<_quad> &quad,double a) // quad[] = hypercube (2a)^ND
    {
    if (ND<2) return;
    int i0,i1,j,k;
    double p[ND];
    _quad q;
    // set camera position and FOV
    for (j=0;j<ND;j++) focus[j]=0.0;
    for (j=2;j<ND;j++) focus[j]=-10.0*a/focal_length;   // perspective projected axises should have offset
    // clear faces
    quad.num=0;
    // perspective debug
    double z,w;
    #define add_quad(z,w) { q.p[0][0]=-a; q.p[0][1]=-a; q.p[0][2]=z; q.p[0][3]=w; q.p[1][0]=+a; q.p[1][1]=-a; q.p[1][2]=z; q.p[1][3]=w; q.p[2][0]=+a; q.p[2][1]=+a; q.p[2][2]=z; q.p[2][3]=w; q.p[3][0]=-a; q.p[3][1]=+a; q.p[3][2]=z; q.p[3][3]=w; quad.add(q); }

    // iterate through all axis aligned i[0]i1 planes combinations
    for (i0=0     ;i0<ND;i0++)
     for (i1=i0+1;i1<ND;i1++)
        {
        // start offset
        for (j=0;j<ND;j++) p[j]=-a; p[i0]=0; p[i1]=0;
        // iterate all offset combinations
        for (;;)
            {
            // add face
            for (j=0;j<ND;j++) q.p[0][j]=p[j]; q.p[0][i0]-=a; q.p[0][i1]-=a;
            for (j=0;j<ND;j++) q.p[1][j]=p[j]; q.p[1][i0]+=a; q.p[1][i1]-=a;
            for (j=0;j<ND;j++) q.p[2][j]=p[j]; q.p[2][i0]+=a; q.p[2][i1]+=a;
            for (j=0;j<ND;j++) q.p[3][j]=p[j]; q.p[3][i0]-=a; q.p[3][i1]+=a;
            quad.add(q);
            if (ND<=2) break;
            // increment offset
            for (k=0;;)
                {
                // first unused axis
                while((k==i0)||(k==i1)) k++;
                if (k>=ND) break;
                p[k]=-p[k];
                if (p[k]<0.0) k++;
                else break;
                }
            if (k>=ND) break;
            }
        }
    }
//---------------------------------------------------------------------------
void quad_draw(List<_quad> &quad)
    {
    for (int i=0;i<quad.num;i++)
        {
        glBegin(GL_QUADS);
        for (int j=0;j<4;j++)
         glVertex3dv(perspective3D(quad.dat[i].p[j]));
        glEnd();
        }
    }
//---------------------------------------------------------------------------

to make this simple I used mine dynamic list template so:


List<double> xxx; is the same as double xxx[];
xxx.add(5); adds 5 to end of the list
xxx[7] access array element (safe)
xxx.dat[7] access array element (unsafe but fast direct access)
xxx.num is the actual used size of the array
xxx.reset() clears the array and set xxx.num=0
xxx.allocate(100) preallocate space for 100 items

usage:

// global storage
List<_quad> quad;

// mesh init
quad_hypercube(quad,0.5);

// render
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
// directional (normal shading)
float lightAmbient  [4]={0.50,0.50,0.50,1.00};      // rozptylene nesmerove
float lightDiffuse  [4]={0.50,0.50,0.50,1.00};      // smerove
float lightDirection[4]={0.00,0.00,+1.0,0.00};      // global smer svetla w=0
glLightfv(GL_LIGHT0,GL_AMBIENT ,lightAmbient );
glLightfv(GL_LIGHT0,GL_DIFFUSE ,lightDiffuse );
glLightfv(GL_LIGHT0,GL_POSITION,lightDirection);
glLightModeli(GL_LIGHT_MODEL_TWO_SIDE, GL_FALSE);
glEnable(GL_COLOR_MATERIAL);
glEnable(GL_LIGHT0);
glDisable(GL_LIGHTING);
glDepthFunc(GL_LEQUAL);
glDisable(GL_CULL_FACE);
glDisable(GL_TEXTURE_2D);   

glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
glLineWidth(2.0);
glColor3f(0.5,0.5,0.5);
quad_draw(quad);
glLineWidth(1.0);
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);

glFlush();
SwapBuffers(hdc);

Here results:

hypercubes

As you can see for 4D+ there is some inconsistency but the generated coordinates looks OK so it is most likely some problem with my projection from ND to 2D somewhere (will investigate latter on) as the internal cubes (due to possibly wrong perspective) merges visually with the back face of the other cube. Most likely I would need to change the FOV of camera on per axis basis.

How it works

simply each face of the hypercube is axis aligned QUAD parallel to base plane. So the i0,i1 for loops generate all possible base planes in which I encode the 2D QUAD coordinates.

Then I generate all the combinations of +/-a for the other axises and add QUAD face for each combination. That is all.

To be more clear let assume 3D cube and plane face base plane xy. So i0=0; i1=1; and each face in cube parallel to xy will have the encoded 4 points x,y coordinates encoded by your gray code:

p0(-a,-a,?)
p1(-a,+a,?)
p2(+a,+a,?)
p3(+a,-a,?)

Now in 3D there is just one axis left (z) so for its each combination of -a and +a generate new face so:

p0(-a,-a,-a) // xy face 1
p1(-a,+a,-a)
p2(+a,+a,-a)
p3(+a,-a,-a)

p0(-a,-a,+a) // xy face 2
p1(-a,+a,+a)
p2(+a,+a,+a)
p3(+a,-a,+a)

When done move to next plane i0,i1 combination which is xz

p0(-a,?,-a)
p1(-a,?,+a)
p2(+a,?,+a)
p3(+a,?,-a)

and then generate the combinations again ...

p0(-a,-a,-a) // xz face 1
p1(-a,-a,+a)
p2(+a,-a,+a)
p3(+a,-a,-a)

p0(-a,+a,-a) // xz face 2
p1(-a,+a,+a)
p2(+a,+a,+a)
p3(+a,+a,-a)

and continue until no base plane combinations are left...

In 3D there are 3 base planes (xy,xz,yz) and 1 remainding axsis so 2^1 = 2 parallel faces per plane so 3*2 = 6 faces together.

In 4D you got 6 base planes (xy,xz,xw,yz,yw,zw) and 2 remainding axises give 4 combinations of +/-a meaning 2^2 = 4 parallel faces per base plane leading to 6*4 = 24 faces.

Spektre
  • 49,595
  • 11
  • 110
  • 380
  • What is Perspective3D and, yet its name, why does it need to act of the mesh points position? – galex-713 Apr 10 '18 at 20:14
  • why `for (;;;)` instead of `while (true)`? does it have a different meaning (in terms of expressivity, not programmatically) – galex-713 Apr 10 '18 at 20:35
  • I’m not sure to be going to end understanding everything, do you have, in addition, your not ND version, not using transformations/computing coordinates, so I can compare – galex-713 Apr 10 '18 at 20:40
  • @galex-713 if you read the comments in the code you would know that Perspective3D just converts ND point into 3D (2D really as Z=0.0) with perspective projection and it is used just for rendering (as OpenGL does not know how to handle 4D,5D etc... and using just 3D without perspective would project all into simple 3D Cube). `i0,i1` are basis axises for the face (3D: xy,xz,yz 4D: xy,xz,xw,yz,yw,zw ) where the hardcoded QUAD pattern is encoded and all the other axises are used for all combinations of `+/-a` to place the quad to all its parallel positions on hypercube. – Spektre Apr 10 '18 at 22:53
  • @galex-713 code style formating and preference in mine code is mine to decide. If you want using `while` in your code instead then do that is your choice. Iterators names `i,j,k` are the usual ones also the `i0,i1,i2`,... so I would not break my head due to naming conventions as the code is really simple and they do not have any other hidden meaning. – Spektre Apr 10 '18 at 22:58
  • @galex-713 added small example that should make it more clear – Spektre Apr 10 '18 at 23:14
  • so you just always generally use `for (;;;)` instead of `while (true)`? it doesn't mean something I could understand? – galex-713 Apr 11 '18 at 11:17
  • @galex-713 yep it is just "infinite" loop ... it has less characters to type and I use `for` more often as it has all the iterations in one place (not this case though) I use `while` only rarely. also it is `for(;;)` not `for(;;;)` that one is nasty and can crash my **BDS2006 C++** compiler IDE due to compiler bug :) – Spektre Apr 11 '18 at 11:20
  • Just to say, I think I understood how some perspective or slicing processing is needed in order to represent ND data, I realize it’s then more complex than I thought, though it can be maybe modularized and thinked about apart (however I’ll be unable to profit really long of your code as I don’t really like, then use, C++), yet I’m still interested in how to compute each vertex N-vector, and associated edges and faces for these figures ^^ – galex-713 Apr 12 '18 at 13:42