Basic 3D Graphics in Java
by Archive
I decided to split up this tutorial into three sections:
Setup
Math
Finalization
Section 1 : Setup
All we need here is a basic Java graphics setup and a main loop.
You may implement this however you like, but I will show you what I’ve done.
Graphics displayer Display.java:
public final class Display extends Frame {
private static final long serialVersionUID = 1L;
private final Surface surface; // Our drawable pixel surface.
private final BufferedImage buffer; // The buffer that the pixel data from the surface is given to.
public Display(int width, int height, String title) {
setSize(width, height);
// Instantiate the buffer and surface.
buffer = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
surface = new Surface(buffer.getRaster(), width, height);
setResizable(false);
// Add window closing event to allow the window to close when the red X is pressed!
addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
setLocationRelativeTo(null);
setTitle(title);
setVisible(true); // Show on screen.
}
// Draws the buffer to the display.
public void drawBuffer() {
this.getGraphics().drawImage(buffer, 0, 0, surface.getWidth(), surface.getHeight(), this);
}
public Surface getSurface() {
return surface;
}
public BufferedImage getBuffer() {
return buffer;
}
}
Drawable pixel surface Surface.java:
public final class Surface {
private final int width; // Width of the surface.
private final int height; // Height of the surface.
private final WritableRaster raster; // Raster we can draw to.
public Surface(WritableRaster raster, int width, int height) {
this.raster = raster;
this.width = width;
this.height = height;
}
// Reset all values to zero (black) to clear the screen.
public void clear() {
for (int i = 0; i < ((DataBufferInt) raster.getDataBuffer()).getData().length; i++) {
((DataBufferInt) raster.getDataBuffer()).getData()[i] = 0;
}
}
// Your basic implementation of the Bresenham Line Algorithm.
public void drawLine(int x1, int y1, int x2, int y2, int color) {
int dx = Math.abs(x2 - x1);
int dy = Math.abs(y2 - y1);
int sx = (x1 < x2) ? 1 : -1;
int sy = (y1 < y2) ? 1 : -1;
int err = dx - dy;
putPixel(x1, y1, color);
while ((x1 != x2) || (y1 != y2)) {
int err2 = err << 1;
if (err2 > -dy) {
err -= dy;
x1 += sx;
}
if (err2 < dx) {
err += dx;
y1 += sy;
}
putPixel(x1, y1, color);
}
}
// Set a pixel on this surface to the given color.
public void putPixel(int x, int y, int color) {
if (x < getWidth() && x > 0 && y < getHeight() && y > 0) {
raster.setPixel(x, y, new int[] { colorClamp(color, 16), colorClamp(color, 8), colorClamp(color, 0) });
}
}
// Make sure 0 < color < 255.
public static int colorClamp(int color, int bits) {
return Math.min(255, Math.max((color >> bits) & 0xFF, 0));
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
}
Main class Demo.java:
public final class Demo {
private boolean running; // tells us if the program is running, used to break the main loop.
private Display display; // The instance of Display that contains the buffer and the surface.
private Rasterizer rasterizer; // The "rasterizer" that draws our 3D object.
// Main entry point
public static void main(String[] args) {
new Demo().run(); // Create new instance of this class and run it.
}
private void setup() { // Setup method that creates our instances.
display = new Display(800, 600, "3D Graphics Tutorial by Archive"); // Create new instance of the Display.
rasterizer = new Rasterizer(display); // Create new instance of Rasterizer.
running = true; // Start running the program!
}
private void run() {
try {
setup(); // Setup.
mainLoop(); // Loop.
} catch (Exception e) {
e.printStackTrace();
} finally {
exit(); // Exit after mainLoop broken.
}
}
private void exit() {
try {
Thread.sleep(1000L);
if (display != null) {
display.dispose(); // Dispose the Display.
}
System.exit(0); // Exit
} catch (Exception e) {
e.printStackTrace();
}
}
private void mainLoop() { // This is the main loop, it loops until running == false.
while (running) {
rasterizer.rasterize(); // Draw our 3D object.
display.drawBuffer(); // Display the buffer that our 3D object was drawn on.
try {
Thread.sleep(10L); // Sleep so the animation doesn't run too fast!
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
That’s it for the setup! You should have a black screen when you run it now!
Section 2 : Math
Probably the most interesting and fun part of this tutorial!
All we need are two classes for the math:
Vector and Matrix
A Vector can represent many things depending on the context it is used in. It can be used to represent a force, a direction, a point, or many other things. A Vector is basically a set of associated values.
For us, we need a Vector to represent a point, and, because we are doing 3D, we need to have a Vector that stores three values, an X, a Y, and a Z.
X, Y, and Z represent the 3D space coordinates.
My very basic implementation of a Vector:
public class Vector {
private float x; // X coordinate
private float y; // Y coordinate
private float z; // Z coordinate
public Vector() {
this(0, 0, 0);
}
public Vector(float x, float y, float z) {
this.x = x;
this.y = y;
this.z = z;
}
// Subtract the components of the given Vector from this instance of Vector
public void subtract(Vector v) {
this.x -= v.getX();
this.y -= v.getY();
this.z -= v.getZ();
}
public float getX() {
return x;
}
public void setX(float x) {
this.x = x;
}
public float getY() {
return y;
}
public void setY(float y) {
this.y = y;
}
public float getZ() {
return z;
}
public void setZ(float z) {
this.z = z;
}
// To make a copy of this object so that the original is not modified.
@Override
public Vector clone() {
return new Vector(x, y, z);
}
// For debugging purposes (Rounds the value after two decimal places)
@Override
public String toString() {
return String.format("<%.2f, %.2f, %.2f>", x, y, z);
}
}
Now we move onto the Matrix
Don’t let a Matrix scare you, Matrices are actually just collections of values, like Vectors. A Vector is actually a 3x1 Matrix (In our case).
In order to rotate a point around the origin, you use Matrices.
The process to rotate a Vector around the origin is this:
- Set the matrix to the desired rotation.
- Multiply the matrix by the Vector you want to rotate.
- That’s it!
If you still do not understand, it will make sense once we implement it.
Our Matrix.java:
public final class Matrix {
// A 3x3 matrix is all we need. 3 (a, d, g) by 3 (a, b, c)
private float a, b, c;
private float d, e, f;
private float g, h, i;
public Vector multiply(Vector v) {
float vx = v.getX();
float vy = v.getY();
float vz = v.getZ();
float newX = (a * vx) + (d * vy) + (g * vz);
float newY = (b * vx) + (e * vy) + (h * vz);
float newZ = (c * vx) + (f * vy) + (i * vz);
return new Vector(newX, newY, newZ);
}
public void rotateX(float theta) {
this.a = 1;
this.b = 0;
this.c = 0;
this.d = 0;
this.e = (float) Math.cos(theta);
this.f = (float) Math.sin(theta);
this.g = 0;
this.h = (float) -Math.sin(theta);
this.i = (float) Math.cos(theta);
}
public void rotateY(float theta) {
this.a = (float) Math.cos(theta);
this.b = 0;
this.c = (float) -Math.sin(theta);
this.d = 0;
this.e = 1;
this.f = 0;
this.g = (float) Math.sin(theta);
this.h = 0;
this.i = (float) Math.cos(theta);
}
public void rotateZ(float theta) {
this.a = (float) Math.cos(theta);
this.b = (float) Math.sin(theta);
this.c = 0;
this.d = (float) -Math.sin(theta);
this.e = (float) Math.cos(theta);
this.f = 0;
this.g = 0;
this.h = 0;
this.i = 1;
}
}
That is it for the math!
Now onto finalizing this project.
Section 3 : Finalization
To make use of all of this math, we need to interpret it and use it for our graphics.
Actually, before I continue, I purposely left an important thing out of our Vector class.
We need to project the points onto the screen. We do that by using a simple calculation:
public void perspectiveProject() {
this.x = (DEPTH_OF_FIELD * this.x / -this.z) + 400; // 400 is half of the surface width.
this.y = 300 - (DEPTH_OF_FIELD * this.y / -this.z); // 300 is half of the surface height.
}
And DEPTH_OF_FIELD is set to 700.
This image (Taken from David Brackeen’s Developing Games in Java) explains where this came from.
Now, to get to finishing this tutorial up.
We need to make the long awaited for Rasterizer class, so here it is, fully commented so you can understand what is going on:
public final class Rasterizer {
private final Display display; // The Display to draw onto.
private float y; // Variable that increments in order to rotate our object smoothly.
public Rasterizer(Display display) {
this.display = display;
}
public void rasterize() {
display.getSurface().clear(); // Clear the surface (make it all black)
Vector camera = new Vector(0, 0, 10); // The camera Vector needs to be made to move back from the object
// because then the camera would be at the origin and it would not look ok
// These vertices make a 1x1x1 cube however it will not look like a cube because to make a cube, the vertices need to be
// connected in a certain order and for the sake of simplicity I left that out.
Vector[] vertices = new Vector[8];
vertices[0] = new Vector(-1, -1, -1);
vertices[1] = new Vector(-1, -1, 1);
vertices[2] = new Vector(-1, 1, -1);
vertices[3] = new Vector(-1, 1, 1);
vertices[4] = new Vector(1, -1, -1);
vertices[5] = new Vector(1, -1, 1);
vertices[6] = new Vector(1, 1, -1);
vertices[7] = new Vector(1, 1, 1);
Matrix mat = new Matrix(); // New matrix to do our rotation on.
y += 0.05f; //.05 gives it a good pace.
if (y > 6.28f) { // 2 pi (2 * 3.14) for full rotation. Remember that we don't use degrees, we use radians.
y = 0;
}
mat.rotateY(y); // Now, set the matrix to the values that you want.
for (int i = 0; i < vertices.length; i++) {
vertices[i] = mat.multiply(vertices[i]); // Multiply the matrix by all of the vertices to rotate them.
}
for (int i = 0; i < vertices.length; i++) {
vertices[i].subtract(camera); // Subtract the camera's location from all the vertices to move the object back.
}
for (int i = 0; i < vertices.length; i++) {
vertices[i].perspectiveProject(); // Project the points onto the screen.
}
// Draw lines between all vertices.
for (int i = 0; i < vertices.length - 1; i++) {
display.getSurface().drawLine((int) vertices[i].getX(), (int) vertices[i].getY(), (int) vertices[i + 1].getX(), (int) vertices[i + 1].getY(), 0xFFFF00);
}
}
}
That is it! You’ve got 3D working!
Please note that this is not efficient at all, there is a new Matrix created, a new camera Vector, and a new array of vertices being created every time a frame gets drawn.
This is what you should have created from this code:
This is my software rendered engine that I actually built off of this same code! Anything is possible!