package net.minecraft.client.renderer.texture; import com.mojang.blaze3d.platform.NativeImage; import com.mojang.blaze3d.systems.RenderSystem; import com.mojang.blaze3d.textures.GpuTexture; import com.mojang.logging.LogUtils; import it.unimi.dsi.fastutil.ints.IntOpenHashSet; import it.unimi.dsi.fastutil.ints.IntSet; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.stream.IntStream; import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; import net.minecraft.CrashReport; import net.minecraft.CrashReportCategory; import net.minecraft.CrashReportDetail; import net.minecraft.ReportedException; import net.minecraft.client.renderer.texture.Stitcher.Entry; import net.minecraft.client.resources.metadata.animation.AnimationFrame; import net.minecraft.client.resources.metadata.animation.AnimationMetadataSection; import net.minecraft.client.resources.metadata.animation.FrameSize; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.packs.resources.ResourceMetadata; import net.minecraft.util.ARGB; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; @Environment(EnvType.CLIENT) public class SpriteContents implements Entry, AutoCloseable { private static final Logger LOGGER = LogUtils.getLogger(); private final ResourceLocation name; final int width; final int height; private final NativeImage originalImage; NativeImage[] byMipLevel; @Nullable private final SpriteContents.AnimatedTexture animatedTexture; private final ResourceMetadata metadata; public SpriteContents(ResourceLocation name, FrameSize frameSize, NativeImage originalImage, ResourceMetadata metadata) { this.name = name; this.width = frameSize.width(); this.height = frameSize.height(); this.metadata = metadata; this.animatedTexture = (SpriteContents.AnimatedTexture)metadata.getSection(AnimationMetadataSection.TYPE) .map(animationMetadataSection -> this.createAnimatedTexture(frameSize, originalImage.getWidth(), originalImage.getHeight(), animationMetadataSection)) .orElse(null); this.originalImage = originalImage; this.byMipLevel = new NativeImage[]{this.originalImage}; } public void increaseMipLevel(int mipLevel) { try { this.byMipLevel = MipmapGenerator.generateMipLevels(this.byMipLevel, mipLevel); } catch (Throwable var6) { CrashReport crashReport = CrashReport.forThrowable(var6, "Generating mipmaps for frame"); CrashReportCategory crashReportCategory = crashReport.addCategory("Sprite being mipmapped"); crashReportCategory.setDetail("First frame", (CrashReportDetail)(() -> { StringBuilder stringBuilder = new StringBuilder(); if (stringBuilder.length() > 0) { stringBuilder.append(", "); } stringBuilder.append(this.originalImage.getWidth()).append("x").append(this.originalImage.getHeight()); return stringBuilder.toString(); })); CrashReportCategory crashReportCategory2 = crashReport.addCategory("Frame being iterated"); crashReportCategory2.setDetail("Sprite name", this.name); crashReportCategory2.setDetail("Sprite size", (CrashReportDetail)(() -> this.width + " x " + this.height)); crashReportCategory2.setDetail("Sprite frames", (CrashReportDetail)(() -> this.getFrameCount() + " frames")); crashReportCategory2.setDetail("Mipmap levels", mipLevel); throw new ReportedException(crashReport); } } private int getFrameCount() { return this.animatedTexture != null ? this.animatedTexture.frames.size() : 1; } @Nullable private SpriteContents.AnimatedTexture createAnimatedTexture(FrameSize frameSize, int width, int height, AnimationMetadataSection metadata) { int i = width / frameSize.width(); int j = height / frameSize.height(); int k = i * j; int l = metadata.defaultFrameTime(); List list; if (metadata.frames().isEmpty()) { list = new ArrayList(k); for (int m = 0; m < k; m++) { list.add(new SpriteContents.FrameInfo(m, l)); } } else { List list2 = (List)metadata.frames().get(); list = new ArrayList(list2.size()); for (AnimationFrame animationFrame : list2) { list.add(new SpriteContents.FrameInfo(animationFrame.index(), animationFrame.timeOr(l))); } int n = 0; IntSet intSet = new IntOpenHashSet(); for (Iterator iterator = list.iterator(); iterator.hasNext(); n++) { SpriteContents.FrameInfo frameInfo = (SpriteContents.FrameInfo)iterator.next(); boolean bl = true; if (frameInfo.time <= 0) { LOGGER.warn("Invalid frame duration on sprite {} frame {}: {}", this.name, n, frameInfo.time); bl = false; } if (frameInfo.index < 0 || frameInfo.index >= k) { LOGGER.warn("Invalid frame index on sprite {} frame {}: {}", this.name, n, frameInfo.index); bl = false; } if (bl) { intSet.add(frameInfo.index); } else { iterator.remove(); } } int[] is = IntStream.range(0, k).filter(ix -> !intSet.contains(ix)).toArray(); if (is.length > 0) { LOGGER.warn("Unused frames in sprite {}: {}", this.name, Arrays.toString(is)); } } return list.size() <= 1 ? null : new SpriteContents.AnimatedTexture(List.copyOf(list), i, metadata.interpolatedFrames()); } void upload(int x, int y, int sourceX, int sourceY, NativeImage[] images, GpuTexture texture) { for (int i = 0; i < this.byMipLevel.length; i++) { RenderSystem.getDevice() .createCommandEncoder() .writeToTexture(texture, images[i], i, x >> i, y >> i, this.width >> i, this.height >> i, sourceX >> i, sourceY >> i); } } @Override public int width() { return this.width; } @Override public int height() { return this.height; } @Override public ResourceLocation name() { return this.name; } public IntStream getUniqueFrames() { return this.animatedTexture != null ? this.animatedTexture.getUniqueFrames() : IntStream.of(1); } @Nullable public SpriteTicker createTicker() { return this.animatedTexture != null ? this.animatedTexture.createTicker() : null; } public ResourceMetadata metadata() { return this.metadata; } public void close() { for (NativeImage nativeImage : this.byMipLevel) { nativeImage.close(); } } public String toString() { return "SpriteContents{name=" + this.name + ", frameCount=" + this.getFrameCount() + ", height=" + this.height + ", width=" + this.width + "}"; } public boolean isTransparent(int frame, int x, int y) { int i = x; int j = y; if (this.animatedTexture != null) { i = x + this.animatedTexture.getFrameX(frame) * this.width; j = y + this.animatedTexture.getFrameY(frame) * this.height; } return ARGB.alpha(this.originalImage.getPixel(i, j)) == 0; } public void uploadFirstFrame(int x, int y, GpuTexture texture) { if (this.animatedTexture != null) { this.animatedTexture.uploadFirstFrame(x, y, texture); } else { this.upload(x, y, 0, 0, this.byMipLevel, texture); } } @Environment(EnvType.CLIENT) class AnimatedTexture { final List frames; private final int frameRowSize; private final boolean interpolateFrames; AnimatedTexture(final List frames, final int frameRowSize, final boolean interpolateFrames) { this.frames = frames; this.frameRowSize = frameRowSize; this.interpolateFrames = interpolateFrames; } int getFrameX(int frameIndex) { return frameIndex % this.frameRowSize; } int getFrameY(int frameIndex) { return frameIndex / this.frameRowSize; } void uploadFrame(int x, int y, int frameIndex, GpuTexture texture) { int i = this.getFrameX(frameIndex) * SpriteContents.this.width; int j = this.getFrameY(frameIndex) * SpriteContents.this.height; SpriteContents.this.upload(x, y, i, j, SpriteContents.this.byMipLevel, texture); } public SpriteTicker createTicker() { return SpriteContents.this.new Ticker(this, this.interpolateFrames ? SpriteContents.this.new InterpolationData() : null); } public void uploadFirstFrame(int x, int y, GpuTexture texture) { this.uploadFrame(x, y, ((SpriteContents.FrameInfo)this.frames.get(0)).index, texture); } public IntStream getUniqueFrames() { return this.frames.stream().mapToInt(frameInfo -> frameInfo.index).distinct(); } } @Environment(EnvType.CLIENT) record FrameInfo(int index, int time) { } @Environment(EnvType.CLIENT) final class InterpolationData implements AutoCloseable { private final NativeImage[] activeFrame = new NativeImage[SpriteContents.this.byMipLevel.length]; InterpolationData() { for (int i = 0; i < this.activeFrame.length; i++) { int j = SpriteContents.this.width >> i; int k = SpriteContents.this.height >> i; this.activeFrame[i] = new NativeImage(j, k, false); } } void uploadInterpolatedFrame(int x, int y, SpriteContents.Ticker ticker, GpuTexture texture) { SpriteContents.AnimatedTexture animatedTexture = ticker.animationInfo; List list = animatedTexture.frames; SpriteContents.FrameInfo frameInfo = (SpriteContents.FrameInfo)list.get(ticker.frame); float f = (float)ticker.subFrame / frameInfo.time; int i = frameInfo.index; int j = ((SpriteContents.FrameInfo)list.get((ticker.frame + 1) % list.size())).index; if (i != j) { for (int k = 0; k < this.activeFrame.length; k++) { int l = SpriteContents.this.width >> k; int m = SpriteContents.this.height >> k; for (int n = 0; n < m; n++) { for (int o = 0; o < l; o++) { int p = this.getPixel(animatedTexture, i, k, o, n); int q = this.getPixel(animatedTexture, j, k, o, n); this.activeFrame[k].setPixel(o, n, ARGB.lerp(f, p, q)); } } } SpriteContents.this.upload(x, y, 0, 0, this.activeFrame, texture); } } private int getPixel(SpriteContents.AnimatedTexture animatedTexture, int frameIndex, int mipLevel, int x, int y) { return SpriteContents.this.byMipLevel[mipLevel] .getPixel( x + (animatedTexture.getFrameX(frameIndex) * SpriteContents.this.width >> mipLevel), y + (animatedTexture.getFrameY(frameIndex) * SpriteContents.this.height >> mipLevel) ); } public void close() { for (NativeImage nativeImage : this.activeFrame) { nativeImage.close(); } } } @Environment(EnvType.CLIENT) class Ticker implements SpriteTicker { int frame; int subFrame; final SpriteContents.AnimatedTexture animationInfo; @Nullable private final SpriteContents.InterpolationData interpolationData; Ticker(final SpriteContents.AnimatedTexture animationInfo, @Nullable final SpriteContents.InterpolationData interpolationData) { this.animationInfo = animationInfo; this.interpolationData = interpolationData; } @Override public void tickAndUpload(int x, int y, GpuTexture texture) { this.subFrame++; SpriteContents.FrameInfo frameInfo = (SpriteContents.FrameInfo)this.animationInfo.frames.get(this.frame); if (this.subFrame >= frameInfo.time) { int i = frameInfo.index; this.frame = (this.frame + 1) % this.animationInfo.frames.size(); this.subFrame = 0; int j = ((SpriteContents.FrameInfo)this.animationInfo.frames.get(this.frame)).index; if (i != j) { this.animationInfo.uploadFrame(x, y, j, texture); } } else if (this.interpolationData != null) { this.interpolationData.uploadInterpolatedFrame(x, y, this, texture); } } @Override public void close() { if (this.interpolationData != null) { this.interpolationData.close(); } } } }