Newer
Older
EPQ-3D-renderer / src / Face.java
import java.awt.image.BufferedImage;
import java.util.ArrayList;

public class Face {
    public PointComp[] points;
    public Point2D[] UVPoints;
    public Vector3D normal;
    public Triangle[] tris;
    public boolean hasEdges;
    public BufferedImage texture;
    public Matrix perspectiveMappingMatrix = new Matrix(3, 3);
    public boolean isInitialised;
    // fixed face
    public Face fixedFace;
    // working variables
    private final Vector2D traVec = new Vector2D(0,0);
    private final Vector2D orig01Vec = new Vector2D(0,0);
    private final Vector2D fin01Vec = new Vector2D(0,0);
    private final Vector2D orig02Vec = new Vector2D(0,0);
    private final Vector2D fin02Vec = new Vector2D(0,0);
    public void initialise(){
        calculateNormal();
        separateTris();
        fixedFace = new Face();
        fixedFace.hasEdges = hasEdges;
        fixedFace.texture = texture;
        fixedFace.normal = normal;
        fixedFace.perspectiveMappingMatrix = new Matrix(3, 3);
        fixedFace.perspectiveMappingMatrix.setItems(new double[][] {
                {1,0,0},
                {0,1,0},
                {0,0,1},
        });
        fixedFace.isInitialised = true;
        isInitialised = true;
    }

    public void invalidate(){
        for (Triangle tri:
             tris) {
            tri.invalidate();
        }
    }
    public int draw(BufferedImage img, BufferedImage zBuf, BufferedImage debugImg, Matrix camMatrix, double FPDis, int scrX, int scrY){
        if(!isInitialised){
            throw new RuntimeException("Face not initialised");
        }
        // check for backface culling has been done previously.
        // initialise points
        int numberOfPixels = 0;
        boolean valid = true;
        for (PointComp point:
             points) {
            point.setRotatedPoint(camMatrix);
            // if any points are behind the camera, we will need to handle it differently.
            if(point.getRotatedPoint().z < 0.1){valid = false;}
            // only worth calculating the projected point if they are all valid
            if(valid){point.setProjectedPoint(FPDis, scrX, scrY);}
        }
        // calculatePerspectiveMapping();

        if(valid){
            for (Triangle tri:
                tris) {
            numberOfPixels += tri.draw(img, zBuf);
        }
        } else {
            ArrayList<PointComp> newPoints = new ArrayList<>();
            // if there are points behind the camera, loop through all the points and interpolate a point that is
            // (including UVS!) // todo UVs
            PointComp lastPoint = points[points.length - 1];
            boolean lastValid = lastPoint.getRotatedPoint().z > 0.1;
            boolean thisValid;
            for (PointComp point:
                    points) {
                thisValid = point.getRotatedPoint().z > 0.1;
                // we need to do different things depending on whether the previous point was also a valid point, or not.
                // first - if only 1 of the last point or this point were valid,
                // interpolate between them to get the point at the screen. (XOR)
                if(lastValid ^ thisValid){
                    // solving for z = 0.1 for the line between thisPoint and lastPoint,
                    // separately in the xz and yz planes.
                    double gradX = (point.getRotatedPoint().z - lastPoint.getRotatedPoint().z) /
                            (point.getRotatedPoint().x - lastPoint.getRotatedPoint().x);
                    double gradY = (point.getRotatedPoint().z - lastPoint.getRotatedPoint().z) /
                            (point.getRotatedPoint().y - lastPoint.getRotatedPoint().y);

                    newPoints.add(new PointComp(
                            (0.1+gradX*point.getRotatedPoint().x-point.getRotatedPoint().z)/gradX,
                            (0.1+gradY*point.getRotatedPoint().y-point.getRotatedPoint().z)/gradY,
                            0.1));
                    if(Double.isFinite(gradX)){
                        newPoints.get(newPoints.size() - 1).point.x = point.getRotatedPoint().x;}
                    if(Double.isFinite(gradY)){
                        newPoints.get(newPoints.size() - 1).point.y = point.getRotatedPoint().y;}
                }
                // finally - if the current point is valid, then add it to the list
                if(thisValid){
                    newPoints.add(new PointComp(
                            point.getRotatedPoint().x,
                            point.getRotatedPoint().y,
                            point.getRotatedPoint().z));
                }
                lastValid = thisValid;
            }
            // finished fixing points, now we need to create a new face consisting of those points.
            fixedFace.points = newPoints.toArray(new PointComp[0]);
            fixedFace.separateTris();
            // invalidate all the points so they are actually calculated
            for (PointComp point:
                 newPoints) {
                point.invalidate();
            }
            fixedFace.draw(img, zBuf, debugImg, fixedFace.perspectiveMappingMatrix, FPDis, scrX, scrY);
        }
        return numberOfPixels;
    }
    public void separateTris(){
        Triangle[] newTris = new Triangle[points.length - 2];
        for(int i = 0; i< newTris.length; i+=1){
            newTris[i] = new Triangle(
                    points[0].getProjectedPoint(),
                    points[i+1].getProjectedPoint(),
                    points[i+2].getProjectedPoint(),
                    new boolean[]{hasEdges, hasEdges, hasEdges},
                    texture, perspectiveMappingMatrix);
        }
        tris = newTris;
    }
    public void calculateNormal(){
        // too many new variables
        Point3D point0 = points[0].point;
        Point3D point1 = points[1].point;
        Point3D point2;
        Vector3D vec1 = new Vector3D(point1.x - point0.x, point1.y - point0.y, point1.z - point0.z);
        Vector3D vec2 = new Vector3D(0,0,0); // initialisation otherwise intellij gets mad
        // find a vector which is not inline with other vectors
        boolean valid = false; int i = 2;
        while(!valid && i < points.length) {
            point2 = points[i].point;
            vec2 = new Vector3D(point2.x - point0.x, point2.y - point0.y, point2.z - point0.z);
            double angle = Math.abs(vec1.angleTo(vec2));
            if(angle > 0.1 && angle < 2*Math.PI - 0.1){
                //  if the angle between the vectors is between a threshold, the two vectors are valid.
                // else, calculate the second vector using a different set of points.
                valid = true;
            }}
        if(!valid){throw new RuntimeException("Could not calculate normal of face");}
        normal = vec1.cross(vec2);
    }
    private void calculatePerspectiveMapping() {
        traVec.createFrom2Points(UVPoints[0], points[0].getProjectedPoint());
        orig01Vec.createFrom2Points(points[0].getProjectedPoint(), points[1].getProjectedPoint());
        orig02Vec.createFrom2Points(points[0].getProjectedPoint(), points[2].getProjectedPoint());
        fin01Vec.createFrom2Points(UVPoints[0], UVPoints[1]);
        fin02Vec.createFrom2Points(UVPoints[0], UVPoints[2]);

        // setting the perspective matrix to the identity matrix -
        // it must remain as the same object so pointers elsewhere still work.
        perspectiveMappingMatrix.setItems(new double[][]{
                {1,0,0},
                {0,1,0},
                {0,0,1},
        });
        Matrix tMat = new Matrix(3, 3);
        tMat.setItems(new double[][]{
                {1, 0, -traVec.x},
                {0, 1, -traVec.y},
                {0, 0, 1}
        });
        double scale = (fin01Vec.getLength()) / orig01Vec.getLength();
        Matrix scaMat = new Matrix(3, 3);
        scaMat.setItems(new double[][]{
                {scale, 0,     0},
                {0,     scale, 0},
                {0,     0,     1}
        });
        double ang = orig01Vec.angleTo(fin01Vec);
        Matrix rotMat = new Matrix(3, 3);
        rotMat.setItems(new double[][]{
                {Math.cos(ang), -Math.sin(ang), 0},
                {Math.sin(ang), Math.cos(ang), 0},
                {0, 0, 1}
        });
        // Matrix that returns the position on a texture from 3d camera space on the face of an object
        // we already know the Z depth because of calculations for Z buffers.
        //perspectiveMappingMatrix.multiply(rotMat.multiplyGetResult(scaMat.multiplyGetResult(tMat)));
        perspectiveMappingMatrix.multiply(scaMat.multiplyGetResult(rotMat.multiplyGetResult(tMat)));
    }
}