Skip to main content
gamedev.net
🔒 Locked

Menger Sponge Intersection

Started by Geometrian May 9, 2009 at 9:56 PM 13 replies 5.2k views
Original Post
Geometrian
Geometrian
Hi, I've spent waaaaaay too long trying to solve this. I need help. An n-level Menger sponge is contained within a unit cube centered and oriented axis-wise at the origin. Given an arbitrary point on the cube and a ray from that point passing through the cube, what is the surface normal of the first surface on the Menger Sponge hit? I really need help with this, Thanks, -Geometrian
[size="1"]And a Unix user said rm -rf *.* and all was null and void...|There's no place like 127.0.0.1|The Application "Programmer" has unexpectedly quit. An error of type A.M. has occurred.
[size="2"]
erissian
erissian
What is this for? Smells like homework.
We''re sorry, but you don''t have the clearance to read this post. Please exit your browser at this time. (Code 23)
Geometrian
Geometrian
No, my own enjoyment. I'm in statistics; I've already had geometry.

The idea is to make a real-time Menger Sponge fractal. By just drawing a cube, and then messing with the appearance of the faces, an extremely complex fractal can be achieved. I got it working in 2D (discard the fragments that are "holes". It's nearly invisible). Alternately, the "holes" can be colored. The 2D result is easily realtime (50fps) at n = 40 (in 3D, that would be 1.099511627776e+52 mini cubes). Here's n = 5 with the holes colored red:


Despite my success in 2D, I'm having trouble generalizing it to 3D. Instead of discarding the fragments (or coloring them red) that are "holes", the ray from the camera needs to be extended to the next surface of the sponge. The normal of that surface is used to calculate the lighting, and so the color of the pixel.

What's the algorithm for that?
Thanks,
-G
[size="1"]And a Unix user said rm -rf *.* and all was null and void...|There's no place like 127.0.0.1|The Application "Programmer" has unexpectedly quit. An error of type A.M. has occurred.
[size="2"]
erissian
erissian
Given the origin of the ray P, and the direction of the ray Q, you have to solve X = P+Qt, minimizing t, where t > 0, and X ∈ Mn.

Do you have a function to test whether X is in a cube? If so, the simplest route may be similar to a line drawing algorithm. That is, advancing through a discrete space whose divisions fall along P+Qt until the first instance where X is in a cube - at which point the surface normal would be found with a nearest-face test. If you go through the entire line without finding an X ∈ Mn, then you have a clear view through the sponge.
We''re sorry, but you don''t have the clearance to read this post. Please exit your browser at this time. (Code 23)
Geometrian
Geometrian
That was basically my attempt that failed. I think it's the right way to go, though.

It shouldn't be necessary to have a small step size (small being able to traverse exactly one of the smallest units) for all things. For example, looking directly through the middle of a level 3 cube means all the rays that don't hit anything in the middle have to do 27 steps instead of 3 (one for each large cube removed in level 1). I think this is where I ran into problems. Ideas?
[size="1"]And a Unix user said rm -rf *.* and all was null and void...|There's no place like 127.0.0.1|The Application "Programmer" has unexpectedly quit. An error of type A.M. has occurred.
[size="2"]
Vorpy
Vorpy
Treat the sponge like an octtree, except that instead of dividing each cell 8 ways it is divided 27 ways. For a given ray intersecting a non-empty cell, do the collision against its 27 children and recursively process the non-emtpy children it hits in the order that it hits them until you find the first intersection with a non-empty leaf cell. Each recursive step returns a boolean indicating if a non-emtpy cell was hit, and if so, the location and normal of the hit.

Now you just need to perform the intersections between the ray and the grid of 27 cells efficiently.
Geometrian
Geometrian
Alrighty. Any suggestions on a good way to test a ray against a level-1 menger sponge (fractal iterator)? Basically, it would need to know if the ray went through the sponge, or hit it, and if so, where.
[size="1"]And a Unix user said rm -rf *.* and all was null and void...|There's no place like 127.0.0.1|The Application "Programmer" has unexpectedly quit. An error of type A.M. has occurred.
[size="2"]
Maze Master
Maze Master
I think an exact algorithm will be unstable. Consider rays close to the one you are interested in, but with slightly perturbed angles. These perturbed rays make a cone, with the actual ray at the center. Because of the fractal nature of the object, nomatter how small the perturbation is, the cone could hit in infinite number of faces oriented in different ways.
Geometrian
Geometrian
I agree. If you think about a single ray, even, you can see that if it hits the sponge (not passing through a face centered hole exactly) it will never hit a side. Hence, level-n, instead of level-∞
[size="1"]And a Unix user said rm -rf *.* and all was null and void...|There's no place like 127.0.0.1|The Application "Programmer" has unexpectedly quit. An error of type A.M. has occurred.
[size="2"]
luca-deltodesco
luca-deltodesco
i've implemented the proposed solution extending an octree to be split in 3 along each axis, i used a more complex traversal algorithm than the one stated though, mine need never 'compare' t values for each sub-cube and steps through those tree nodes that are being intersected, rather than every child of a node, it seems to work pretty well (and is pretty much just as fast as ray tracing against a single solid cube), i havn't got any normal detection yet, but it's nothing complex to add, atm it colours the pixel based on the distance travelled along the ray to the intersection.

Geometrian
Geometrian
Wow! This is really great! I'm glad my idea of just modifying the look of a cube works. I tried the adapted-octree method but it didn't work--I ran into problems when the ray went from a higher n hole to a lower one, such as what occurs on the edges. Can I see the code?
[size="1"]And a Unix user said rm -rf *.* and all was null and void...|There's no place like 127.0.0.1|The Application "Programmer" has unexpectedly quit. An error of type A.M. has occurred.
[size="2"]
luca-deltodesco
luca-deltodesco
mine 'does' use the adapted octree if you read my post properly :P

I have found quite a big problem with my solution though, and that is that it's pretty much impossible to render anything above a 5th iteration menger sponge, because it just requires FAR FAR too much memory to pre-generate the menger cube, so i'm going to see if i can rework it to not require it to be pre-generated.

I'm not even sure if it'll be any slower, yes it'll be doing more processing work, but it's not going to have to wade through gigabytes of data so i will probably gain there.

[Edited by - luca-deltodesco on May 17, 2009 5:58:40 PM]
luca-deltodesco
luca-deltodesco
the adaption to the algorithm actually turned out to be neater :P

first has 6 iterations, second 7 iterations, both have 16 samples per pixel (although even that isn't enough it seems to get fully smooth rendering in the 7 iteration one)





source:

main.cpp
#include <dgl/dgl.hpp> //my own library for window management etc etc etc - not important#include <dgl/dgli.hpp>#include "menger.hpp"void cb_main();void cb_mdown(int but);/*************************************************************************************************************************/Menger::Sponge sponge;int main(){    //init window    dgl::WINDOW_STYLE style;    style.width = 800;    style.height = 600;    style.xPos = 50;    style.yPos = 50;    style.name = "Menger Cube";    style.fullscreen = false;    style.maximise = false;    style.minimise = true;    style.resize = false;    dgl::init(style);    dgl::mainFunc(cb_main);    dgl::mouseDownFunc(cb_mdown);    /*********************/    //opengl settings    glClearColor(0,0,0,0);    dgl::proj2D(style.width,style.height,true); //sets up a 2d orthographic projection, true sets it to have y=0 at bottom of screen    /*********************/    //init menger cube instance    sponge.iterations = 7; //number of iterations    sponge.p = vec3(0,0,0); //position    sponge.A = vec33::scale(200,200,200); //transformation    sponge.iA = sponge.A.inverse();    sponge.itA = sponge.iA.transpose();    /*********************/    //enter main loop of program    dgl::mainloop();    return 0;}//captures right clicks and saves a screenshot to sshot.jpgvoid cb_mdown(int but){    if(but==dgl::MOUSE_RIGHT)    {        dgli::writeJpeg("sshot.jpg",90,dgli::capture());    }}/*************************************************************************************************************************///return pixel colour from ray cast through screen coordinate (sx,sy)vec3 cast(real sx, real sy){    //build ray    Menger::Ray cray;    cray.p = vec3(0,0,0); //camera is at origin    cray.v = vec3(sx-400.0,sy-300.0,400.0).normal(); //camera is facing foward along z-axis, no rotation to this    //transform into menger cube coordinate system    Menger::Ray tray = sponge.transform(cray);    //find intersection with time value + normal should it intersect    real t;    vec3 n;    if(Menger::intersectSponge(sponge,tray,t,n))    {        real c = 1-(t/400.0-0.25);        //transform normal back into world space, shift to (0,0,0) to (1,1,1) ranges, scale by time value)        return (sponge.normal(n).normal()*0.5 + vec3(0.5,0.5,0.5))*c;    }    else //background colour        return vec3(0,0,0);}/*************************************************************************************************************************///used in multisamplingconst real f13 = 1.0/3.0;//used in rotation of cubereal r = 0.0;void cb_main(){    /***************************************** render ********************************************/    //rendered as points    glBegin(GL_POINTS);    //skip over 5x5 blocks of pixels    for(int ay = 0; ay<600; ay+=5)    {        for(int ax = 0; ax<800; ax+=5)        {            //iterate through block's pixels            const int y2 = ay+5;            for(int y = ay; y<y2; y++)            {                const int x2 = ax+5;                for(int x = ax; x<x2; x++)                {                    real rx = x;                    real ry = y;                    /*16x multisampling*/                    //vec3 col = cast(rx,ry)*0.25 + (cast(rx-f13,ry)+cast(rx,ry-f13)+cast(rx+f13,ry)+cast(rx,ry+f13))*0.125                    //         +(cast(rx-f13,ry-f13)+cast(rx-f13,ry+f13)+cast(rx+f13,ry+f13)+cast(rx+f13,ry-f13))*0.0625;                    /*4x multisampling*/                    //vec3 col = (cast(rx-f13,ry-f13)+cast(rx-f13,ry+f13)+cast(rx+f13,ry+f13)+cast(rx+f13,ry-f13))*0.25;                    /*single ray/pixel*/                    vec3 col = cast(rx,ry);                    glColor3f(col.x,col.y,col.z);                    glVertex2i(x,y);                }            }        }    }    glEnd();    dgl::swapBuffers();    /*********************************** rotate menger sponge  ****************************************/    r += 0.06;    sponge.A*=vec33::rot_y(0.12*cos(r)*sin(3*r))*vec33::rot_x(0.12*sin(r)*sin(r*r))*vec33::rot_z(0.12*cos(2*r)*sin(r));    sponge.iA = sponge.A.inverse();    sponge.itA = sponge.iA.transpose();}


menger.hpp
#ifndef menger_hpp#define menger_hpp#include <floats.hpp> //my own library for vectors+matrices of floats - not important//typedef incase i want to swap and use doubles instead of floatstypedef float real;typedef float3 vec3;typedef float33 vec33;/*************************************************************************************/namespace Menger{    //Ray class    class Ray    {        public:            vec3 p; //origin            vec3 v; //direction    };    /*************************************************************************************/    //Menger Sponge class    class Sponge    {        public:            int iterations; //number of subdivisions            vec3 p; //position            vec33 A, iA, itA; //transformation + inverse and inverse transpose                              //note that with a unit transformation, the sponge will be contained in a unit-cube            Ray transform(const Ray & r) const; //transforms ray to sponge's coordinate system            vec3 normal(const vec3 & n) const; //transforms normal to world coordinate system    };    /*************************************************************************************/    //used in ray traversal, 3 way min-max    inline real Min(real x, real y, real z)    {        real t;        return ((t=((x<y)?x:y))<z)?t:z;    }    inline real Max(real x, real y, real z)    {        real t;        return ((t=((x>y)?x:y))>z)?t:z;    }    /*************************************************************************************/    //given t values, find the index of the first node in the cubic-octree (my name for it)    int find_firstNode(real tx0, real ty0, real tz0,                       real tx1, real tx2,                       real ty1, real ty2,                       real tz1, real tz2);    //given t values, find the index of the next node in the tree (either x,y or z)    int find_nextNode(real tx3, real ty3, real tz3,                      int x, int y, int z);    //process a node of the cubic-octree    bool proc_subtree(real tx0, real ty0, real tz0,                      real tx3, real ty3, real tz3,                      int cite);    /*************************************************************************************/    //intersect a ray with a menger sponge (ray in sponge coordinates)    //out: t, n    //time value for ray, and normal at intersection (in sponge coordinates)    bool intersectSponge(const Sponge & o, Ray r, real & t, vec3 & n);};#endif


menger.cpp
#include "menger.hpp"/*************************************************************************************/Menger::Ray Menger::Sponge::transform(const Menger::Ray & r) const{    Ray t;    t.p = iA*(r.p-p);    t.v = iA*r.v;    return t;}vec3 Menger::Sponge::normal(const vec3 & n) const{    return itA*n;}/*************************************************************************************///used so that i don't have to pass them into all of the methods that make use of themstatic real tval;static vec3 norm;static bool flipx;static bool flipy;static bool flipz;/*************************************************************************************//*note on cell indices: they are ordered according the formulaindex = x+(y*3)+(z*9)*/int Menger::find_firstNode(real tx0, real ty0, real tz0,                           real tx1, real tx2,                           real ty1, real ty2,                           real tz1, real tz2){    real max = Menger::Max(tx0,ty0,tz0);    int index = 0;    if(tx0==max)    {//enters cell in yz plane        if(ty2<tx0)      index += 6;        else if(ty1<tx0) index += 3;        if(tz2<tx0)      index += 18;        else if(tz1<tx0) index += 9;    }else if(ty0==max)    {//xz        if(tx2<ty0)      index += 2;        else if(tx1<ty0) index ++;        if(tz2<ty0)      index += 18;        else if(tz1<ty0) index += 9;    }else    {//xy        if(tx2<tz0)      index += 2;        else if(tx1<tz0) index ++;        if(ty2<tz0)      index += 6;        else if(ty1<tz0) index += 3;    }    return index;}/******************************************/int Menger::find_nextNode(real tx3, real ty3, real tz3,                          int x, int y, int z){    real min = Menger::Min(tx3,ty3,tz3);    if(tx3==min)      return x; //exits through yz plane    else if(ty3==min) return y; //xz    else              return z; //xy}/******************************************///used to determine time values for the nodes in the sub-treestatic const real f13 = 1.0/3.0;static const real f23 = 2.0/3.0;bool Menger::proc_subtree(real tx0, real ty0, real tz0,                          real tx3, real ty3, real tz3,                          int cite){    //does not intersect    if(tx3<=0||ty3<=0||tz3<=0)        return false;    //at lowest subdivision, traversal can end    if(cite==0)    {        //tvalue        tval = Menger::Max(tx0,ty0,tz0);        //normal - since ray traverses positively along all axi, normal component is always -1, it is negated to 1        //         the ray was reflected along that direction.        if(tx0==tval)      norm = vec3(flipx?1:-1,0,0);        else if(ty0==tval) norm = vec3(0,flipy?1:-1,0);        else               norm = vec3(0,0,flipz?1:-1);        return true;    }    //t values for mid-planes    real tx1 = f23*tx0+f13*tx3;    real tx2 = f13*tx0+f23*tx3;    real ty1 = f23*ty0+f13*ty3;    real ty2 = f13*ty0+f23*ty3;    real tz1 = f23*tz0+f13*tz3;    real tz2 = f13*tz0+f23*tz3;    //determine the first node in the sub-tree that is intersected    int currNode = find_firstNode(tx0,ty0,tz0,tx1,tx2,ty1,ty2,tz1,tz2);    do    {        //switch through the current node, if it is a cube that is not one of the wholes, process sub-tree        // proc_subtree being called only for certain cases is hard-coding the subdivion pattern of the menger cube,        // if you were to swap it round and have proc_subtree called elsewhere, you'd end up with a different pattern.        //advance to next node in the sub-tree, the index to step into next is hardcoded in the parameter for each exit plane        switch(currNode)        {            case 0: if(proc_subtree(tx0,ty0,tz0, tx1,ty1,tz1, cite-1)) return true;                    currNode = find_nextNode(    tx1,ty1,tz1,1,3,9);                    break;            case 1: if(proc_subtree(tx1,ty0,tz0, tx2,ty1,tz1, cite-1)) return true;                    currNode = find_nextNode(    tx2,ty1,tz1,2,4,10);                    break;            case 2: if(proc_subtree(tx2,ty0,tz0, tx3,ty1,tz1, cite-1)) return true;                    currNode = find_nextNode(    tx3,ty1,tz1,27,5,11);                    break;            case 3: if(proc_subtree(tx0,ty1,tz0, tx1,ty2,tz1, cite-1)) return true;                    currNode = find_nextNode(    tx1,ty2,tz1,4,6,12);                    break;            case 4:                    currNode = find_nextNode(    tx2,ty2,tz1,5,7,13);                    break;            case 5: if(proc_subtree(tx2,ty1,tz0, tx3,ty2,tz1, cite-1)) return true;                    currNode = find_nextNode(    tx3,ty2,tz1,27,8,14);                    break;            case 6: if(proc_subtree(tx0,ty2,tz0, tx1,ty3,tz1, cite-1)) return true;                    currNode = find_nextNode(    tx1,ty3,tz1,7,27,15);                    break;            case 7: if(proc_subtree(tx1,ty2,tz0, tx2,ty3,tz1, cite-1)) return true;                    currNode = find_nextNode(    tx2,ty3,tz1,8,27,16);                    break;            case 8: if(proc_subtree(tx2,ty2,tz0, tx3,ty3,tz1, cite-1)) return true;                    currNode = find_nextNode(    tx3,ty3,tz1,27,27,17);                    break;            case 9: if(proc_subtree(tx0,ty0,tz1, tx1,ty1,tz2, cite-1)) return true;                    currNode = find_nextNode(    tx1,ty1,tz2,10,12,18);                    break;            case 10:                    currNode = find_nextNode(    tx2,ty1,tz2,11,13,19);                    break;            case 11:if(proc_subtree(tx2,ty0,tz1, tx3,ty1,tz2, cite-1)) return true;                    currNode = find_nextNode(    tx3,ty1,tz2,27,14,20);                    break;            case 12:                    currNode = find_nextNode(    tx1,ty2,tz2,13,15,21);                    break;            case 13:                    currNode = find_nextNode(    tx2,ty2,tz2,14,16,22);                    break;            case 14:                    currNode = find_nextNode(    tx3,ty2,tz2,27,17,23);                    break;            case 15:if(proc_subtree(tx0,ty2,tz1, tx1,ty3,tz2, cite-1)) return true;                    currNode = find_nextNode(    tx1,ty3,tz2,16,27,24);                    break;            case 16:                    currNode = find_nextNode(    tx2,ty3,tz2,17,27,25);                    break;            case 17:if(proc_subtree(tx2,ty2,tz1, tx3,ty3,tz2, cite-1)) return true;                    currNode = find_nextNode(    tx3,ty3,tz2,27,27,26);                    break;            case 18:if(proc_subtree(tx0,ty0,tz2, tx1,ty1,tz3, cite-1)) return true;                    currNode = find_nextNode(    tx1,ty1,tz3,19,21,27);                    break;            case 19:if(proc_subtree(tx1,ty0,tz2, tx2,ty1,tz3, cite-1)) return true;                    currNode = find_nextNode(    tx2,ty1,tz3,20,22,27);                    break;            case 20:if(proc_subtree(tx2,ty0,tz2, tx3,ty1,tz3, cite-1)) return true;                    currNode = find_nextNode(    tx3,ty1,tz3,27,23,27);                    break;            case 21:if(proc_subtree(tx0,ty1,tz2, tx1,ty2,tz3, cite-1)) return true;                    currNode = find_nextNode(    tx1,ty2,tz3,22,24,27);                    break;            case 22:                    currNode = find_nextNode(    tx2,ty2,tz3,23,25,27);                    break;            case 23:if(proc_subtree(tx2,ty1,tz2, tx3,ty2,tz3, cite-1)) return true;                    currNode = find_nextNode(    tx3,ty2,tz3,27,26,27);                    break;            case 24:if(proc_subtree(tx0,ty2,tz2, tx1,ty3,tz3, cite-1)) return true;                    currNode = find_nextNode(    tx1,ty3,tz3,25,27,27);                    break;            case 25:if(proc_subtree(tx1,ty2,tz2, tx2,ty3,tz3, cite-1)) return true;                    currNode = find_nextNode(    tx2,ty3,tz3,26,27,27);                    break;            case 26:if(proc_subtree(tx2,ty2,tz2, tx3,ty3,tz3, cite-1)) return true;                    currNode = 27;                    break;        }    }while(currNode < 27); //currNode is set to 27 when leaving the sub-tree cell altogether,                           //aka doesn't intersect anything in this sub-tree    return false;}/*************************************************************************************/bool Menger::intersectSponge(const Menger::Sponge & o, Menger::Ray r, real & t, vec3 & n){    vec3 & p = r.p;    vec3 & v = r.v;    //the algorithm is much simpler if the ray is only ever traversed positively throug the axi    //exploiting the symmetry of the sponge, we can simply reflect in each axial plane with the same end-result    if(flipx = v.x<0)    {        p.x = -p.x;        v.x = -v.x;    }    if(flipy = v.y<0)    {        p.y = -p.y;        v.y = -v.y;    }    if(flipz = v.z<0)    {        p.z = -p.z;        v.z = -v.z;    }    //determine time values for intersection of each outer-cube plane    //to deal with axis-aligned rays easily, if the value is 0, it is very slightly increment    //so as not to effect outcome of rendering, but allow the algorithm to work properly    if(v.x==0) v.x=1e-10;    if(v.y==0) v.y=1e-10;    if(v.z==0) v.z=1e-10;    vec3 iv = v.inv(); //float3::inv() -> component wise multiplicative inverse aka (1/x,1/y,1/z)    real tx0 = (-0.5-p.x)*iv.x; real tx3 = (0.5-p.x)*iv.x;    real ty0 = (-0.5-p.y)*iv.y; real ty3 = (0.5-p.y)*iv.y;    real tz0 = (-0.5-p.z)*iv.z; real tz3 = (0.5-p.z)*iv.z;    //determine if ray intersects lowest cube    real t0 = Max(tx0,ty0,tz0);    real t1 = Min(tx3,ty3,tz3);    if(t0<t1 && t1>0)    {        //returns true if the ray intersects the sponge        if(proc_subtree(tx0,ty0,tz0,tx3,ty3,tz3, o.iterations))        {            //set n,t parameters to determined time value and normal            t = tval;            n = norm;            return true;        }    }    //ray has not intersected the sponge    return false;}


[Edited by - luca-deltodesco on May 17, 2009 6:55:38 PM]
Geometrian
Geometrian
Really awesome! But I was thinking in shaders--not actual code. Thanks though. Your code has given me a good starting point for my GLSL code, which produced this:

Unfortunately, there's still a few problems, but mengers 4, 5, and 6 are realtime--the reduction in speed is linear, not exponential, and no preprocessing is required. Thanks for your excellent help,
Ian
[size="1"]And a Unix user said rm -rf *.* and all was null and void...|There's no place like 127.0.0.1|The Application "Programmer" has unexpectedly quit. An error of type A.M. has occurred.
[size="2"]

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.