329 lines
15 KiB
Java
329 lines
15 KiB
Java
/*
|
|
* Decompiled with CFR 0.152.
|
|
*
|
|
* Could not load the following classes:
|
|
* com.mojang.logging.LogUtils
|
|
* it.unimi.dsi.fastutil.ints.Int2ObjectMap
|
|
* it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
|
|
* it.unimi.dsi.fastutil.ints.IntOpenHashSet
|
|
* it.unimi.dsi.fastutil.ints.IntSet
|
|
* org.jspecify.annotations.Nullable
|
|
* org.slf4j.Logger
|
|
*/
|
|
package net.minecraft.client.renderer.texture;
|
|
|
|
import com.mojang.blaze3d.buffers.GpuBufferSlice;
|
|
import com.mojang.blaze3d.buffers.Std140SizeCalculator;
|
|
import com.mojang.blaze3d.platform.NativeImage;
|
|
import com.mojang.blaze3d.systems.GpuDevice;
|
|
import com.mojang.blaze3d.systems.RenderPass;
|
|
import com.mojang.blaze3d.systems.RenderSystem;
|
|
import com.mojang.blaze3d.textures.FilterMode;
|
|
import com.mojang.blaze3d.textures.GpuSampler;
|
|
import com.mojang.blaze3d.textures.GpuTexture;
|
|
import com.mojang.blaze3d.textures.GpuTextureView;
|
|
import com.mojang.blaze3d.textures.TextureFormat;
|
|
import com.mojang.logging.LogUtils;
|
|
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
|
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
|
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.Optional;
|
|
import java.util.stream.IntStream;
|
|
import net.minecraft.CrashReport;
|
|
import net.minecraft.CrashReportCategory;
|
|
import net.minecraft.ReportedException;
|
|
import net.minecraft.client.renderer.RenderPipelines;
|
|
import net.minecraft.client.renderer.texture.MipmapGenerator;
|
|
import net.minecraft.client.renderer.texture.MipmapStrategy;
|
|
import net.minecraft.client.renderer.texture.Stitcher;
|
|
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.Identifier;
|
|
import net.minecraft.server.packs.metadata.MetadataSectionType;
|
|
import net.minecraft.util.ARGB;
|
|
import org.jspecify.annotations.Nullable;
|
|
import org.slf4j.Logger;
|
|
|
|
public class SpriteContents
|
|
implements AutoCloseable,
|
|
Stitcher.Entry {
|
|
private static final Logger LOGGER = LogUtils.getLogger();
|
|
public static final int UBO_SIZE = new Std140SizeCalculator().putMat4f().putMat4f().putFloat().putFloat().putInt().get();
|
|
private final Identifier name;
|
|
private final int width;
|
|
private final int height;
|
|
private final NativeImage originalImage;
|
|
private NativeImage[] byMipLevel;
|
|
private final @Nullable AnimatedTexture animatedTexture;
|
|
private final List<MetadataSectionType.WithValue<?>> additionalMetadata;
|
|
private final MipmapStrategy mipmapStrategy;
|
|
|
|
public SpriteContents(Identifier name, FrameSize frameSize, NativeImage image) {
|
|
this(name, frameSize, image, Optional.empty(), List.of(), MipmapStrategy.AUTO);
|
|
}
|
|
|
|
public SpriteContents(Identifier name, FrameSize frameSize, NativeImage image, Optional<AnimationMetadataSection> animationInfo, List<MetadataSectionType.WithValue<?>> additionalMetadata, MipmapStrategy mipmapStrategy) {
|
|
this.name = name;
|
|
this.width = frameSize.width();
|
|
this.height = frameSize.height();
|
|
this.additionalMetadata = additionalMetadata;
|
|
this.animatedTexture = animationInfo.map(animation -> this.createAnimatedTexture(frameSize, image.getWidth(), image.getHeight(), (AnimationMetadataSection)animation)).orElse(null);
|
|
this.originalImage = image;
|
|
this.byMipLevel = new NativeImage[]{this.originalImage};
|
|
this.mipmapStrategy = mipmapStrategy;
|
|
}
|
|
|
|
public void increaseMipLevel(int mipLevel) {
|
|
try {
|
|
this.byMipLevel = MipmapGenerator.generateMipLevels(this.name, this.byMipLevel, mipLevel, this.mipmapStrategy);
|
|
}
|
|
catch (Throwable t) {
|
|
CrashReport report = CrashReport.forThrowable(t, "Generating mipmaps for frame");
|
|
CrashReportCategory frameCategory = report.addCategory("Frame being iterated");
|
|
frameCategory.setDetail("Sprite name", this.name);
|
|
frameCategory.setDetail("Sprite size", () -> this.width + " x " + this.height);
|
|
frameCategory.setDetail("Sprite frames", () -> this.getFrameCount() + " frames");
|
|
frameCategory.setDetail("Mipmap levels", mipLevel);
|
|
frameCategory.setDetail("Original image size", () -> this.originalImage.getWidth() + "x" + this.originalImage.getHeight());
|
|
throw new ReportedException(report);
|
|
}
|
|
}
|
|
|
|
private int getFrameCount() {
|
|
return this.animatedTexture != null ? this.animatedTexture.frames.size() : 1;
|
|
}
|
|
|
|
public boolean isAnimated() {
|
|
return this.getFrameCount() > 1;
|
|
}
|
|
|
|
private @Nullable AnimatedTexture createAnimatedTexture(FrameSize frameSize, int fullWidth, int fullHeight, AnimationMetadataSection metadata) {
|
|
ArrayList<FrameInfo> frames;
|
|
int frameRowSize = fullWidth / frameSize.width();
|
|
int frameColumnSize = fullHeight / frameSize.height();
|
|
int totalFrameCount = frameRowSize * frameColumnSize;
|
|
int defaultFrameTime = metadata.defaultFrameTime();
|
|
if (metadata.frames().isEmpty()) {
|
|
frames = new ArrayList<FrameInfo>(totalFrameCount);
|
|
for (int i = 0; i < totalFrameCount; ++i) {
|
|
frames.add(new FrameInfo(i, defaultFrameTime));
|
|
}
|
|
} else {
|
|
List<AnimationFrame> metadataFrames = metadata.frames().get();
|
|
frames = new ArrayList(metadataFrames.size());
|
|
for (AnimationFrame frame : metadataFrames) {
|
|
frames.add(new FrameInfo(frame.index(), frame.timeOr(defaultFrameTime)));
|
|
}
|
|
int index = 0;
|
|
IntOpenHashSet usedFrameIndices = new IntOpenHashSet();
|
|
Iterator iterator = frames.iterator();
|
|
while (iterator.hasNext()) {
|
|
FrameInfo frame = (FrameInfo)iterator.next();
|
|
boolean isValid = true;
|
|
if (frame.time <= 0) {
|
|
LOGGER.warn("Invalid frame duration on sprite {} frame {}: {}", new Object[]{this.name, index, frame.time});
|
|
isValid = false;
|
|
}
|
|
if (frame.index < 0 || frame.index >= totalFrameCount) {
|
|
LOGGER.warn("Invalid frame index on sprite {} frame {}: {}", new Object[]{this.name, index, frame.index});
|
|
isValid = false;
|
|
}
|
|
if (isValid) {
|
|
usedFrameIndices.add(frame.index);
|
|
} else {
|
|
iterator.remove();
|
|
}
|
|
++index;
|
|
}
|
|
int[] unusedFrameIndices = IntStream.range(0, totalFrameCount).filter(arg_0 -> SpriteContents.lambda$createAnimatedTexture$4((IntSet)usedFrameIndices, arg_0)).toArray();
|
|
if (unusedFrameIndices.length > 0) {
|
|
LOGGER.warn("Unused frames in sprite {}: {}", (Object)this.name, (Object)Arrays.toString(unusedFrameIndices));
|
|
}
|
|
}
|
|
if (frames.size() <= 1) {
|
|
return null;
|
|
}
|
|
return new AnimatedTexture(List.copyOf(frames), frameRowSize, metadata.interpolatedFrames());
|
|
}
|
|
|
|
@Override
|
|
public int width() {
|
|
return this.width;
|
|
}
|
|
|
|
@Override
|
|
public int height() {
|
|
return this.height;
|
|
}
|
|
|
|
@Override
|
|
public Identifier name() {
|
|
return this.name;
|
|
}
|
|
|
|
public IntStream getUniqueFrames() {
|
|
return this.animatedTexture != null ? this.animatedTexture.getUniqueFrames() : IntStream.of(1);
|
|
}
|
|
|
|
public @Nullable AnimationState createAnimationState(GpuBufferSlice uboSlice, int spriteUboSize) {
|
|
return this.animatedTexture != null ? this.animatedTexture.createAnimationState(uboSlice, spriteUboSize) : null;
|
|
}
|
|
|
|
public <T> Optional<T> getAdditionalMetadata(MetadataSectionType<T> type) {
|
|
for (MetadataSectionType.WithValue<?> metadata : this.additionalMetadata) {
|
|
Optional<T> result = metadata.unwrapToType(type);
|
|
if (!result.isPresent()) continue;
|
|
return result;
|
|
}
|
|
return Optional.empty();
|
|
}
|
|
|
|
@Override
|
|
public void close() {
|
|
for (NativeImage image : this.byMipLevel) {
|
|
image.close();
|
|
}
|
|
}
|
|
|
|
public String toString() {
|
|
return "SpriteContents{name=" + String.valueOf(this.name) + ", frameCount=" + this.getFrameCount() + ", height=" + this.height + ", width=" + this.width + "}";
|
|
}
|
|
|
|
public boolean isTransparent(int frame, int x, int y) {
|
|
int actualX = x;
|
|
int actualY = y;
|
|
if (this.animatedTexture != null) {
|
|
actualX += this.animatedTexture.getFrameX(frame) * this.width;
|
|
actualY += this.animatedTexture.getFrameY(frame) * this.height;
|
|
}
|
|
return ARGB.alpha(this.originalImage.getPixel(actualX, actualY)) == 0;
|
|
}
|
|
|
|
public void uploadFirstFrame(GpuTexture destination, int level) {
|
|
RenderSystem.getDevice().createCommandEncoder().writeToTexture(destination, this.byMipLevel[level], level, 0, 0, 0, this.width >> level, this.height >> level, 0, 0);
|
|
}
|
|
|
|
private static /* synthetic */ boolean lambda$createAnimatedTexture$4(IntSet usedFrameIndices, int i) {
|
|
return !usedFrameIndices.contains(i);
|
|
}
|
|
|
|
private class AnimatedTexture {
|
|
private final List<FrameInfo> frames;
|
|
private final int frameRowSize;
|
|
private final boolean interpolateFrames;
|
|
|
|
private AnimatedTexture(List<FrameInfo> frames, int frameRowSize, boolean interpolateFrames) {
|
|
this.frames = frames;
|
|
this.frameRowSize = frameRowSize;
|
|
this.interpolateFrames = interpolateFrames;
|
|
}
|
|
|
|
private int getFrameX(int index) {
|
|
return index % this.frameRowSize;
|
|
}
|
|
|
|
private int getFrameY(int index) {
|
|
return index / this.frameRowSize;
|
|
}
|
|
|
|
public AnimationState createAnimationState(GpuBufferSlice uboSlice, int spriteUboSize) {
|
|
GpuDevice device = RenderSystem.getDevice();
|
|
Int2ObjectOpenHashMap frameTexturesByIndex = new Int2ObjectOpenHashMap();
|
|
GpuBufferSlice[] spriteUbosByMip = new GpuBufferSlice[SpriteContents.this.byMipLevel.length];
|
|
for (int frame : this.getUniqueFrames().toArray()) {
|
|
GpuTexture texture = device.createTexture(() -> String.valueOf(SpriteContents.this.name) + " animation frame " + frame, 5, TextureFormat.RGBA8, SpriteContents.this.width, SpriteContents.this.height, 1, SpriteContents.this.byMipLevel.length + 1);
|
|
int offsetX = this.getFrameX(frame) * SpriteContents.this.width;
|
|
int offsetY = this.getFrameY(frame) * SpriteContents.this.height;
|
|
for (int level = 0; level < SpriteContents.this.byMipLevel.length; ++level) {
|
|
RenderSystem.getDevice().createCommandEncoder().writeToTexture(texture, SpriteContents.this.byMipLevel[level], level, 0, 0, 0, SpriteContents.this.width >> level, SpriteContents.this.height >> level, offsetX >> level, offsetY >> level);
|
|
}
|
|
frameTexturesByIndex.put(frame, (Object)RenderSystem.getDevice().createTextureView(texture));
|
|
}
|
|
for (int level = 0; level < SpriteContents.this.byMipLevel.length; ++level) {
|
|
spriteUbosByMip[level] = uboSlice.slice(level * spriteUboSize, spriteUboSize);
|
|
}
|
|
return new AnimationState(SpriteContents.this, this, (Int2ObjectMap<GpuTextureView>)frameTexturesByIndex, spriteUbosByMip);
|
|
}
|
|
|
|
public IntStream getUniqueFrames() {
|
|
return this.frames.stream().mapToInt(f -> f.index).distinct();
|
|
}
|
|
}
|
|
|
|
private record FrameInfo(int index, int time) {
|
|
}
|
|
|
|
public class AnimationState
|
|
implements AutoCloseable {
|
|
private int frame;
|
|
private int subFrame;
|
|
private final AnimatedTexture animationInfo;
|
|
private final Int2ObjectMap<GpuTextureView> frameTexturesByIndex;
|
|
private final GpuBufferSlice[] spriteUbosByMip;
|
|
private boolean isDirty;
|
|
|
|
private AnimationState(SpriteContents this$0, AnimatedTexture animationInfo, Int2ObjectMap<GpuTextureView> frameTexturesByIndex, GpuBufferSlice[] spriteUbosByMip) {
|
|
this.animationInfo = animationInfo;
|
|
this.frameTexturesByIndex = frameTexturesByIndex;
|
|
this.spriteUbosByMip = spriteUbosByMip;
|
|
}
|
|
|
|
public void tick() {
|
|
++this.subFrame;
|
|
this.isDirty = false;
|
|
FrameInfo currentFrame = this.animationInfo.frames.get(this.frame);
|
|
if (this.subFrame >= currentFrame.time) {
|
|
int oldFrame = currentFrame.index;
|
|
this.frame = (this.frame + 1) % this.animationInfo.frames.size();
|
|
this.subFrame = 0;
|
|
int newFrame = this.animationInfo.frames.get((int)this.frame).index;
|
|
if (oldFrame != newFrame) {
|
|
this.isDirty = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
public GpuBufferSlice getDrawUbo(int level) {
|
|
return this.spriteUbosByMip[level];
|
|
}
|
|
|
|
public boolean needsToDraw() {
|
|
return this.animationInfo.interpolateFrames || this.isDirty;
|
|
}
|
|
|
|
public void drawToAtlas(RenderPass renderPass, GpuBufferSlice ubo) {
|
|
GpuSampler sampler = RenderSystem.getSamplerCache().getClampToEdge(FilterMode.NEAREST, true);
|
|
List<FrameInfo> frames = this.animationInfo.frames;
|
|
int oldFrame = frames.get((int)this.frame).index;
|
|
float frameProgress = (float)this.subFrame / (float)this.animationInfo.frames.get((int)this.frame).time;
|
|
int frameProgressAsInt = (int)(frameProgress * 1000.0f);
|
|
if (this.animationInfo.interpolateFrames) {
|
|
int newFrame = frames.get((int)((this.frame + 1) % frames.size())).index;
|
|
renderPass.setPipeline(RenderPipelines.ANIMATE_SPRITE_INTERPOLATE);
|
|
renderPass.bindTexture("CurrentSprite", (GpuTextureView)this.frameTexturesByIndex.get(oldFrame), sampler);
|
|
renderPass.bindTexture("NextSprite", (GpuTextureView)this.frameTexturesByIndex.get(newFrame), sampler);
|
|
} else if (this.isDirty) {
|
|
renderPass.setPipeline(RenderPipelines.ANIMATE_SPRITE_BLIT);
|
|
renderPass.bindTexture("Sprite", (GpuTextureView)this.frameTexturesByIndex.get(oldFrame), sampler);
|
|
}
|
|
renderPass.setUniform("SpriteAnimationInfo", ubo);
|
|
renderPass.draw(frameProgressAsInt << 3, 6);
|
|
}
|
|
|
|
@Override
|
|
public void close() {
|
|
for (GpuTextureView view : this.frameTexturesByIndex.values()) {
|
|
view.texture().close();
|
|
view.close();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|