Skip to content

Instantly share code, notes, and snippets.

@Rogach
Last active November 28, 2024 06:10
Show Gist options
  • Save Rogach/f3dfd457d7ddb5fcfd99 to your computer and use it in GitHub Desktop.
Save Rogach/f3dfd457d7ddb5fcfd99 to your computer and use it in GitHub Desktop.
import javax.swing.*;
import java.awt.*;
import java.util.List;
import java.util.ArrayList;
import java.awt.geom.*;
import java.awt.image.BufferedImage;
public class DemoViewer {
public static void main(String[] args) {
JFrame frame = new JFrame();
Container pane = frame.getContentPane();
pane.setLayout(new BorderLayout());
// slider to control horizontal rotation
JSlider headingSlider = new JSlider(-180, 180, 0);
pane.add(headingSlider, BorderLayout.SOUTH);
// slider to control vertical rotation
JSlider pitchSlider = new JSlider(SwingConstants.VERTICAL, -90, 90, 0);
pane.add(pitchSlider, BorderLayout.EAST);
// slider to control roll
JSlider rollSlider = new JSlider(SwingConstants.VERTICAL, -90, 90, 0);
pane.add(rollSlider, BorderLayout.WEST);
// slider to control FoV
JSlider FoVSlider = new JSlider(1, 179, 60);
pane.add(FoVSlider, BorderLayout.NORTH);
// panel to display render results
JPanel renderPanel = new JPanel() {
public void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D) g;
g2.setColor(Color.GRAY);
g2.fillRect(0, 0, getWidth(), getHeight());
List<Triangle> tris = new ArrayList<>();
//A
tris.add(new Triangle(new Vertex(-100, 100, 100, 1),
new Vertex(100, 100, 100, 1),
new Vertex(-100, 100, -100, 1),
Color.WHITE));
//B
tris.add(new Triangle(new Vertex(100, 100, 100, 1),
new Vertex(100, 100, -100, 1),
new Vertex(-100, 100, -100, 1),
Color.WHITE));
//C
tris.add(new Triangle(new Vertex(100, -100, 100, 1),
new Vertex(100, 100, -100, 1),
new Vertex(100, 100, 100, 1),
Color.WHITE));
//D
tris.add(new Triangle(new Vertex(100, -100, 100, 1),
new Vertex(100, -100, -100, 1),
new Vertex(100, 100, -100, 1),
Color.WHITE));
//E
tris.add(new Triangle(new Vertex(-100, -100, 100, 1),
new Vertex(100, -100, 100, 1),
new Vertex(-100, 100, 100, 1),
Color.WHITE));
//F
tris.add(new Triangle(new Vertex(100, -100, 100, 1),
new Vertex(100, 100, 100, 1),
new Vertex(-100, 100, 100, 1),
Color.WHITE));
//G
tris.add(new Triangle(new Vertex(-100, -100, 100, 1),
new Vertex(-100, 100, 100, 1),
new Vertex(-100, -100, -100, 1),
Color.WHITE));
//H
tris.add(new Triangle(new Vertex(-100, 100, 100, 1),
new Vertex(-100, 100, -100, 1),
new Vertex(-100, -100, -100, 1),
Color.WHITE));
//I
tris.add(new Triangle(new Vertex(-100, 100, -100, 1),
new Vertex(100, 100, -100, 1),
new Vertex(-100, -100, -100, 1),
Color.WHITE));
//J
tris.add(new Triangle(new Vertex(-100, -100, -100, 1),
new Vertex(100, 100, -100, 1),
new Vertex(100, -100, -100, 1),
Color.WHITE));
//K
tris.add(new Triangle(new Vertex(100, -100, 100, 1),
new Vertex(-100, -100, 100, 1),
new Vertex(-100, -100, -100, 1),
Color.WHITE));
//L
tris.add(new Triangle(new Vertex(-100, -100, -100, 1),
new Vertex(100, -100, -100, 1),
new Vertex(100, -100, 100, 1),
Color.WHITE));
double heading = Math.toRadians(headingSlider.getValue());
Matrix4 headingTransform = new Matrix4(new double[] {
Math.cos(heading), 0, -Math.sin(heading), 0,
0, 1, 0, 0,
Math.sin(heading), 0, Math.cos(heading), 0,
0, 0, 0, 1
});
double pitch = Math.toRadians(pitchSlider.getValue());
Matrix4 pitchTransform = new Matrix4(new double[] {
1, 0, 0, 0,
0, Math.cos(pitch), Math.sin(pitch), 0,
0, -Math.sin(pitch), Math.cos(pitch), 0,
0, 0, 0, 1
});
double roll = Math.toRadians(rollSlider.getValue());
Matrix4 rollTransform = new Matrix4(new double[] {
Math.cos(roll), -Math.sin(roll), 0, 0,
Math.sin(roll), Math.cos(roll), 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
});
Matrix4 panOutTransform = new Matrix4(new double[] {
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, -400, 1
});
double viewportWidth = getWidth();
double viewportHeight = getHeight();
double fovAngle = Math.toRadians(FoVSlider.getValue());
double fov = Math.tan(fovAngle / 2) * 170;
Matrix4 transform =
headingTransform
.multiply(pitchTransform)
.multiply(rollTransform)
.multiply(panOutTransform);
BufferedImage img = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB);
double[] zBuffer = new double[img.getWidth() * img.getHeight()];
// initialize array with extremely far away depths
for (int q = 0; q < zBuffer.length; q++) {
zBuffer[q] = Double.NEGATIVE_INFINITY;
}
for (Triangle t : tris) {
Vertex v1 = transform.transform(t.v1);
Vertex v2 = transform.transform(t.v2);
Vertex v3 = transform.transform(t.v3);
Vertex ab = new Vertex(v2.x - v1.x, v2.y - v1.y, v2.z - v1.z, v2.w - v1.w);
Vertex ac = new Vertex(v3.x - v1.x, v3.y - v1.y, v3.z - v1.z, v3.w - v1.w);
Vertex norm = new Vertex(
ab.y * ac.z - ab.z * ac.y,
ab.z * ac.x - ab.x * ac.z,
ab.x * ac.y - ab.y * ac.x,
1
);
double normalLength = Math.sqrt(norm.x * norm.x + norm.y * norm.y + norm.z * norm.z);
norm.x /= normalLength;
norm.y /= normalLength;
norm.z /= normalLength;
double angleCos = Math.abs(norm.z);
v1.x = v1.x / (-v1.z) * fov;
v1.y = v1.y / (-v1.z) * fov;
v2.x = v2.x / (-v2.z) * fov;
v2.y = v2.y / (-v2.z) * fov;
v3.x = v3.x / (-v3.z) * fov;
v3.y = v3.y / (-v3.z) * fov;
v1.x += viewportWidth / 2;
v1.y += viewportHeight / 2;
v2.x += viewportWidth / 2;
v2.y += viewportHeight / 2;
v3.x += viewportWidth / 2;
v3.y += viewportHeight / 2;
int minX = (int) Math.max(0, Math.ceil(Math.min(v1.x, Math.min(v2.x, v3.x))));
int maxX = (int) Math.min(img.getWidth() - 1, Math.floor(Math.max(v1.x, Math.max(v2.x, v3.x))));
int minY = (int) Math.max(0, Math.ceil(Math.min(v1.y, Math.min(v2.y, v3.y))));
int maxY = (int) Math.min(img.getHeight() - 1, Math.floor(Math.max(v1.y, Math.max(v2.y, v3.y))));
double triangleArea = (v1.y - v3.y) * (v2.x - v3.x) + (v2.y - v3.y) * (v3.x - v1.x);
for (int y = minY; y <= maxY; y++) {
for (int x = minX; x <= maxX; x++) {
double b1 = ((y - v3.y) * (v2.x - v3.x) + (v2.y - v3.y) * (v3.x - x)) / triangleArea;
double b2 = ((y - v1.y) * (v3.x - v1.x) + (v3.y - v1.y) * (v1.x - x)) / triangleArea;
double b3 = ((y - v2.y) * (v1.x - v2.x) + (v1.y - v2.y) * (v2.x - x)) / triangleArea;
if (b1 >= 0 && b1 <= 1 && b2 >= 0 && b2 <= 1 && b3 >= 0 && b3 <= 1) {
double depth = b1 * v1.z + b2 * v2.z + b3 * v3.z;
int zIndex = y * img.getWidth() + x;
if (zBuffer[zIndex] < depth) {
img.setRGB(x, y, getShade(t.color, angleCos).getRGB());
zBuffer[zIndex] = depth;
}
}
}
}
}
g2.drawImage(img, 0, 0, null);
}
};
pane.add(renderPanel, BorderLayout.CENTER);
headingSlider.addChangeListener(e -> renderPanel.repaint());
pitchSlider.addChangeListener(e -> renderPanel.repaint());
rollSlider.addChangeListener(e -> renderPanel.repaint());
FoVSlider.addChangeListener(e -> renderPanel.repaint());
frame.setSize(400, 400);
frame.setVisible(true);
}
public static Color getShade(Color color, double shade) {
double redLinear = Math.pow(color.getRed(), 2.4) * shade;
double greenLinear = Math.pow(color.getGreen(), 2.4) * shade;
double blueLinear = Math.pow(color.getBlue(), 2.4) * shade;
int red = (int) Math.pow(redLinear, 1/2.4);
int green = (int) Math.pow(greenLinear, 1/2.4);
int blue = (int) Math.pow(blueLinear, 1/2.4);
return new Color(red, green, blue);
}
}
class Vertex {
double x;
double y;
double z;
double w;
Vertex(double x, double y, double z, double w) {
this.x = x;
this.y = y;
this.z = z;
this.w = w;
}
}
class Triangle {
Vertex v1;
Vertex v2;
Vertex v3;
Color color;
Triangle(Vertex v1, Vertex v2, Vertex v3, Color color) {
this.v1 = v1;
this.v2 = v2;
this.v3 = v3;
this.color = color;
}
}
class Matrix4 {
double[] values;
Matrix4(double[] values) {
this.values = values;
}
Matrix4 multiply(Matrix4 other) {
double[] result = new double[16];
for (int row = 0; row < 4; row++) {
for (int col = 0; col < 4; col++) {
for (int i = 0; i < 4; i++) {
result[row * 4 + col] +=
this.values[row * 4 + i] * other.values[i * 4 + col];
}
}
}
return new Matrix4(result);
}
Vertex transform(Vertex in) {
return new Vertex(
in.x * values[0] + in.y * values[4] + in.z * values[8] + in.w * values[12],
in.x * values[1] + in.y * values[5] + in.z * values[9] + in.w * values[13],
in.x * values[2] + in.y * values[6] + in.z * values[10] + in.w * values[14],
in.x * values[3] + in.y * values[7] + in.z * values[11] + in.w * values[15]
);
}
@Override public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("[");
for (int row = 0; row < 4; row++) {
for (int col = 0; col < 4; col++) {
sb.append(values[row * 4 + col]);
if (col != 3) {
sb.append(",");
}
}
if (row != 3) {
sb.append(";\n ");
}
}
sb.append("]");
return sb.toString();
}
}
@cergean
Copy link

cergean commented Mar 11, 2016

if the vertices in triangles was correctly ordered (in same order for each triangle clockwise for example) - it would be very easy to implement backface culling right here. for example, if norm.z<0 you could skip drawing entire triangle. You need just swap second and third points for first and third Triangle in initial tris list, and do the same for second and third resulting triangles in the inflate method. Thank you for such an simple yet working code)

@Strauteka
Copy link

You saved my day! Thanks!!

@kerrman
Copy link

kerrman commented Jul 23, 2018

How do I write a texture to the "triangle"? I was hoping to be able to do that with a basic 3d render program.

@rvishwajith
Copy link

Any chance you would be able to do this with camera motion as well? Like in a game engine. If you would be willing to do this, that would be immensely helpful for something I'm trying to do.

@Rogach
Copy link
Author

Rogach commented Mar 2, 2020

@rohithv2312 Unfortunately I'm currently very pressed for time, so I won't be able to create a big example. But the basic idea for camera movement is already present in the code - what headingSlider, pitchSlider and rollSlider do is actually to move the camera around the object (and panOutTransform shows an example of translation alongside Z axis). I think that you can just move the panOutTransform before all the other transforms in the multiplication:

                    Matrix4 transform =
                        panOutTransform
                        .multiply(headingTransform)
                        .multiply(pitchTransform)
                        .multiply(rollTransform);

and you will get a camera that rotates around its own position, not around the object. After that you will only need to create the controls to change X, Y, Z coordinates of the camera (and place those into panOutTransform, which by then should probably be called cameraPositionTransform).

@rvishwajith
Copy link

Thank you so much! I made the mistake of thinking the cube was rotating on its own.

@StephanPreibisch
Copy link

Nice script, thanks so much!

@FabulousCodingFox
Copy link

The script isnt accepted by IntelliJ 2020.1, you have to change "2" to "2.0" at line 87 and the following ones. Else its working perfectly!

@donell123
Copy link

can this 3D engine be used for representing an l-system?

@donell123
Copy link

and how did you figured out the coordinates of each verticle in order to have a proper representation an the 3D onbjet, in this case the pyramid?

@Rogach
Copy link
Author

Rogach commented Mar 25, 2023

@donell123

can this 3D engine be used for representing an l-system?

Sure, why not.

and how did you figured out the coordinates of each verticle in order to have a proper representation an the 3D onbjet, in this case the pyramid?

By thinking a lot and using trigonometry :) But for the tetrahedron ("the pyramid") the values are very simple, no trigonometry needed - if you align two of the edges with the axes.

@donell123
Copy link

Hello, i was making some test with this rendering engine and I figured out something. I wanted to change the location of the tetrahedron
using a translation matrix in order to translate each single vertex of each triangle that composing it. But By doing so, the transformation change the size of the tetrahedron. But I just want to change the location without doing anything else transformation, how can achieve that ?

@Rogach
Copy link
Author

Rogach commented Mar 26, 2023

@donell123 Can you show what translation matrix did you use?

@donell123
Copy link

of course , i did something like this:
matrix4 T=new Matrix4(new double[] {
1, 0, 0, a,
0, 1, 0, b,
0, 0, 1, c,
0, 0, 0, 1
});
with a,b,c the value i wanted to add. I wanted to put two tetrahedron one above the other, to be precise I wanted on triangle to be located for example 100 unit in the y-axis above the other. So I thought that the following matrix could do the job:

[ 1, 0, 0, 0
0, 1, 0, 100
0, 0, 1, 0
0, 0, 0, 1 ]

I tried several times, but transforming each vertixes by a translation matrix manage to change the size of the tetrahedron.

@Rogach
Copy link
Author

Rogach commented Mar 27, 2023

@donell123 I'm not sure (because I don't see your entire code), but if your Matrix4 class was simply my Matrix3 extended to 4th row and column, then your matrix needs to be transposed:

[ 1, 0, 0, 0
0, 1, 0, 0
0, 0, 1, 0
0, 100, 0, 1 ]

@kenvifire
Copy link

Why do we need this part in inflate function?
for (Triangle t : result) { for (Vertex v : new Vertex[] { t.v1, t.v2, t.v3 }) { double l = Math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z) / Math.sqrt(30000); v.x /= l; v.y /= l; v.z /= l; } }

@Rogach
Copy link
Author

Rogach commented Nov 5, 2023

@kenvifire
This part moves all the vertices such that they will all be at the same distance from the origin (point 0,0,0). Without it the subdivided triangles will still be looking like the same original triangle.
Try removing this part and you will see what I mean.

@kenvifire
Copy link

@kenvifire This part moves all the vertices such that they will all be at the same distance from the origin (point 0,0,0). Without it the subdivided triangles will still be looking like the same original triangle. Try removing this part and you will see what I mean.

Got it, thanks.

@yawarinti
Copy link

Excelent article, thank you a lot to share, you are awesome.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment