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[] perspectiveMappingMatrices = new Matrix[]{null, null, null, null, null, null}; public boolean isInitialised; // fixed face public Face fixedFace; // working variables private final Vector3D traVec = new Vector3D(0,0, 0); // private final Vector2D scaVec = new Vector2D(0,0); private final Vector3D real01Vec = new Vector3D(0,0,0); private final Vector2D UV01Vec = new Vector2D(0,0); private final Vector3D real02Vec = new Vector3D(0,0,0); private final Vector2D UV02Vec = new Vector2D(0,0); double[] result; public void initialise(){ calculateNormal(); separateTris(); // fixedFace is a constant object which is used temporarily when the face intersects with the camera, // and thus needs to be sliced. fixedFace = new Face(); fixedFace.hasEdges = hasEdges; fixedFace.texture = texture; fixedFace.normal = normal; fixedFace.isInitialised = true; // the fixed face inherits the perspective mapping matrices from the true face. This is because the transforms // stay the same, just the edge bounds of the face have changed. fixedFace.perspectiveMappingMatrices = perspectiveMappingMatrices; 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 = applyPointTransforms(camMatrix, FPDis, scrX, scrY); //bakePerspectiveMatrices(); if (valid) { drawTris(img, zBuf, FPDis, scrX, scrY);} else { ArrayList<PointComp> newPoints = new ArrayList<>(); // if there are points behind the camera, loop through all the points and interpolate a point that is. // The perspective mapping matrix is calculated beforehand, so we don't need to move 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(); } Matrix identMat = new Matrix(3,3); // we use an identity matrix because the points of the fixed face are in camera coordinates already. // we just need the projected 2d point. identMat.setItems(new double[][]{ {1,0,0}, {0,1,0}, {0,0,1} }); fixedFace.applyPointTransforms(identMat, FPDis, scrX, scrY); fixedFace.drawTris(img, zBuf, FPDis, scrX, scrY); } } return numberOfPixels; } public void drawTris(BufferedImage img, BufferedImage zBuf, double FPDis, int scrX, int scrY) { for (Triangle tri : tris) { tri.draw(img, zBuf, FPDis, scrX, scrY); } } public boolean applyPointTransforms(Matrix camMatrix, double FPDis, int scrX, int scrY){ 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);} } return valid; } 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, perspectiveMappingMatrices[i]); } 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 bakePerspectiveMatrices() { // // one mapping matrix for each triangle // // to achieve perspective mapping, we need to convert from the 2d screen position, to the 3d world position by // // reverse projecting and interpolating in the triangle - this gives camera coordinates. // // next, we need to use the inverse camera matrix to convert from camera coordinates to world coordinates. // // then, we can use the perspective mapping matrix unique and baked to each triangle on each face to convert from those world coordinates, into uv coordinates. // // // // real01Vec.createFrom2Points(points[0].getRotatedPoint(), points[1].getRotatedPoint()); // real02Vec.createFrom2Points(points[0].getRotatedPoint(), points[2].getRotatedPoint()); // // scaVec.createFrom2Points(points[0].getProjectedPoint(), points[1].getProjectedPoint()); // UV01Vec.createFrom2Points(UVPoints[0], UVPoints[1]); // UV02Vec.createFrom2Points(UVPoints[0], UVPoints[2]); // // it must remain as the same object so pointers elsewhere still work. // // invert x and y coordinates because in rotated coordinates, they are the wrong way round. // perspectiveMappingMatrix.setItems(new double[][]{ // {0,1,0,0}, // {1,0,0,0}, // {0,0,1,0}, // {0,0,0,1}, // }); // // traVec.createFrom2Points(new Point3D(UVPoints[0].y, UVPoints[0].x, 0), points[0].getRotatedPoint()); // Matrix tMat = new Matrix(4, 4); //// tMat.setItems(new double[][]{ //// {1, 0, 0, traVec.x}, //// {0, 1, 0, traVec.y}, //// {0, 0, 1, traVec.z}, //// {0, 0, 0, 1}, //// }); // tMat.setItems(new double[][]{ // {1, 0, 0, 0}, // {0, 1, 0, 0}, // {0, 0, 1, points[0].getRotatedPoint().z}, // {0, 0, 0, 1}, // }); // double scale = 0.1;//(real01Vec.getLength() / UV01Vec.getLength()); // Matrix scaMat = new Matrix(4, 4); // scaMat.setItems(new double[][]{ // {scale, 0, 0, 0}, // {0, scale, 0, 0}, // {0, 0, scale, 0}, // {0, 0, 0, 1} // }); // // find z rotation and define matrix // double zAng = new Vector2D(real01Vec.x, real01Vec.y).angleTo(UV01Vec); // Matrix zMat = new Matrix(4, 4); // zMat.setItems(new double[][]{ // {Math.cos(zAng), Math.sin(zAng), 0, 0}, // {-Math.sin(zAng), Math.cos(zAng), 0, 0}, // {0, 0, 1, 0}, // {0, 0, 0, 1}} // ); // // rotate "real" vectors using the Z matrix // result = zMat.multiplyPoint3raw(real01Vec.x, real01Vec.y, real01Vec.z); // real01Vec.x = result[0];real01Vec.y = result[1];real01Vec.z = result[2]; // result = zMat.multiplyPoint3raw(real02Vec.x, real02Vec.y, real02Vec.z); // real02Vec.x = result[0];real02Vec.y = result[1];real02Vec.z = result[2]; // // invert the Z matrix (todo cleanup) // zMat.setItems(new double[][]{ // {Math.cos(-zAng), Math.sin(-zAng), 0, 0}, // {-Math.sin(-zAng), Math.cos(-zAng), 0, 0}, // {0, 0, 1, 0}, // {0, 0, 0, 1}} // ); // // find Y rotation and define matrix // double yAng = new Vector2D(real01Vec.x, real01Vec.z).angleTo(new Vector2D(UV01Vec.x, 0)); // Matrix yMat = new Matrix(4, 4); // yMat.setItems(new double[][]{ // {Math.cos(yAng), 0, -Math.sin(yAng), 0}, // {0, 1, 0, 0}, // {Math.sin(yAng), 0, Math.cos(yAng), 0}, // {0, 0, 0, 1}} // ); // result = yMat.multiplyPoint3raw(real02Vec.x, real02Vec.y, real02Vec.z); // real02Vec.x = result[0];real02Vec.y = result[1];real02Vec.z = result[2]; // yMat.setItems(new double[][]{ // {Math.cos(-yAng), 0, -Math.sin(-yAng), 0}, // {0, 1, 0, 0}, // {Math.sin(-yAng), 0, Math.cos(-yAng), 0}, // {0, 0, 0, 1}} // ); // double xAng = new Vector2D(real02Vec.y, real02Vec.z).angleTo(new Vector2D(UV01Vec.x, 0)); // this is fine // Matrix xMat = new Matrix(4, 4); // xMat.setItems(new double[][]{ // {1, 0, 0, 0}, // {0, Math.cos(-xAng), Math.sin(-xAng), 0}, // {0, -Math.sin(-xAng), Math.cos(-xAng), 0}, // {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(scaMat); // perspectiveMappingMatrix.multiply(scaMat.multiplyGetResult(tMat)); // //perspectiveMappingMatrix.multiply((scaMat.multiplyGetResult(xMat.multiplyGetResult(yMat.multiplyGetResult(zMat.multiplyGetResult(tMat)))))); // // perspectiveMappingMatrix.multiply(scaMat.multiplyGetResult(zMat.multiplyGetResult(yMat.multiplyGetResult(xMat.multiplyGetResult(tMat))))); // } }