Introduction
In this tutorial, we'll build a mesmerizing starfield effect that simulates flying through space at high speed. You'll learn about 3D to 2D projection, animation timing, and interactive controls. This classic demo effect has been used in everything from screensavers to space games!
- 3D coordinate systems and perspective projection
- Animation with Java Swing Timer
- Custom painting with Graphics2D
- Keyboard and mouse event handling
- Particle system basics
Step 1: Setting Up the Project Structure
Start by creating a JPanel that will contain our starfield. We'll extend JPanel and implement the necessary interfaces for animation and interactivity.
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.Random;
public class StarfieldZoom extends JPanel
implements ActionListener, KeyListener, MouseWheelListener {
private static final int WIDTH = 800;
private static final int HEIGHT = 600;
private static final int NUM_STARS = 500;
private Star[] stars;
private double speed = 2.0;
private Timer timer;
private Random random;
}
ActionListener- Receives timer events for animationKeyListener- Handles keyboard input for speed controlMouseWheelListener- Handles mouse wheel for fine speed adjustments
Step 2: Understanding the Star Class
Each star is a point in 3D space. We'll use x, y, z coordinates where:
- x and y represent horizontal and vertical position (range: -1 to 1)
- z represents depth - how far away the star is (range: 0 to 1, where 1 is far, 0 is close)
private class Star {
double x, y, z;
Star() {
reset();
}
void reset() {
// Random position in 3D space
x = random.nextDouble() * 2 - 1; // -1 to 1
y = random.nextDouble() * 2 - 1; // -1 to 1
z = random.nextDouble(); // 0 to 1
}
}
Step 3: The Magic - 3D to 2D Projection
This is where the magic happens! We need to convert 3D coordinates (x, y, z) into 2D screen coordinates (px, py). We use perspective projection, which makes closer objects appear larger.
k = focal_length / z
px = x * k + screen_center_x
py = y * k + screen_center_y
As z gets smaller (closer to camera), k gets larger, making the star appear farther from center and larger!
void draw(Graphics2D g2d) {
if (z <= 0) return; // Don't draw if star passed us
// Project 3D to 2D with perspective
double k = 128 / z; // 128 is our focal length
int px = (int)(x * k + WIDTH / 2);
int py = (int)(y * k + HEIGHT / 2);
// Check if star is on screen
if (px < 0 || px >= WIDTH || py < 0 || py >= HEIGHT) {
reset();
return;
}
// Size and brightness based on depth
int size = (int)((1 - z) * 3) + 1;
int brightness = (int)((1 - z) * 255);
brightness = Math.min(255, Math.max(0, brightness));
g2d.setColor(new Color(brightness, brightness, brightness));
g2d.fillOval(px - size / 2, py - size / 2, size, size);
}
Step 4: Creating Motion
To create the flythrough effect, we decrease each star's z value over time. As z approaches 0,
the star appears to rush towards us!
void update() {
z -= speed * 0.001; // Move star closer
if (z <= 0) {
reset(); // Star passed us, create new one far away
z = 1;
}
}
Step 5: Adding Motion Trails
To enhance the sense of speed, we draw a line from each star's current position to where it was in the previous frame.
// Draw motion trail (inside draw method)
double prevZ = z + speed * 0.001;
if (prevZ < 1) {
double prevK = 128 / prevZ;
int prevX = (int)(x * prevK + WIDTH / 2);
int prevY = (int)(y * prevK + HEIGHT / 2);
g2d.setColor(new Color(brightness / 2, brightness / 2, brightness / 2));
g2d.drawLine(px, py, prevX, prevY);
}
Step 6: Initializing the Starfield
public StarfieldZoom() {
setPreferredSize(new Dimension(WIDTH, HEIGHT));
setBackground(Color.BLACK);
setFocusable(true);
addKeyListener(this);
addMouseWheelListener(this);
random = new Random();
// Create all stars
stars = new Star[NUM_STARS];
for (int i = 0; i < NUM_STARS; i++) {
stars[i] = new Star();
}
// Start animation timer (16ms ≈ 60 FPS)
timer = new Timer(16, this);
timer.start();
}
Step 7: The Animation Loop
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
// Enable anti-aliasing for smoother graphics
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
// Draw all stars
for (Star star : stars) {
star.draw(g2d);
}
// Draw UI
g2d.setColor(Color.WHITE);
g2d.setFont(new Font("Arial", Font.PLAIN, 12));
g2d.drawString("Speed: " + String.format("%.1f", speed), 10, 20);
g2d.drawString("Up/Down arrows or Mouse Wheel to adjust speed", 10, 40);
}
@Override
public void actionPerformed(ActionEvent e) {
// Update all stars
for (Star star : stars) {
star.update();
}
repaint(); // Trigger redraw
}
Step 8: Adding Interactivity
@Override
public void keyPressed(KeyEvent e) {
switch (e.getKeyCode()) {
case KeyEvent.VK_UP:
speed = Math.min(20, speed + 0.5); // Max speed: 20
break;
case KeyEvent.VK_DOWN:
speed = Math.max(0.1, speed - 0.5); // Min speed: 0.1
break;
case KeyEvent.VK_SPACE:
// Reset all stars
for (Star star : stars) {
star.reset();
}
break;
}
}
@Override
public void mouseWheelMoved(MouseWheelEvent e) {
if (e.getWheelRotation() < 0) {
speed = Math.min(20, speed + 0.3); // Scroll up
} else {
speed = Math.max(0.1, speed - 0.3); // Scroll down
}
}
Step 9: Creating the Window
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
JFrame frame = new JFrame("Starfield Zoom Flythrough");
StarfieldZoom starfield = new StarfieldZoom();
frame.add(starfield);
frame.pack();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLocationRelativeTo(null); // Center on screen
frame.setVisible(true);
});
}
This ensures all Swing components are created on the Event Dispatch Thread (EDT), which is the proper way to build Swing GUIs and prevents threading issues.
How It All Works Together
Download complete code here: StarfieldZoom.java
Here's the execution flow:
- Initialization: 500 stars are created with random 3D positions
- Timer Tick (every 16ms):
- Each star's z coordinate decreases (moves closer)
- Stars that reach z=0 are reset to z=1 (far away)
- Screen is repainted
- Rendering:
- 3D coordinates are projected to 2D screen space
- Stars are drawn with size and brightness based on depth
- Motion trails are drawn for speed effect
- User Input: Speed is adjusted via keyboard or mouse wheel
Enhancements to Try
- Color Stars: Add random colors to stars instead of white
- Star Clusters: Create dense clusters by grouping stars
- Camera Control: Add mouse control to steer through the starfield
- Nebula Background: Draw colored gradients in the background
- Parallax Layers: Create multiple layers moving at different speeds
- Sound Effects: Add whoosh sounds when speed increases
The Math Behind It
Imagine you're looking down a long hallway. Objects farther away appear smaller and closer to the center of your vision. This is perspective!
The formula k = focal_length / z creates this effect mathematically:
- When z = 1 (far), k = 128, so x*128 = small displacement from center
- When z = 0.1 (close), k = 1280, so x*1280 = large displacement from center
This makes stars appear to "explode" outward from the center as they approach!