THE LAST CRYSTAL
Creative Direction/Physical Fabrication | 3D Printed PLA, Risograph on Tabloids
Project Supervision: Camila Morales
Course: Hands-On Fabrication Studio
Project Duration: 5 weeks
The Last Crystal is a data visualization project that transforms image data into data crystals aiming to raise awareness on the climate crisis and evoke visceral reaction. By transmuting the environmental adversities into a beautiful artifact, I wanted to deliver the idea that "The best way to overcome adversity is to embrace them."
Production Pipeline
1.Pre-production Research/Brainstorming
2. Image Modification/Code Generation
3. Modeling/Prompt Engineering
4. Refined Modeling
5. Video generation/Projection
6. Display
3D Modeling In the World of AI
With the rise of AI and the latest advancement on MCP(Model Context Protocol) and AI Agents, we are likely to see major strides toward AI generated models. Meshy is one of the companies that capitalized on this endeavor by providing users with Text to 3D/Image to 3D as well as AI texturing models for designers and artists to fully incorporate AI into their workflow.
When I first started using AI, ChatGPT in my case, I was quite skeptical of its power, but was quickly blown away by its capabilities as a "vibe coding" machine.
Hence, this project was an effort to personally explore the limits of AI and illuminate on the use of AI as an artistic assistant, questioning if they are useful at all.
Inspiration
The concept of the work came from the hypothesis: "What if our oceans vanishes, what's its legacy?"
By visualizing Sea Surface Temperature anomalies with crystal like 3D data crystals, I aimed to display the last pieces of our 5 oceans.
Execution
All the materials on the table are 3D printed with total print time accounting up to 70 hours, and the model crystals were a culmination of iterative prototyping and a combined effort of human and machine labor.
The Last Crystal is a multi-sensory experience where tactile, auditory, and visual sensations are catalyzed through the components comprising the work. For example, viewers could pick up the crystal and touch the finest detail of each crystal while immersing in the auditory and visual signals.
At the same time, the work inquires us to think about the ocean pollution due to plastic and its ultimate contribution to climate change. It is a work that displays the irony of our lives as the perpetuating cause of the problem gives a direct testimony of the phenomenon.
Nonetheless, it is an experience that encourages viewers to take a positive view of how environmental adversities could be transmuted into beautiful pieces of work that encapsulates the notion of modern capitalism, political greed, and humanity's innate desire for beauty and dignity.
After all, the most effective solution to our adversity is not to condemn nor to reject them, but to celebrate them and face the truth.
Chat-GPT produced Code
import java.io.BufferedWriter;import java.io.File;import java.io.FileWriter;import java.io.IOException;
int cols, rows; // Number of columns and rows for the gridfloat radius = 300; // Radius of the circular basefloat maxHeight = 200; // Maximum height for the 3D surfacefloat noiseScale = 0.05; // Scale of the Perlin noisefloat noiseStrength = 500; // Strength of the Perlin noiseint exportCount = 1; // To change file namePImage img; // The image to use for height mapping
void setup() { size(800, 800, P3D); cols = 100; // Number of segments around the circle rows = 50; // Number of height layers along the radius img = loadImage("/Users/jinhoyang/Desktop/artic.png"); // Load the image (change filename to your image) // Check if the image is loaded successfully if (img == null) { println("Error loading image"); exit(); // Exit if image is not found } img.resize(cols, rows); // Resize the image to fit the grid (optional)
noLoop(); // Only draw once}
void draw() { background(200); translate(width / 2, height / 2); // Center the model rotateX(PI / 3); // Rotate the model for a better view rotateZ(PI / 4); // Rotate slightly stroke(0); noFill(); // Generate the tessellated circular surface based on Perlin noise and image heightmap for (int i = 0; i < cols; i++) { for (int j = 0; j < rows - 1; j++) { // Calculate polar coordinates (radius, angle) for the tessellated grid float angle1 = map(i, 0, cols, 0, TWO_PI); float angle2 = map(i + 1, 0, cols, 0, TWO_PI); float radius1 = map(j, 0, rows - 1, 0, radius); // Radius at j float radius2 = map(j + 1, 0, rows - 1, 0, radius); // Radius at j+1
// Get the height values from Perlin noise + the image heightmap float h1 = map(noise(i * noiseScale, j * noiseScale), 0, 1, 0, maxHeight) + getHeightFromImage(i, j); float h2 = map(noise((i + 1) * noiseScale, j * noiseScale), 0, 1, 0, maxHeight) + getHeightFromImage(i + 1, j); float h3 = map(noise(i * noiseScale, (j + 1) * noiseScale), 0, 1, 0, maxHeight) + getHeightFromImage(i, j + 1); float h4 = map(noise((i + 1) * noiseScale, (j + 1) * noiseScale), 0, 1, 0, maxHeight) + getHeightFromImage(i + 1, j + 1);
// Convert polar coordinates to Cartesian coordinates float x1 = radius1 * cos(angle1); float y1 = radius1 * sin(angle1); float x2 = radius2 * cos(angle2); float y2 = radius2 * sin(angle2);
// Draw the two triangles for tessellation beginShape(TRIANGLES); vertex(x1, y1, h1); vertex(x2, y1, h2); vertex(x1, y2, h3); vertex(x2, y1, h2); vertex(x2, y2, h4); vertex(x1, y2, h3); endShape(); } }}
void keyPressed() { if (key == 's' || key == 'S') { String fileName = "lastdance_artic" + exportCount + ".stl"; String desktopPath = System.getProperty("user.home") + "/Desktop/"; // Path to Desktop exportSTL(desktopPath + fileName); println("Exported " + desktopPath + fileName); exportCount++; } }
// Function to get height from the image based on pixel brightnessfloat getHeightFromImage(int i, int j) { if (i >= 0 && i < img.width && j >= 0 && j < img.height) { color c = img.get(i, j); // Get pixel color float brightnessValue = brightness(c); // Convert to brightness (grayscale) return map(brightnessValue, 0, 255, 0, 100); // Map brightness to height } else { return 0; // Return a default height value if out of bounds }}
// Function to export the model to an STL filevoid exportSTL(String filename) { File outputFile = new File(filename); try (BufferedWriter writer = new BufferedWriter(new FileWriter(outputFile))) { writer.write("solid tessellated_surface\n");
// Generate surface triangles for (int i = 0; i < cols; i++) { for (int j = 0; j < rows - 1; j++) { // Calculate polar coordinates (radius, angle) for the tessellated grid float angle1 = map(i, 0, cols, 0, TWO_PI); float angle2 = map(i + 1, 0, cols, 0, TWO_PI); float radius1 = map(j, 0, rows - 1, 0, radius); // Radius at j float radius2 = map(j + 1, 0, rows - 1, 0, radius); // Radius at j+1
// Get the height values from Perlin noise + the image heightmap float h1 = map(noise(i * noiseScale, j * noiseScale), 0, 1, 0, maxHeight) + getHeightFromImage(i, j); float h2 = map(noise((i + 1) * noiseScale, j * noiseScale), 0, 1, 0, maxHeight) + getHeightFromImage(i + 1, j); float h3 = map(noise(i * noiseScale, (j + 1) * noiseScale), 0, 1, 0, maxHeight) + getHeightFromImage(i, j + 1); float h4 = map(noise((i + 1) * noiseScale, (j + 1) * noiseScale), 0, 1, 0, maxHeight) + getHeightFromImage(i + 1, j + 1);
// Convert polar coordinates to Cartesian coordinates float x1 = radius1 * cos(angle1); float y1 = radius1 * sin(angle1); float x2 = radius2 * cos(angle2); float y2 = radius2 * sin(angle2);
// Write the first triangle (vertices x1, y1, h1), (x2, y1, h2), (x1, y2, h3) writeTriangle(writer, x1, y1, h1, x2, y1, h2, x1, y2, h3);
// Write the second triangle (vertices x2, y1, h2), (x2, y2, h4), (x1, y2, h3) writeTriangle(writer, x2, y1, h2, x2, y2, h4, x1, y2, h3); } }
// Add the base (at z = 0) for (int i = 0; i < cols; i++) { float angle1 = map(i, 0, cols, 0, TWO_PI); float angle2 = map(i + 1, 0, cols, 0, TWO_PI); float x1 = radius * cos(angle1); float y1 = radius * sin(angle1); float x2 = radius * cos(angle2); float y2 = radius * sin(angle2);
// Write the base (triangle between center and two consecutive points on the circle) writeTriangle(writer, 0, 0, 0, x1, y1, 0, x2, y2, 0); }
// Add the filling: connect the top surface to the base center for (int i = 0; i < cols; i++) { for (int j = 0; j < rows - 1; j++) { // Calculate polar coordinates (radius, angle) for the tessellated grid float angle1 = map(i, 0, cols, 0, TWO_PI); float angle2 = map(i + 1, 0, cols, 0, TWO_PI); float radius1 = map(j, 0, rows - 1, 0, radius); // Radius at j float radius2 = map(j + 1, 0, rows - 1, 0, radius); // Radius at j+1
// Get the height values from Perlin noise + the image heightmap float h1 = map(noise(i * noiseScale, j * noiseScale), 0, 1, 0, maxHeight) + getHeightFromImage(i, j); float h2 = map(noise((i + 1) * noiseScale, j * noiseScale), 0, 1, 0, maxHeight) + getHeightFromImage(i + 1, j); float h3 = map(noise(i * noiseScale, (j + 1) * noiseScale), 0, 1, 0, maxHeight) + getHeightFromImage(i, j + 1); float h4 = map(noise((i + 1) * noiseScale, (j + 1) * noiseScale), 0, 1, 0, maxHeight) + getHeightFromImage(i + 1, j + 1);
// Convert polar coordinates to Cartesian coordinates float x1 = radius1 * cos(angle1); float y1 = radius1 * sin(angle1); float x2 = radius2 * cos(angle2); float y2 = radius2 * sin(angle2);
// Write the filling triangles: from top surface vertices to base center (0, 0, 0) writeTriangle(writer, x1, y1, h1, x2, y1, h2, 0, 0, 0); writeTriangle(writer, x2, y1, h2, x2, y2, h4, 0, 0, 0); writeTriangle(writer, x2, y2, h4, x1, y2, h3, 0, 0, 0); writeTriangle(writer, x1, y2, h3, x1, y1, h1, 0, 0, 0); } }
writer.write("endsolid tessellated_circular_surface\n"); println("STL file exported to " + filename); } catch (IOException e) { e.printStackTrace(); }}
// Function to write a triangle to the STL filevoid writeTriangle(BufferedWriter writer, float x1, float y1, float z1, float x2, float y2, float z2, float x3, float y3, float z3) { try { // Create PVector objects for each vertex of the triangle PVector v1 = new PVector(x1, y1, z1); PVector v2 = new PVector(x2, y2, z2); PVector v3 = new PVector(x3, y3, z3); // Calculate the normal vector for the triangle using cross product PVector normal = v1.copy().sub(v2).cross(v1.copy().sub(v3)).normalize(); // Write the facet normal to the STL file writer.write(" facet normal " + normal.x + " " + normal.y + " " + normal.z + "\n"); writer.write(" outer loop\n"); // Write the three vertices of the triangle writer.write(" vertex " + x1 + " " + y1 + " " + z1 + "\n"); writer.write(" vertex " + x2 + " " + y2 + " " + z2 + "\n"); writer.write(" vertex " + x3 + " " + y3 + " " + z3 + "\n"); // End the outer loop and facet for the triangle writer.write(" endloop\n"); writer.write(" endfacet\n"); } catch (IOException e) { e.printStackTrace(); }}
*Code was generated by ChatGPT with manual modifications
Above graphics represent initial renderings of parametric mesh produced from processing using heat map image data from each ocean. The jaggedness and the lack of uniformity present in the models offer a glimpse of the unique fingerprint and the qualities each ocean possesses. Meanwhile, there is an order found amidst the chaos of strokes of lines that construct the models which lends us an insight into the oneness of our vast oceans that could not be distinguished by human-made borders. Below graphics exhibit some of the failed meshes that encountered print issues, yet still offer an aesthetically appealing form.
Exhibition @ 370 Jay Street
For the live exhibition, I projected the compilation of 3d models video with added effects on top of my prints, having them as a projection wall. I filled up the empty spaces between 3d models with custom printed risograph prints of the 2d version of meshes I got straight out of processing. The projection on top of white 3d printed filaments created interesting dichotomy that almost resembled that of the fashion runway. I played ambient soundscapes mixed with ocean ambience to accentuate the exhibition experience.
REFLECTION
When I first embarked on this project, I was as interested in fully realizing the potential of AI as an tool for artistic endeavor, as much as I wanted to create something beautiful as my final output.
At the same time, as a nascent AI user, I couldn't erase the guilt of deploying AI and copy pasting the code that it generated as if I had created sense of emotional void from losing integrity by pretending to have made an original creation. At the same time, I realized while AI is a powerful tool that generates powerful outputs, it is not a replacement by any means, and only those with foundational skills, whether it be software engineering, design, or any discipline, would thrive through taking advantage of the tool. Without much knowledge, I found myself constantly stuck in the cycle of “Slot Machine” and — failing to build something something that compounds. It was almost like building a sand castle in front of the wave as it failed to sustain its vitality.