Rubik's Cube in C
This is my coding process of creating a Rubik’s Cube simulation that combines the computational efficiency of C and the portability of HTML, resulting in an interactive 3-D Rubik’s Cube that can be played on any device with a browser. Click here to go to the playable simulation .
Project Overview
The project is divided into three main parts:
- Custom graphics library: In order to conveniently port the C program to WebAssembly, there can’t be any external libraries, such as OpenGL. To compensate, I created my own graphics library that is more like a specified, bulk array editor.
- Rubik’s Cube Logic in C: The core logic of the Rubik’s Cube, including managing the cube state, rotations, and animating the turns. It also calculates where the pixels should be placed on the screen so it has a 3-D effect.
- Rendering on HTML canvas: Using JavaScript to coordinate the keyboard inputs, it communicates with the WebAssembly to make the cube interactive.
Graphics Library
The first major component of this project was developing a graphics library. For this project, I wanted to make the whole thing without depending on any external libraries, which will allow me to have control over all the functionalities and not have to read a ton of documentation.
Key Features
- Drawing Primitives: Functions to draw a point, a line, a circle, and most importantly, a triangle. Triangles are essential for any graphics engine because all shapes can be decomposed into small triangles.
- Color management: There is support for transparent pixels and it can read and display any ARGB (alpha + RGB) hex value.
- Fast rendering: Use of efficient algorithms, allowing the C program to quickly communicate the current state with the HTML canvas.
Here is an example function from the library to fill in a triangle:
void fill_triangle(uint32_t *canvas, size_t width, size_t height,
int x1, int y1,
int x2, int y2,
int x3, int y3, uint32_t color)
{
// sort points such that x1 >= x2 >= x3
sort_points(&x1, &y1, &x2, &y2, &x3, &y3);
// find midpoint
int mid_x = (int)(((float)x1 +
((float)(y2 - y1) / (float)(y3 - y1)) * (float)(x3 - x1)));
// draw flat top and flat bottom triangle to make whole triangle
fill_flat_bottom_triangle
(canvas, width, height, x1, y1, x2, y2, mid_x, y2, color);
fill_flat_top_triangle
(canvas, width, height, x2, y2, mid_x, y2, x3, y3, color);
}
Rubik’s Cube Logic
Data Structure
The Rubik’s Cube is represented with a simple 1D array. The simplicity allows for scalability, meaning that the code to create a 2x2x2 cube is the same as a 9x9x9 cube.
struct Point_3D {
float x, y, z;
};
struct cube {
struct Point_3D points[NUM_CORNERS];
uint32_t currState;
};
struct cube cubes[num_cubes];
Rotations Functions
To rotate the faces, I created functions that will rotate the colors on each face. These values have been coded in such a way that it allows for any sized cube to work with the same function. Here’s an example function that rotates the middle of any face that sits on the side:
/*
* Function: rotate_middle
* ------------------------------------------------------------------------
* Rotates the middle plane of any outward facing face.
*
* Input params:
* struct cube **select_cubes: slice of cubes to be rotated
* int cubes_per_plane: number of cubes in the slice
* int mode: axis of rotations
* int direction: counterclockwise or clockwise (0 or 1)
*
* Return:
* No return value. Cube states are directly changed in function.
*/
void rotate_middle(struct cube **select_cubes, int cubes_per_plane,
int mode, int direction) {
int i, j;
int count = 0;
int num_cubes = cubes_per_plane - 4 * (DIMENSION - 1);
struct cube *grouped_cubes[num_cubes];
int n = DIMENSION - 2;
if (n < 1) {
return;
}
// separates moving cubes from non-moving cubes
for (j = 0; j < cubes_per_plane; j++) {
uint32_t currState = select_cubes[j]->currState;
int num_active_planes = get_num_active_planes(currState);
if (num_active_planes == 1) {
grouped_cubes[count] = select_cubes[j];
count++;
}
}
// performs the rotation based on desired direction (CW or CCW)
if (direction) {
for (i = 0; i < (n + 1) / 2; i++) {
for (j = 0; j < n / 2; j++) {
uint32_t temp = grouped_cubes[(n - 1 - j) * n + i]->currState;
grouped_cubes[(n - 1 - j) * n + i]->currState =
grouped_cubes[(n - 1 - i) * n + n - j - 1]->currState;
grouped_cubes[(n - 1 - i) * n + n - j - 1]->currState =
grouped_cubes[j * n + n - 1 - i]->currState;
grouped_cubes[j * n + n - 1 - i]->currState =
grouped_cubes[i * n + j]->currState;
grouped_cubes[i * n + j]->currState = temp;
}
}
}
else {
for (i = 0; i < (n + 1) / 2; i++) {
for (j = 0; j < n / 2; j++) {
uint32_t temp = grouped_cubes[i * n + j]->currState;
grouped_cubes[i * n + j]->currState =
grouped_cubes[j * n + n - 1 - i]->currState;
grouped_cubes[j * n + n - 1 - i]->currState =
grouped_cubes[(n - 1 - i) * n + n - j - 1]->currState;
grouped_cubes[(n - 1 - i) * n + n - j - 1]->currState =
grouped_cubes[(n - 1 - j) * n + i]->currState;
grouped_cubes[(n - 1 - j) * n + i]->currState = temp;
}
}
}
}
Rendering to HTML
The last component is displaying the Rubik’s Cube. The C program will convert the cubes into a 2D array of pixels, then it will feed the pixels’ information to the HTML canvas.
Converting Cubes to a 2D Canvas to Look 3D
Here is the logic for drawing the cubes. For each cube, it takes the three closest faces to the camera, and it will use the graphics library to draw it in such a way that the perspective is kept, retaining its 3D appearance.
/*
* Function: draw_planes
* ------------------------------------------------------------------------
* Given an array of planes, it will draw the planes on the canvas in the
* correct order of back to front by sorting the planes by its z value.
*
* Input params:
* uint32_t *canvas: array of pixels on screen
* int width: width of canvas
* int height: height of canvas
* struct plane *p: array of planes to be drawn
* int count: number of planes (size of struct plane *p)
*
* Return:
* No return value. Directly places the pixels into canvas.
*/
void draw_planes(uint32_t *canvas, int width, int height,
struct plane *p, int count) {
// sort planes based on average z value
int i, j, min_idx;
// selection sort
for (i = 0; i < count - 1; i++) {
// Find the minimum element in unsorted array
min_idx = i;
for (j = i + 1; j < count; j++)
if (get_average_z(p[j]) > get_average_z(p[min_idx]))
min_idx = j;
// Swap the found minimum element with the first element
if (min_idx != i) {
struct plane temp = p[min_idx];
p[min_idx] = p[i];
p[i] = temp;
}
}
// paint the two triangles
for (i = count / 2; i < count; i++) {
fill_triangle(canvas, width, height,
p[i].s_pointA.x, p[i].s_pointA.y,
p[i].s_pointB.x, p[i].s_pointB.y,
p[i].s_pointC.x, p[i].s_pointC.y,
p[i].color);
fill_triangle(canvas, width, height,
p[i].s_pointD.x, p[i].s_pointD.y,
p[i].s_pointB.x, p[i].s_pointB.y,
p[i].s_pointC.x, p[i].s_pointC.y,
p[i].color);
}
}
Javascript and HTML Integration
After compiling the C code into WebAssembly, JavaScript is used to call the methods and display the pixels on the screen. Here is the render function in JavaScript without any extra parameters:
function render(instance) {
const pixels = instance.exports.render();
const buffer = instance.exports.memory.buffer;
const imageData = new ImageData(
new Uint8ClampedArray(buffer, pixels, app.width * app.height * 4),
app.width
);
ctx.putImageData(imageData, 0, 0);
}
The program would not be possible without using WebAssembly due to the limitations of browsers.
Conclusion
This project allowed me to practice creating efficient code by leveraging the lightweight nature of C while being restricted by the browser's technical limitations. It also allowed me to combine coding and Rubik's Cubes together; two things I'm passionate about. You can find the link to the Github repository for this project here .