Rubik's Cube Simulation
A small project postmortem
Tl;dr: Try it out here
Overview
This is a simple Rubik's cube simulation from scratch. It has a trackball camera + point & click controls. My goal was to have intuitive controls that reflected a faithful simulation and to make it as pretty as possible.
These design goals naturally lead to some technical constraints and necessary pattern implementations.
Everything (model, rendering system, model i/o, raycast, aabb-ray intersection, ray-quad intersection, etc) was handmade for educational purposes.
Rendering side of things
Making a model
Making a Rubik's cube is a pretty popular 3D modeling project and there are a large amount of tutorials available online (mine mirrors the first search result) by much more competent people than me. I'm not a 3D artist and have just used blender off and on for a few years as the occasional indie game dev project has called for it.
Loading the mesh
Importing a mesh seems daunting at first, but if you've messed around with graphics enough, at the end of the day you know that it's just big arrays representing vertex data (positions, normals, colors etc). I imported with the .ply format and parsed it accordingly.
In practice this can be a little arduous and debugging this kind of thing is painful, eye bleeding work. To quote a programming superior:
The model I/O is the worst and almost everybody tries to get somebody else’s code to do this. – Peter Shirley, Raytracing in One Weekend
Instancing
Given that my unoptimized model (a single "cubie") is on the order of \(5\) x \(10^4\) triangles (retopologyy is a thing for a reason) I needed to speed things up by instancing them.
I highly recommend this tutorial by webggl2fundamentals.
Lighting Model
I'm using the traditional Blinn-Phong lighting model. Check out my article on the half-way vector if you're derivation curious and/ or need a review.
Programming side of things
Selecting the cube - Ray-Box Intersection
I wanted intuitive controls that reflected a faithful simulation e.g. rubikscu.be
But where to start?
When I first started on this project, I happened to also have been working on some small raytracing projects and was fortunate to have Peter Shirley as a resource. Raytracing methods often use axis aligned bounding boxes in Oct-trees as a broadphase check using the slab method. If you're interested in learning how that works, check out this little demo I made to debug/ understand my own convoluted slab method implementation.
I opted for a broadphase check of a camera ray and an AABB representing the entire cube, if that passes an AABB of each cubie's bounds is checked with the ray taking an early out if the maximum possible, diagonal cross section is collected. The minimum distance of of those selections is our clicked cubie.
Getting a direction for a rotation
This was by far the stickiest part of the project, I eventually came up with, what I think is a standard solution for this kind of thing: take the relative difference vector of two different surface intersections and project them onto the plane's basis vectors (of course culling them with the 2D quad bounds of the cubie surface), Some edge cases ( quite literally, say if the first first intersection was exactly on an edge) and bugs were definitely in this one.
Please see the wikipedia article or my own article for a line-plane intersection derivation for further discussion.
(This is another good reminder of the importance of asking for help/ avoiding reinventing the wheel… this was all for educational purposes however and deeper understanding is impossible without error and redudancy)
Rotating a plane of the Cube
This is kind of where programming and graphics programming meet each other in the project.
Recall that for a homogenous transformation matrix used in rendering, the translation component is contained in the 12th, 13th and 14th index.
mat4 aMat4 = mat4(1.0, 0.0, 0.0, 0.0, // 1. column 0.0, 1.0, 0.0, 0.0, // 2. column 0.0, 0.0, 1.0, 0.0, // 3. column T_x, T_y, T_z, 1.0); // 4. column
If you're curious or confused and want to take a deeper dive into that, check out my article on the matrix math of the rasterization process
So far in the input handling cycle, a specific cubie (and associated rendering object) has been selected and the axis of rotation has been decided with the mouse. To affect a rotation, each cubie is looped over and if their translation component that corresponds with the axis of rotation matches the selected cubie, it's transform is hit with a rotation matrix. But the GPU buffer also must be changed to see any corresponding change:
// update a substitue float32 array and then offset into buffer correctly, replacing 9 matrices instead of 27 for(let j = 0; j < 16; ++j) { this.theSubArr[j] = this.renderer.renderables[0].cubieTransforms[i][j]; } gl.bufferSubData(this.gl.ARRAY_BUFFER, i * 16 * 4, this.theSubArr);
Floating point errors in the cubie's transformation matrix will accrue over time and its basis vectors and affine translation should be rounded after a rotation. (as a small optimization this need only be done on the final, corrective rotation to nearest \(\frac {\pi}{2}\))
Wrapping up
To riff on a famous quote, projects are never finished only abandonded. While this was a great project for me to cement some programming and graphics fundamentals, there are many ways it could be improved. Beyond the obvious stuff (less terrible code: reduce the size and responsibility of some classes & functions, make things a little more SOLID / DRY; use a lower level language) I think the biggest boost in performance and visuals would come in reducing the size of the cubie model (retopologize) and using a better/ more sophisticated lighting model (energy conserving, PBR, raytracing etc) or alter the current lighting model to account for certain glaring gaps / artifacts (shadow mapping - the cubies that are covered are still being shaded currently).
It's always humbling how much is required for so little (how complicated even seemingly very simple things turn our to be / how coarse our proconceptions perpetually are). To do these projects that the world doesn't need continues to be a confusing balance between unproductivity and educational value for me.
Cheers.