Last active
July 27, 2024 19:47
-
-
Save gigaherz/56b259126f715807c8e6c3dc055b924b to your computer and use it in GitHub Desktop.
Animated Gif for Minecraft -- Public Domain, or the closest to it, see CC0 at https://creativecommons.org/share-your-work/public-domain/cc0/
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package gigaherz.eyes.client; | |
import gigaherz.eyes.EyesInTheDarkness; | |
import gigaherz.eyes.entity.EyesEntity; | |
import net.minecraft.client.Minecraft; | |
import net.minecraftforge.api.distmarker.Dist; | |
import net.minecraftforge.client.event.ModelRegistryEvent; | |
import net.minecraftforge.client.event.RenderGameOverlayEvent; | |
import net.minecraftforge.event.TickEvent; | |
import net.minecraftforge.eventbus.api.SubscribeEvent; | |
import net.minecraftforge.fml.client.registry.RenderingRegistry; | |
import net.minecraftforge.fml.common.Mod; | |
import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent; | |
import java.io.IOException; | |
import java.nio.file.Files; | |
import java.nio.file.Paths; | |
@Mod.EventBusSubscriber(value = Dist.CLIENT, modid = EyesInTheDarkness.MODID, bus = Mod.EventBusSubscriber.Bus.MOD) | |
public class ClientEvents | |
{ | |
private static AnimatedGif gifImage; | |
private static AnimatedGif.GifPlayer gifPlayer; | |
@SubscribeEvent | |
public static void registerEntityRenders(final FMLClientSetupEvent event) | |
{ | |
RenderingRegistry.registerEntityRenderingHandler(EyesEntity.TYPE, EyesRenderer::new); | |
event.enqueueWork(() -> { | |
try | |
{ | |
gifImage = AnimatedGif.fromPath(Paths.get("F:\\yeet.gif")); | |
//gifImage.exportToMcAnim(Paths.get("D:\\crap\\avatar-1080-brow-gif.png")); | |
gifPlayer = gifImage.makeGifPlayer(); | |
gifPlayer.setAutoplay(true); | |
gifPlayer.setLooping(true); | |
// TODO: gifPlayer.close(); when the player isn't needed, else you'll leak OpenGL Textures!!! | |
} | |
catch (IOException e) | |
{ | |
e.printStackTrace(); | |
} | |
}); | |
} | |
@Mod.EventBusSubscriber(value = Dist.CLIENT, modid = EyesInTheDarkness.MODID, bus = Mod.EventBusSubscriber.Bus.FORGE) | |
public static class ForgeEvents | |
{ | |
@SubscribeEvent | |
public static void render(RenderGameOverlayEvent.Pre event) | |
{ | |
if (event.getType() != RenderGameOverlayEvent.ElementType.ALL) | |
return; | |
gifPlayer.render(event.getMatrixStack(), 50, 50, gifImage.getWidth(), gifImage.getHeight(), event.getPartialTicks()); | |
} | |
@SubscribeEvent | |
public static void tick(TickEvent.ClientTickEvent event) | |
{ | |
if (event.phase != TickEvent.Phase.START) | |
return; | |
if (Minecraft.getInstance().player != null) | |
{ | |
gifPlayer.tick(); | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
try | |
{ | |
AnimatedGif gifImage = AnimatedGif.fromPath(Paths.get("path/to/srcassets/image.gif")); | |
gifImage.exportToMcAnim(Paths.get("path/to/assets/mod/textures/image.png")); | |
} | |
catch (IOException e) | |
{ | |
e.printStackTrace(); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package gigaherz.eyes.client; | |
import com.google.common.collect.Lists; | |
import com.google.gson.Gson; | |
import com.google.gson.JsonArray; | |
import com.google.gson.JsonObject; | |
import com.mojang.blaze3d.matrix.MatrixStack; | |
import com.mojang.blaze3d.systems.RenderSystem; | |
import net.minecraft.client.gui.AbstractGui; | |
import net.minecraft.client.renderer.texture.AtlasTexture; | |
import net.minecraft.client.renderer.texture.NativeImage; | |
import net.minecraft.client.renderer.texture.TextureAtlasSprite; | |
import net.minecraft.client.renderer.texture.TextureUtil; | |
import net.minecraft.client.resources.data.AnimationFrame; | |
import net.minecraft.client.resources.data.AnimationMetadataSection; | |
import net.minecraft.util.ResourceLocation; | |
import net.minecraft.util.math.MathHelper; | |
import org.lwjgl.PointerBuffer; | |
import org.lwjgl.stb.STBImage; | |
import org.lwjgl.system.MemoryStack; | |
import org.lwjgl.system.MemoryUtil; | |
import java.io.FileWriter; | |
import java.io.IOException; | |
import java.nio.ByteBuffer; | |
import java.nio.IntBuffer; | |
import java.nio.file.Files; | |
import java.nio.file.Path; | |
import java.util.Arrays; | |
import java.util.List; | |
public class AnimatedGif | |
{ | |
private static final int GIF_TICKS_PER_SECOND = 1000; | |
private static final int MC_TICKS_PER_SECOND = 20; | |
private static final int MIN_GIF_TICKS = MathHelper.ceil(0.01f * GIF_TICKS_PER_SECOND); // Most browsers have approximately 0.1s minimum interval between frames. Let's respect that! | |
private static final int MIN_MC_TICKS = MathHelper.ceil(MIN_GIF_TICKS * (float)MC_TICKS_PER_SECOND / GIF_TICKS_PER_SECOND); | |
public static AnimatedGif fromPath(Path path) throws IOException | |
{ | |
byte[] bytes = Files.readAllBytes(path); | |
return fromMemory(bytes); | |
} | |
private static AnimatedGif fromMemory(byte[] fileData) | |
{ | |
final ByteBuffer gif = MemoryUtil.memAlloc(fileData.length); | |
try | |
{ | |
gif.put(fileData); | |
gif.position(0); | |
try (final MemoryStack stack = MemoryStack.stackPush()) | |
{ | |
final PointerBuffer delaysBuffer = stack.mallocPointer(1); | |
final IntBuffer x = stack.mallocInt(1); | |
final IntBuffer y = stack.mallocInt(1); | |
final IntBuffer z = stack.mallocInt(1); | |
final IntBuffer channels = stack.mallocInt(1); | |
final ByteBuffer image = STBImage.stbi_load_gif_from_memory(gif, delaysBuffer, x, y, z, channels, 0); | |
try | |
{ | |
if (image == null) | |
{ | |
throw new RuntimeException(STBImage.stbi_failure_reason()); // assumes program termination: if not, cleanup of resources is required | |
} | |
int nch = channels.get(); | |
if (nch != 4) | |
{ | |
throw new RuntimeException("Unexpected number of channels " + nch + ", expected 4"); | |
} | |
int width = x.get(); | |
int height = y.get(); | |
int frames = z.get(); | |
IntBuffer delaysIntBuffer = delaysBuffer.getIntBuffer(frames); | |
int[] delays = new int[frames]; | |
delaysIntBuffer.get(delays); | |
IntBuffer pixelData = image.asIntBuffer(); | |
int[] pixels = new int[width * height * frames]; | |
pixelData.get(pixels); | |
return new AnimatedGif(width, height, frames, pixels, delays); | |
} | |
finally | |
{ | |
if (image != null) | |
STBImage.stbi_image_free(image); | |
} | |
} | |
} | |
finally | |
{ | |
MemoryUtil.memFree(gif); | |
} | |
} | |
private final int width; | |
private final int height; | |
private final int frames; | |
private final int[] pixels; | |
private final int[] delays; | |
public AnimatedGif(int width, int height, int frames, int[] pixels, int[] delays) | |
{ | |
this.width = width; | |
this.height = height; | |
this.frames = frames; | |
this.pixels = pixels; | |
this.delays = delays; | |
if (pixels.length != width * height * frames) | |
throw new IllegalArgumentException("Pixels array length must be == width*height*frames, was " + pixels.length); | |
if (delays.length != frames) | |
throw new IllegalArgumentException("Delays array length must be == frames, was " + delays.length); | |
} | |
public int getWidth() | |
{ | |
return width; | |
} | |
public int getHeight() | |
{ | |
return height; | |
} | |
public int getFrames() | |
{ | |
return frames; | |
} | |
public NativeImage toNativeImage() | |
{ | |
NativeImage img = new NativeImage(NativeImage.PixelFormat.RGBA, width, height * frames, false); | |
for (int y = 0; y < height * frames; y++) | |
{ | |
for (int x = 0; x < width; x++) | |
{ | |
img.setPixelRGBA(x, y, pixels[y * width + x]); | |
} | |
} | |
return img; | |
} | |
public int convertToMcTick(int delay) | |
{ | |
return MathHelper.ceil(delay * (float)MC_TICKS_PER_SECOND / GIF_TICKS_PER_SECOND); // gif delays at in 1/100s increments, mc ticks are 1/20 | |
} | |
// UNTESTED!!! | |
public TextureAtlasSprite toAnimatedSprite(ResourceLocation location, AtlasTexture atlas, int mipmapLevels, int atlasWidth, int atlasHeight, int atlasX, int atlasY) | |
{ | |
NativeImage img = toNativeImage(); | |
List<AnimationFrame> frameList = Lists.newArrayList(); | |
int accDelay = 0; | |
for (int i = 0; i < frames; i++) | |
{ | |
frameList.add(new AnimationFrame(i, accDelay)); | |
accDelay += Math.max(convertToMcTick(delays[i]), MIN_MC_TICKS); | |
} | |
AnimationMetadataSection animation = new AnimationMetadataSection(frameList, width, height, 0, false); | |
TextureAtlasSprite.Info info = new TextureAtlasSprite.Info(location, width, height * frames, animation); | |
return new TextureAtlasSprite(atlas, info, mipmapLevels, atlasWidth, atlasHeight, atlasX, atlasY, img) | |
{ | |
}; | |
} | |
public void exportToMcAnim(Path path) throws IOException | |
{ | |
try (NativeImage nativeImage = toNativeImage()) | |
{ | |
nativeImage.writeToFile(path); | |
} | |
JsonObject meta = new JsonObject(); | |
JsonObject anim = new JsonObject(); | |
anim.addProperty("frametime", 0); | |
anim.addProperty("interpolate", false); | |
JsonArray frameTimes = new JsonArray(); | |
for (int delay : delays) frameTimes.add(Math.max(convertToMcTick(delay),MIN_MC_TICKS)); | |
anim.add("frames", frameTimes); | |
meta.add("animation", anim); | |
try (FileWriter writer = new FileWriter(path.toFile().getAbsolutePath() + ".mcmeta")) | |
{ | |
writer.write(new Gson().toJson(meta)); | |
} | |
} | |
public GifPlayer makeGifPlayer() | |
{ | |
return new GifPlayer(); | |
} | |
public class GifPlayer implements AutoCloseable | |
{ | |
private final int glTexture; | |
private final int totalFrameTicks; | |
private boolean playing; | |
private int animationProgress; | |
private int lastFrame; | |
/* | |
While playing, holds the partial offset from the start tick. | |
While stopped, holds the partial tick progress at the point it was stopped. | |
*/ | |
private float partialStart; | |
private boolean autoplay; | |
private boolean looping = true; | |
private GifPlayer() | |
{ | |
totalFrameTicks = Arrays.stream(delays).map(d -> Math.max(MIN_GIF_TICKS, d)).sum(); | |
glTexture = TextureUtil.generateTextureId(); | |
TextureUtil.prepareImage(glTexture, 0, width, height * frames); | |
toNativeImage().upload(0, 0, 0, 0, 0, width, height * frames, false, true); | |
} | |
public void reset() | |
{ | |
animationProgress = 0; | |
partialStart = 0; | |
playing = false; | |
} | |
public void restart(float partialTicks) | |
{ | |
reset(); | |
start(partialTicks); | |
} | |
public void start(float partialTicks) | |
{ | |
playing = true; | |
partialStart = partialTicks - partialStart; | |
} | |
public void stop(float partialTicks) | |
{ | |
partialStart = partialTicks - partialStart; | |
playing = false; | |
autoplay = false; | |
} | |
/** | |
* Call this on your gui tick. Necessary to keep proper time. | |
*/ | |
public void tick() | |
{ | |
if (playing) | |
{ | |
animationProgress++; | |
} | |
} | |
public void render(MatrixStack matrixStack, int x, int y, int w, int h, float partialTicks) | |
{ | |
if (totalFrameTicks == 0) | |
return; | |
if (!playing && autoplay) | |
start(partialTicks); | |
if (playing) | |
{ | |
float frameTime = ((animationProgress + partialTicks - partialStart) * (float)GIF_TICKS_PER_SECOND) / MC_TICKS_PER_SECOND; | |
int frameNumber = MathHelper.floor(frameTime) % totalFrameTicks; | |
int frameIndex = -1; | |
for (int i = 0; i < delays.length; i++) | |
{ | |
int d = Math.max(delays[i], MIN_GIF_TICKS); | |
if (d > frameNumber) | |
{ | |
frameIndex = i; | |
break; | |
} | |
frameNumber -= d; | |
} | |
if (frameIndex < 0) | |
{ | |
if (looping) | |
{ | |
frameIndex = 0; | |
} | |
else | |
{ | |
playing = false; | |
return; | |
} | |
} | |
lastFrame = frameIndex; | |
} | |
RenderSystem.enableTexture(); | |
RenderSystem.enableAlphaTest(); | |
RenderSystem.disableBlend(); | |
RenderSystem.bindTexture(glTexture); | |
AbstractGui.blit(matrixStack, x, y, w, h, 0, lastFrame * height, width, height, width, height * frames); | |
} | |
public void close() | |
{ | |
TextureUtil.releaseTextureId(glTexture); | |
} | |
public void setAutoplay(boolean autoplay) | |
{ | |
this.autoplay = autoplay; | |
} | |
public boolean getAutoplay() | |
{ | |
return autoplay; | |
} | |
public void setLooping(boolean looping) | |
{ | |
this.looping = looping; | |
} | |
public boolean getLooping() | |
{ | |
return looping; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment