diff options
author | isxander <xander@isxander.dev> | 2024-04-11 18:43:06 +0100 |
---|---|---|
committer | isxander <xander@isxander.dev> | 2024-04-11 18:43:06 +0100 |
commit | 04fe933f4c24817100f3101f088accf55a621f8a (patch) | |
tree | feff94ca3ab4484160e69a24f4ee38522381950e /src/main/java/dev/isxander/yacl3/gui/image | |
parent | 831b894fdb7fe3e173d81387c8f6a2402b8ccfa9 (diff) | |
download | YetAnotherConfigLib-04fe933f4c24817100f3101f088accf55a621f8a.tar.gz YetAnotherConfigLib-04fe933f4c24817100f3101f088accf55a621f8a.tar.bz2 YetAnotherConfigLib-04fe933f4c24817100f3101f088accf55a621f8a.zip |
Extremely fragile and broken multiversion build with stonecutter
Diffstat (limited to 'src/main/java/dev/isxander/yacl3/gui/image')
7 files changed, 679 insertions, 0 deletions
diff --git a/src/main/java/dev/isxander/yacl3/gui/image/ImageRenderer.java b/src/main/java/dev/isxander/yacl3/gui/image/ImageRenderer.java new file mode 100644 index 0000000..d3fb4bf --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/gui/image/ImageRenderer.java @@ -0,0 +1,11 @@ +package dev.isxander.yacl3.gui.image; + +import net.minecraft.client.gui.GuiGraphics; + +public interface ImageRenderer { + int render(GuiGraphics graphics, int x, int y, int renderWidth, float tickDelta); + + void close(); + + default void tick() {} +} diff --git a/src/main/java/dev/isxander/yacl3/gui/image/ImageRendererFactory.java b/src/main/java/dev/isxander/yacl3/gui/image/ImageRendererFactory.java new file mode 100644 index 0000000..d9d2e2d --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/gui/image/ImageRendererFactory.java @@ -0,0 +1,24 @@ +package dev.isxander.yacl3.gui.image; + +public interface ImageRendererFactory { + /** + * Prepares the image. This can be run off-thread, + * and should NOT contain any GL calls whatsoever. + */ + ImageSupplier prepareImage() throws Exception; + + default boolean requiresOffThreadPreparation() { + return true; + } + + interface ImageSupplier { + ImageRenderer completeImage() throws Exception; + } + + interface OnThread extends ImageRendererFactory { + @Override + default boolean requiresOffThreadPreparation() { + return false; + } + } +} diff --git a/src/main/java/dev/isxander/yacl3/gui/image/ImageRendererManager.java b/src/main/java/dev/isxander/yacl3/gui/image/ImageRendererManager.java new file mode 100644 index 0000000..0c9b8a3 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/gui/image/ImageRendererManager.java @@ -0,0 +1,120 @@ +package dev.isxander.yacl3.gui.image; + +import com.mojang.blaze3d.systems.RenderSystem; +import dev.isxander.yacl3.gui.image.impl.AnimatedDynamicTextureImage; +import dev.isxander.yacl3.impl.utils.YACLConstants; +import net.minecraft.client.Minecraft; +import net.minecraft.resources.ResourceLocation; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.*; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; + +public class ImageRendererManager { + private static final ExecutorService SINGLE_THREAD_EXECUTOR = Executors.newSingleThreadExecutor(task -> new Thread(task, "YACL Image Prep")); + + private static final Map<ResourceLocation, CompletableFuture<ImageRenderer>> IMAGE_CACHE = new ConcurrentHashMap<>(); + static final Map<ResourceLocation, ImageRenderer> PRELOADED_IMAGE_CACHE = new ConcurrentHashMap<>(); + + static final List<PreloadedImageFactory> PRELOADED_IMAGE_FACTORIES = List.of( + new PreloadedImageFactory( + location -> location.getPath().endsWith(".webp"), + AnimatedDynamicTextureImage::createWEBPFromTexture + ), + new PreloadedImageFactory( + location -> location.getPath().endsWith(".gif"), + AnimatedDynamicTextureImage::createGIFFromTexture + ) + ); + + public static <T extends ImageRenderer> Optional<T> getImage(ResourceLocation id) { + if (PRELOADED_IMAGE_CACHE.containsKey(id)) { + return Optional.of((T) PRELOADED_IMAGE_CACHE.get(id)); + } + + if (IMAGE_CACHE.containsKey(id)) { + return Optional.ofNullable((T) IMAGE_CACHE.get(id).getNow(null)); + } + + return Optional.empty(); + } + + @SuppressWarnings("unchecked") + public static <T extends ImageRenderer> CompletableFuture<T> registerImage(ResourceLocation id, ImageRendererFactory factory) { + System.out.println(PRELOADED_IMAGE_CACHE.get(id)); + + if (IMAGE_CACHE.containsKey(id)) { + return (CompletableFuture<T>) IMAGE_CACHE.get(id); + } + + var future = new CompletableFuture<ImageRenderer>(); + IMAGE_CACHE.put(id, future); + + SINGLE_THREAD_EXECUTOR.submit(() -> { + Supplier<Optional<ImageRendererFactory.ImageSupplier>> supplier = + factory.requiresOffThreadPreparation() + ? new CompletedSupplier<>(safelyPrepareFactory(id, factory)) + : () -> safelyPrepareFactory(id, factory); + + Minecraft.getInstance().execute(() -> completeImageFactory(id, supplier, future)); + }); + + return (CompletableFuture<T>) future; + } + + private static <T extends ImageRenderer> void completeImageFactory(ResourceLocation id, Supplier<Optional<ImageRendererFactory.ImageSupplier>> supplier, CompletableFuture<ImageRenderer> future) { + RenderSystem.assertOnRenderThread(); + + ImageRendererFactory.ImageSupplier completableImage = supplier.get().orElse(null); + if (completableImage == null) { + return; + } + + // sanity check - this should never happen + if (future.isDone()) { + YACLConstants.LOGGER.error("Image '{}' was already completed", id); + return; + } + + ImageRenderer image; + try { + image = completableImage.completeImage(); + } catch (Exception e) { + YACLConstants.LOGGER.error("Failed to create image '{}'", id, e); + return; + } + + future.complete(image); + } + + public static void closeAll() { + SINGLE_THREAD_EXECUTOR.shutdownNow(); + IMAGE_CACHE.values().removeIf(future -> { + if (future.isDone()) { + future.join().close(); + } + return true; + }); + } + + static Optional<ImageRendererFactory.ImageSupplier> safelyPrepareFactory(ResourceLocation id, ImageRendererFactory factory) { + try { + return Optional.of(factory.prepareImage()); + } catch (Exception e) { + YACLConstants.LOGGER.error("Failed to prepare image '{}'", id, e); + IMAGE_CACHE.remove(id); + return Optional.empty(); + } + } + + public record PreloadedImageFactory(Predicate<ResourceLocation> predicate, Function<ResourceLocation, ImageRendererFactory> factory) { + } + + private record CompletedSupplier<T>(T get) implements Supplier<T> { + } + +} diff --git a/src/main/java/dev/isxander/yacl3/gui/image/YACLImageReloadListener.java b/src/main/java/dev/isxander/yacl3/gui/image/YACLImageReloadListener.java new file mode 100644 index 0000000..b6524a7 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/gui/image/YACLImageReloadListener.java @@ -0,0 +1,110 @@ +package dev.isxander.yacl3.gui.image; + +import dev.isxander.yacl3.impl.utils.YACLConstants; +import net.minecraft.CrashReport; +import net.minecraft.CrashReportCategory; +import net.minecraft.ReportedException; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.PreparableReloadListener; +import net.minecraft.server.packs.resources.Resource; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.util.profiling.ProfilerFiller; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + +public class YACLImageReloadListener + implements PreparableReloadListener + /*? if fabric {*/, + net.fabricmc.fabric.api.resource.IdentifiableResourceReloadListener + /*?}*/ +{ + @Override + public CompletableFuture<Void> reload( + PreparationBarrier preparationBarrier, + ResourceManager resourceManager, + ProfilerFiller preparationsProfiler, + ProfilerFiller reloadProfiler, + Executor backgroundExecutor, + Executor gameExecutor + ) { + Map<ResourceLocation, Resource> imageResources = resourceManager.listResources( + "", + location -> ImageRendererManager.PRELOADED_IMAGE_FACTORIES + .stream() + .anyMatch(factory -> factory.predicate().test(location)) + ); + + // extreme mojang hackery. + // for some reason this wait method needs to be called for the reload + // instance to be marked as complete + if (imageResources.isEmpty()) { + preparationBarrier.wait(null); + } + + List<CompletableFuture<?>> futures = new ArrayList<>(imageResources.size()); + + for (Map.Entry<ResourceLocation, Resource> entry : imageResources.entrySet()) { + ResourceLocation location = entry.getKey(); + Resource resource = entry.getValue(); + + ImageRendererFactory imageFactory = ImageRendererManager.PRELOADED_IMAGE_FACTORIES + .stream() + .filter(factory -> factory.predicate().test(location)) + .map(factory -> factory.factory().apply(location)) + .findAny() + .orElseThrow(); + + CompletableFuture<Optional<ImageRenderer>> imageFuture = + CompletableFuture.supplyAsync( + () -> ImageRendererManager.safelyPrepareFactory( + location, imageFactory + ), + backgroundExecutor + ) + .thenCompose(preparationBarrier::wait) + .thenApplyAsync(imageSupplierOpt -> { + if (imageSupplierOpt.isEmpty()) { + return Optional.empty(); + } + ImageRendererFactory.ImageSupplier supplier = imageSupplierOpt.get(); + + ImageRenderer imageRenderer; + try { + imageRenderer = supplier.completeImage(); + } catch (Exception e) { + YACLConstants.LOGGER.error("Failed to create image '{}'", location, e); + return Optional.empty(); + } + + ImageRendererManager.PRELOADED_IMAGE_CACHE.put(location, imageRenderer); + + return Optional.of(imageRenderer); + }, gameExecutor); + + futures.add(imageFuture); + + imageFuture.whenComplete((result, throwable) -> { + if (throwable != null) { + CrashReport crashReport = CrashReport.forThrowable(throwable, "Failed to load image"); + CrashReportCategory category = crashReport.addCategory("YACL Gui"); + category.setDetail("Image identifier", location.toString()); + throw new ReportedException(crashReport); + } + }); + } + + return CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new)); + } + + /*? if fabric {*/ + @Override + public ResourceLocation getFabricId() { + return new ResourceLocation("yet_another_config_lib_v3", "image_reload_listener"); + } + /*?}*/ +} diff --git a/src/main/java/dev/isxander/yacl3/gui/image/impl/AnimatedDynamicTextureImage.java b/src/main/java/dev/isxander/yacl3/gui/image/impl/AnimatedDynamicTextureImage.java new file mode 100644 index 0000000..39ddb55 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/gui/image/impl/AnimatedDynamicTextureImage.java @@ -0,0 +1,286 @@ +package dev.isxander.yacl3.gui.image.impl; + +import com.mojang.blaze3d.Blaze3D; +import com.mojang.blaze3d.platform.GlConst; +import com.mojang.blaze3d.platform.GlStateManager; +import com.mojang.blaze3d.platform.NativeImage; +import com.twelvemonkeys.imageio.plugins.webp.WebPImageReaderSpi; +import dev.isxander.yacl3.debug.DebugProperties; +import dev.isxander.yacl3.gui.image.ImageRendererFactory; +import dev.isxander.yacl3.impl.utils.YACLConstants; +import net.minecraft.CrashReport; +import net.minecraft.CrashReportCategory; +import net.minecraft.ReportedException; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.Resource; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.util.FastColor; + +import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.metadata.IIOMetadataNode; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Field; +import java.nio.file.Path; +import java.util.List; +import java.util.stream.IntStream; + +public class AnimatedDynamicTextureImage extends DynamicTextureImage { + private int currentFrame; + private double lastFrameTime; + + private final double[] frameDelays; + private final int frameCount; + + private final int packCols, packRows; + private final int frameWidth, frameHeight; + + public AnimatedDynamicTextureImage(NativeImage image, int frameWidth, int frameHeight, int frameCount, double[] frameDelayMS, int packCols, int packRows, ResourceLocation uniqueLocation) { + super(image, uniqueLocation); + this.frameWidth = frameWidth; + this.frameHeight = frameHeight; + this.frameCount = frameCount; + this.frameDelays = frameDelayMS; + this.packCols = packCols; + this.packRows = packRows; + } + + @Override + public int render(GuiGraphics graphics, int x, int y, int renderWidth, float tickDelta) { + if (image == null) return 0; + + float ratio = renderWidth / (float)frameWidth; + int targetHeight = (int) (frameHeight * ratio); + + int currentCol = currentFrame % packCols; + int currentRow = (int) Math.floor(currentFrame / (double)packCols); + + graphics.pose().pushPose(); + graphics.pose().translate(x, y, 0); + graphics.pose().scale(ratio, ratio, 1); + + if (DebugProperties.IMAGE_FILTERING) { + GlStateManager._texParameter(GlConst.GL_TEXTURE_2D, GlConst.GL_TEXTURE_MAG_FILTER, GlConst.GL_LINEAR); + GlStateManager._texParameter(GlConst.GL_TEXTURE_2D, GlConst.GL_TEXTURE_MIN_FILTER, GlConst.GL_LINEAR); + } + + graphics.blit( + uniqueLocation, + 0, 0, + frameWidth * currentCol, frameHeight * currentRow, + frameWidth, frameHeight, + this.width, this.height + ); + graphics.pose().popPose(); + + if (frameCount > 1) { + double timeMS = Blaze3D.getTime() * 1000; + if (lastFrameTime == 0) lastFrameTime = timeMS; + if (timeMS - lastFrameTime >= frameDelays[currentFrame]) { + currentFrame++; + lastFrameTime = timeMS; + } + if (currentFrame >= frameCount - 1) + currentFrame = 0; + } + + return targetHeight; + } + + public static ImageRendererFactory createGIFFromTexture(ResourceLocation textureLocation) { + return () -> { + ResourceManager resourceManager = Minecraft.getInstance().getResourceManager(); + Resource resource = resourceManager.getResource(textureLocation).orElseThrow(); + + return createGIFSupplier(resource.open(), textureLocation); + }; + } + + public static ImageRendererFactory createGIFFromPath(Path path, ResourceLocation uniqueLocation) { + return () -> createGIFSupplier(new FileInputStream(path.toFile()), uniqueLocation); + } + + public static ImageRendererFactory createWEBPFromTexture(ResourceLocation textureLocation) { + return () -> { + ResourceManager resourceManager = Minecraft.getInstance().getResourceManager(); + Resource resource = resourceManager.getResource(textureLocation).orElseThrow(); + + return createWEBPSupplier(resource.open(), textureLocation); + }; + } + + public static ImageRendererFactory createWEBPFromPath(Path path, ResourceLocation uniqueLocation) { + return () -> createWEBPSupplier(new FileInputStream(path.toFile()), uniqueLocation); + } + + private static ImageRendererFactory.ImageSupplier createGIFSupplier(InputStream is, ResourceLocation uniqueLocation) { + try (is) { + ImageReader reader = ImageIO.getImageReadersBySuffix("gif").next(); + reader.setInput(ImageIO.createImageInputStream(is)); + + AnimFrameProvider animFrameFunction = i -> { + IIOMetadata metadata = reader.getImageMetadata(i); + String metaFormatName = metadata.getNativeMetadataFormatName(); + IIOMetadataNode root = (IIOMetadataNode) metadata.getAsTree(metaFormatName); + IIOMetadataNode graphicsControlExtensionNode = (IIOMetadataNode) root.getElementsByTagName("GraphicControlExtension").item(0); + int delay = Integer.parseInt(graphicsControlExtensionNode.getAttribute("delayTime")) * 10; + + return new AnimFrame(delay, 0, 0); + }; + + return createFromImageReader(reader, animFrameFunction, uniqueLocation); + } catch (Exception e) { + CrashReport crashReport = CrashReport.forThrowable(e, "Failed to load GIF image"); + CrashReportCategory category = crashReport.addCategory("YACL Gui"); + category.setDetail("Image identifier", uniqueLocation.toString()); + throw new ReportedException(crashReport); + } + } + + private static ImageRendererFactory.ImageSupplier createWEBPSupplier(InputStream is, ResourceLocation uniqueLocation) { + try (is) { + ImageReader reader = new WebPImageReaderSpi().createReaderInstance(); + reader.setInput(ImageIO.createImageInputStream(is)); + + int numImages = reader.getNumImages(true); // Force reading of all frames + AnimFrameProvider animFrameFunction = i -> null; + if (numImages > 1) { + // WebP reader does not expose frame delay, prepare for reflection hell + Class<?> webpReaderClass = Class.forName("com.twelvemonkeys.imageio.plugins.webp.WebPImageReader"); + Field framesField = webpReaderClass.getDeclaredField("frames"); + framesField.setAccessible(true); + java.util.List<?> frames = (List<?>) framesField.get(reader); + + Class<?> animationFrameClass = Class.forName("com.twelvemonkeys.imageio.plugins.webp.AnimationFrame"); + Field durationField = animationFrameClass.getDeclaredField("duration"); + durationField.setAccessible(true); + Field boundsField = animationFrameClass.getDeclaredField("bounds"); + boundsField.setAccessible(true); + + animFrameFunction = i -> { + Rectangle bounds = (Rectangle) boundsField.get(frames.get(i)); + return new AnimFrame((int) durationField.get(frames.get(i)), bounds.x, bounds.y); + }; + // that was fun + } + + return createFromImageReader(reader, animFrameFunction, uniqueLocation); + } catch (Throwable e) { + CrashReport crashReport = CrashReport.forThrowable(e, "Failed to load WEBP image"); + CrashReportCategory category = crashReport.addCategory("YACL Gui"); + category.setDetail("Image identifier", uniqueLocation.toString()); + throw new ReportedException(crashReport); + } + } + + private static ImageRendererFactory.ImageSupplier createFromImageReader(ImageReader reader, AnimFrameProvider animationProvider, ResourceLocation uniqueLocation) throws Exception { + if (reader.isSeekForwardOnly()) { + throw new RuntimeException("Image reader is not seekable"); + } + + int frameCount = reader.getNumImages(true); + + // Because this is being backed into a texture atlas, we need a maximum dimension + // so you can get the texture atlas size. + // Smaller frames are given black borders + int frameWidth = IntStream.range(0, frameCount).map(i -> { + try { + return reader.getWidth(i); + } catch (IOException e) { + throw new RuntimeException(e); + } + }).max().orElseThrow(); + int frameHeight = IntStream.range(0, frameCount).map(i -> { + try { + return reader.getHeight(i); + } catch (IOException e) { + throw new RuntimeException(e); + } + }).max().orElseThrow(); + + // Packs the frames into an optimal 1:1 texture. + // OpenGL can only have texture axis with a max of 32768 pixels, + // and packing them to that length is not efficient, apparently. + double ratio = frameWidth / (double)frameHeight; + int cols = (int)Math.ceil(Math.sqrt(frameCount) / Math.sqrt(ratio)); + int rows = (int)Math.ceil(frameCount / (double)cols); + + NativeImage image = new NativeImage(NativeImage.Format.RGBA, frameWidth * cols, frameHeight * rows, false); + +// // Fill whole atlas with black, as each frame may have different dimensions +// // that would cause borders of transparent pixels to appear around the frames +// for (int x = 0; x < frameWidth * cols; x++) { +// for (int y = 0; y < frameHeight * rows; y++) { +// image.setPixelRGBA(x, y, 0xFF000000); +// } +// } + + BufferedImage bi = null; + Graphics2D graphics = null; + + // each frame may have a different delay + double[] frameDelays = new double[frameCount]; + + for (int i = 0; i < frameCount; i++) { + AnimFrame frame = animationProvider.get(i); + if (frameCount > 1) // frame will be null if not animation + frameDelays[i] = frame.durationMS; + + if (bi == null) { + // first frame... + bi = reader.read(i); + graphics = bi.createGraphics(); + } else { + // WebP reader sometimes provides delta frames, (only the pixels that changed since the last frame) + // so instead of overwriting the image every frame, we draw delta frames on top of the previous frame + // to keep a complete image. + BufferedImage deltaFrame = reader.read(i); + graphics.drawImage(deltaFrame, frame.xOffset, frame.yOffset, null); + } + + // Each frame may have different dimensions, so we need to center them. + int xOffset = (frameWidth - bi.getWidth()) / 2; + int yOffset = (frameHeight - bi.getHeight()) / 2; + + for (int w = 0; w < bi.getWidth(); w++) { + for (int h = 0; h < bi.getHeight(); h++) { + int rgb = bi.getRGB(w, h); + int r = FastColor.ARGB32.red(rgb); + int g = FastColor.ARGB32.green(rgb); + int b = FastColor.ARGB32.blue(rgb); + int a = FastColor.ARGB32.alpha(rgb); + + int col = i % cols; + int row = (int) Math.floor(i / (double)cols); + + image.setPixelRGBA( + frameWidth * col + w + xOffset, + frameHeight * row + h + yOffset, + FastColor.ABGR32.color(a, b, g, r) // NativeImage uses ABGR for some reason + ); + } + } + } + + if (graphics != null) + graphics.dispose(); + reader.dispose(); + + return () -> new AnimatedDynamicTextureImage(image, frameWidth, frameHeight, frameCount, frameDelays, cols, rows, uniqueLocation); + } + + @FunctionalInterface + private interface AnimFrameProvider { + AnimFrame get(int frame) throws Exception; + } + + private record AnimFrame(int durationMS, int xOffset, int yOffset) {} + +} diff --git a/src/main/java/dev/isxander/yacl3/gui/image/impl/DynamicTextureImage.java b/src/main/java/dev/isxander/yacl3/gui/image/impl/DynamicTextureImage.java new file mode 100644 index 0000000..2d2abb9 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/gui/image/impl/DynamicTextureImage.java @@ -0,0 +1,72 @@ +package dev.isxander.yacl3.gui.image.impl; + +import com.mojang.blaze3d.platform.GlConst; +import com.mojang.blaze3d.platform.GlStateManager; +import com.mojang.blaze3d.platform.NativeImage; +import com.mojang.blaze3d.systems.RenderSystem; +import dev.isxander.yacl3.debug.DebugProperties; +import dev.isxander.yacl3.gui.image.ImageRenderer; +import dev.isxander.yacl3.gui.image.ImageRendererFactory; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.renderer.texture.DynamicTexture; +import net.minecraft.client.renderer.texture.TextureManager; +import net.minecraft.resources.ResourceLocation; + +import java.io.FileInputStream; +import java.nio.file.Path; + +public class DynamicTextureImage implements ImageRenderer { + protected static final TextureManager textureManager = Minecraft.getInstance().getTextureManager(); + + protected NativeImage image; + protected DynamicTexture texture; + protected final ResourceLocation uniqueLocation; + protected final int width, height; + + public DynamicTextureImage(NativeImage image, ResourceLocation location) { + RenderSystem.assertOnRenderThread(); + + this.image = image; + this.texture = new DynamicTexture(image); + this.uniqueLocation = location; + textureManager.register(this.uniqueLocation, this.texture); + this.width = image.getWidth(); + this.height = image.getHeight(); + } + + @Override + public int render(GuiGraphics graphics, int x, int y, int renderWidth, float tickDelta) { + if (image == null) return 0; + + float ratio = renderWidth / (float)this.width; + int targetHeight = (int) (this.height * ratio); + + graphics.pose().pushPose(); + graphics.pose().translate(x, y, 0); + graphics.pose().scale(ratio, ratio, 1); + + if (DebugProperties.IMAGE_FILTERING) { + GlStateManager._texParameter(GlConst.GL_TEXTURE_2D, GlConst.GL_TEXTURE_MAG_FILTER, GlConst.GL_LINEAR); + GlStateManager._texParameter(GlConst.GL_TEXTURE_2D, GlConst.GL_TEXTURE_MIN_FILTER, GlConst.GL_LINEAR); + } + + graphics.blit(uniqueLocation, 0, 0, 0, 0, this.width, this.height, this.width, this.height); + + graphics.pose().popPose(); + + return targetHeight; + } + + @Override + public void close() { + image.close(); + image = null; + texture = null; + textureManager.release(uniqueLocation); + } + + public static ImageRendererFactory fromPath(Path imagePath, ResourceLocation location) { + return (ImageRendererFactory.OnThread) () -> () -> new DynamicTextureImage(NativeImage.read(new FileInputStream(imagePath.toFile())), location); + } +} diff --git a/src/main/java/dev/isxander/yacl3/gui/image/impl/ResourceTextureImage.java b/src/main/java/dev/isxander/yacl3/gui/image/impl/ResourceTextureImage.java new file mode 100644 index 0000000..abbeec7 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/gui/image/impl/ResourceTextureImage.java @@ -0,0 +1,56 @@ +package dev.isxander.yacl3.gui.image.impl; + +import com.mojang.blaze3d.platform.GlConst; +import com.mojang.blaze3d.platform.GlStateManager; +import dev.isxander.yacl3.debug.DebugProperties; +import dev.isxander.yacl3.gui.image.ImageRenderer; +import dev.isxander.yacl3.gui.image.ImageRendererFactory; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.resources.ResourceLocation; + +public class ResourceTextureImage implements ImageRenderer { + private final ResourceLocation location; + private final int width, height; + private final int textureWidth, textureHeight; + private final float u, v; + + public ResourceTextureImage(ResourceLocation location, float u, float v, int width, int height, int textureWidth, int textureHeight) { + this.location = location; + this.width = width; + this.height = height; + this.textureWidth = textureWidth; + this.textureHeight = textureHeight; + this.u = u; + this.v = v; + } + + @Override + public int render(GuiGraphics graphics, int x, int y, int renderWidth, float tickDelta) { + float ratio = renderWidth / (float)this.width; + int targetHeight = (int) (this.height * ratio); + + graphics.pose().pushPose(); + graphics.pose().translate(x, y, 0); + graphics.pose().scale(ratio, ratio, 1); + + if (DebugProperties.IMAGE_FILTERING) { + GlStateManager._texParameter(GlConst.GL_TEXTURE_2D, GlConst.GL_TEXTURE_MAG_FILTER, GlConst.GL_LINEAR); + GlStateManager._texParameter(GlConst.GL_TEXTURE_2D, GlConst.GL_TEXTURE_MIN_FILTER, GlConst.GL_LINEAR); + } + + graphics.blit(location, 0, 0, this.u, this.v, this.width, this.height, this.textureWidth, this.textureHeight); + + graphics.pose().popPose(); + + return targetHeight; + } + + @Override + public void close() { + + } + + public static ImageRendererFactory createFactory(ResourceLocation location, float u, float v, int width, int height, int textureWidth, int textureHeight) { + return (ImageRendererFactory.OnThread) () -> () -> new ResourceTextureImage(location, u, v, width, height, textureWidth, textureHeight); + } +} |