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.
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!
// 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;
}
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:
Now, we can take the 3D geometry (red) that's inside of the box, and flatten it to 2D:
Next, let's add a camera (blue) and some example fragments we want to render (green):
With this setup, we can make a line between our fragments and the camera, and check which face they go through:
If we apply this method to our box, and give a color to each face we get this fun effect!
// 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;
}
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.
// 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;
}
Then we'll just add some interesting animated objects, a little directional lighting for depth, and we're done!
Thanks for reading! I hope you enjoyed it as much as I had making it!
Top comments (0)