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)); } lastPoint = point; lastValid = thisValid; } // there must be at least 3 points in the face for it to be drawn successfully if(newPoints.size() >= 3) { // 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))); } }