DEV Community

Cover image for Creating an Impossible Box with GLSL & THREE.js
Jesse Solomon
Jesse Solomon

Posted on

Creating an Impossible Box with GLSL & THREE.js

One of my favorite types of visualizations are the physically impossible ones. And of those, one of the most fun is a bigger-on-the-inside box!

The Final Product

If you want to play around with it yourself, you can view it on my website.

And you can check out the fully commented GitGub repository for more insight.

Demo

Let's Build It!

If you're not familiar with the basics of how shaders and WebGL works, I highly recommend you check out this MDN article on the topic.

Now let's dive into how I went about building this!

To begin, let's talk about the feature that made this whole thing work discard.

The fragment shader has a special keyword that works similarly to return in a common programming language. discard instructs the GPU to not render the current fragment, allowing whatever is behind it to show through. You can read more about it here.

Using this feature we can turn a boring regular cube, into a super cool transparent cube!

Source: outer_fragment.glsl

// Check if the fragment is far enough along any axis
bool x_edge = abs(worldPosition.x) > 0.4;
bool y_edge = abs(worldPosition.y) > 0.64;
bool z_edge = abs(worldPosition.z) > 0.4;

// Check that the fragment is at the edge of at least two axis'
if (!y_edge && !z_edge) {
    discard;
}

if (!y_edge && !x_edge) {
    discard;
}
Enter fullscreen mode Exit fullscreen mode

Transparent Box

Now we just need to find a way to tell which face we're seeing though. This was by far the most difficult part, not because the solution was terribly hard, mostly because I'm not very good at math.

So let's go over how I went about implementing this.

1st, since there's a top and bottom to our box, we don't really need to work in 3D for this. So let's think of our 3D box, as a 2D box:

Whiteboard Box

Now, we can take the 3D geometry (red) that's inside of the box, and flatten it to 2D:

Whiteboard Geometry

Next, let's add a camera (blue) and some example fragments we want to render (green):

Whiteboard Rendering

With this setup, we can make a line between our fragments and the camera, and check which face they go through:

Whiteboard Line Intersection

If we apply this method to our box, and give a color to each face we get this fun effect!

Source: inner_fragment.glsl

// Define all the corners of our box
const vec2 corners[4] = vec2[](vec2(0.5, 0.5), vec2(-0.5, 0.5), vec2(-0.5, -0.5), vec2(0.5, -0.5));

// Define a line from the fragment's position (A) to the camera (B)
vec2 a = worldPosition.xz;
vec2 b = cameraPosition.xz;

int intersectedFace = -1;

// Iterate over each face
for (int i = 0; i < 4; i++) {
    // Get the second point for our face
    int next = int(mod(float(i + 1), 4.0));

    // Create a line from 2 corners based on the face
    vec2 c = corners[i];
    vec2 d = corners[next];

    // Does line 1 and 2 intersect? If so, assign the intersected face
    if (intersect(a, b, c, d)) {
        intersectedFace = i;
        break;
    }
}

// Color the fragment based on the face
switch (intersectedFace) {
    case -1:
        gl_FragColor = vec4(1, 0, 1, 1);
        break;
    case 0:
        gl_FragColor = vec4(1, 0, 0, 1);
        break;
    case 1:
        gl_FragColor = vec4(0, 1, 0, 1);
        break;
    case 2:
        gl_FragColor = vec4(0, 0, 1, 1);
        break;
    case 3:
        gl_FragColor = vec4(0, 1, 1, 1);
        break;
}
Enter fullscreen mode Exit fullscreen mode

Face Detection

From here, we can just assign a face to each object we want inside, and discard any fragments that don't pass through the given face.

Source: inner_fragment.glsl

// Define a line from the fragment's position (A) to the camera (B)
vec2 a = worldPosition.xz;
vec2 b = cameraPosition.xz;

// Get the second point to define the face
int next = int(mod(float(face + 1), 4.0));

// Define a line at the given face
vec2 c = corners[face];
vec2 d = corners[next];

// If the defined lines do NOT intersect, then discard the fragment
if (!intersect(a, b, c, d)) {
    discard;
}
Enter fullscreen mode Exit fullscreen mode

Then we'll just add some interesting animated objects, a little directional lighting for depth, and we're done!

Demo

Thanks for reading! I hope you enjoyed it as much as I had making it!

Top comments (0)