mirror of
https://github.com/GTNewHorizons/Angelica.git
synced 2026-10-25 20:22:22 +00:00
Add fast texture atlas stitcher
This commit is contained in:
1 parent
c22935660a
commit
267378a381
4 files changed
+383
-1
No files matched your search
@@ -463,7 +463,9 @@ public enum Mixins implements IMixins {
|
||||
|
||||
OPTIMIZE_TEXTURE_LOADING(new MixinBuilder()
|
||||
.setPhase(Phase.EARLY)
|
||||
.addClientMixins("angelica.textures.MixinTextureUtil_OptimizeMipmap")
|
||||
.addClientMixins(
|
||||
"angelica.textures.MixinTextureUtil_OptimizeMipmap",
|
||||
"angelica.textures.MixinStitcher")
|
||||
.setApplyIf(() -> AngelicaConfig.optimizeTextureLoading)),
|
||||
|
||||
REPLACE_FFP(new MixinBuilder()
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
package com.gtnewhorizons.angelica.textures;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import net.minecraft.client.renderer.StitcherException;
|
||||
import net.minecraft.client.renderer.texture.Stitcher;
|
||||
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
|
||||
import net.minecraft.util.MathHelper;
|
||||
|
||||
public final class FastTextureStitcher {
|
||||
|
||||
private FastTextureStitcher() {}
|
||||
|
||||
public static Result stitch(Stitcher.Holder[] holders, int maxWidth, int maxHeight, boolean forcePowerOf2) {
|
||||
if (holders.length == 0) {
|
||||
return new Result(0, 0, Collections.<TextureAtlasSprite>emptyList());
|
||||
}
|
||||
|
||||
long area = 0L;
|
||||
int minWidth = 1;
|
||||
int minHeight = 1;
|
||||
boolean[] initialRotations = new boolean[holders.length];
|
||||
|
||||
for (int i = 0; i < holders.length; i++) {
|
||||
Stitcher.Holder holder = holders[i];
|
||||
initialRotations[i] = holder.isRotated();
|
||||
int width = holder.getWidth();
|
||||
int height = holder.getHeight();
|
||||
|
||||
if (width > maxWidth || height > maxHeight) {
|
||||
throw unableToFit(holder, maxWidth, maxHeight);
|
||||
}
|
||||
|
||||
area += (long) width * height;
|
||||
minWidth = Math.max(minWidth, width);
|
||||
minHeight = Math.max(minHeight, height);
|
||||
}
|
||||
|
||||
int width = roundDimension(Math.max(minWidth, (int) Math.ceil(Math.sqrt(area))), forcePowerOf2);
|
||||
width = Math.min(width, maxWidth);
|
||||
int height = roundDimension(Math.max(minHeight, (int) ((area + width - 1L) / width)), forcePowerOf2);
|
||||
height = Math.min(height, maxHeight);
|
||||
|
||||
while (width <= maxWidth && height <= maxHeight) {
|
||||
restoreRotations(holders, initialRotations);
|
||||
Result result = tryStitch(holders, width, height, forcePowerOf2);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if ((width <= height && width < maxWidth) || height >= maxHeight) {
|
||||
width = grow(width, maxWidth, forcePowerOf2);
|
||||
} else {
|
||||
height = grow(height, maxHeight, forcePowerOf2);
|
||||
}
|
||||
}
|
||||
|
||||
throw unableToFit(holders[0], maxWidth, maxHeight);
|
||||
}
|
||||
|
||||
private static Result tryStitch(Stitcher.Holder[] holders, int atlasWidth, int atlasHeight, boolean forcePowerOf2) {
|
||||
List<Node> skyline = new ArrayList<>();
|
||||
List<TextureAtlasSprite> sprites = new ArrayList<>(holders.length);
|
||||
int[] xs = new int[holders.length];
|
||||
int[] ys = new int[holders.length];
|
||||
skyline.add(new Node(0, 0, atlasWidth));
|
||||
|
||||
int usedWidth = 0;
|
||||
int usedHeight = 0;
|
||||
|
||||
for (int i = 0; i < holders.length; i++) {
|
||||
Stitcher.Holder holder = holders[i];
|
||||
Placement placement = findPlacement(skyline, atlasWidth, atlasHeight, holder);
|
||||
if (placement == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (holder.isRotated() != placement.rotated) {
|
||||
holder.rotate();
|
||||
}
|
||||
|
||||
place(skyline, placement);
|
||||
int width = holder.getWidth();
|
||||
int height = holder.getHeight();
|
||||
usedWidth = Math.max(usedWidth, placement.x + width);
|
||||
usedHeight = Math.max(usedHeight, placement.y + height);
|
||||
sprites.add(holder.getAtlasSprite());
|
||||
xs[i] = placement.x;
|
||||
ys[i] = placement.y;
|
||||
}
|
||||
|
||||
int resultWidth = forcePowerOf2 ? atlasWidth : usedWidth;
|
||||
int resultHeight = forcePowerOf2 ? atlasHeight : usedHeight;
|
||||
|
||||
for (int i = 0; i < holders.length; i++) {
|
||||
Stitcher.Holder holder = holders[i];
|
||||
TextureAtlasSprite sprite = holder.getAtlasSprite();
|
||||
sprite.initSprite(resultWidth, resultHeight, xs[i], ys[i], holder.isRotated());
|
||||
}
|
||||
|
||||
return new Result(resultWidth, resultHeight, sprites);
|
||||
}
|
||||
|
||||
private static Placement findPlacement(List<Node> skyline, int atlasWidth, int atlasHeight, Stitcher.Holder holder) {
|
||||
Placement best = findPlacement(skyline, atlasWidth, atlasHeight, holder.getWidth(), holder.getHeight(), holder.isRotated());
|
||||
|
||||
holder.rotate();
|
||||
Placement rotated = findPlacement(skyline, atlasWidth, atlasHeight, holder.getWidth(), holder.getHeight(), holder.isRotated());
|
||||
holder.rotate();
|
||||
|
||||
if (best == null) {
|
||||
return rotated;
|
||||
}
|
||||
|
||||
if (rotated == null) {
|
||||
return best;
|
||||
}
|
||||
|
||||
return rotated.bottom < best.bottom || rotated.bottom == best.bottom && rotated.x < best.x ? rotated : best;
|
||||
}
|
||||
|
||||
private static Placement findPlacement(List<Node> skyline, int atlasWidth, int atlasHeight, int width, int height, boolean rotated) {
|
||||
Placement best = null;
|
||||
|
||||
for (int i = 0; i < skyline.size(); i++) {
|
||||
Node node = skyline.get(i);
|
||||
int y = fit(skyline, i, width, height, atlasWidth, atlasHeight);
|
||||
|
||||
if (y >= 0) {
|
||||
int bottom = y + height;
|
||||
if (best == null || bottom < best.bottom || bottom == best.bottom && node.x < best.x) {
|
||||
best = new Placement(i, node.x, y, width, bottom, rotated);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return best;
|
||||
}
|
||||
|
||||
private static int fit(List<Node> skyline, int index, int width, int height, int atlasWidth, int atlasHeight) {
|
||||
Node node = skyline.get(index);
|
||||
if (node.x + width > atlasWidth) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int y = node.y;
|
||||
int widthLeft = width;
|
||||
|
||||
for (int i = index; widthLeft > 0; i++) {
|
||||
if (i >= skyline.size()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
Node current = skyline.get(i);
|
||||
y = Math.max(y, current.y);
|
||||
if (y + height > atlasHeight) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
widthLeft -= current.width;
|
||||
}
|
||||
|
||||
return y;
|
||||
}
|
||||
|
||||
private static void place(List<Node> skyline, Placement placement) {
|
||||
skyline.add(placement.index, new Node(placement.x, placement.bottom, placement.width));
|
||||
|
||||
for (int i = placement.index + 1; i < skyline.size(); i++) {
|
||||
Node previous = skyline.get(i - 1);
|
||||
Node current = skyline.get(i);
|
||||
int shrink = previous.x + previous.width - current.x;
|
||||
|
||||
if (shrink <= 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
current.x += shrink;
|
||||
current.width -= shrink;
|
||||
|
||||
if (current.width <= 0) {
|
||||
skyline.remove(i--);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < skyline.size() - 1; i++) {
|
||||
Node current = skyline.get(i);
|
||||
Node next = skyline.get(i + 1);
|
||||
|
||||
if (current.y == next.y) {
|
||||
current.width += next.width;
|
||||
skyline.remove(i-- + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void restoreRotations(Stitcher.Holder[] holders, boolean[] rotations) {
|
||||
for (int i = 0; i < holders.length; i++) {
|
||||
if (holders[i].isRotated() != rotations[i]) {
|
||||
holders[i].rotate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int roundDimension(int value, boolean forcePowerOf2) {
|
||||
return forcePowerOf2 ? MathHelper.roundUpToPowerOfTwo(value) : value;
|
||||
}
|
||||
|
||||
private static int grow(int value, int max, boolean forcePowerOf2) {
|
||||
if (value >= max) {
|
||||
return max + 1;
|
||||
}
|
||||
|
||||
int next = forcePowerOf2 ? MathHelper.roundUpToPowerOfTwo(value + 1) : value * 2;
|
||||
return Math.min(Math.max(next, value + 1), max);
|
||||
}
|
||||
|
||||
private static StitcherException unableToFit(Stitcher.Holder holder, int maxWidth, int maxHeight) {
|
||||
return new StitcherException(holder, "Unable to fit: " + holder.getAtlasSprite().getIconName()
|
||||
+ " - size: " + holder.getAtlasSprite().getIconWidth() + "x" + holder.getAtlasSprite().getIconHeight()
|
||||
+ " - max atlas: " + maxWidth + "x" + maxHeight);
|
||||
}
|
||||
|
||||
public static final class Result {
|
||||
public final int width;
|
||||
public final int height;
|
||||
public final List<TextureAtlasSprite> sprites;
|
||||
|
||||
private Result(int width, int height, List<TextureAtlasSprite> sprites) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.sprites = sprites;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Node {
|
||||
int x;
|
||||
int y;
|
||||
int width;
|
||||
|
||||
private Node(int x, int y, int width) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.width = width;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Placement {
|
||||
final int index;
|
||||
final int x;
|
||||
final int y;
|
||||
final int width;
|
||||
final int bottom;
|
||||
final boolean rotated;
|
||||
|
||||
private Placement(int index, int x, int y, int width, int bottom, boolean rotated) {
|
||||
this.index = index;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.width = width;
|
||||
this.bottom = bottom;
|
||||
this.rotated = rotated;
|
||||
}
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package com.gtnewhorizons.angelica.mixins.early.angelica.textures;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import com.gtnewhorizons.angelica.textures.FastTextureStitcher;
|
||||
import net.minecraft.client.renderer.texture.Stitcher;
|
||||
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
|
||||
import org.spongepowered.asm.mixin.Final;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Overwrite;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
|
||||
@Mixin(Stitcher.class)
|
||||
public abstract class MixinStitcher {
|
||||
|
||||
@Shadow @Final private Set setStitchHolders;
|
||||
@Shadow @Final private int maxWidth;
|
||||
@Shadow @Final private int maxHeight;
|
||||
@Shadow @Final private boolean forcePowerOf2;
|
||||
@Shadow private int currentWidth;
|
||||
@Shadow private int currentHeight;
|
||||
|
||||
@Unique private List<TextureAtlasSprite> angelica$stitchedSprites = Collections.emptyList();
|
||||
|
||||
/**
|
||||
* @author Angelica
|
||||
* @reason Use a skyline packer instead of vanilla recursive slots.
|
||||
*/
|
||||
@Overwrite
|
||||
public void doStitch() {
|
||||
Stitcher.Holder[] holders = (Stitcher.Holder[]) this.setStitchHolders.toArray(new Stitcher.Holder[this.setStitchHolders.size()]);
|
||||
Arrays.sort(holders);
|
||||
|
||||
FastTextureStitcher.Result result = FastTextureStitcher.stitch(holders, this.maxWidth, this.maxHeight, this.forcePowerOf2);
|
||||
this.currentWidth = result.width;
|
||||
this.currentHeight = result.height;
|
||||
this.angelica$stitchedSprites = result.sprites;
|
||||
}
|
||||
|
||||
/**
|
||||
* @author Angelica
|
||||
* @reason FastTextureStitcher initializes sprites directly.
|
||||
*/
|
||||
@Overwrite
|
||||
public List getStichSlots() {
|
||||
return new ArrayList(this.angelica$stitchedSprites);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.gtnewhorizons.angelica.textures;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.awt.Rectangle;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import net.minecraft.client.renderer.texture.Stitcher;
|
||||
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class FastTextureStitcherTest {
|
||||
|
||||
@Test
|
||||
void packsSpritesWithoutOverlap() {
|
||||
Stitcher.Holder[] holders = {
|
||||
holder("a", 32, 16),
|
||||
holder("b", 16, 32),
|
||||
holder("c", 16, 16),
|
||||
holder("d", 8, 40),
|
||||
holder("e", 24, 8)
|
||||
};
|
||||
Arrays.sort(holders);
|
||||
|
||||
FastTextureStitcher.Result result = FastTextureStitcher.stitch(holders, 128, 128, true);
|
||||
|
||||
assertTrue(result.width <= 128);
|
||||
assertTrue(result.height <= 128);
|
||||
assertEquals(holders.length, result.sprites.size());
|
||||
|
||||
List<Rectangle> rectangles = new ArrayList<>();
|
||||
for (Stitcher.Holder holder : holders) {
|
||||
TextureAtlasSprite sprite = holder.getAtlasSprite();
|
||||
Rectangle rectangle = new Rectangle(sprite.getOriginX(), sprite.getOriginY(), holder.getWidth(), holder.getHeight());
|
||||
assertTrue(rectangle.x >= 0);
|
||||
assertTrue(rectangle.y >= 0);
|
||||
assertTrue(rectangle.x + rectangle.width <= result.width);
|
||||
assertTrue(rectangle.y + rectangle.height <= result.height);
|
||||
|
||||
for (Rectangle other : rectangles) {
|
||||
assertFalse(rectangle.intersects(other), rectangle + " overlaps " + other);
|
||||
}
|
||||
|
||||
rectangles.add(rectangle);
|
||||
}
|
||||
}
|
||||
|
||||
private static Stitcher.Holder holder(String name, int width, int height) {
|
||||
TextureAtlasSprite sprite = new TextureAtlasSprite(name);
|
||||
sprite.setIconWidth(width);
|
||||
sprite.setIconHeight(height);
|
||||
return new Stitcher.Holder(sprite, 0);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user