Skip to content

Commit

Permalink
[WIP-ish] Implement initial gear rotation generator, refactor unit te…
Browse files Browse the repository at this point in the history
…sts 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.
  • Loading branch information
NeRdTheNed committed Aug 2, 2021
1 parent 7a80037 commit 567425a
Show file tree
Hide file tree
Showing 7 changed files with 234 additions and 36 deletions.
15 changes: 9 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.
3 changes: 2 additions & 1 deletion src/main/java/mcTextureGen/MCTextureGenerator.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) {
Expand Down
138 changes: 138 additions & 0 deletions src/main/java/mcTextureGen/generators/GearRotationFramesGenerator.java
Original file line number Diff line number Diff line change
@@ -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;
}

}
34 changes: 34 additions & 0 deletions src/main/java/mcTextureGen/generators/TextureGenerator.java
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}

}
Binary file added src/main/resources/gear.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/main/resources/gearmiddle.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
80 changes: 51 additions & 29 deletions src/test/java/mcTextureGen/test/MCTextureGeneratorTest.java
Original file line number Diff line number Diff line change
@@ -1,46 +1,68 @@
package mcTextureGen.test;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

import mcTextureGen.MCTextureGenerator;
import mcTextureGen.data.TextureGroup;
import mcTextureGen.generators.TextureGenerator;

// TODO refactor tests to use MethodSource
// TODO refactor
public class MCTextureGeneratorTest {

@Test
@DisplayName("Ensure all names of TextureGenerators and TextureGroups only contain characters which are safe to be used in file names")
void testSafeCharactersInNames() {
// This regex matches if the whole string only contains alpha-numeric characters and / or underscores.
final 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.
final Matcher checkUnsafeCharacters = checkUnsafeCharactersRegex.matcher("(/@_@/) `` _|__|_");

final String unsafeCharacterStart = "The name of the ";
final String unsafeCharacterQuotesStart = " \"";
final String unsafeCharacterEnd = "\" contained a character which might be potentially unsafe to use in a file name";
for (final TextureGenerator generator : MCTextureGenerator.getTextureGenerators()) {
// Re-use Matcher instead of creating a new one for each String
checkUnsafeCharacters.reset(generator.getGeneratorName());
// Lambda used for lazy evaluation of error message
assertTrue(checkUnsafeCharacters.matches(), () -> (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<TextureGenerator> textureGeneratorProvider() {
return Stream.of(MCTextureGenerator.getTextureGenerators());
}

// TODO this is bad
private static final Stream<TextureGroup> 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));
}

}

0 comments on commit 567425a

Please sign in to comment.