This will be the first post in a series about returning to a project I did about a year ago and trying to improve on it and maybe learn something along the way!
A year ago I built an interactive Rubik's cube using Three.js.
The inspiration behind it was probably reading Erno Rubik's book Cubed: The Puzzle of Us All, a memoir where he talks about the cube.
There are two things I found interesting & still remember from the book.
- That Rubik originally considered purple for the sixth color, but ended up settling on white, because he considers the cube male.
- That after he created the cube he actually solved it himself. Does being the creator of the magic cube make it easier to solve? I like the cube as an object and sometimes play with it until I get frustrated, but out of principle I refuse to "learn" how to solve it. But I also never seriously consider the possibility of actually figuring it out.
When you open the page, you see a (solved) cube rotating in empty space*. When you hover over a face of the cube, that side of the cube is highlighted. If you click it, you can rotate the side, just like with a real cube.
This feature set is enough for me to consider the simulation complete, since this is pretty much what you can do with a physical cube. There is no "scramble" or "solve" function, nor do I plan to implement these since at the moment the cube interests me as a physical rather than mathematical object. I don't currently represent the 'state' of the cube, beyond the positions and rotations of the little cubes (which Rubik in his book calls cubies, so I adopted this terminology in the code).
However, because it has to look nice, one thing I did bother with is a custom shader for the cubies, so they are not just a solid color, but also have a black border, to imitate the look of colored stickers on a black background:
material.onBeforeCompile = (shader) => {
shader.fragmentShader = shader.fragmentShader.replace(
'#include <color_fragment>',
`
#include <color_fragment>
vec2 cUv = vUv - 0.5;
if (abs(cUv.x) > 0.45 || abs(cUv.y) > 0.45) diffuseColor = vec4(0.0, 0.0, 0.0, 1.0);
if (abs(cUv.x) + abs(cUv.y) > 0.85) diffuseColor = vec4(0.0, 0.0, 0.0, 1.0);
`
);
};
cUv stands for "centered UV", basically we want the coordinates to move between -0.5 and 0.5 instead of 0 and 1, so we can reason about left/right and top/bottom similarly.
the next two lines define where we want the square to be black: (1) anywhere that's less than 0.05 from the edge, and (2) the corners.
Also, if you're wondering about lines 2-5, we are replacing part of the fragment shader code, basically we are replacing "#include <color_fragment>" with "include <color_fragment>" plus our code, aka inserting our code after "include <color_fragment>". Why there specifically? It took a bit of trial and error, trying different places until one of them didn't break anything.
This is all well and good, so it's finished, right? I finally completed a side project? Well, almost. There is a small issue, the explanation for which will probably be "...something something floating point". If you play with the cube long enough, it eventually starts acting weird. Some of the cubies get stuck in weird positions, and seemingly even become a bit smaller than the rest. What's worse, I thought I'd fixed this exact issue before, but the bug is still there when running the code on a better computer.
So my goal now is to return to code I wrote a year ago, understand it, and try to figure out what's going on. Come along for the journey?
Some things I want to do:
- try to vibe code the entire thing from scratch, for fun
- describe the problem. when does it happen? there's actually two ways to rotate the cube: with the keyboard or with the mouse. if one uses only the keyboard, and doesn't hover over the cube with the mouse, i believe the bug doesn't occur. so it must have something to do with what happens when we raycast / highlight the selected side.
- ✨✨✨ make the code cleaner ✨✨✨, get it to a point where I can explain all of it (without significant embarrassment)
- possibly add some tests. Right now I'm not sure how to write tests for three.js. also, this might involve representing the state of the cube in some way, to then be able to compare that to a desired state after a series of rotations.
*Initially, I considered a white room as the background. When I experimented with removing it, it turned out that it improves performance by a lot. I was surprised by this, it was a simple white box with what, 24 vertices? Well, it turns out not rendering anything on 70% of the canvas makes a difference.
btw if you want to follow along, here is the code I'm starting with.
And the latest version can be played with here.
Top comments (0)