fbpx
Responsive Ad Area
Home » Blog » Simple 3D graphics using Java

Share This Post

Desktop / Featured Slider / Java

Simple 3D graphics using Java

Simple 3D graphics using Java

Isometric or Orthogonal 3D views

In this article, I demonstrate 3D Graphics showing you how to project a surface in 3D using 3 modes for drawing the surface. There is a left-hand mode, a center mode, and a right-hand mode. This same transformation math works for 3D polyhedron objects as well and you could use if for voxel type games. I show some effects you can use on the surface such as zoom, pan, skew etc. This is entirely wire frame. I do not remove hidden lines. I do not fill or texture map. And I do not rotate. I may do all this in future versions of this article. 90-degree rotation is very easy to do. Using basic trigonometry math more fine rotation is not difficult.

Center Mode 3D surface

Center Mode

Left Mode 3D Surface

Left Mode

Right Mode 3D Surface

Right Mode

The Iso3D class

I did not have to put this code in its own class. As a matter of fact, in such a simple demo application it was more difficult to do so. However, it might make the code easier to understand. Not that I couldn’t do more in that way. And its a basis for other Iso3D apps. For example, I may do an article to go with this one demonstrating the use of polyhedron voxel objects.

So as you can see I created a couple of Point objects. Point2D and Point3D. The transformation method takes a Point3D and returns a Point2D. There is a mode field which selects which transformation mode to use, Left, Center or Right.

public class Iso3D{
 public class Point2D{
  public Point2D(int x, int y){
   this.x=x;
   this.y=y;
  }
  int x=0;
  int y=0;
 }
 public class Point3D{
  public Point3D(int x, int y, int z){
   this.x=x;
   this.y=y;
   this.z=z;
  }
  int x=0;
  int y=0;
  int z=0;
 }
 public Iso3D(){
 }
 int mode=1;
 public final int LEFT_ISO=0;
 public final int CENTER_ISO=1;
 public final int RIGHT_ISO=2;
 public float ySkew=1.0f;
 public void setTransformMode(int mode){
  this.mode=mode;
 }
 public Point2D transform3D(Point3D point3D) throws Exception{
  switch(mode) {
   case LEFT_ISO:
   	 return new Point2D(point3D.x+point3D.z,(int)(((-point3D.y)+point3D.z)*ySkew));
   case CENTER_ISO:
   	return new Point2D(point3D.x+point3D.z,(int)(((-point3D.y)+point3D.z-point3D.x)*ySkew));
   case RIGHT_ISO:
   	return new Point2D(point3D.x-point3D.z,(int)(((-point3D.y)+point3D.z)*ySkew));
  default:throw new Exception("Invalid Transformation Mode! ["+mode+"]?");
  }
 }
 public static void main(String args[]){
  new Iso3D();
 }
}

The Panel Class, IsoPanel, and the Surface Data Generator Class

This is the 3D Graphics Panel Class that draws the surface and contains the control buttons. Also, this class has an Inner Class which contains and accesses the surface data which is a 2D array.  The surface has some parameters for sizes and such. There is grid size and grid square size. And to give a perception of some terrain parameters for a number of random elevation peaks to generate, max height, and min-height. Also, we have two 2D arrays. One for the initial data and one for an average that smooths the surface. It does this by scanning the surface and taking an average of all 9 points surrounding a given elevation point. We also scan the surface a few times.

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;

public class IsoPanel extends JPanel{
 public class Surface3D{
  int xSquareSize=5, ySquareSize=5;
  int xGridSize, yGridSize;
  int[][] surfaceElevations = null;
  int[][] surfaceElevationsAverage = null;
  public int getElevation(int x, int y){
   return surfaceElevations[y][x];
  }
  public void setElevation(int x, int y, int elevation){
   surfaceElevations[y][x]=elevation;
  }
  public int randomElevation(int minHeight, int maxHeight){
   return (int)(Math.random()*(maxHeight-minHeight)+minHeight);
  }
  public int getElevationAvg(int x, int y){
   return surfaceElevations[y][x];
  }
  public void setElevationAvg(int x, int y, int elevation){
   surfaceElevationsAverage[y][x]=elevation;
  }
  public int average(int x,int y){
   int total =getElevation(x-1,y-1);
   total+=getElevation(x,y-1);
   total+=getElevation(x+1,y-1);
   total+=getElevation(x-1,y);
   total+=getElevation(x,y);
   total+=getElevation(x+1,y);
   total+=getElevation(x-1,y+1);
   total+=getElevation(x,y+1);
   total+=getElevation(x+1,y+1);
   return (int)total/9;
  }
  Surface3D(int xGridSize, int yGridSize, int nRandomHeights, int minHeight, int maxHeight){
   this.xGridSize=xGridSize;
   this.yGridSize=yGridSize;
   surfaceElevations=new int[xGridSize][yGridSize];
   int randomX, randomY = 0;
   for(int i=0;i<nRandomHeights;i++){
    randomX=(int)(Math.random()*(xGridSize-2)+1);//-2 leaves 0's in outsize edge points
    randomY=(int)(Math.random()*(yGridSize-2)+1);
    setElevation(randomX, randomY, randomElevation(minHeight,maxHeight));
   }
   surfaceElevationsAverage=new int[xGridSize][yGridSize];
   for(int i=0;i<2;i++){
 for(int y=1;y<yGridSize-1;y++)
  for(int x=1;x<xGridSize-1;x++)
   setElevationAvg(x,y,average(x,y));
 surfaceElevations=surfaceElevationsAverage;
   }
  }  
 }
 Surface3D surface=new Surface3D(50,50,150,-300,300);
 public IsoPanel(){
 }
 public int originX=0;
 public int originY=0;
 public float ySkew=1.0f;
 public Iso3D iso3D= new Iso3D();
 public void paintComponent(Graphics g){
  g.setColor(Color.black);
  g.fillRect(0,0,1000,1000);
  g.setColor(Color.blue);
  Iso3D.Point2D point1= null;
  Iso3D.Point2D point2= null;
  int x=2;
  int y=1;
  int xs=50;
  int ys=50;
  xs=surface.xSquareSize;
  ys=surface.ySquareSize;
  g.setColor(Color.green);
  for(y=0;y<surface.yGridSize;y++)
   for(x=0;x<surface.xGridSize;x++){
    try{
      if(x<surface.xGridSize-1){
    point1=iso3D.transform3D(iso3D.new Point3D(x*xs,(int)(surface.getElevationAvg(x,y)*ySkew),y*ys));
    point2=iso3D.transform3D(iso3D.new Point3D((x+1)*xs,(int)(surface.getElevationAvg(x+1,y)*ySkew),y*ys));   
    g.drawLine(point1.x+originX,point1.y+originY,point2.x+originX,point2.y+originY);
   }
   if(y<surface.yGridSize-1){
    point1=iso3D.transform3D(iso3D.new Point3D(x*xs,(int)(surface.getElevationAvg(x,y)*ySkew),y*ys));
    point2=iso3D.transform3D(iso3D.new Point3D(x*xs,(int)(surface.getElevationAvg(x,y+1)*ySkew),(y+1)*ys));
    g.drawLine(point1.x+originX,point1.y+originY,point2.x+originX,point2.y+originY);
      }
 }catch(Exception e){
  e.printStackTrace();
 }
   }
 }
}

 

The Frame or Main Class

The main class simply sets up the 3D Graphics app frame and installs the IsoPanel. It sets up and installs the control buttons. Basically, after each press of a control button, we change some parameters and repaint the panel. In the panel, I had to draw a big black rectangle each time it paints. If you don’t then it doesn’t erase each previous draw.

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;

public class IsoTest extends JFrame {
 IsoTest(){
   JFrame aFrame=new JFrame("Isometric 3D Test");
   int step=50;
   IsoPanel isoPanel=new IsoPanel();
   JButton mode0Button = new JButton("Mode Left");
    mode0Button.addActionListener(new ActionListener() {
  public void actionPerformed(ActionEvent ae) {
   isoPanel.iso3D.mode=0;
   isoPanel.repaint();
  }
 }
   );
   isoPanel.add(mode0Button);
   JButton mode1Button = new JButton("Mode Center");
    mode1Button.addActionListener(new ActionListener() {
  public void actionPerformed(ActionEvent ae) {
   isoPanel.iso3D.mode=1;
   isoPanel.repaint();
  }
 }
   );
   isoPanel.add(mode1Button);
   JButton mode2Button = new JButton("Mode Right");
    mode2Button.addActionListener(new ActionListener() {
  public void actionPerformed(ActionEvent ae) {
   isoPanel.iso3D.mode=2;
   isoPanel.repaint();
  }
 }
   );
   isoPanel.add(mode2Button);
   
   JButton leftButton = new JButton("Left");
    leftButton.addActionListener(new ActionListener() {
  public void actionPerformed(ActionEvent ae) {
   isoPanel.originX-=step;
   isoPanel.repaint();
  }
 }
   );
   isoPanel.add(leftButton);
   JButton rightButton = new JButton("Right");
    rightButton.addActionListener(new ActionListener() {
  public void actionPerformed(ActionEvent ae) {
   isoPanel.originX+=step;
   isoPanel.repaint();
  }
 }
   );
   isoPanel.add(rightButton);
   JButton upButton = new JButton("Up");
    upButton.addActionListener(new ActionListener() {
  public void actionPerformed(ActionEvent ae) {
   isoPanel.originY-=step;
   isoPanel.repaint();
  }
 }
   );
   isoPanel.add(upButton);
   JButton downButton = new JButton("Down");
    downButton.addActionListener(new ActionListener() {
  public void actionPerformed(ActionEvent ae) {
   isoPanel.originY+=step;
   isoPanel.repaint();
  }
 }
   );
   isoPanel.add(downButton);
   JButton yIncButton = new JButton("Y+");
    yIncButton.addActionListener(new ActionListener() {
  public void actionPerformed(ActionEvent ae) {
   isoPanel.ySkew-=0.1f;
   if(isoPanel.ySkew==0)isoPanel.ySkew=-0.1f;
   isoPanel.repaint();
  }
 }
   );
   isoPanel.add(yIncButton);
   JButton yDecButton = new JButton("Y-");
    yDecButton.addActionListener(new ActionListener() {
  public void actionPerformed(ActionEvent ae) {
   isoPanel.ySkew+=0.1f;
   if(isoPanel.ySkew==0)isoPanel.ySkew=0.1f;
   isoPanel.repaint();
  }
 }
   );
   isoPanel.add(yDecButton);
   
   JButton yInc2DButton = new JButton("2D Y+");
    yInc2DButton.addActionListener(new ActionListener() {
  public void actionPerformed(ActionEvent ae) {
   isoPanel.iso3D.ySkew-=.1f;
   if(isoPanel.ySkew==0.0f)isoPanel.iso3D.ySkew=-0.1f;
   isoPanel.repaint();
  }
 }
   );
   isoPanel.add(yInc2DButton);
   JButton yDec2DButton = new JButton("2D Y-");
    yDec2DButton.addActionListener(new ActionListener() {
  public void actionPerformed(ActionEvent ae) {
   isoPanel.iso3D.ySkew+=.1f;
   if(isoPanel.ySkew==0.0f)isoPanel.iso3D.ySkew=0.1f;
   isoPanel.repaint();
  }
 }
   );
   isoPanel.add(yDec2DButton);
   JButton zoomInButton = new JButton("Zoom+");
    zoomInButton.addActionListener(new ActionListener() {
  public void actionPerformed(ActionEvent ae) {
   isoPanel.surface.xSquareSize-=1;
   isoPanel.surface.ySquareSize-=1;
   if(isoPanel.surface.xSquareSize<=0)isoPanel.surface.xSquareSize=1;
   if(isoPanel.surface.ySquareSize<=0)isoPanel.surface.ySquareSize=1;
   isoPanel.repaint();
  }
 }
   );
   isoPanel.add(zoomInButton);
   JButton zoomOutButton = new JButton("Zoom-");
    zoomOutButton.addActionListener(new ActionListener() {
  public void actionPerformed(ActionEvent ae) {
   isoPanel.surface.xSquareSize+=1;
   isoPanel.surface.ySquareSize+=1;
   isoPanel.repaint();
  }
 }
   );
   isoPanel.add(zoomOutButton);
   aFrame.add(isoPanel);
   aFrame.setSize(1000,800);                                   
   aFrame.setDefaultCloseOperation(aFrame.EXIT_ON_CLOSE);
   aFrame.show();
 }
 public static void main(String args[]){
  new IsoTest();
 }
}

What more could we do?

If you want to learn to store the surface data to disk see my article Reading and Writing files Tutorial Trail. We could do even more with this 3D Graphics app for fun. Filled polygon squares and use painters algorithm to remove hidden lines.  Colored lines or color filled polygons according to elevations. Its usually cool to color lower elevations in dark to light blue to look like water, middle elevations in brown to green for land and higher in black to white for mountains. We could provide some real terrain data. We could find a fractal algorithm to generate the surface with. On a project of mine on SourceForge Java Games and Graphics Project, I have some similar to this which generates what appears to be a tropical or volcanic island. It’s a simple algorithm that uses a stack starting from a high point and working down in random directions to sea level.

3D Graphics: Shows application that draws a tropical island looking surface.

Island3D of Java Games and Graphics Project

And lastly, we could find some affine transform algorithms and do some texture mapping to the surface. Could even work in some kind of shading based on a light source. We could add 90-degree rotation with ease, 360 degrees wouldn’t be difficult using standard trig functions though there are more efficient methods using trig tables or matrix math.

Could we turn this into some kind of 3D Graphics game?

Yes, I’ve often wanted to turn something like this into a game. A turn-based game would be nice. All depending on what features we have implemented we could do things one way or another. If you have ever played games like Alpha Centauri, it was on a slightly elevated surface. But not much. When you get too steep with the terrain you need rotation so that you can see whats on all sides of it. Or you need hollow painting algorithms that let you see behind terrain by not displaying what is in front.

For a game, we might actually use stick figure pieces and place them on each square. Depending on zoom level we might display colored dots in far zoom out to scaled vector graphics as we zoom in. 3D stick figure wireframe objects could even work. When you begin to get too much clutter in a wireframe scene, simply draw fewer details and focus on the action. There could be ways to work it using wireframe only.

We could also use bitmap graphics scaled at various zoom levels. These images might be flat or drawing with 90-degree rotations. Could use some animation to show movement or combat. Or not. It all depends on the amount of work you would like to put into it. Much can be done. I do not really see using this kind of surface for an RTS game but its probably possible.

Surface Editor

Could we write a surface editor? All that is needed is to make some controls to move a cursor around the grid. Then a control to raise or lower elevation points. And if we do this we need to make the data persistent by saving it to a file. You can see my Java files tutorial for ways to do this.

Share This Post

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>