diff --git a/src/main/java/tech/fastj/resources/models/ModelUtil.java b/src/main/java/tech/fastj/resources/models/ModelUtil.java index 31c15350..f79a47c8 100644 --- a/src/main/java/tech/fastj/resources/models/ModelUtil.java +++ b/src/main/java/tech/fastj/resources/models/ModelUtil.java @@ -23,11 +23,13 @@ private ModelUtil() { } private static final Map, Polygon2D[]>> ModelParser = Map.of( - SupportedModelFormats.Psdf, PsdfUtil::parse + SupportedModelFormats.Psdf, PsdfUtil::parse, + SupportedModelFormats.Obj, ObjUtil::parse ); private static final Map> ModelWriter = Map.of( - SupportedModelFormats.Psdf, PsdfUtil::write + SupportedModelFormats.Psdf, PsdfUtil::write, + SupportedModelFormats.Obj, ObjUtil::write ); /** diff --git a/src/main/java/tech/fastj/resources/models/MtlUtil.java b/src/main/java/tech/fastj/resources/models/MtlUtil.java new file mode 100644 index 00000000..7eeda6f9 --- /dev/null +++ b/src/main/java/tech/fastj/resources/models/MtlUtil.java @@ -0,0 +1,224 @@ +package tech.fastj.resources.models; + +import tech.fastj.engine.CrashMessages; +import tech.fastj.engine.FastJEngine; +import tech.fastj.math.Maths; +import tech.fastj.math.Pointf; +import tech.fastj.graphics.Boundary; +import tech.fastj.graphics.game.Model2D; +import tech.fastj.graphics.game.Polygon2D; + +import tech.fastj.resources.files.FileUtil; +import tech.fastj.resources.images.ImageResource; +import tech.fastj.resources.images.ImageUtil; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.LinearGradientPaint; +import java.awt.Paint; +import java.awt.RadialGradientPaint; +import java.awt.TexturePaint; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.text.SimpleDateFormat; +import java.util.Date; + +public class MtlUtil { + + private static final String LineSeparator = System.lineSeparator(); + + private MtlUtil() { + throw new java.lang.IllegalStateException(); + } + + public static void write(Path destinationPath, Model2D model) { + Path destinationPathWithoutSpaces = Path.of(destinationPath.toString().replace(' ', '_')); + StringBuilder fileContents = new StringBuilder(); + writeTimestamp(fileContents); + + for (int i = 0; i < model.getPolygons().length; i++) { + Polygon2D polygon2D = model.getPolygons()[i]; + writeMaterial(fileContents, polygon2D, destinationPathWithoutSpaces, i + 1); + } + + try { + Files.writeString(destinationPathWithoutSpaces, fileContents, StandardCharsets.US_ASCII); + } catch (IOException exception) { + throw new IllegalStateException(CrashMessages.theGameCrashed("a .mtl file writing error."), exception); + } + } + + private static void writeMaterial(StringBuilder fileContents, Polygon2D polygon, Path destinationPath, int materialIndex) { + fileContents.append(ParsingKeys.NewMaterial) + .append(' ') + .append("Polygon2D_material_") + .append(materialIndex) + .append(LineSeparator); + + Paint material = polygon.getFill(); + if (material instanceof LinearGradientPaint || material instanceof RadialGradientPaint) { + writeGradientMaterial(fileContents, polygon, destinationPath, materialIndex); + } else if (material instanceof Color) { + writeColorMaterial(fileContents, (Color) material); + } else if (material instanceof TexturePaint) { + writeTextureMaterial(fileContents, (TexturePaint) material, destinationPath, materialIndex); + } else { + FastJEngine.error( + CrashMessages.UnimplementedMethodError.errorMessage, + new UnsupportedOperationException( + "Writing paints other than LinearGradientPaint, RadialGradientPaint, Color, or TexturePaint is not supported." + + System.lineSeparator() + + "Check the github to confirm you are on the latest version, as that version may have more implemented features." + ) + ); + } + } + + private static void writeGradientMaterial(StringBuilder fileContents, Polygon2D polygon, Path destinationPath, int materialIndex) { + writeDefaultColorValues(fileContents); + + int extensionIndex = destinationPath.toString().indexOf(FileUtil.getFileExtension(destinationPath)); + Path texturePath = Path.of(destinationPath.toString().substring(0, extensionIndex - 1).replace(' ', '_') + "_gradient_" + materialIndex + ".png"); + + Pointf polygonSize = Pointf.subtract(polygon.getBound(Boundary.BottomRight), polygon.getBound(Boundary.TopLeft)).multiply(2f); + BufferedImage bufferedImage = ImageUtil.createBufferedImage((int) (polygonSize.x / 2) + 1, (int) (polygonSize.y / 2) + 1); + + Graphics2D graphics2D = bufferedImage.createGraphics(); + graphics2D.setPaint(polygon.getFill()); + graphics2D.translate(-bufferedImage.getWidth(), -bufferedImage.getHeight()); + graphics2D.fillRect(0, 0, (int) polygonSize.x + 1, (int) polygonSize.y + 1); + graphics2D.dispose(); + + ImageUtil.writeBufferedImage(bufferedImage, texturePath); + fileContents.append(ParsingKeys.TextureImage) + .append(' ') + .append(texturePath) + .append(LineSeparator) + .append(LineSeparator); + } + + private static void writeTextureMaterial(StringBuilder fileContents, TexturePaint material, Path destinationPath, int materialIndex) { + writeDefaultColorValues(fileContents); + + Path texturePath; + try { + texturePath = FastJEngine.getResourceManager(ImageResource.class).tryFindPathOfResource(material.getImage()).toAbsolutePath(); + if (texturePath.toString().contains("\\s+")) { + throw new IllegalStateException("The file path for a texture image in .mtl cannot contain whitespace."); + } + } catch (IllegalArgumentException exception) { + int extensionIndex = destinationPath.toString().indexOf(FileUtil.getFileExtension(destinationPath)); + texturePath = Path.of(destinationPath.toString().substring(0, extensionIndex - 1) + "_image_" + materialIndex + ".png"); + ImageUtil.writeBufferedImage(material.getImage(), texturePath); + } + + fileContents.append(ParsingKeys.TextureImage) + .append(' ') + .append(texturePath) + .append(LineSeparator) + .append(LineSeparator); + } + + private static void writeDefaultColorValues(StringBuilder fileContents) { + fileContents.append(ParsingKeys.AmbientColor) + .append(' ') + .append(String.format("%6f", 1f)) + .append(' ') + .append(String.format("%6f", 1f)) + .append(' ') + .append(String.format("%6f", 1f)) + .append(LineSeparator); + fileContents.append(ParsingKeys.DiffuseColor) + .append(' ') + .append(String.format("%6f", 1f)) + .append(' ') + .append(String.format("%6f", 1f)) + .append(' ') + .append(String.format("%6f", 1f)) + .append(LineSeparator); + writeSpecularValues(fileContents); + fileContents.append(ParsingKeys.Transparency) + .append(' ') + .append(String.format("%6f", 1f)) + .append(LineSeparator); + fileContents.append(ParsingKeys.IlluminationMode) + .append(' ') + .append(1) + .append(LineSeparator); + } + + private static void writeTimestamp(StringBuilder fileContents) { + fileContents.append("# Generated by the FastJ Game Engine https://github.com/fastjengine/FastJ") + .append(LineSeparator) + .append("# Timestamp: ") + .append(new SimpleDateFormat("dd-MM-yyyy HH:mm:ss").format(new Date())) + .append(LineSeparator) + .append(LineSeparator); + } + + private static void writeColorMaterial(StringBuilder fileContents, Color colorMaterial) { + fileContents.append(ParsingKeys.AmbientColor) + .append(' ') + .append(String.format("%6f", Maths.normalize(colorMaterial.getRed(), 0f, 255f))) + .append(' ') + .append(String.format("%6f", Maths.normalize(colorMaterial.getGreen(), 0f, 255f))) + .append(' ') + .append(String.format("%6f", Maths.normalize(colorMaterial.getBlue(), 0f, 255f))) + .append(LineSeparator); + fileContents.append(ParsingKeys.DiffuseColor) + .append(' ') + .append(String.format("%6f", Maths.normalize(colorMaterial.getRed(), 0f, 255f))) + .append(' ') + .append(String.format("%6f", Maths.normalize(colorMaterial.getGreen(), 0f, 255f))) + .append(' ') + .append(String.format("%6f", Maths.normalize(colorMaterial.getBlue(), 0f, 255f))) + .append(LineSeparator); + + writeSpecularValues(fileContents); + + fileContents.append(ParsingKeys.Transparency) + .append(' ') + .append(String.format("%6f", Maths.normalize(colorMaterial.getAlpha(), 0f, 255f))) + .append(LineSeparator); + fileContents.append(ParsingKeys.IlluminationMode) + .append(' ') + .append(1) + .append(LineSeparator) + .append(LineSeparator); + } + + private static void writeSpecularValues(StringBuilder fileContents) { + fileContents.append(ParsingKeys.SpecularColor) + .append(' ') + .append(String.format("%6f", 0f)) + .append(' ') + .append(String.format("%6f", 0f)) + .append(' ') + .append(String.format("%6f", 0f)) + .append(LineSeparator); + fileContents.append(ParsingKeys.SpecularExponent) + .append(' ') + .append(String.format("%6f", 1f)) + .append(LineSeparator); + } + + public static class ParsingKeys { + private ParsingKeys() { + throw new java.lang.IllegalStateException(); + } + + public static final String Empty = ""; + public static final String NewMaterial = "newmtl"; + public static final String AmbientColor = "Ka"; + public static final String DiffuseColor = "Kd"; + public static final String SpecularColor = "Ks"; + public static final String SpecularExponent = "Ns"; + public static final String Transparency = "d"; + public static final String IlluminationMode = "illum"; + public static final String TextureMap = "map_Ka"; + public static final String TextureImage = "map_Kd"; + } +} diff --git a/src/main/java/tech/fastj/resources/models/ObjUtil.java b/src/main/java/tech/fastj/resources/models/ObjUtil.java index 98ca4d1f..9dbc472f 100644 --- a/src/main/java/tech/fastj/resources/models/ObjUtil.java +++ b/src/main/java/tech/fastj/resources/models/ObjUtil.java @@ -1,10 +1,14 @@ package tech.fastj.resources.models; import tech.fastj.engine.CrashMessages; +import tech.fastj.math.Maths; import tech.fastj.math.Pointf; +import tech.fastj.graphics.Boundary; import tech.fastj.graphics.game.Model2D; import tech.fastj.graphics.game.Polygon2D; +import tech.fastj.resources.files.FileUtil; + import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -29,17 +33,27 @@ public static void write(Path destinationPath, Model2D model) { StringBuilder fileContents = new StringBuilder(); writeTimestamp(fileContents); - int vertexCount = 0; + Path destinationPathWithoutSpaces = Path.of(destinationPath.toString().replace(' ', '_')); + int extensionIndex = destinationPathWithoutSpaces.toString().indexOf(FileUtil.getFileExtension(destinationPathWithoutSpaces)); + Path materialPath = Path.of(destinationPathWithoutSpaces.toString().substring(0, extensionIndex) + "mtl"); + + writeMaterialLib(fileContents, materialPath); + int vertexCount = 0; for (int i = 0; i < model.getPolygons().length; i++) { - writeVertexes(fileContents, model.getPolygons()[i]); + writeVertexes(fileContents, model.getPolygons()[i], i); } + fileContents.append(LineSeparator); - writeVertexTextures(fileContents); + for (int i = 0; i < model.getPolygons().length; i++) { + writeVertexTextures(fileContents, model.getPolygons()[i]); + } + fileContents.append(LineSeparator); for (int i = 0; i < model.getPolygons().length; i++) { Polygon2D polygon = model.getPolygons()[i]; - writeObject(fileContents, polygon, i + 1); + writeObject(fileContents, i + 1); + writeMaterialUsage(fileContents, i + 1); writeFaces(fileContents, polygon, vertexCount); vertexCount += polygon.getPoints().length; @@ -48,8 +62,13 @@ public static void write(Path destinationPath, Model2D model) { try { Files.writeString(destinationPath, fileContents, StandardCharsets.US_ASCII); } catch (IOException exception) { - throw new IllegalStateException(CrashMessages.theGameCrashed("a ." + SupportedModelFormats.Obj + " file writing error."), exception); + throw new IllegalStateException( + CrashMessages.theGameCrashed("a ." + SupportedModelFormats.Obj + " file writing error."), + exception + ); } + + MtlUtil.write(materialPath, model); } private static void writeTimestamp(StringBuilder fileContents) { @@ -61,7 +80,16 @@ private static void writeTimestamp(StringBuilder fileContents) { .append(LineSeparator); } - private static void writeVertexes(StringBuilder fileContents, Polygon2D polygon) { + private static void writeMaterialLib(StringBuilder fileContents, Path materialPath) { + fileContents.append(ParsingKeys.MaterialLib) + .append(' ') + .append(materialPath.toString()) + .append(LineSeparator) + .append(LineSeparator); + } + + private static void writeVertexes(StringBuilder fileContents, Polygon2D polygon, int polygonIndex) { + float vertexSpace = polygonIndex / 1000f; for (int j = 0; j < polygon.getPoints().length; j++) { Pointf vertex = polygon.getPoints()[j]; fileContents.append(ParsingKeys.Vertex) @@ -70,41 +98,28 @@ private static void writeVertexes(StringBuilder fileContents, Polygon2D polygon) .append(' ') .append(String.format("%4f", vertex.y)) .append(' ') - .append(String.format("%4f", 0f)) + .append(String.format("%4f", vertexSpace)) .append(LineSeparator); } } - private static void writeVertexTextures(StringBuilder fileContents) { - fileContents.append(LineSeparator); - fileContents.append(ParsingKeys.VertexTexture) - .append(' ') - .append(0) - .append(' ') - .append(0) - .append(LineSeparator); - fileContents.append(ParsingKeys.VertexTexture) - .append(' ') - .append(1) - .append(' ') - .append(0) - .append(LineSeparator); - fileContents.append(ParsingKeys.VertexTexture) - .append(' ') - .append(1) - .append(' ') - .append(1) - .append(LineSeparator); - fileContents.append(ParsingKeys.VertexTexture) - .append(' ') - .append(0) - .append(' ') - .append(1) - .append(LineSeparator) - .append(LineSeparator); + private static void writeVertexTextures(StringBuilder fileContents, Polygon2D polygon) { + Pointf space = Pointf.subtract(polygon.getBound(Boundary.BottomRight), polygon.getBound(Boundary.TopLeft)); + Pointf topLeft = polygon.getBound(Boundary.TopLeft); + + for (int j = 0; j < polygon.getPoints().length; j++) { + float circleX = Maths.normalize(polygon.getPoints()[j].x - topLeft.x, 0f, space.x); + float circleY = Maths.normalize(polygon.getPoints()[j].y - topLeft.y, 0f, space.y); + fileContents.append(ParsingKeys.VertexTexture) + .append(' ') + .append(String.format("%4f", circleX)) + .append(' ') + .append(String.format("%4f", circleY)) + .append(LineSeparator); + } } - private static void writeObject(StringBuilder fileContents, Polygon2D polygon, int polygonIndex) { + private static void writeObject(StringBuilder fileContents, int polygonIndex) { fileContents.append(ParsingKeys.ObjectName) .append(' ') .append("Polygon2D_") @@ -112,10 +127,21 @@ private static void writeObject(StringBuilder fileContents, Polygon2D polygon, i .append(LineSeparator); } + private static void writeMaterialUsage(StringBuilder fileContents, int polygonIndex) { + fileContents.append(ParsingKeys.UseMaterial) + .append(' ') + .append("Polygon2D_material_") + .append(polygonIndex) + .append(LineSeparator); + } + private static void writeFaces(StringBuilder fileContents, Polygon2D polygon, int vertexCount) { fileContents.append(ParsingKeys.ObjectFace); for (int i = 1; i <= polygon.getPoints().length; i++) { - fileContents.append(' ').append(vertexCount + i); + fileContents.append(' ') + .append(vertexCount + i) + .append('/') + .append(vertexCount + i); } fileContents.append(LineSeparator).append(LineSeparator); } @@ -126,9 +152,11 @@ private ParsingKeys() { } public static final String Empty = ""; + public static final String MaterialLib = "mtllib"; public static final String Vertex = "v"; public static final String VertexTexture = "vt"; public static final String ObjectName = "g"; + public static final String UseMaterial = "usemtl"; public static final String ObjectFace = "f"; } } diff --git a/src/main/java/tech/fastj/resources/models/SupportedModelFormats.java b/src/main/java/tech/fastj/resources/models/SupportedModelFormats.java index 55fea719..acf9caae 100644 --- a/src/main/java/tech/fastj/resources/models/SupportedModelFormats.java +++ b/src/main/java/tech/fastj/resources/models/SupportedModelFormats.java @@ -4,6 +4,7 @@ public class SupportedModelFormats { public static final String Psdf = "psdf"; + public static final String Obj = "obj"; public static final String valuesString = Arrays.toString(values()); @@ -13,7 +14,8 @@ private SupportedModelFormats() { public static String[] values() { return new String[]{ - Psdf + Psdf, + Obj }; } }