From 567425acd8822c8d7bb5b5ce00c8710eecc3aa40 Mon Sep 17 00:00:00 2001 From: Ned Loynd <41816363+NeRdTheNed@users.noreply.github.com> Date: Mon, 2 Aug 2021 21:14:52 +1000 Subject: [PATCH] [WIP-ish] Implement initial gear rotation generator, refactor unit tests to be worse - MC-TextureGen now generates textures for every angle of the gear rotation animation! The code is very unreadable right now, and needs to be refactored. - The TextureGenerator class now has some math utility methods to help with the gear rotation generator. These utility methods should produce identical results to Minecraft's, because they're both based on Riven's code. - The unit tests are now worse. I've implemented a messy way of signalling that something went wrong with a TextureGenerator, but this needs to be reworked. I've had this code nearly finished for some weeks now, and I've decided that I might as well commit it as-is. I've been moving into a new house, and haven't had much time to work on this project, so this code is not really up to standard. I'll probably refactor it a bit before a new release. --- README.md | 15 +- .../java/mcTextureGen/MCTextureGenerator.java | 3 +- .../GearRotationFramesGenerator.java | 138 ++++++++++++++++++ .../generators/TextureGenerator.java | 34 +++++ src/main/resources/gear.png | Bin 0 -> 1116 bytes src/main/resources/gearmiddle.png | Bin 0 -> 228 bytes .../test/MCTextureGeneratorTest.java | 80 ++++++---- 7 files changed, 234 insertions(+), 36 deletions(-) create mode 100644 src/main/java/mcTextureGen/generators/GearRotationFramesGenerator.java create mode 100755 src/main/resources/gear.png create mode 100755 src/main/resources/gearmiddle.png diff --git a/README.md b/README.md index f881987..7d426a0 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,17 @@ # MC Texture Generator + A Java program that programatically generates textures generated by certain versions of Minecraft at runtime, and then saves them to individual files. -Currently, this program generates textures from the following Minecraft versions: -- Minecraft 4k-1 -- Minecraft 4k-2 +Currently, this program generates: + +- All textures generated by Minecraft 4k-1 +- All textures generated by Minecraft 4k-2 +- All frames of the gear rotation animation -### How to generate the textures. +## How to generate the textures. -Make sure to have a Java runtime environment of 7 or higher installed on your computer. The easiest way to run this program is just to double click the .jar file, which will generate and save all textures to a folder named "GeneratedTextures". The .jar file can also be run from the command line. Doing so will allow you to see log output when generating the textures, which can be useful if you need to debug anything. When this program is run from the command line, the "GeneratedTextures" folder will be placed at the current directory that your command line is in. +Make sure to have a Java runtime environment of 6 or higher installed on your computer. The easiest way to run this program is just to double click the .jar file, which will generate and save all textures to a folder named "GeneratedTextures". The .jar file can also be run from the command line. Doing so will allow you to see log output when generating the textures, which can be useful if you need to debug anything. When this program is run from the command line, the "GeneratedTextures" folder will be placed at the current directory that your command line is in. -### Why have a dedicated program for this? +## Why have a dedicated program for this? The idea behind this program is to create an improvably accurate source for these textures - if this code contains any mistakes, the mistakes can simply be fixed, and the textures will then be generated accurately. The same improvement in accuracy cannot be achieved for an inaccurate screenshot, or a file accidentally saved in a reduced resolution or color depth. diff --git a/src/main/java/mcTextureGen/MCTextureGenerator.java b/src/main/java/mcTextureGen/MCTextureGenerator.java index 0c342be..c0eecc4 100644 --- a/src/main/java/mcTextureGen/MCTextureGenerator.java +++ b/src/main/java/mcTextureGen/MCTextureGenerator.java @@ -6,6 +6,7 @@ import javax.imageio.ImageIO; import mcTextureGen.data.TextureGroup; +import mcTextureGen.generators.GearRotationFramesGenerator; import mcTextureGen.generators.MC4k1Generator; import mcTextureGen.generators.MC4k2Generator; import mcTextureGen.generators.TextureGenerator; @@ -15,7 +16,7 @@ public final class MCTextureGenerator { // private static boolean hasDebugInfo = true; public static TextureGenerator[] getTextureGenerators() { - return new TextureGenerator[] { new MC4k1Generator(), new MC4k2Generator() }; + return new TextureGenerator[] { new MC4k1Generator(), new MC4k2Generator(), new GearRotationFramesGenerator() }; } public static void main(final String[] args) { diff --git a/src/main/java/mcTextureGen/generators/GearRotationFramesGenerator.java b/src/main/java/mcTextureGen/generators/GearRotationFramesGenerator.java new file mode 100644 index 0000000..5eea884 --- /dev/null +++ b/src/main/java/mcTextureGen/generators/GearRotationFramesGenerator.java @@ -0,0 +1,138 @@ +package mcTextureGen.generators; + +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferByte; +import java.io.IOException; + +import javax.imageio.ImageIO; + +import mcTextureGen.data.TextureGroup; + +public final class GearRotationFramesGenerator extends TextureGenerator { + + /** + * This variable controls how many distinct angles (frames) the gear animation has. + * Minecraft had 64 distinct angles. + * The gear rotation animation would advance by one frame on every tick. + */ + private static final int gearRotationSteps = 64; + + /** + * This variable can be changed to create higher resolution output rotated textures. + * This is just for convenience. + */ + private static final int rotatedTextureSizeMultiplier = 1; + + /** + * gearmiddle.png has a resolution of 16 by 16. + */ + private static final int middleGearTextureSize = 16; + + /** + * gear.png has a resolution of 32 by 32. + */ + private static final int originalGearTextureSize = 32; + + /** + * This variable controls the resolution of the output rotated gear textures. + */ + private static final int rotatedTextureSize = middleGearTextureSize * rotatedTextureSizeMultiplier; + + // Integer arrays to store the ARGB values of the original gear images + private static final int[] gearMiddleARGBValues = new int[middleGearTextureSize * middleGearTextureSize]; + private static final int[] gearARGBValues = new int[originalGearTextureSize * originalGearTextureSize]; + + // Only used during testing. + private static boolean generationIssueFlag = false; + + static { + // TODO this is janky + try { + ImageIO.read(ClassLoader.getSystemResource("gear.png")).getRGB(0, 0, originalGearTextureSize, originalGearTextureSize, gearARGBValues, 0, originalGearTextureSize); + ImageIO.read(ClassLoader.getSystemResource("gearmiddle.png")).getRGB(0, 0, middleGearTextureSize, middleGearTextureSize, gearMiddleARGBValues, 0, middleGearTextureSize); + } catch (final IOException e) { + // Should never happen when running, as the tests must pass to build the application, and the tests don't pass if this happens. + generationIssueFlag = true; + } + } + + private TextureGroup gearRotationTextures() { + final BufferedImage[] gearTextures = new BufferedImage[gearRotationSteps]; + + // For each angle of the gear animation, generate a texture + for (int i = 0; i < gearRotationSteps; i++) { + gearTextures[i] = generateGearTextureForRotation(i); + } + + // Only one TextureGroup is returned, as the clockwise and counter-clockwise animations + // are comprised of identical frames, played in the opposite order. + return new TextureGroup("Gear_Rotations", gearTextures); + } + + // TODO better documentation, variable names are way to verbose, check if gear rotation animation was consistent across all versions of Minecraft + private BufferedImage generateGearTextureForRotation(int rotationStep) { + final BufferedImage rotatedImage = new BufferedImage(rotatedTextureSize, rotatedTextureSize, BufferedImage.TYPE_4BYTE_ABGR); + final byte[] imageByteData = ((DataBufferByte) rotatedImage.getRaster().getDataBuffer()).getData(); + // Convert the current rotation step into an angle in radians, + // and use the lookup table to get the current sine and cosine of the angle. + final float sinRotationAngle = lookupSin((rotationStep / (float) gearRotationSteps) * (float) Math.PI * 2.0F); + final float cosRotationAngle = lookupCos((rotationStep / (float) gearRotationSteps) * (float) Math.PI * 2.0F); + + for (int rotatedImageX = 0; rotatedImageX < rotatedTextureSize; ++rotatedImageX) { + for (int rotatedImageY = 0; rotatedImageY < rotatedTextureSize; ++rotatedImageY) { + // TODO I'm not sure why this is done this way + final float gearImageX = ((rotatedImageX / (rotatedTextureSize - 1.0F)) - 0.5F) * (originalGearTextureSize - 1.0F); + final float gearImageY = ((rotatedImageY / (rotatedTextureSize - 1.0F)) - 0.5F) * (originalGearTextureSize - 1.0F); + // Rotate the coordinates TODO document more + final float rotatedOffsetGearImageX = (cosRotationAngle * gearImageX) - (sinRotationAngle * gearImageY); + final float rotatedOffsetGearImageY = (cosRotationAngle * gearImageY) + (sinRotationAngle * gearImageX); + // Fix the offset after rotating + final int rotatedGearImageX = (int) (rotatedOffsetGearImageX + (originalGearTextureSize / 2)); + final int rotatedGearImageY = (int) (rotatedOffsetGearImageY + (originalGearTextureSize / 2)); + final int ARGB; + + if ((rotatedGearImageX >= 0) && (rotatedGearImageY >= 0) && (rotatedGearImageX < originalGearTextureSize) && (rotatedGearImageY < originalGearTextureSize)) { + final int gearDiv = rotatedTextureSize / middleGearTextureSize; + final int gearMiddleARGB = gearMiddleARGBValues[(rotatedImageX / gearDiv) + ((rotatedImageY / gearDiv) * middleGearTextureSize)]; + + // Is the alpha component of the RGBA value for the middle piece of the gear greater than 128? + // (i.e. in the context of the gear images, is there a non-transparent pixel at that position? + // TODO refactor, this is dumb) + if ((gearMiddleARGB >>> 24) > 128) { + // If so, use the RGBA value for the middle of the gear as the RGBA value + ARGB = gearMiddleARGB; + } else { + ARGB = gearARGBValues[rotatedGearImageX + (rotatedGearImageY * originalGearTextureSize)]; + } + } else { + ARGB = 0; + } + + final int imageOffset = (rotatedImageX + (rotatedImageY * rotatedTextureSize)) * 4; + // Set ABGR values + imageByteData[imageOffset + 0] = (byte) ((ARGB >>> 24) > 128 ? 255 : 0); + imageByteData[imageOffset + 1] = (byte) ((ARGB >> 16) & 0xFF); + imageByteData[imageOffset + 2] = (byte) ((ARGB >> 8) & 0xFF); + imageByteData[imageOffset + 3] = (byte) (ARGB & 0xFF); + } + } + + return rotatedImage; + } + + @Override + public String getGeneratorName() { + return "Gear_Rotation"; + } + + @Override + public TextureGroup[] getTextureGroups() { + return new TextureGroup[] { gearRotationTextures() }; + } + + @Override + public boolean hasGenerationIssue() { + return generationIssueFlag; + } + +} diff --git a/src/main/java/mcTextureGen/generators/TextureGenerator.java b/src/main/java/mcTextureGen/generators/TextureGenerator.java index f1d2c28..952f399 100644 --- a/src/main/java/mcTextureGen/generators/TextureGenerator.java +++ b/src/main/java/mcTextureGen/generators/TextureGenerator.java @@ -8,4 +8,38 @@ public abstract class TextureGenerator { public abstract TextureGroup[] getTextureGroups(); + // Only used during tests, signifies that the generator would not be able to generate a texture correctly. + // TODO probably refactor + public boolean hasGenerationIssue() { + return false; + } + + // Math utilities + + /* + * Lookup tables for sin and cos, mainly adapted from + * https://jvm-gaming.org/t/fast-math-sin-cos-lookup-tables/36660 + */ + private static final int SIN_BITS = 16; + private static final int SIN_MASK = ~(-1 << SIN_BITS); + private static final int SIN_COUNT = SIN_MASK + 1; + + private static final float RADIANS_TO_INDEX = SIN_COUNT / (float) (Math.PI * 2.0); + + private static final float[] SINE_TABLE = new float[SIN_COUNT]; + + static { + for (int i = 0; i < SIN_COUNT; ++i) { + SINE_TABLE[i] = (float) Math.sin((i * Math.PI * 2.0) / SIN_COUNT); + } + } + + static final float lookupCos(float radians) { + return SINE_TABLE[(int) ((radians * RADIANS_TO_INDEX) + (SIN_COUNT / 4)) & SIN_MASK]; + } + + static final float lookupSin(float radians) { + return SINE_TABLE[(int) (radians * RADIANS_TO_INDEX) & SIN_MASK]; + } + } diff --git a/src/main/resources/gear.png b/src/main/resources/gear.png new file mode 100755 index 0000000000000000000000000000000000000000..54846917294f4f43d819b27f713c105822fb3ec1 GIT binary patch literal 1116 zcmV-i1f%Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D02y>eSaefwW^{L9 za%BKeVQFr3E>1;MAa*k@H7+qu=Q#NQ00YcPL_t(oN3EAvPXbX8h5ft4XcPqtb~J(o z^wk)f1opK9qj7w?>Dfvw&vh^U|`_?ROsvLGZ4p($WdT(bJI39HvD>MXef4lE|)Wq zC{`i?gM))EZ?jDk6B9N*K5mDHhjwsq;KupgZ0+pqtmX50o0^)k$;rvqB!-8F4Pf;6 z_}Gm`Hx&v6o1dSz+uK{azP`4rt1AbNKuBU?VZn;UqTSuy`P%8}X(tk^1Pef=Qn6~a z>YH$e5sWP@E!pz&veoN#$H&J<1Mlze{w#`Gt!7V8PxkQe;I^V9f02P9xV*e{A~!cT z4H6J}ettHLzP-J<^Uu#u`}+EFaJ{;^YAY)%RxX!)&Fkx{uV*g=64e2+LjnS*fZWI! z#r^%gJw85qgb)KxL<&F-sYA&{cyV#jK-&Z~W{-}JydzFePwo8tyipsFqCS8Kg=)OKytrx`&KwsP7fwR$$2IqKUv-0GN)f4~2xC>4o163Y2nU87619xuOx>Y2 ztHno*7LTQ!9eq@^o7csP7G3!(&6SjdiZaw-BC#UUxv;lx?OH5~+WKYWS#ae(O% z(l-lrAYnKxNI4`DMFRn@S*w<19m=SPb5`c@r;^FW zS5D)i*E$s=-9&5a1-2~$Dn<;@orRMg`}_MYyV{L!javRI=pds*C~WWhRv+~#Mg{Icinm_<55T!*MRaZ0>k33` zAr<%X>b&$uEXi>gechIk@T?&JYTYnEoi9A6o=C-?X-K6~*4^E0y}iA${|%x_N%m&O z!<78#TnorIh}(d)q=i%fRN{BsdO9Jo^CSx;r&?2EHp$+9Swpm>LG?hcnRx$VJgJeM io*u6e&?;Sj+4}}wP0kD45iGL+0000 (unsafeCharacterStart + TextureGenerator.class.getSimpleName() + unsafeCharacterQuotesStart + generator.getGeneratorName() + unsafeCharacterEnd)); - for (final TextureGroup group : generator.getTextureGroups()) { - // Re-use Matcher instead of creating a new one for each String - checkUnsafeCharacters.reset(group.textureGroupName); - // Lambda used for lazy evaluation of error message - assertTrue(checkUnsafeCharacters.matches(), () -> (unsafeCharacterStart + TextureGroup.class.getSimpleName() + unsafeCharacterQuotesStart + group.textureGroupName + unsafeCharacterEnd)); - } - } - } + // TODO this is bad + private static final Stream textureGeneratorProvider() { + return Stream.of(MCTextureGenerator.getTextureGenerators()); + } + + // TODO this is bad + private static final Stream textureGroupProvider() { + return Stream.of(MCTextureGenerator.getTextureGenerators()).map(x -> x.getTextureGroups()).flatMap(Stream::of); + } + + // This regex matches if the whole string only contains alpha-numeric characters and / or underscores. + private final static Pattern checkUnsafeCharactersRegex = Pattern.compile("^\\w+$"); + // The Matcher is initially given a dummy value, as it is reused each loop. + // If it somehow fails to get reset, this ASCII table flip should cause the test to fail. + // P.S applicants welcome to submit a better ASCII-only table flip (I really tried). + // Using non-ASCII characters causes Eclipse to space lines weirdly. + private final static Matcher checkUnsafeCharacters = checkUnsafeCharactersRegex.matcher("(/@_@/) `` _|__|_"); + + private final static String unsafeCharacterStart = "The name of the "; + private final static String unsafeCharacterQuotesStart = " \""; + private final static String unsafeCharacterEnd = "\" contained a character which might be potentially unsafe to use in a file name"; + + private static final boolean isSafeName(String toCheck) { + return checkUnsafeCharacters.reset(toCheck).matches(); + } + + @ParameterizedTest + @MethodSource("textureGeneratorProvider") + @DisplayName("Test if any TextureGenerator reports generation errors.") + final void testGenerationIssues(TextureGenerator generator) { + assertFalse(generator.hasGenerationIssue(), () -> ("The " + TextureGenerator.class.getSimpleName() + " \"" + generator.getGeneratorName() + "\" has an unspecified texture generation issue.")); + } + + @ParameterizedTest + @MethodSource("textureGeneratorProvider") + @DisplayName("Ensure all names of TextureGenerators only contain characters which are safe to be used in file names") + final void testSafeCharactersInTextureGeneratorNames(TextureGenerator generator) { + assertTrue(isSafeName(generator.getGeneratorName()), () -> (unsafeCharacterStart + TextureGenerator.class.getSimpleName() + unsafeCharacterQuotesStart + generator.getGeneratorName() + unsafeCharacterEnd)); + } + + @ParameterizedTest + @MethodSource("textureGroupProvider") + @DisplayName("Ensure all names of TextureGenerators only contain characters which are safe to be used in file names") + final void testSafeCharactersInTextureGroupNames(TextureGroup group) { + assertTrue(isSafeName(group.textureGroupName), () -> (unsafeCharacterStart + TextureGroup.class.getSimpleName() + unsafeCharacterQuotesStart + group.textureGroupName + unsafeCharacterEnd)); + } }