diff --git a/src/main/java/eu/crushedpixel/replaymod/ReplayMod.java b/src/main/java/eu/crushedpixel/replaymod/ReplayMod.java index b967620e..7a9be80d 100755 --- a/src/main/java/eu/crushedpixel/replaymod/ReplayMod.java +++ b/src/main/java/eu/crushedpixel/replaymod/ReplayMod.java @@ -1,8 +1,10 @@ package eu.crushedpixel.replaymod; +import com.google.common.util.concurrent.ListenableFutureTask; import eu.crushedpixel.replaymod.api.ApiClient; import eu.crushedpixel.replaymod.chat.ChatMessageHandler; import eu.crushedpixel.replaymod.events.*; +import eu.crushedpixel.replaymod.holders.KeyframeSet; import eu.crushedpixel.replaymod.localization.LocalizedResourcePack; import eu.crushedpixel.replaymod.online.authentication.AuthenticationHandler; import eu.crushedpixel.replaymod.recording.ConnectionEventHandler; @@ -10,10 +12,14 @@ import eu.crushedpixel.replaymod.reflection.MCPNames; import eu.crushedpixel.replaymod.registry.*; import eu.crushedpixel.replaymod.renderer.InvisibilityRender; import eu.crushedpixel.replaymod.renderer.SafeEntityRenderer; +import eu.crushedpixel.replaymod.replay.ReplayHandler; +import eu.crushedpixel.replaymod.replay.ReplayProcess; import eu.crushedpixel.replaymod.replay.ReplaySender; +import eu.crushedpixel.replaymod.settings.RenderOptions; import eu.crushedpixel.replaymod.settings.ReplaySettings; import eu.crushedpixel.replaymod.utils.ReplayFile; import eu.crushedpixel.replaymod.utils.ReplayFileIO; +import eu.crushedpixel.replaymod.video.frame.*; import net.minecraft.client.Minecraft; import net.minecraft.client.settings.GameSettings; import net.minecraftforge.common.MinecraftForge; @@ -28,8 +34,10 @@ import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; import org.apache.commons.io.FilenameUtils; import java.io.File; +import java.io.IOException; import java.lang.reflect.Field; import java.util.List; +import java.util.Queue; @Mod(modid = ReplayMod.MODID, version = ReplayMod.VERSION) public class ReplayMod { @@ -116,7 +124,7 @@ public class ReplayMod { } @EventHandler - public void postInit(FMLPostInitializationEvent event) { + public void postInit(FMLPostInitializationEvent event) throws IOException { GameSettings.Options.RENDER_DISTANCE.setValueMax(64f); overlay = new GuiReplayOverlay(); @@ -164,6 +172,102 @@ public class ReplayMod { FMLCommonHandler.instance().exitJava(0, false); } */ + + if (System.getProperty("replaymod.render.file") != null) { + final File file = new File(System.getProperty("replaymod.render.file")); + if (!file.exists()) { + throw new IOException("File \"" + file.getPath() + "\" not found."); + } + + String renderDistance = System.getProperty("mc.renderdistance"); + if (renderDistance != null) { + mc.gameSettings.renderDistanceChunks = Integer.parseInt(renderDistance); + } + + final String path = System.getProperty("replaymod.render.path"); + String type = System.getProperty("replaymod.render.type"); + String quality = System.getProperty("replaymod.render.quality"); + String fps = System.getProperty("replaymod.render.fps"); + String waitForChunks = System.getProperty("replaymod.render.waitforchunks"); + String linearMovement = System.getProperty("replaymod.render.linearmovement"); + FrameRenderer renderer; + if (type != null) { + String[] parts = type.split(":"); + type = parts[0].toUpperCase(); + if ("NORMAL".equals(type) || "DEFAULT".equals(type)) { + renderer = new DefaultFrameRenderer(); + } else if ("TILED".equals(type)) { + if (parts.length < 3) { + throw new IllegalArgumentException("Tiled renderer requires width and height."); + } + int width = Integer.parseInt(parts[1]); + int height = Integer.parseInt(parts[2]); + renderer = new TilingFrameRenderer(width, height); + } else if ("STEREO".equals(type) || "STEREOSCOPIC".equals(type)) { + renderer = new StereoscopicFrameRenderer(); + } else if ("CUBIC".equals(type)) { + if (parts.length < 2) { + throw new IllegalArgumentException("Cubic renderer requires boolean for whether it's stable."); + } + renderer = new CubicFrameRenderer(Boolean.parseBoolean(parts[1])); + } else if ("EQUIRECTANGULAR".equals(type)) { + if (parts.length < 2) { + throw new IllegalArgumentException("Equirectangular renderer requires boolean for whether it's stable."); + } + renderer = new EquirectangularFrameRenderer(Boolean.parseBoolean(parts[1])); + } else { + throw new IllegalArgumentException("Unknown type: " + parts[0]); + } + } else { + renderer = new DefaultFrameRenderer(); + } + final RenderOptions options = new RenderOptions(renderer); + if (quality != null) { + options.setQuality(Float.parseFloat(quality)); + } + if (fps != null) { + options.setFps(Integer.parseInt(fps)); + } + if (waitForChunks != null) { + options.setWaitForChunks(Boolean.parseBoolean(waitForChunks)); + } + if (linearMovement != null) { + options.setLinearMovement(Boolean.parseBoolean(linearMovement)); + } + + @SuppressWarnings("unchecked") + Queue tasks = mc.scheduledTasks; + synchronized (mc.scheduledTasks) { + tasks.add(ListenableFutureTask.create(new Runnable() { + @Override + public void run() { + System.out.println("Loading replay..."); + ReplayHandler.startReplay(file, false); + + int index = 0; + if (path != null) { + for (KeyframeSet set : ReplayHandler.getKeyframeRepository()) { + if (set.getName().equals(path)) { + break; + } + index++; + } + if (index >= ReplayHandler.getKeyframeRepository().length) { + throw new IllegalArgumentException("No path named \"" + path + "\"."); + } + } else if (ReplayHandler.getKeyframeRepository().length == 0) { + throw new IllegalArgumentException("Replay file has no paths defined."); + } + ReplayHandler.useKeyframePresetFromRepository(index); + + System.out.println("Rendering started..."); + ReplayProcess.startReplayProcess(options); + System.out.println("Rendering done. Shutting down..."); + mc.shutdown(); + } + }, null)); + } + } } private void removeTmcprFiles() { diff --git a/src/main/java/eu/crushedpixel/replaymod/coremod/CameraRollCT.java b/src/main/java/eu/crushedpixel/replaymod/coremod/CameraRollCT.java index 710be795..b09310fe 100644 --- a/src/main/java/eu/crushedpixel/replaymod/coremod/CameraRollCT.java +++ b/src/main/java/eu/crushedpixel/replaymod/coremod/CameraRollCT.java @@ -30,8 +30,22 @@ public class CameraRollCT implements IClassTransformer { boolean success = false; for (MethodNode m : classNode.methods) { if ("(F)V".equals(m.desc) && name_orientCamera.equals(m.name)) { - inject(m.instructions.iterator()); - success = true; + ListIterator iter = m.instructions.iterator(); + int f = 0; + while (iter.hasNext()) { + AbstractInsnNode node = iter.next(); + if ((f == 0 || f == 1) && node.getOpcode() == FCONST_0) { + f++; + } else if ((f == 2) && node instanceof LdcInsnNode && ((LdcInsnNode) node).cst.equals(-0.1f)) { + f++; + } else if (f == 3) { + inject(iter); + success = true; + break; + } else { + f = 0; + } + } } } if (!success) { diff --git a/src/main/java/eu/crushedpixel/replaymod/events/GuiEventHandler.java b/src/main/java/eu/crushedpixel/replaymod/events/GuiEventHandler.java index d7267de3..d9a01eac 100755 --- a/src/main/java/eu/crushedpixel/replaymod/events/GuiEventHandler.java +++ b/src/main/java/eu/crushedpixel/replaymod/events/GuiEventHandler.java @@ -1,7 +1,6 @@ package eu.crushedpixel.replaymod.events; import eu.crushedpixel.replaymod.ReplayMod; -import eu.crushedpixel.replaymod.gui.GuiCancelRender; import eu.crushedpixel.replaymod.gui.GuiConstants; import eu.crushedpixel.replaymod.gui.GuiReplaySaving; import eu.crushedpixel.replaymod.gui.GuiReplaySettings; @@ -18,7 +17,6 @@ import eu.crushedpixel.replaymod.studio.VersionValidator; import eu.crushedpixel.replaymod.timer.MCTimerHandler; import eu.crushedpixel.replaymod.utils.MouseUtils; import eu.crushedpixel.replaymod.utils.ReplayFileIO; -import eu.crushedpixel.replaymod.video.VideoWriter; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.*; import net.minecraft.client.gui.inventory.GuiInventory; @@ -52,11 +50,6 @@ public class GuiEventHandler { @SubscribeEvent public void onGui(GuiOpenEvent event) { - if(VideoWriter.isRecording() && !(event.gui instanceof GuiCancelRender)) { - event.gui = null; - return; - } - if(event.gui instanceof GuiMainMenu) { if(ReplayMod.firstMainMenu) { ReplayMod.firstMainMenu = false; diff --git a/src/main/java/eu/crushedpixel/replaymod/events/GuiReplayOverlay.java b/src/main/java/eu/crushedpixel/replaymod/events/GuiReplayOverlay.java index 99cbc537..bd414c10 100755 --- a/src/main/java/eu/crushedpixel/replaymod/events/GuiReplayOverlay.java +++ b/src/main/java/eu/crushedpixel/replaymod/events/GuiReplayOverlay.java @@ -11,8 +11,10 @@ import eu.crushedpixel.replaymod.recording.ConnectionEventHandler; import eu.crushedpixel.replaymod.registry.ReplayGuiRegistry; import eu.crushedpixel.replaymod.replay.ReplayHandler; import eu.crushedpixel.replaymod.replay.ReplayProcess; +import eu.crushedpixel.replaymod.settings.RenderOptions; import eu.crushedpixel.replaymod.utils.MouseUtils; -import eu.crushedpixel.replaymod.video.VideoWriter; +import eu.crushedpixel.replaymod.video.frame.FrameRenderer; +import eu.crushedpixel.replaymod.video.frame.DefaultFrameRenderer; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.Gui; import net.minecraft.client.gui.GuiChat; @@ -123,9 +125,8 @@ public class GuiReplayOverlay extends Gui { @SubscribeEvent public void onRenderGui(RenderGameOverlayEvent.Post event) throws IllegalArgumentException, IllegalAccessException { - if(!ReplayHandler.isInReplay() || FMLClientHandler.instance().isGUIOpen(GuiPlayerOverview.class) - || VideoWriter.isRecording() || FMLClientHandler.instance().isGUIOpen(GuiKeyframeRepository.class)) { + || FMLClientHandler.instance().isGUIOpen(GuiKeyframeRepository.class)) { return; } @@ -185,7 +186,19 @@ public class GuiReplayOverlay extends Gui { if(hover) { playOrPause(); } else if(mouseX >= exportButtonX && mouseX <= exportButtonX + 20 && mouseY >= exportButtonY && exportButtonY <= exportButtonY + 20) { - ReplayHandler.startPath(true); + // TODO Put all those settings into a nice little GUI + FrameRenderer renderer = new DefaultFrameRenderer(); +// FrameRenderer renderer = new TilingReadPixelFrameRenderer(mc.displayWidth*3, mc.displayHeight*2); +// FrameRenderer renderer = new TilingReadPixelFrameRenderer(3840, 2160); +// FrameRenderer renderer = new StereoscopicFrameRenderer(); +// FrameRenderer renderer = new CubicFrameRenderer(false); +// FrameRenderer renderer = new EquirectangularFrameRenderer(false); + RenderOptions options = new RenderOptions(renderer); + options.setLinearMovement(ReplayMod.replaySettings.isLinearMovement()); + options.setWaitForChunks(ReplayMod.replaySettings.getWaitForChunks()); + options.setFps(ReplayMod.replaySettings.getVideoFramerate()); + options.setQuality((float) ReplayMod.replaySettings.getVideoQuality()); + ReplayHandler.startPath(options); } if(mouseX >= timelineX + 4 && mouseX <= width - 18 && mouseY >= 11 && mouseY <= 29) { @@ -743,7 +756,7 @@ public class GuiReplayOverlay extends Gui { if(ReplayHandler.isInPath()) { ReplayHandler.interruptReplay(); } else { - ReplayHandler.startPath(false); + ReplayHandler.startPath(null); } } diff --git a/src/main/java/eu/crushedpixel/replaymod/events/TickAndRenderListener.java b/src/main/java/eu/crushedpixel/replaymod/events/TickAndRenderListener.java index 63a8b3ce..7376a79d 100644 --- a/src/main/java/eu/crushedpixel/replaymod/events/TickAndRenderListener.java +++ b/src/main/java/eu/crushedpixel/replaymod/events/TickAndRenderListener.java @@ -2,8 +2,6 @@ package eu.crushedpixel.replaymod.events; import eu.crushedpixel.replaymod.ReplayMod; import eu.crushedpixel.replaymod.chat.ChatMessageHandler; -import eu.crushedpixel.replaymod.gui.GuiCancelRender; -import eu.crushedpixel.replaymod.gui.GuiMouseInput; import eu.crushedpixel.replaymod.replay.ReplayHandler; import eu.crushedpixel.replaymod.replay.ReplayProcess; import eu.crushedpixel.replaymod.video.ReplayScreenshot; @@ -40,6 +38,7 @@ public class TickAndRenderListener { public void onRenderWorld(RenderWorldLastEvent event) throws InvocationTargetException, IOException, IllegalAccessException, IllegalArgumentException { if(!ReplayHandler.isInReplay()) return; //If not in Replay, cancel + if (ReplayProcess.isVideoRecording()) return; // If recording, cancel if(requestScreenshot == 1) { mc.addScheduledTask(new Runnable() { @@ -77,16 +76,12 @@ public class TickAndRenderListener { @SubscribeEvent public void tick(TickEvent event) { - if(!ReplayHandler.isInReplay()) return; + if(!ReplayHandler.isInReplay() || ReplayProcess.isVideoRecording()) return; if(ReplayHandler.getCameraEntity() != null) ReplayHandler.getCameraEntity().updateMovement(); if(ReplayHandler.isInPath()) { ReplayProcess.unblockAndTick(true); - if(ReplayProcess.isVideoRecording() && - !(mc.currentScreen instanceof GuiMouseInput || mc.currentScreen instanceof GuiCancelRender)) { - mc.displayGuiScreen(new GuiMouseInput()); - } } else onMouseMove(new MouseEvent()); FMLCommonHandler.instance().bus().post(new InputEvent.KeyInputEvent()); diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/GuiVideoRenderer.java b/src/main/java/eu/crushedpixel/replaymod/gui/GuiVideoRenderer.java new file mode 100644 index 00000000..db7674a9 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/gui/GuiVideoRenderer.java @@ -0,0 +1,95 @@ +package eu.crushedpixel.replaymod.gui; + +import eu.crushedpixel.replaymod.video.VideoRenderer; +import eu.crushedpixel.replaymod.video.frame.FrameRenderer; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiScreen; +import net.minecraftforge.fml.client.config.GuiCheckBox; + +import java.io.IOException; + +public class GuiVideoRenderer extends GuiScreen { + private final VideoRenderer renderer; + + private final String PAUSE_RENDERING = "Pause Rendering"; + private final String RESUME_RENDERING = "Resume Rendering"; + private final String CANCEL = "Cancel Rendering"; + private final String CANCEL_CONFIRM = "Sure?"; + + private GuiButton pauseButton; + private GuiButton cancelButton; + private GuiCheckBox previewCheckBox; + + public GuiVideoRenderer(VideoRenderer renderer) { + this.renderer = renderer; + } + + @Override + @SuppressWarnings("unchecked") // I blame forge for not re-adding generics to that list + public void initGui() { + FrameRenderer frameRenderer = renderer.getFrameRenderer(); + + String text = "Show Preview (Performance might suffer)"; + buttonList.add(previewCheckBox = new GuiCheckBox(0, (width - fontRendererObj.getStringWidth(text)) / 2 - 8, + frameRenderer.getPreviewHeight(this), text, false)); + + text = PAUSE_RENDERING; + buttonList.add(pauseButton = new GuiButton(1, width / 2 - 202, height / 2 - 23, text)); + + text = CANCEL; + buttonList.add(cancelButton = new GuiButton(1, width / 2 + 2, height / 2 - 23, text)); + } + + @Override + protected void mouseClicked(int mouseX, int mouseY, int mouseButton) throws IOException { + String cancelBefore = cancelButton.displayString; + super.mouseClicked(mouseX, mouseY, mouseButton); + String cancelAfter = cancelButton.displayString; + if (cancelBefore.equals(cancelAfter)) { + cancelButton.displayString = CANCEL; + } + } + + @Override + protected void actionPerformed(GuiButton button) throws IOException { + if (button == pauseButton) { + if (PAUSE_RENDERING.equals(pauseButton.displayString)) { + renderer.setPaused(true); + pauseButton.displayString = RESUME_RENDERING; + } else if (RESUME_RENDERING.equals(pauseButton.displayString)) { + renderer.setPaused(false); + pauseButton.displayString = PAUSE_RENDERING; + } else { + throw new IllegalStateException(pauseButton.displayString); + } + } else if (button == cancelButton) { + if (CANCEL.equals(cancelButton.displayString)) { + cancelButton.displayString = CANCEL_CONFIRM; + } else if (CANCEL_CONFIRM.equals(cancelButton.displayString)) { + renderer.cancel(); + } + } else if (button == previewCheckBox) { + renderer.getFrameRenderer().setPreviewActive(previewCheckBox.isChecked()); + } + } + + @Override + public void drawScreen(int mouseX, int mouseY, float partialTicks) { + FrameRenderer frameRenderer = renderer.getFrameRenderer(); + int centerX = width / 2; + int centerY = height / 2; + + drawBackground(0); + + String framesProgress = String.format("Frames rendered: %d / %d", renderer.getFramesDone(), renderer.getTotalFrames()); + drawCenteredString(fontRendererObj, framesProgress, centerX, centerY - 45, 0xffffffff); + + if (previewCheckBox.isChecked()) { + frameRenderer.renderPreview(this); + } else { + int previewHeight = frameRenderer.getPreviewHeight(this) - height / 2 - 10; + FrameRenderer.renderNoPreview(width, height, previewHeight); + } + super.drawScreen(mouseX, mouseY, partialTicks); + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/interpolation/Interpolation.java b/src/main/java/eu/crushedpixel/replaymod/interpolation/Interpolation.java new file mode 100644 index 00000000..9c0a63b7 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/interpolation/Interpolation.java @@ -0,0 +1,7 @@ +package eu.crushedpixel.replaymod.interpolation; + +public interface Interpolation { + void prepare(); + T getPoint(float position); + void addPoint(T pos); +} diff --git a/src/main/java/eu/crushedpixel/replaymod/interpolation/LinearInterpolation.java b/src/main/java/eu/crushedpixel/replaymod/interpolation/LinearInterpolation.java index 0bbfeb28..ae4b0c6f 100755 --- a/src/main/java/eu/crushedpixel/replaymod/interpolation/LinearInterpolation.java +++ b/src/main/java/eu/crushedpixel/replaymod/interpolation/LinearInterpolation.java @@ -5,7 +5,7 @@ import akka.japi.Pair; import java.util.ArrayList; import java.util.List; -public abstract class LinearInterpolation { +public abstract class LinearInterpolation implements Interpolation { protected List points = new ArrayList(); @@ -13,6 +13,11 @@ public abstract class LinearInterpolation { points = new ArrayList(); } + @Override + public void prepare() { + + } + public abstract K getPoint(float position); public void addPoint(K point) { diff --git a/src/main/java/eu/crushedpixel/replaymod/interpolation/SplinePoint.java b/src/main/java/eu/crushedpixel/replaymod/interpolation/SplinePoint.java index fb98f0f5..9a3539d0 100755 --- a/src/main/java/eu/crushedpixel/replaymod/interpolation/SplinePoint.java +++ b/src/main/java/eu/crushedpixel/replaymod/interpolation/SplinePoint.java @@ -6,7 +6,7 @@ import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.util.Vector; -public class SplinePoint extends BasicSpline { +public class SplinePoint extends BasicSpline implements Interpolation { private static final Object[] EMPTYOBJ = new Object[]{}; private Vector points; private Vector xCubics; @@ -60,7 +60,8 @@ public class SplinePoint extends BasicSpline { return points; } - public void calcSpline() { + @Override + public void prepare() { try { calcNaturalCubic(points, vectorX, xCubics); calcNaturalCubic(points, vectorY, yCubics); diff --git a/src/main/java/eu/crushedpixel/replaymod/renderer/ChunkLoadingRenderGlobal.java b/src/main/java/eu/crushedpixel/replaymod/renderer/ChunkLoadingRenderGlobal.java new file mode 100644 index 00000000..20be8dae --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/renderer/ChunkLoadingRenderGlobal.java @@ -0,0 +1,110 @@ +package eu.crushedpixel.replaymod.renderer; + +import eu.crushedpixel.replaymod.utils.JailingQueue; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.RegionRenderCacheBuilder; +import net.minecraft.client.renderer.RenderGlobal; +import net.minecraft.client.renderer.chunk.ChunkCompileTaskGenerator; +import net.minecraft.client.renderer.chunk.ChunkRenderDispatcher; +import net.minecraft.client.renderer.chunk.ChunkRenderWorker; +import net.minecraft.client.renderer.chunk.RenderChunk; +import net.minecraft.client.renderer.culling.ICamera; +import net.minecraft.entity.Entity; +import net.minecraft.util.BlockPos; + +import java.util.Iterator; +import java.util.concurrent.BlockingQueue; + +public class ChunkLoadingRenderGlobal extends RenderGlobal { + public static void install(Minecraft mc) { + RenderGlobal org = mc.renderGlobal; + if (org instanceof ChunkLoadingRenderGlobal) { + ((ChunkLoadingRenderGlobal) org).uninstall(); + } + mc.renderGlobal = new ChunkLoadingRenderGlobal(mc); + } + + private final RenderGlobal originalRenderGlobal; + private final JailingQueue workerJailingQueue; + private final CustomChunkRenderWorker renderWorker; + private int frame; + + @SuppressWarnings("unchecked") + private ChunkLoadingRenderGlobal(Minecraft mc) { + super(mc); + this.originalRenderGlobal = mc.renderGlobal; + this.renderWorker = new CustomChunkRenderWorker(renderDispatcher, new RegionRenderCacheBuilder()); + if (mc.theWorld != null) { + setWorldAndLoadRenderers(mc.theWorld); + } + + int workerThreads = renderDispatcher.listThreadedWorkers.size(); + BlockingQueue queueChunkUpdates = renderDispatcher.queueChunkUpdates; + workerJailingQueue = new JailingQueue(queueChunkUpdates); + renderDispatcher.queueChunkUpdates = workerJailingQueue; + ChunkCompileTaskGenerator element = new ChunkCompileTaskGenerator(null, null); + element.finish(); + for (int i = 0; i < workerThreads; i++) { + queueChunkUpdates.add(element); + } + workerJailingQueue.jail(workerThreads); + renderDispatcher.queueChunkUpdates = queueChunkUpdates; + } + + @Override + public void setupTerrain(Entity viewEntity, double partialTicks, ICamera camera, int frameCount, boolean playerSpectator) { + // There has to be a better way to force minecraft into queueing all chunks at once + // Someone should probably find and implement it! + do { + super.setupTerrain(viewEntity, partialTicks, camera, frame++, playerSpectator); + } while (displayListEntitiesDirty); + } + + @Override + public boolean isPositionInRenderChunk(BlockPos p_174983_1_, RenderChunk p_174983_2_) { + return true; + } + + @Override + @SuppressWarnings("unchecked") + public void updateChunks(long finishTimeNano) { + while (renderDispatcher.runChunkUploads(0)) { + displayListEntitiesDirty = true; + } + + while (!renderDispatcher.queueChunkUpdates.isEmpty()) { + try { + renderWorker.processTask((ChunkCompileTaskGenerator) renderDispatcher.queueChunkUpdates.poll()); + } catch (InterruptedException ignored) { } + } + + Iterator iterator = chunksToUpdate.iterator(); + while (iterator.hasNext()) { + RenderChunk renderchunk = iterator.next(); + + renderDispatcher.updateChunkNow(renderchunk); + + renderchunk.setNeedsUpdate(false); + iterator.remove(); + } + } + + public void uninstall() { + workerJailingQueue.freeAll(); + Minecraft.getMinecraft().renderGlobal = originalRenderGlobal; + } + + /** + * Custom ChunkRenderWorker class providing access to the protected processTask method + */ + private static class CustomChunkRenderWorker extends ChunkRenderWorker { + public CustomChunkRenderWorker(ChunkRenderDispatcher p_i46202_1_, RegionRenderCacheBuilder p_i46202_2_) { + super(p_i46202_1_, p_i46202_2_); + } + + @Override + protected void processTask(ChunkCompileTaskGenerator p_178474_1_) throws InterruptedException { + super.processTask(p_178474_1_); + } + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/replay/ReplayHandler.java b/src/main/java/eu/crushedpixel/replaymod/replay/ReplayHandler.java index 86982483..61c7dd6b 100755 --- a/src/main/java/eu/crushedpixel/replaymod/replay/ReplayHandler.java +++ b/src/main/java/eu/crushedpixel/replaymod/replay/ReplayHandler.java @@ -5,6 +5,7 @@ import eu.crushedpixel.replaymod.ReplayMod; import eu.crushedpixel.replaymod.entities.CameraEntity; import eu.crushedpixel.replaymod.holders.*; import eu.crushedpixel.replaymod.registry.PlayerHandler; +import eu.crushedpixel.replaymod.settings.RenderOptions; import eu.crushedpixel.replaymod.utils.ReplayFile; import eu.crushedpixel.replaymod.utils.ReplayFileIO; import net.minecraft.client.Minecraft; @@ -86,8 +87,8 @@ public class ReplayHandler { return currentEntity == cameraEntity; } - public static void startPath(boolean save) { - if(!ReplayHandler.isInPath()) ReplayProcess.startReplayProcess(save); + public static void startPath(RenderOptions renderOptions) { + if(!ReplayHandler.isInPath()) ReplayProcess.startReplayProcess(renderOptions); } public static void interruptReplay() { @@ -322,7 +323,11 @@ public class ReplayHandler { return inReplay; } - public static void startReplay(File file) throws NoSuchMethodException, SecurityException, NoSuchFieldException { + public static void startReplay(File file) { + startReplay(file, true); + } + + public static void startReplay(File file, boolean asyncMode) { ReplayMod.chatMessageHandler.initialize(); mc.ingameGUI.getChatGUI().clearChatMessages(); @@ -358,7 +363,7 @@ public class ReplayHandler { PlayerVisibility visibility = currentReplayFile.visibility().get(); PlayerHandler.loadPlayerVisibilityConfiguration(visibility); - ReplayMod.replaySender = new ReplaySender(currentReplayFile, true); + ReplayMod.replaySender = new ReplaySender(currentReplayFile, asyncMode); channel.pipeline().addFirst(ReplayMod.replaySender); channel.pipeline().fireChannelActive(); diff --git a/src/main/java/eu/crushedpixel/replaymod/replay/ReplayProcess.java b/src/main/java/eu/crushedpixel/replaymod/replay/ReplayProcess.java index 40af0ca4..b82182a4 100755 --- a/src/main/java/eu/crushedpixel/replaymod/replay/ReplayProcess.java +++ b/src/main/java/eu/crushedpixel/replaymod/replay/ReplayProcess.java @@ -2,7 +2,6 @@ package eu.crushedpixel.replaymod.replay; import eu.crushedpixel.replaymod.ReplayMod; import eu.crushedpixel.replaymod.chat.ChatMessageHandler.ChatMessageType; -import eu.crushedpixel.replaymod.gui.GuiCancelRender; import eu.crushedpixel.replaymod.holders.Keyframe; import eu.crushedpixel.replaymod.holders.Position; import eu.crushedpixel.replaymod.holders.PositionKeyframe; @@ -10,14 +9,13 @@ import eu.crushedpixel.replaymod.holders.TimeKeyframe; import eu.crushedpixel.replaymod.interpolation.LinearPoint; import eu.crushedpixel.replaymod.interpolation.LinearTimestamp; import eu.crushedpixel.replaymod.interpolation.SplinePoint; +import eu.crushedpixel.replaymod.settings.RenderOptions; import eu.crushedpixel.replaymod.timer.EnchantmentTimer; import eu.crushedpixel.replaymod.timer.MCTimerHandler; -import eu.crushedpixel.replaymod.video.ScreenCapture; -import eu.crushedpixel.replaymod.video.VideoWriter; +import eu.crushedpixel.replaymod.video.VideoRenderer; import net.minecraft.client.Minecraft; -import net.minecraft.client.renderer.chunk.RenderChunk; -import java.awt.image.BufferedImage; +import java.io.IOException; public class ReplayProcess { @@ -79,11 +77,11 @@ public class ReplayProcess { EnchantmentTimer.resetRecordingTime(); } - public static void startReplayProcess(boolean record) { + public static void startReplayProcess(RenderOptions renderOptions) { ReplayHandler.selectKeyframe(null); resetProcess(); - isVideoRecording = record; + isVideoRecording = renderOptions != null; ReplayMod.chatMessageHandler.initialize(); @@ -97,24 +95,28 @@ public class ReplayProcess { ReplayHandler.setInPath(true); ReplayMod.replaySender.setAsyncMode(false); - //gets first Timestamp and sets Replay Time to it - TimeKeyframe tf = ReplayHandler.getFirstTimeKeyframe(); - if(tf != null) { - int ts = tf.getTimestamp(); - if(ts < ReplayMod.replaySender.currentTimeStamp()) { - mc.displayGuiScreen(null); + if (renderOptions == null) { + //gets first Timestamp and sets Replay Time to it + TimeKeyframe tf = ReplayHandler.getFirstTimeKeyframe(); + if (tf != null) { + int ts = tf.getTimestamp(); + if (ts < ReplayMod.replaySender.currentTimeStamp()) { + mc.displayGuiScreen(null); + } + ReplayMod.replaySender.sendPacketsTill(ts); } - ReplayMod.replaySender.sendPacketsTill(ts); - } - ReplayMod.chatMessageHandler.addLocalizedChatMessage("replaymod.chat.pathstarted", ChatMessageType.INFORMATION); - - //if video is recording, the Replay Process takes control over the Minecraft Timer - if(isVideoRecording()) { + ReplayMod.chatMessageHandler.addLocalizedChatMessage("replaymod.chat.pathstarted", ChatMessageType.INFORMATION); MCTimerHandler.setTimerSpeed(1f); - MCTimerHandler.setPassiveTimer(); } else { - MCTimerHandler.setTimerSpeed(1f); + try { + isVideoRecording = true; + boolean success = new VideoRenderer(renderOptions).renderVideo(); + isVideoRecording = false; + stopReplayProcess(success); + } catch (IOException e) { + e.printStackTrace(); + } } } @@ -125,9 +127,6 @@ public class ReplayProcess { if(finished) ReplayMod.chatMessageHandler.addLocalizedChatMessage("replaymod.chat.pathfinished", ChatMessageType.INFORMATION); else { ReplayMod.chatMessageHandler.addLocalizedChatMessage("replaymod.chat.pathinterrupted", ChatMessageType.INFORMATION); - if(isVideoRecording()) { - VideoWriter.abortRecording(); - } } ReplayHandler.setInPath(false); @@ -143,14 +142,17 @@ public class ReplayProcess { public static void unblockAndTick(boolean justCheck) { if(!deepBlock) blocked = false; if(!blocked || !isVideoRecording()) - pathTick(isVideoRecording(), justCheck); + ReplayProcess.tickReplay(justCheck); } //if justCheck is true, no Screenshot will be taken, it will only be checked //whether all chunks have been rendered. This is necessary because no Render ticks //are called if the Timer speed is set to 0, leading to this method never being //called from the RenderWorldLastEvent handlers. - private static void pathTick(boolean recording, boolean justCheck) { + public static void tickReplay(boolean justCheck) { + if (isVideoRecording) { + return; + } if(ReplayMod.replaySender.isHurrying()) { lastRealTime = System.currentTimeMillis(); return; @@ -166,35 +168,8 @@ public class ReplayProcess { MCTimerHandler.setTicks(100); } - if(recording && ((ReplayMod.replaySettings.getWaitForChunks() && RenderChunk.renderChunksUpdated != 0) || mc.currentScreen instanceof GuiCancelRender)) { - if(!firstTime) { - MCTimerHandler.setTimerSpeed(0f); - MCTimerHandler.setPartialTicks(0f); - MCTimerHandler.setRenderPartialTicks(0f); - MCTimerHandler.setTicks(0); - resetTimer = true; - } - return; - } else if(recording && ReplayMod.replaySettings.getWaitForChunks()) { - MCTimerHandler.setTimerSpeed((float) lastSpeed); - //MCTimerHandler.setRenderPartialTicks(lastRenderPartialTicks); - if(resetTimer) { - MCTimerHandler.setPartialTicks(lastPartialTicks); - MCTimerHandler.setRenderPartialTicks(lastRenderPartialTicks); - MCTimerHandler.setTicks(lastTicks); - resetTimer = false; - } - } - if(justCheck) return; - if(recording) { - if(blocked) return; - - deepBlock = true; - blocked = true; - } - int posCount = ReplayHandler.getPosKeyframeCount(); int timeCount = ReplayHandler.getTimeKeyframeCount(); @@ -233,16 +208,11 @@ public class ReplayProcess { if(!calculated) { calculated = true; if(posCount > 1 && motionSpline != null) - motionSpline.calcSpline(); + motionSpline.prepare(); } long curTime = System.currentTimeMillis(); - long timeStep; - if(recording) { - timeStep = 1000 / ReplayMod.replaySettings.getVideoFramerate(); - } else { - timeStep = curTime - lastRealTime; - } + long timeStep = curTime - lastRealTime; int curRealReplayTime = (int) (lastRealReplayTime + timeStep); @@ -333,14 +303,13 @@ public class ReplayProcess { } } - ReplayHandler.setCameraTilt(pos.getRoll()); - Integer curTimestamp = null; if(timeLinear != null && timeCount > 1) { curTimestamp = timeLinear.getPoint(Math.max(0, Math.min(1, timePos))); } if(pos != null) { + ReplayHandler.setCameraTilt(pos.getRoll()); ReplayHandler.getCameraEntity().movePath(pos); } @@ -348,11 +317,6 @@ public class ReplayProcess { //if(curSpeed > 0) lastSpeed = curSpeed; - if(recording) { - MCTimerHandler.updateTimer((1f / ReplayMod.replaySettings.getVideoFramerate())); - EnchantmentTimer.increaseRecordingTime((1000 / ReplayMod.replaySettings.getVideoFramerate())); - } - lastPartialTicks = MCTimerHandler.getPartialTicks(); lastRenderPartialTicks = MCTimerHandler.getRenderTicks(); lastTicks = MCTimerHandler.getTicks(); @@ -365,33 +329,13 @@ public class ReplayProcess { lastRealReplayTime = curRealReplayTime; lastRealTime = curTime; - if(isVideoRecording()) { - try { - if(!VideoWriter.isRecording() && ReplayHandler.isInPath()) { - VideoWriter.startRecording(mc.displayWidth, mc.displayHeight); - } else { - final BufferedImage screen = ScreenCapture.captureScreen(); - VideoWriter.writeImage(screen); - } - } catch(Exception e) { - e.printStackTrace(); - } - } - if(requestFinish) { stopReplayProcess(true); requestFinish = false; - if(recording) { - VideoWriter.endRecording(); - } } if((splinePos >= 1 || posCount <= 1) && (timePos >= 1 || timeCount <= 1)) { requestFinish = true; } - - if(recording) { - deepBlock = false; - } } } diff --git a/src/main/java/eu/crushedpixel/replaymod/settings/RenderOptions.java b/src/main/java/eu/crushedpixel/replaymod/settings/RenderOptions.java new file mode 100644 index 00000000..682e530b --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/settings/RenderOptions.java @@ -0,0 +1,68 @@ +package eu.crushedpixel.replaymod.settings; + +import eu.crushedpixel.replaymod.video.frame.FrameRenderer; + +import static org.apache.commons.lang3.Validate.*; + +public final class RenderOptions { + private FrameRenderer renderer; + private float quality = 0.5f; + private int fps = 30; + private boolean waitForChunks = true; + private boolean isLinearMovement = false; + + public RenderOptions(FrameRenderer renderer) { + this.renderer = notNull(renderer); + } + + public FrameRenderer getRenderer() { + return renderer; + } + + public void setRenderer(FrameRenderer renderer) { + this.renderer = notNull(renderer); + } + + public float getQuality() { + return quality; + } + + public void setQuality(float quality) { + inclusiveBetween(0f, 1f, quality); + this.quality = quality; + } + + public int getFps() { + return fps; + } + + public void setFps(int fps) { + isTrue(fps > 0, "Fps must be positive."); + this.fps = fps; + } + + public boolean isWaitForChunks() { + return waitForChunks; + } + + public void setWaitForChunks(boolean waitForChunks) { + this.waitForChunks = waitForChunks; + } + + public boolean isLinearMovement() { + return isLinearMovement; + } + + public void setLinearMovement(boolean isLinearMovement) { + this.isLinearMovement = isLinearMovement; + } + + public RenderOptions copy() { + RenderOptions copy = new RenderOptions(renderer); + copy.quality = this.quality; + copy.fps = this.fps; + copy.waitForChunks = this.waitForChunks; + copy.isLinearMovement = this.isLinearMovement; + return copy; + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/timer/MCTimerHandler.java b/src/main/java/eu/crushedpixel/replaymod/timer/MCTimerHandler.java index 6bc6f8ff..37711ed2 100755 --- a/src/main/java/eu/crushedpixel/replaymod/timer/MCTimerHandler.java +++ b/src/main/java/eu/crushedpixel/replaymod/timer/MCTimerHandler.java @@ -44,6 +44,10 @@ public class MCTimerHandler { return mc.timer.renderPartialTicks; } + public static Timer getTimer() { + return mc.timer; + } + public static void advanceTicks(int ticks) { mc.timer.elapsedTicks += ticks; } diff --git a/src/main/java/eu/crushedpixel/replaymod/utils/JailingQueue.java b/src/main/java/eu/crushedpixel/replaymod/utils/JailingQueue.java new file mode 100644 index 00000000..e5339b2d --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/utils/JailingQueue.java @@ -0,0 +1,120 @@ +package eu.crushedpixel.replaymod.utils; + +import com.google.common.base.Preconditions; + +import java.util.*; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; + +public class JailingQueue extends AbstractQueue implements BlockingQueue { + private final BlockingQueue delegate; + private final Set jailed = new HashSet(); + + public JailingQueue(BlockingQueue delegate) { + this.delegate = delegate; + } + + public synchronized void jail(int atLeast) { + while (jailed.size() < atLeast) { + try { + wait(); + } catch (InterruptedException e) { + Thread.interrupted(); + } + } + } + + public synchronized void free(Thread thread) { + Preconditions.checkState(jailed.remove(thread), "Thread is not jailed."); + thread.interrupt(); + } + + public synchronized void freeAll() { + jailed.clear(); + notifyAll(); + } + + private synchronized void tryAccess() { + jailed.add(Thread.currentThread()); + notifyAll(); + while (jailed.contains(Thread.currentThread())) { + try { + wait(); + } catch (InterruptedException e) { + Thread.interrupted(); + } + } + } + + @Override + public Iterator iterator() { + tryAccess(); + return delegate.iterator(); + } + + @Override + public int size() { + tryAccess(); + return delegate.size(); + } + + @Override + public void put(T t) throws InterruptedException { + tryAccess(); + delegate.put(t); + } + + @Override + public boolean offer(T t, long timeout, TimeUnit unit) throws InterruptedException { + tryAccess(); + return delegate.offer(t, timeout, unit); + } + + @Override + public T take() throws InterruptedException { + tryAccess(); + return delegate.take(); + } + + @Override + public T poll(long timeout, TimeUnit unit) throws InterruptedException { + tryAccess(); + return delegate.poll(timeout, unit); + } + + @Override + public int remainingCapacity() { + tryAccess(); + return delegate.remainingCapacity(); + } + + @Override + public int drainTo(Collection c) { + tryAccess(); + return delegate.drainTo(c); + } + + @Override + public int drainTo(Collection c, int maxElements) { + tryAccess(); + return delegate.drainTo(c, maxElements); + } + + @Override + public boolean offer(T t) { + tryAccess(); + return delegate.offer(t); + } + + @Override + public T poll() { + tryAccess(); + return delegate.poll(); + } + + @Override + public T peek() { + tryAccess(); + return delegate.peek(); + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/video/ReplayTimer.java b/src/main/java/eu/crushedpixel/replaymod/video/ReplayTimer.java index 99dcfb4e..55d2ffa5 100755 --- a/src/main/java/eu/crushedpixel/replaymod/video/ReplayTimer.java +++ b/src/main/java/eu/crushedpixel/replaymod/video/ReplayTimer.java @@ -1,6 +1,5 @@ package eu.crushedpixel.replaymod.video; -import eu.crushedpixel.replaymod.replay.ReplayProcess; import net.minecraft.util.Timer; public class ReplayTimer extends Timer { @@ -11,7 +10,5 @@ public class ReplayTimer extends Timer { @Override public void updateTimer() { - if(ReplayProcess.isVideoRecording()) return; - super.updateTimer(); } } diff --git a/src/main/java/eu/crushedpixel/replaymod/video/VideoRenderer.java b/src/main/java/eu/crushedpixel/replaymod/video/VideoRenderer.java new file mode 100644 index 00000000..dcef8492 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/video/VideoRenderer.java @@ -0,0 +1,391 @@ +package eu.crushedpixel.replaymod.video; + +import eu.crushedpixel.replaymod.ReplayMod; +import eu.crushedpixel.replaymod.entities.CameraEntity; +import eu.crushedpixel.replaymod.gui.GuiVideoRenderer; +import eu.crushedpixel.replaymod.holders.Keyframe; +import eu.crushedpixel.replaymod.holders.Position; +import eu.crushedpixel.replaymod.holders.PositionKeyframe; +import eu.crushedpixel.replaymod.holders.TimeKeyframe; +import eu.crushedpixel.replaymod.interpolation.Interpolation; +import eu.crushedpixel.replaymod.interpolation.LinearPoint; +import eu.crushedpixel.replaymod.interpolation.LinearTimestamp; +import eu.crushedpixel.replaymod.interpolation.SplinePoint; +import eu.crushedpixel.replaymod.renderer.ChunkLoadingRenderGlobal; +import eu.crushedpixel.replaymod.replay.ReplayHandler; +import eu.crushedpixel.replaymod.replay.ReplaySender; +import eu.crushedpixel.replaymod.settings.RenderOptions; +import eu.crushedpixel.replaymod.timer.EnchantmentTimer; +import eu.crushedpixel.replaymod.timer.MCTimerHandler; +import eu.crushedpixel.replaymod.video.frame.FrameRenderer; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.ScaledResolution; +import net.minecraft.util.Timer; +import org.lwjgl.input.Mouse; +import org.lwjgl.opengl.Display; + +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.util.concurrent.FutureTask; + +import static net.minecraft.client.renderer.GlStateManager.*; +import static org.lwjgl.opengl.GL11.GL_COLOR_BUFFER_BIT; +import static org.lwjgl.opengl.GL11.GL_DEPTH_BUFFER_BIT; + +public class VideoRenderer { + private final Minecraft mc = Minecraft.getMinecraft(); + private final FrameRenderer frameRenderer; + private final VideoWriter videoWriter; + private final ReplaySender replaySender; + private final RenderOptions options; + + private int fps; + private boolean mouseWasGrabbed; + + private Interpolation movement; + private Interpolation time; + + private int framesDone; + private int totalFrames; + + private final GuiVideoRenderer gui; + private boolean frameRendererUpdatesGui; + private boolean paused; + private boolean cancelled; + + public VideoRenderer(RenderOptions options) throws IOException { + this.frameRenderer = options.getRenderer(); + this.videoWriter = new VideoWriter(frameRenderer.getVideoWidth(), frameRenderer.getVideoHeight(), + options.getFps(), options.getQuality(), 10); + this.gui = new GuiVideoRenderer(this); + this.replaySender = ReplayMod.replaySender; + this.options = options; + } + + /** + * Render this video. + * @return {@code true} if rendering was successful, {@code false} if the user aborted rendering (or the window was closed) + * @throws IOException {@link Minecraft#runTick()} can apparently throw these, don't question it! + */ + public boolean renderVideo() throws IOException { + setup(); + + Timer timer = MCTimerHandler.getTimer(); + + replaySender.sendPacketsTill(time.getPoint(0)); + // Pre-tick twice, once to process all the packets + tick(); + // the second time to prevent interpolation of the player position + tick(); + + + framesDone = 0; + while (framesDone < totalFrames && !cancelled) { + pauseIfNeeded(); + + if (Display.isActive() && Display.isCloseRequested()) { + mc.shutdown(); + return false; + } + + updateTime(timer, framesDone); + + int elapsedTicks = timer.elapsedTicks; + while (elapsedTicks-- > 0) { + tick(); + } + + frame(timer); + framesDone++; + + if (!frameRendererUpdatesGui) { + drawGui(); + } + System.gc(); + } + + finish(); + return !cancelled; + } + + private void setup() { + if (Mouse.isGrabbed()) { + mouseWasGrabbed = true; + } + Mouse.setGrabbed(false); + MCTimerHandler.setTimerSpeed(1f); + MCTimerHandler.setPassiveTimer(); + + fps = options.getFps(); + if (options.isLinearMovement()) { + movement = new LinearPoint(); + } else { + movement = new SplinePoint(); + } + time = new LinearTimestamp(); + + int duration = 0; + int posKeyframes = 0; + for (Keyframe keyframe : ReplayHandler.getKeyframes()) { + if (keyframe.getRealTimestamp() > duration) { + duration = keyframe.getRealTimestamp(); + } + if(keyframe instanceof PositionKeyframe) { + movement.addPoint(((PositionKeyframe) keyframe).getPosition()); + posKeyframes++; + } + if (keyframe instanceof TimeKeyframe) { + time.addPoint(((TimeKeyframe) keyframe).getTimestamp()); + } + } + totalFrames = duration*fps/1000; + + if (posKeyframes >= 2) { + movement.prepare(); + } else { + movement = null; // No movement occurring + } + time.prepare(); + + frameRendererUpdatesGui = frameRenderer.setRenderPreviewCallback(new Runnable() { + @Override + public void run() { + drawGui(); + } + }); + frameRenderer.setup(); + + ScaledResolution scaled = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); + gui.setWorldAndResolution(mc, scaled.getScaledWidth(), scaled.getScaledHeight()); + + if (options.isWaitForChunks()) { + ChunkLoadingRenderGlobal.install(mc); + } + } + + private void finish() { + if (cancelled) { + videoWriter.abortRecording(); + } else { + videoWriter.endRecording(); + } + try { + videoWriter.waitForFinish(); + } catch (InterruptedException ignored) { } + + frameRenderer.tearDown(); + if (mouseWasGrabbed) { + Mouse.setGrabbed(true); + } + mc.displayGuiScreen(null); + if (mc.renderGlobal instanceof ChunkLoadingRenderGlobal) { + ((ChunkLoadingRenderGlobal) mc.renderGlobal).uninstall(); + } + } + + private void updateCam() { + if (ReplayHandler.getCameraEntity() == null) { + if (mc.theWorld == null) { + return; // World hasn't been sent yet + } + ReplayHandler.setCameraEntity(new CameraEntity(mc.theWorld)); + } + int videoTime = framesDone * 1000 / fps; + int posCount = ReplayHandler.getPosKeyframeCount(); + + Position pos; + PositionKeyframe lastPos = ReplayHandler.getPreviousPositionKeyframe(videoTime); + if (movement == null || lastPos == null) { + // Stay at one position, no movement + pos = ReplayHandler.getNextPositionKeyframe(-1).getPosition(); + } else { + // Position interpolation + PositionKeyframe nextPos = ReplayHandler.getNextPositionKeyframe(videoTime); + + int lastPosStamp = lastPos.getRealTimestamp(); + int nextPosStamp = (nextPos == null ? lastPos : nextPos).getRealTimestamp(); + + int diffLength = nextPosStamp - lastPosStamp; + float diffPct = (float) (videoTime - lastPosStamp) / diffLength; + if(Float.isInfinite(diffPct) || Float.isNaN(diffPct)) diffPct = 0; + + float totalPct = (ReplayHandler.getKeyframeIndex(lastPos) + diffPct) / (posCount-1); + pos = movement.getPoint(Math.max(0, Math.min(1, totalPct))); + } + ReplayHandler.setCameraTilt(pos.getRoll()); + ReplayHandler.getCameraEntity().movePath(pos); + + // Make sure we're spectating the camera entity + // We do not use .spectateCamera() as that method sets the position of the camera to the previous + // entity, overriding our calculations + ReplayHandler.spectateEntity(ReplayHandler.getCameraEntity()); + } + + private void updateTime(Timer timer, int framesDone) { + int videoTime = framesDone * 1000 / fps; + int timeCount = ReplayHandler.getTimeKeyframeCount(); + + // WARNING: The rest of this method contains some magic for which Marius is responsible + // Time interpolation + TimeKeyframe lastTime = ReplayHandler.getPreviousTimeKeyframe(videoTime); + TimeKeyframe nextTime = ReplayHandler.getNextTimeKeyframe(videoTime); + + int lastTimeStamp = 0; + int nextTimeStamp = 0; + + double curSpeed = 0; + + if(nextTime != null || lastTime != null) { + if(nextTime != null && lastTime != null && nextTime.getRealTimestamp() == lastTime.getRealTimestamp()) { + curSpeed = 0; + } else { + if(nextTime != null) { + nextTimeStamp = nextTime.getRealTimestamp(); + } else { + nextTimeStamp = lastTime.getRealTimestamp(); + } + + if(lastTime != null) { + lastTimeStamp = lastTime.getRealTimestamp(); + } else { + lastTimeStamp = nextTime.getRealTimestamp(); + } + + if(!(nextTime == null || lastTime == null)) { + if(lastTimeStamp == nextTimeStamp) curSpeed = 0f; + else curSpeed = ((double)((nextTime.getTimestamp()-lastTime.getTimestamp())))/((double)((nextTimeStamp-lastTimeStamp))); + } + + if(lastTimeStamp == nextTimeStamp) { + curSpeed = 0f; + } + } + } + + int currentTimeDiff = nextTimeStamp - lastTimeStamp; + int currentTime = videoTime - lastTimeStamp; + + float currentTimeStepPerc = (float)currentTime/(float)currentTimeDiff; //The percentage of the travelled path between the current timestamps + if(Float.isInfinite(currentTimeStepPerc) || Float.isNaN(currentTimeStepPerc)) currentTimeStepPerc = 0; + + float timePos = (ReplayHandler.getKeyframeIndex(lastTime) + currentTimeStepPerc) / (timeCount-1f); + + Integer replayTime = time.getPoint(Math.max(0, Math.min(1, timePos))); + if(replayTime != null) { + replaySender.sendPacketsTill(replayTime); + } + + if(curSpeed > 0) { + replaySender.setReplaySpeed(curSpeed); + } + + // Update Timer + EnchantmentTimer.increaseRecordingTime(1000 / fps); + + timer.elapsedPartialTicks += timer.timerSpeed * 20 / fps; + timer.elapsedTicks = (int) timer.elapsedPartialTicks; + timer.elapsedPartialTicks -= timer.elapsedTicks; + timer.renderPartialTicks = timer.elapsedPartialTicks; + } + + private void tick() throws IOException { + synchronized (mc.scheduledTasks) { + while (!mc.scheduledTasks.isEmpty()) { + ((FutureTask) mc.scheduledTasks.poll()).run(); + } + } + + mc.currentScreen = gui; + mc.runTick(); + } + + private void frame(Timer timer) { + updateCam(); + + BufferedImage frame = null; + while (frame == null) { + try { + frame = frameRenderer.captureFrame(timer); + } catch (OutOfMemoryError e) { + System.out.println("Caught oom error-> calling garbage collector, decreasing queue size and waiting for video writer"); + System.gc(); + videoWriter.waitTillQueueEmpty(); + } + } + while (!videoWriter.writeImage(frame, false)) { + try { + Thread.sleep(10); + } catch (InterruptedException e) { + return; + } + drawGui(); + } + } + + private void pauseIfNeeded() { + while (paused && !cancelled && !Display.isCloseRequested()) { + drawGui(); + try { + Thread.sleep(20); + } catch (InterruptedException e) { + return; + } + } + } + + public void drawGui() { + pushMatrix(); + clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + enableTexture2D(); + mc.getFramebuffer().bindFramebuffer(true); + mc.entityRenderer.setupOverlayRendering(); + + try { + gui.handleInput(); + } catch (IOException e) { + // That's a strange exception from this kind of method O_o + // It isn't actually thrown here, so we'll deal with it the easy way + throw new RuntimeException(e); + } + + ScaledResolution scaled = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); + int mouseX = Mouse.getX() * scaled.getScaledWidth() / mc.displayWidth; + int mouseY = scaled.getScaledHeight() - Mouse.getY() * scaled.getScaledHeight() / mc.displayHeight - 1; + gui.drawScreen(mouseX, mouseY, 0); + + mc.getFramebuffer().unbindFramebuffer(); + popMatrix(); + pushMatrix(); + mc.getFramebuffer().framebufferRender(mc.displayWidth, mc.displayHeight); + popMatrix(); + + Display.update(); + if (Mouse.isGrabbed()) { + Mouse.setGrabbed(false); + } + } + + public int getFramesDone() { + return framesDone; + } + + public int getTotalFrames() { + return totalFrames; + } + + public FrameRenderer getFrameRenderer() { + return frameRenderer; + } + + public void setPaused(boolean paused) { + this.paused = paused; + } + + public boolean isPaused() { + return paused; + } + + public void cancel() { + this.cancelled = true; + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/video/VideoWriter.java b/src/main/java/eu/crushedpixel/replaymod/video/VideoWriter.java index 6f75a0b8..bc6d599c 100755 --- a/src/main/java/eu/crushedpixel/replaymod/video/VideoWriter.java +++ b/src/main/java/eu/crushedpixel/replaymod/video/VideoWriter.java @@ -1,6 +1,6 @@ package eu.crushedpixel.replaymod.video; -import eu.crushedpixel.replaymod.ReplayMod; +import com.google.common.base.Preconditions; import eu.crushedpixel.replaymod.utils.ReplayFileIO; import org.monte.media.*; import org.monte.media.FormatKeys.MediaType; @@ -9,132 +9,170 @@ import org.monte.media.math.Rational; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; +import java.nio.file.Files; import java.text.SimpleDateFormat; import java.util.Calendar; +import java.util.LinkedList; import java.util.Queue; -import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; public class VideoWriter { private static final String DATE_FORMAT = "yyyy_MM_dd_HH_mm_ss"; - private static final SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT); + private static final SimpleDateFormat FILE_FORMAT = new SimpleDateFormat(DATE_FORMAT); private static final String VIDEO_EXTENSION = ".avi"; - private static MovieWriter out; - private static File file; - private static boolean isRecording = false; - private static boolean requestFinish = false; - private static boolean abort = false; + private final File file; + private final MovieWriter out; + private final Buffer buf; + private final int track; - private static Buffer buf; - private static int track; - private static Queue toWrite = new LinkedBlockingQueue(); + private volatile boolean active = true; + private volatile boolean cancelled = false; - public static boolean isRecording() { - return isRecording; + private int queueLimit; + private final Queue toWrite; + private final Thread writerThread; + + private final Lock lock = new ReentrantLock(); + private final Condition emptyCondition = lock.newCondition(); + private final Condition noLongerEmptyCondition = lock.newCondition(); + private final Condition noLongerFullCondition = lock.newCondition(); + + public VideoWriter(int width, int height, int fps, float quality) throws IOException { + this(width, height, fps, quality, Integer.MAX_VALUE); } - public static void startRecording(int width, int height) { - if(isRecording) { - IllegalStateException up = new IllegalStateException("VideoWriter is already recording!"); - throw up; //lolololo - } - isRecording = true; + public VideoWriter(int width, int height, int fps, float quality, int queueLimit) throws IOException { + this.queueLimit = queueLimit; + this.toWrite = new LinkedList(); - toWrite = new LinkedBlockingQueue(); + File folder = ReplayFileIO.getRenderFolder(); + String fileName = FILE_FORMAT.format(Calendar.getInstance().getTime()); + file = new File(folder, fileName + VIDEO_EXTENSION); + Files.createFile(file.toPath()); - try { - File folder = ReplayFileIO.getRenderFolder(); + out = Registry.getInstance().getWriter(file); + Format format = new Format(FormatKeys.MediaTypeKey, MediaType.VIDEO, + FormatKeys.EncodingKey, VideoFormatKeys.ENCODING_AVI_MJPG, + FormatKeys.FrameRateKey, new Rational(fps, 1), + VideoFormatKeys.WidthKey, width, + VideoFormatKeys.HeightKey, height, + VideoFormatKeys.DepthKey, 24, + VideoFormatKeys.QualityKey, quality); - String fileName = sdf.format(Calendar.getInstance().getTime()); + track = out.addTrack(format); - file = new File(folder, fileName + VIDEO_EXTENSION); - file.createNewFile(); + buf = new Buffer(); + buf.format = new Format(VideoFormatKeys.DataClassKey, BufferedImage.class); + buf.sampleDuration = out.getFormat(track).get(VideoFormatKeys.FrameRateKey).inverse(); - out = Registry.getInstance().getWriter(file); - Format format = new Format(FormatKeys.MediaTypeKey, MediaType.VIDEO, - FormatKeys.EncodingKey, VideoFormatKeys.ENCODING_AVI_MJPG, - FormatKeys.FrameRateKey, new Rational(ReplayMod.replaySettings.getVideoFramerate(), 1), - VideoFormatKeys.WidthKey, width, - VideoFormatKeys.HeightKey, height, - VideoFormatKeys.DepthKey, 24, - VideoFormatKeys.QualityKey, (float) ReplayMod.replaySettings.getVideoQuality()); - - - track = out.addTrack(format); - - buf = new Buffer(); - buf.format = new Format(VideoFormatKeys.DataClassKey, BufferedImage.class); - buf.sampleDuration = out.getFormat(track).get(VideoFormatKeys.FrameRateKey).inverse(); - } catch(IOException e) { - e.printStackTrace(); - } - - Thread t = new Thread(new Runnable() { + writerThread = new Thread(new Runnable() { @Override public void run() { - while(true) { - if(toWrite.isEmpty() || abort) { - if(requestFinish) { - requestFinish = false; - isRecording = false; - try { - out.close(); - if(abort) { - file.delete(); - } - } catch(IOException e) { - e.printStackTrace(); - } - abort = false; - toWrite = new LinkedBlockingQueue(); - return; - } + while(!cancelled && (active || !toWrite.isEmpty())) { + try { + lock.lockInterruptibly(); + BufferedImage img; try { - Thread.sleep(10); - } catch(Exception e) { + img = toWrite.poll(); + if (img == null) { + noLongerEmptyCondition.await(); + img = toWrite.poll(); + } + noLongerFullCondition.signal(); + if (toWrite.isEmpty()) { + emptyCondition.signalAll(); + } + } finally { + lock.unlock(); + } + buf.data = img; + try { + out.write(track, buf); + } catch (IOException e) { e.printStackTrace(); } - } else { - write(); + } catch (InterruptedException ignored) { } } + try { + toWrite.clear(); + out.close(); + if (cancelled) { + Files.delete(file.toPath()); + } + } catch (IOException e) { + e.printStackTrace(); + } } }); - t.start(); + writerThread.start(); } - public static void writeImage(BufferedImage image) { - if(requestFinish || !isRecording) { - IllegalStateException up = new IllegalStateException( - "The VideoWriter is currently not available. Please try again later."); - throw up; //lolololo^2 - } - - toWrite.add(image); + public void setQueueLimit(int limit) { + this.queueLimit = limit; } - public static void endRecording() { - if(!isRecording) return; - requestFinish = true; - } + /** + * Add the image to the writer queue. + * @param image The image + * @param waitIfFull Whether to wait if the queue is full or to return immediately + * @return {@code} true if the image was added, {@code false} if it could not be added due to the queue being full + * and {@code waitIfFull} being {@code false} + */ + public boolean writeImage(BufferedImage image, boolean waitIfFull) { + Preconditions.checkState(active, "This VideoWriter has already been closed."); - private static void write() { + lock.lock(); try { - BufferedImage img = toWrite.poll(); - if(img != null) { - buf.data = img; - out.write(track, buf); + while (toWrite.size() >= queueLimit) { + if (waitIfFull) { + noLongerFullCondition.await(); + } else { + return false; + } } - } catch(IOException e) { + toWrite.offer(image); + noLongerEmptyCondition.signal(); + return true; + } catch (InterruptedException ignored) { + } finally { + lock.unlock(); + } + return false; + } + + public void endRecording() { + active = false; + writerThread.interrupt(); + } + + public void abortRecording() { + cancelled = true; + active = false; + writerThread.interrupt(); + } + + public void waitTillQueueEmpty() { + lock.lock(); + try { + if (!toWrite.isEmpty()) { + emptyCondition.await(); + } + } catch (InterruptedException e) { e.printStackTrace(); + } finally { + lock.unlock(); } } - public static void abortRecording() { - requestFinish = true; - abort = true; + public void waitForFinish() throws InterruptedException { + Preconditions.checkState(!active, "Video writer still active."); + writerThread.join(); } } diff --git a/src/main/java/eu/crushedpixel/replaymod/video/entity/CubicEntityRenderer.java b/src/main/java/eu/crushedpixel/replaymod/video/entity/CubicEntityRenderer.java new file mode 100644 index 00000000..f9fe24df --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/video/entity/CubicEntityRenderer.java @@ -0,0 +1,109 @@ +package eu.crushedpixel.replaymod.video.entity; + +import eu.crushedpixel.replaymod.replay.ReplayHandler; +import net.minecraft.client.renderer.GlStateManager; +import net.minecraft.entity.Entity; + +public class CubicEntityRenderer extends CustomEntityRenderer { + + public static enum Direction { + TOP(1), BOTTOM(9), LEFT(4), FRONT(5), RIGHT(6), BACK(7); + + private final int frame; + + Direction(int frame) { + this.frame = frame; + } + + public static Direction forFrame(int frame) { + for (Direction d : values()) { + if (d.frame == frame) { + return d; + } + } + return null; + } + + public int getCubicFrame() { + return frame; + } + } + + private final boolean stable; + private Direction direction; + + public CubicEntityRenderer(boolean stable) { + this.stable = stable; + } + + public void setFrame(int id) { + setDirection(Direction.forFrame(id)); + } + + public void setDirection(Direction direction) { + this.direction = direction; + } + + @Override + protected void gluPerspective(float fovY, float aspect, float zNear, float zFar) { + super.gluPerspective(90, 1, zNear, zFar); + } + + @Override + protected void setupCameraTransform(float partialTicks) { + Entity entity = mc.getRenderViewEntity(); + float orgYaw = entity.rotationYaw; + float orgPitch = entity.rotationPitch; + float orgPrevYaw = entity.prevRotationYaw; + float orgPrevPitch = entity.prevRotationPitch; + float orgRoll = ReplayHandler.getCameraTilt(); + + super.setupCameraTransform(partialTicks); + + entity.rotationYaw = orgYaw; + entity.rotationPitch = orgPitch; + entity.prevRotationYaw = orgPrevYaw; + entity.prevRotationPitch = orgPrevPitch; + if (stable) { + ReplayHandler.setCameraTilt(orgRoll); + } + } + + @Override + protected void orientCamera(float partialTicks) { + Entity entity = mc.getRenderViewEntity(); + + // Rotate + switch(direction) { + case FRONT: + GlStateManager.rotate(0, 0.0F, 1.0F, 0.0F); + break; + case RIGHT: + GlStateManager.rotate(90, 0.0F, 1.0F, 0.0F); + break; + case BACK: + GlStateManager.rotate(180, 0.0F, 1.0F, 0.0F); + break; + case LEFT: + GlStateManager.rotate(-90, 0.0F, 1.0F, 0.0F); + break; + case BOTTOM: + GlStateManager.rotate(90, 1.0F, 0.0F, 0.0F); + break; + case TOP: + GlStateManager.rotate(-90, 1.0F, 0.0F, 0.0F); + break; + } + + if (stable) { + // Stop the minecraft code from doing any rotation + entity.prevRotationPitch = entity.rotationPitch = 0; + entity.prevRotationYaw = entity.rotationYaw = 0; + ReplayHandler.setCameraTilt(0); + } + + // Minecraft goes back a little so we have to invert that + GlStateManager.translate(0.0F, 0.0F, 0.1F); + super.orientCamera(partialTicks); + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/video/entity/CustomEntityRenderer.java b/src/main/java/eu/crushedpixel/replaymod/video/entity/CustomEntityRenderer.java new file mode 100644 index 00000000..bebf1dae --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/video/entity/CustomEntityRenderer.java @@ -0,0 +1,225 @@ +package eu.crushedpixel.replaymod.video.entity; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.particle.EffectRenderer; +import net.minecraft.client.renderer.*; +import net.minecraft.client.renderer.culling.ClippingHelperImpl; +import net.minecraft.client.renderer.culling.ICamera; +import net.minecraft.client.renderer.texture.TextureMap; +import net.minecraft.entity.Entity; +import net.minecraft.util.AxisAlignedBB; +import net.minecraft.util.EnumWorldBlockLayer; +import net.minecraft.util.MathHelper; +import org.lwjgl.util.glu.Project; + +import static net.minecraft.client.renderer.GlStateManager.*; +import static org.lwjgl.opengl.GL11.*; + +public abstract class CustomEntityRenderer { + protected final Minecraft mc = Minecraft.getMinecraft(); + protected final EntityRenderer proxied = mc.entityRenderer; + protected int frameCount; + + protected void gluPerspective(float fovY, float aspect, float zNear, float zFar) { + Project.gluPerspective(fovY, aspect, zNear, zFar); + } + + public void updateCameraAndRender(float renderPartialTicks) { + if (mc.theWorld != null) { + renderWorld(renderPartialTicks, 0); + + if (OpenGlHelper.shadersSupported) { + mc.renderGlobal.renderEntityOutlineFramebuffer(); + + if (proxied.theShaderGroup != null && proxied.useShader) { + matrixMode(GL_TEXTURE); + pushMatrix(); + loadIdentity(); + proxied.theShaderGroup.loadShaderGroup(renderPartialTicks); + popMatrix(); + } + + mc.getFramebuffer().bindFramebuffer(true); + } + } + } + + protected void renderWorld(float partialTicks, long finishTimeNano) { + proxied.updateLightmap(partialTicks); + + enableDepth(); + enableAlpha(); + alphaFunc(516, 0.5F); + + renderWorldPass(partialTicks, finishTimeNano, 2); + } + + protected void renderWorldPass(float partialTicks, long finishTimeNano, int renderPass) { + RenderGlobal renderglobal = mc.renderGlobal; + EffectRenderer effectrenderer = mc.effectRenderer; + Entity entity = mc.getRenderViewEntity(); + enableCull(); + viewport(0, 0, mc.displayWidth, mc.displayHeight); + proxied.updateFogColor(partialTicks); + clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + setupCameraTransform(partialTicks); + ActiveRenderInfo.updateRenderInfo(mc.thePlayer, mc.gameSettings.thirdPersonView == 2); + ClippingHelperImpl.getInstance(); + ICamera camera = new NoCullingCamera(); + + if (this.mc.gameSettings.renderDistanceChunks >= 4) { + proxied.setupFog(-1, partialTicks); + matrixMode(GL_PROJECTION); + loadIdentity(); + gluPerspective(proxied.getFOVModifier(partialTicks, true), (float) mc.displayWidth / mc.displayHeight, 0.05F, proxied.farPlaneDistance * 2.0F); + matrixMode(GL_MODELVIEW); + renderglobal.renderSky(partialTicks, renderPass); + matrixMode(GL_PROJECTION); + loadIdentity(); + gluPerspective(proxied.getFOVModifier(partialTicks, true), (float) mc.displayWidth / mc.displayHeight, 0.05F, proxied.farPlaneDistance * MathHelper.SQRT_2); + matrixMode(GL_MODELVIEW); + } + + proxied.setupFog(0, partialTicks); + shadeModel(GL_SMOOTH); + + if (entity.posY + entity.getEyeHeight() < 128) { + renderCloudsCheck(renderglobal, partialTicks, renderPass); + } + + proxied.setupFog(0, partialTicks); + mc.getTextureManager().bindTexture(TextureMap.locationBlocksTexture); + RenderHelper.disableStandardItemLighting(); + renderglobal.setupTerrain(entity, partialTicks, camera, frameCount++, mc.thePlayer.isSpectator()); + + renderglobal.updateChunks(finishTimeNano); + + matrixMode(GL_MODELVIEW); + pushMatrix(); + disableAlpha(); + renderglobal.renderBlockLayer(EnumWorldBlockLayer.SOLID, partialTicks, renderPass, entity); + enableAlpha(); + renderglobal.renderBlockLayer(EnumWorldBlockLayer.CUTOUT_MIPPED, partialTicks, renderPass, entity); + mc.getTextureManager().getTexture(TextureMap.locationBlocksTexture).setBlurMipmap(false, false); + renderglobal.renderBlockLayer(EnumWorldBlockLayer.CUTOUT, partialTicks, renderPass, entity); + mc.getTextureManager().getTexture(TextureMap.locationBlocksTexture).restoreLastBlurMipmap(); + shadeModel(GL_FLAT); + alphaFunc(516, 0.1F); + + matrixMode(GL_MODELVIEW); + popMatrix(); + pushMatrix(); + RenderHelper.enableStandardItemLighting(); + + net.minecraftforge.client.ForgeHooksClient.setRenderPass(0); + renderglobal.renderEntities(entity, camera, partialTicks); + net.minecraftforge.client.ForgeHooksClient.setRenderPass(0); + RenderHelper.disableStandardItemLighting(); + proxied.disableLightmap(); + matrixMode(GL_MODELVIEW); + popMatrix(); + + enableBlend(); + tryBlendFuncSeparate(770, 1, 1, 0); + renderglobal.drawBlockDamageTexture(Tessellator.getInstance(), Tessellator.getInstance().getWorldRenderer(), entity, partialTicks); + disableBlend(); + + proxied.enableLightmap(); + effectrenderer.renderLitParticles(entity, partialTicks); + RenderHelper.disableStandardItemLighting(); + proxied.setupFog(0, partialTicks); + effectrenderer.renderParticles(entity, partialTicks); + proxied.disableLightmap(); + + depthMask(false); + enableCull(); + proxied.renderRainSnow(partialTicks); + depthMask(true); + renderglobal.renderWorldBorder(entity, partialTicks); + disableBlend(); + enableCull(); + tryBlendFuncSeparate(770, 771, 1, 0); + alphaFunc(516, 0.1F); + proxied.setupFog(0, partialTicks); + enableBlend(); + depthMask(false); + mc.getTextureManager().bindTexture(TextureMap.locationBlocksTexture); + shadeModel(GL_SMOOTH); + + if (mc.gameSettings.fancyGraphics) { + enableBlend(); + tryBlendFuncSeparate(770, 771, 1, 0); + renderglobal.renderBlockLayer(EnumWorldBlockLayer.TRANSLUCENT, partialTicks, renderPass, entity); + disableBlend(); + } else { + renderglobal.renderBlockLayer(EnumWorldBlockLayer.TRANSLUCENT, partialTicks, renderPass, entity); + } + + RenderHelper.enableStandardItemLighting(); + net.minecraftforge.client.ForgeHooksClient.setRenderPass(1); + renderglobal.renderEntities(entity, camera, partialTicks); + net.minecraftforge.client.ForgeHooksClient.setRenderPass(-1); + RenderHelper.disableStandardItemLighting(); + + shadeModel(GL_FLAT); + depthMask(true); + enableCull(); + disableBlend(); + disableFog(); + + if (entity.posY + entity.getEyeHeight() >= 128) { + renderCloudsCheck(renderglobal, partialTicks, renderPass); + } + + net.minecraftforge.client.ForgeHooksClient.dispatchRenderLast(renderglobal, partialTicks); + } + + protected void setupCameraTransform(float partialTicks) { + proxied.farPlaneDistance = (float)(this.mc.gameSettings.renderDistanceChunks * 16); + + matrixMode(GL_PROJECTION); + loadIdentity(); + + gluPerspective(proxied.getFOVModifier(partialTicks, true), (float) this.mc.displayWidth / (float) this.mc.displayHeight, 0.05F, proxied.farPlaneDistance * MathHelper.SQRT_2); + + matrixMode(GL_MODELVIEW); + loadIdentity(); + + orientCamera(partialTicks); + } + + protected void orientCamera(float partialTicks) { + proxied.orientCamera(partialTicks); + } + + protected void renderCloudsCheck(RenderGlobal renderglobal, float partialTicks, int renderPass) { + if (this.mc.gameSettings.shouldRenderClouds()) { + matrixMode(GL_PROJECTION); + loadIdentity(); + gluPerspective(proxied.getFOVModifier(partialTicks, true), (float) this.mc.displayWidth / (float) this.mc.displayHeight, 0.05F, proxied.farPlaneDistance * 4.0F); + matrixMode(GL_MODELVIEW); + pushMatrix(); + proxied.setupFog(0, partialTicks); + renderglobal.renderClouds(partialTicks, renderPass); + disableFog(); + popMatrix(); + matrixMode(GL_PROJECTION); + loadIdentity(); + gluPerspective(proxied.getFOVModifier(partialTicks, true), (float)this.mc.displayWidth / (float)this.mc.displayHeight, 0.05F, proxied.farPlaneDistance * MathHelper.SQRT_2); + matrixMode(GL_MODELVIEW); + } + } + + public static final class NoCullingCamera implements ICamera { + + @Override + public boolean isBoundingBoxInFrustum(AxisAlignedBB p_78546_1_) { + return true; + } + + @Override + public void setPosition(double p_78547_1_, double p_78547_3_, double p_78547_5_) { + + } + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/video/entity/StereoscopicEntityRenderer.java b/src/main/java/eu/crushedpixel/replaymod/video/entity/StereoscopicEntityRenderer.java new file mode 100644 index 00000000..b984de46 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/video/entity/StereoscopicEntityRenderer.java @@ -0,0 +1,39 @@ +package eu.crushedpixel.replaymod.video.entity; + +import net.minecraft.client.renderer.GlStateManager; +import net.minecraft.util.MathHelper; + +import static net.minecraft.client.renderer.GlStateManager.loadIdentity; +import static net.minecraft.client.renderer.GlStateManager.matrixMode; +import static org.lwjgl.opengl.GL11.GL_MODELVIEW; +import static org.lwjgl.opengl.GL11.GL_PROJECTION; + +public class StereoscopicEntityRenderer extends CustomEntityRenderer { + + private boolean leftEye; + + public void setEye(boolean leftEye) { + this.leftEye = leftEye; + } + + protected void translateStereoscopic() { + GlStateManager.translate(leftEye ? 0.07 : -0.07, 0, 0); + } + + @Override + protected void setupCameraTransform(float partialTicks) { + proxied.farPlaneDistance = (float)(this.mc.gameSettings.renderDistanceChunks * 16); + + matrixMode(GL_PROJECTION); + loadIdentity(); + translateStereoscopic(); + + gluPerspective(proxied.getFOVModifier(partialTicks, true), (float) this.mc.displayWidth / (float) this.mc.displayHeight, 0.05F, proxied.farPlaneDistance * MathHelper.SQRT_2); + + matrixMode(GL_MODELVIEW); + loadIdentity(); + translateStereoscopic(); + + orientCamera(partialTicks); + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/video/entity/TilingEntityRenderer.java b/src/main/java/eu/crushedpixel/replaymod/video/entity/TilingEntityRenderer.java new file mode 100644 index 00000000..4deaec11 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/video/entity/TilingEntityRenderer.java @@ -0,0 +1,41 @@ +package eu.crushedpixel.replaymod.video.entity; + +import eu.crushedpixel.replaymod.video.frame.TilingFrameRenderer; + +import static net.minecraft.client.renderer.GlStateManager.scale; +import static net.minecraft.client.renderer.GlStateManager.translate; +import static org.lwjgl.opengl.GL11.glFrustum; + +public class TilingEntityRenderer extends CustomEntityRenderer { + private final TilingFrameRenderer renderer; + private int tileX, tileY; + + public TilingEntityRenderer(TilingFrameRenderer renderer) { + this.renderer = renderer; + } + + public void setTile(int tileX, int tileY) { + this.tileX = tileX; + this.tileY = tileY; + } + + @Override + public void gluPerspective(float fovY, float aspect, float zNear, float zFar) { + double height = Math.tan(fovY / 360 * Math.PI) * zNear; + double width = height * aspect; + + int videoWidth = renderer.getVideoWidth(); + int videoHeight = renderer.getVideoHeight(); + int tileWidth = renderer.getTileWidth(); + int tileHeight = renderer.getTileHeight(); + double tilesX = (double) videoWidth / tileWidth; + double tilesY = (double) videoHeight / tileHeight; + double tilesMin = Math.min(tilesX, tilesY); + scale(tilesMin, tilesMin, 1); + double xOffset = (2 * tileX - tilesX + 1) / tilesMin; + double yOffset = (2 * tileY - tilesY + 1) / tilesMin; + translate(-xOffset, yOffset, 0); + + glFrustum(-width, width, -height, height, zNear, zFar); + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/video/frame/CubicFrameRenderer.java b/src/main/java/eu/crushedpixel/replaymod/video/frame/CubicFrameRenderer.java new file mode 100644 index 00000000..cbaccbeb --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/video/frame/CubicFrameRenderer.java @@ -0,0 +1,79 @@ +package eu.crushedpixel.replaymod.video.frame; + +import eu.crushedpixel.replaymod.video.entity.CubicEntityRenderer; +import net.minecraft.client.Minecraft; +import net.minecraft.crash.CrashReport; +import net.minecraft.util.ReportedException; +import net.minecraft.util.Timer; + +import java.awt.image.BufferedImage; + +public class CubicFrameRenderer extends FrameRenderer { + protected static int getDisplaySize() { + Minecraft mc = Minecraft.getMinecraft(); + return Math.min(mc.displayWidth, mc.displayHeight); + } + + protected final CubicEntityRenderer entityRenderer; + protected final int displaySize; + + protected CubicFrameRenderer(int videoWidth, int videoHeight, boolean stable) { + super(videoWidth, videoHeight, getDisplaySize() * getDisplaySize() * 3); + this.displaySize = getDisplaySize(); + this.entityRenderer = new CubicEntityRenderer(stable); + setCustomEntityRenderer(entityRenderer); + } + + public CubicFrameRenderer(boolean stable) { + this(getDisplaySize() * 4, getDisplaySize() * 3, stable); + } + + @Override + public BufferedImage captureFrame(Timer timer) { + BufferedImage image = new BufferedImage(getVideoWidth(), getVideoHeight(), BufferedImage.TYPE_INT_RGB); + for (CubicEntityRenderer.Direction direction : CubicEntityRenderer.Direction.values()) { + try { + captureFrame(timer, image, direction); + } catch (Throwable t) { + CrashReport crash = CrashReport.makeCrashReport(t, "Rendering frame " + direction + "."); + throw new ReportedException(crash); + } + } + updateDefaultPreview(image); + return image; + } + + protected void captureFrame(Timer timer, BufferedImage into, CubicEntityRenderer.Direction direction) { + renderFrameToBuffer(timer, direction); + + // Copy to image + int frame = direction.getCubicFrame(); + int xVideo = frame % 4 * displaySize; + int yVideo = frame / 4 * displaySize; + copyPixelsToImage(buffer, displaySize, into, xVideo, yVideo); + } + + protected void renderFrameToBuffer(Timer timer, CubicEntityRenderer.Direction direction) { + // Setup aspect ratio + int originalWidth = mc.displayWidth; + int originalHeight = mc.displayHeight; + mc.displayWidth = mc.displayHeight = displaySize; + updateFrameBufferSize(); + + // Render frame + entityRenderer.setDirection(direction); + renderFrame(timer); + + // Read pixels + readPixels(buffer); + + mc.displayWidth = originalWidth; + mc.displayHeight = originalHeight; + updateFrameBufferSize(); + } + + private void updateFrameBufferSize() { + mc.getFramebuffer().createBindFramebuffer(mc.displayWidth, mc.displayHeight); + mc.entityRenderer.updateShaderGroupSize(mc.displayWidth, mc.displayHeight); + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/video/frame/DefaultFrameRenderer.java b/src/main/java/eu/crushedpixel/replaymod/video/frame/DefaultFrameRenderer.java new file mode 100644 index 00000000..0bef054e --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/video/frame/DefaultFrameRenderer.java @@ -0,0 +1,27 @@ +package eu.crushedpixel.replaymod.video.frame; + +import net.minecraft.client.Minecraft; +import net.minecraft.util.Timer; + +import java.awt.image.BufferedImage; + +public class DefaultFrameRenderer extends FrameRenderer { + + public DefaultFrameRenderer() { + super(Minecraft.getMinecraft().displayWidth, Minecraft.getMinecraft().displayHeight); + } + + @Override + public BufferedImage captureFrame(Timer timer) { + renderFrame(timer); + + int width = getVideoWidth(); + int height = getVideoHeight(); + + readPixels(buffer); + + BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + copyPixelsToImage(buffer, width, image, 0, 0); + return image; + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/video/frame/EquirectangularFrameRenderer.java b/src/main/java/eu/crushedpixel/replaymod/video/frame/EquirectangularFrameRenderer.java new file mode 100644 index 00000000..5fbbb784 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/video/frame/EquirectangularFrameRenderer.java @@ -0,0 +1,101 @@ +package eu.crushedpixel.replaymod.video.frame; + +import eu.crushedpixel.replaymod.video.entity.CubicEntityRenderer; +import net.minecraft.crash.CrashReport; +import net.minecraft.util.ReportedException; +import net.minecraft.util.Timer; + +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; + +import static java.lang.Math.PI; + +public class EquirectangularFrameRenderer extends CubicFrameRenderer { + + public EquirectangularFrameRenderer(boolean stable) { + super(getDisplaySize() * 4, getDisplaySize() * 2, stable); + } + + @Override + public BufferedImage captureFrame(Timer timer) { + BufferedImage top = captureFrame(timer, CubicEntityRenderer.Direction.TOP); + BufferedImage bottom = captureFrame(timer, CubicEntityRenderer.Direction.BOTTOM); + BufferedImage front = captureFrame(timer, CubicEntityRenderer.Direction.FRONT); + BufferedImage back = captureFrame(timer, CubicEntityRenderer.Direction.BACK); + BufferedImage left = captureFrame(timer, CubicEntityRenderer.Direction.LEFT); + BufferedImage right = captureFrame(timer, CubicEntityRenderer.Direction.RIGHT); + try { + BufferedImage result = cubicToEquirectangular(top, bottom, front, back, left, right); + updateDefaultPreview(result); + return result; + } catch (Throwable t) { + CrashReport crash = CrashReport.makeCrashReport(t, "Transforming cubic to equirectangular image."); + throw new ReportedException(crash); + } + } + + protected BufferedImage cubicToEquirectangular(BufferedImage top, BufferedImage bottom, BufferedImage front, BufferedImage back, BufferedImage left, BufferedImage right) { + int rWidth = getVideoWidth(); + int rHeight = getVideoHeight(); + BufferedImage result = new BufferedImage(rWidth, rHeight, BufferedImage.TYPE_INT_RGB); + int[] resultPixels = ((DataBufferInt) result.getRaster().getDataBuffer()).getData(); + for (int i = 0; i < rWidth; i++) { + double yaw = PI * 2 * i / rWidth; + int piQuarter = 8 * i / rWidth - 4; + BufferedImage target; + if (piQuarter < -3) { + target = back; + } else if (piQuarter < -1) { + target = left; + } else if (piQuarter < 1) { + target = front; + } else if (piQuarter < 3) { + target = right; + } else { + target = back; + } + double fYaw = (yaw + PI/4) % (PI / 2) - PI/4; + double d = 1 / Math.cos(fYaw); + double gcXN = (Math.tan(fYaw) + 1) / 2; + for (int j = 0; j < rHeight; j++) { + double cXN = gcXN; + BufferedImage pt = target; + double pitch = PI * j / rHeight - PI / 2; + double cYN = (Math.tan(pitch) * d + 1) / 2; + + if (cYN >= 1) { + double pd = Math.tan(PI/2 - pitch); + cXN = (-Math.sin(yaw) * pd + 1) / 2; + cYN = (Math.cos(yaw) * pd + 1) / 2; + pt = bottom; + } + if (cYN < 0) { + double pd = Math.tan(PI/2 - pitch); + cXN = (Math.sin(yaw) * pd + 1) / 2; + cYN = (Math.cos(yaw) * pd + 1) / 2; + pt = top; + } + + int cX = Math.min(displaySize - 1, (int) (cXN * displaySize)); + int cY = Math.min(displaySize - 1, (int) (cYN * displaySize)); + resultPixels[i + j * rWidth] = 0xff000000 | pt.getRGB(cX, cY); // Make sure we got alpha value as well + } + } + + return result; + } + + protected BufferedImage captureFrame(Timer timer, CubicEntityRenderer.Direction direction) { + try { + renderFrameToBuffer(timer, direction); + + // Copy to image + BufferedImage image = new BufferedImage(displaySize, displaySize, BufferedImage.TYPE_INT_RGB); + copyPixelsToImage(buffer, displaySize, image, displaySize, 0, 0); + return image; + } catch (Throwable t) { + CrashReport crash = CrashReport.makeCrashReport(t, "Rendering frame " + direction); + throw new ReportedException(crash); + } + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/video/frame/FrameRenderer.java b/src/main/java/eu/crushedpixel/replaymod/video/frame/FrameRenderer.java new file mode 100644 index 00000000..7006f4fb --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/video/frame/FrameRenderer.java @@ -0,0 +1,225 @@ +package eu.crushedpixel.replaymod.video.frame; + +import com.google.common.base.Optional; +import com.google.common.base.Preconditions; +import com.google.common.util.concurrent.Runnables; +import eu.crushedpixel.replaymod.gui.GuiVideoRenderer; +import eu.crushedpixel.replaymod.video.entity.CustomEntityRenderer; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Gui; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.renderer.texture.DynamicTexture; +import net.minecraft.client.renderer.texture.TextureUtil; +import net.minecraft.util.ResourceLocation; +import net.minecraft.util.Timer; +import org.lwjgl.BufferUtils; +import org.lwjgl.opengl.GL11; + +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; +import java.nio.ByteBuffer; +import java.util.Arrays; + +import static net.minecraft.client.renderer.GlStateManager.*; +import static org.lwjgl.opengl.GL11.GL_COLOR_BUFFER_BIT; +import static org.lwjgl.opengl.GL11.GL_DEPTH_BUFFER_BIT; + +public abstract class FrameRenderer { + private static final ResourceLocation noPreviewTexture = new ResourceLocation("replaymod", "logo.jpg"); + protected final Minecraft mc = Minecraft.getMinecraft(); + private final int videoWidth; + private final int videoHeight; + private Optional customEntityRenderer; + private DynamicTexture previewTexture; + private Runnable renderPreviewCallback; + private boolean previewActive = false; + protected final ByteBuffer buffer; + + public FrameRenderer(int videoWidth, int videoHeight) { + this.videoWidth = videoWidth; + this.videoHeight = videoHeight; + this.buffer = BufferUtils.createByteBuffer(mc.displayWidth * mc.displayHeight * 3); + this.customEntityRenderer = Optional.absent(); + } + + public FrameRenderer(int videoWidth, int videoHeight, int bufferSize) { + this.videoWidth = videoWidth; + this.videoHeight = videoHeight; + this.buffer = BufferUtils.createByteBuffer(bufferSize); + this.customEntityRenderer = Optional.absent(); + } + + public final int getVideoWidth() { + return videoWidth; + } + + public final int getVideoHeight() { + return videoHeight; + } + + public void setPreviewActive(boolean previewActive) { + this.previewActive = previewActive; + if (previewActive) { + Arrays.fill(previewTexture.getTextureData(), 0xff000000); + previewTexture.updateDynamicTexture(); + } + } + + public boolean isPreviewActive() { + return previewActive; + } + + public boolean setRenderPreviewCallback(Runnable renderPreviewCallback) { + Preconditions.checkState(this.renderPreviewCallback == null, "Render preview callback already set."); + this.renderPreviewCallback = renderPreviewCallback; + return false; + } + + public Runnable getRenderPreviewCallback() { + return renderPreviewCallback != null ? renderPreviewCallback : Runnables.doNothing(); + } + + public void setCustomEntityRenderer(CustomEntityRenderer customEntityRenderer) { + this.customEntityRenderer = Optional.fromNullable(customEntityRenderer); + } + + public abstract BufferedImage captureFrame(Timer timer); + + protected void readPixels(ByteBuffer buffer) { + buffer.clear(); + GL11.glPixelStorei(GL11.GL_PACK_ALIGNMENT, 1); + GL11.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 1); + GL11.glReadPixels(0, 0, mc.displayWidth, mc.displayHeight, GL11.GL_RGB, GL11.GL_UNSIGNED_BYTE, buffer); + buffer.rewind(); + } + + protected void updateDefaultPreview(BufferedImage image) { + if (isPreviewActive()) { + int[] pixels = ((DataBufferInt) image.getRaster().getDataBuffer()).getData(); + updateDefaultPreview(pixels); + } + } + + protected void updateDefaultPreview(int[] pixels) { + if (isPreviewActive()) { + System.arraycopy(pixels, 0, getPreviewTexture().getTextureData(), 0, pixels.length); + getPreviewTexture().updateDynamicTexture(); + } + } + + protected void copyPixelsToImage(ByteBuffer buffer, int bufferWidth, BufferedImage image, int offsetX, int offsetY) { + int[] pixels = ((DataBufferInt) image.getRaster().getDataBuffer()).getData(); + copyPixelsToImage(buffer, bufferWidth, pixels, offsetX, offsetY); + } + + protected void copyPixelsToImage(ByteBuffer buffer, int bufferWidth, BufferedImage image, int imageWidth, int offsetX, int offsetY) { + int[] pixels = ((DataBufferInt) image.getRaster().getDataBuffer()).getData(); + copyPixelsToImage(buffer, bufferWidth, pixels, imageWidth, offsetX, offsetY); + } + + protected void copyPixelsToImage(ByteBuffer buffer, int bufferWidth, int[] image, int offsetX, int offsetY) { + copyPixelsToImage(buffer, bufferWidth, image, videoWidth, offsetX, offsetY); + } + + protected void copyPixelsToImage(ByteBuffer buffer, int bufferWidth, int[] image, int imageWidth, int offsetX, int offsetY) { + int bufferSize = buffer.remaining() / 3; + // Read the OpenGL image row by row from right to left (flipped horizontally) + for (int i = bufferSize - 1; i >= 0; i--) { + // Coordinates in the final image + int x = offsetX + bufferWidth - i % bufferWidth - 1; // X coord of OpenGL image has to be flipped first + int y = offsetY + i / bufferWidth; + if (x >= imageWidth || y * imageWidth >= image.length) { + buffer.position(buffer.position() + 3); // Pixel would end up outside of image + continue; + } + // Write to image (row by row, left to right) + image[y * imageWidth + x] = 0xff << 24 | (buffer.get() & 0xff) << 16 | (buffer.get() & 0xff) << 8 | buffer.get() & 0xff; + } + buffer.rewind(); + } + + protected void renderFrame(Timer timer) { + pushMatrix(); + clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + mc.getFramebuffer().bindFramebuffer(true); + + enableTexture2D(); + + if (customEntityRenderer.isPresent()) { + customEntityRenderer.get().updateCameraAndRender(timer.renderPartialTicks); + } else { + mc.entityRenderer.updateCameraAndRender(timer.renderPartialTicks); + } + + mc.getFramebuffer().unbindFramebuffer(); + popMatrix(); + + pushMatrix(); + mc.getFramebuffer().framebufferRender(mc.displayWidth, mc.displayHeight); + popMatrix(); + } + + public void setup() { + this.previewTexture = new DynamicTexture(videoWidth, videoHeight) { + @Override + public void updateDynamicTexture() { + bindTexture(getGlTextureId()); + TextureUtil.uploadTextureSub(0, getTextureData(), getVideoWidth(), getVideoHeight(), 0, 0, true, false, false); + } + }; + } + + public void tearDown() { + this.previewTexture.deleteGlTexture(); + } + + public DynamicTexture getPreviewTexture() { + return previewTexture; + } + + /** + * Return the height including any borders of the preview. + * No rendering should be performed by {@link #renderPreview(GuiVideoRenderer)} below this height. + * @param guiScreen The gui screen + * @return Bottom of the preview + */ + public int getPreviewHeight(GuiScreen guiScreen) { + int width = guiScreen.width / 2; + int height = guiScreen.height / 3; + if (width / height < getVideoWidth() / getVideoHeight()) { + height = width * getVideoHeight() / getVideoWidth(); + } + return guiScreen.height / 2 + height + 10; + } + + /** + * Draw a preview of the current scene on the lower half of the screen. + * @param guiScreen The gui screen + */ + public void renderPreview(GuiVideoRenderer guiScreen) { + int width = guiScreen.width / 2; + int height = guiScreen.height / 3; + if (width / height > getVideoWidth() / getVideoHeight()) { + width = height * getVideoWidth() / getVideoHeight(); + } else { + height = width * getVideoHeight() / getVideoWidth(); + } + + int x = guiScreen.width / 2 - width / 2; + int y = guiScreen.height / 2 + 5; + + bindTexture(previewTexture.getGlTextureId()); + color(1, 1, 1, 1); + Gui.drawScaledCustomSizeModalRect(x, y, 0, 0, videoWidth, videoHeight, width, height, videoWidth, videoHeight); + } + + public static void renderNoPreview(int guiWidth, int guiHeight, int height) { + int width = height * 1280 / 720; + int x = guiWidth / 2 - width / 2; + int y = guiHeight / 2 + 5; + + Minecraft.getMinecraft().getTextureManager().bindTexture(noPreviewTexture); + color(1, 1, 1, 1); + Gui.drawScaledCustomSizeModalRect(x, y, 0, 0, 1280, 720, width, height, 1280, 720); + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/video/frame/StereoscopicFrameRenderer.java b/src/main/java/eu/crushedpixel/replaymod/video/frame/StereoscopicFrameRenderer.java new file mode 100644 index 00000000..77d91371 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/video/frame/StereoscopicFrameRenderer.java @@ -0,0 +1,51 @@ +package eu.crushedpixel.replaymod.video.frame; + +import eu.crushedpixel.replaymod.video.entity.StereoscopicEntityRenderer; +import net.minecraft.client.Minecraft; +import net.minecraft.crash.CrashReport; +import net.minecraft.util.ReportedException; +import net.minecraft.util.Timer; + +import java.awt.image.BufferedImage; + +public class StereoscopicFrameRenderer extends FrameRenderer { + private final StereoscopicEntityRenderer entityRenderer = new StereoscopicEntityRenderer(); + + public StereoscopicFrameRenderer() { + super(Minecraft.getMinecraft().displayWidth * 2, Minecraft.getMinecraft().displayHeight); + setCustomEntityRenderer(entityRenderer); + } + + @Override + public BufferedImage captureFrame(Timer timer) { + BufferedImage image = new BufferedImage(getVideoWidth(), getVideoHeight(), BufferedImage.TYPE_INT_RGB); + try { + captureFrame(timer, image, true); + } catch (Throwable t) { + CrashReport crash = CrashReport.makeCrashReport(t, "Rendering left eye."); + throw new ReportedException(crash); + } + try { + captureFrame(timer, image, false); + } catch (Throwable t) { + CrashReport crash = CrashReport.makeCrashReport(t, "Rendering right eye."); + throw new ReportedException(crash); + } + return image; + } + + protected void captureFrame(Timer timer, BufferedImage into, boolean leftEye) { + int displayWidth = getVideoWidth() / 2; + + // Render frame + entityRenderer.setEye(leftEye); + renderFrame(timer); + + // Read pixels + readPixels(buffer); + + // Copy to image + copyPixelsToImage(buffer, displayWidth, into, leftEye ? 0 : displayWidth, 0); + updateDefaultPreview(into); + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/video/frame/TilingFrameRenderer.java b/src/main/java/eu/crushedpixel/replaymod/video/frame/TilingFrameRenderer.java new file mode 100644 index 00000000..a61f8816 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/video/frame/TilingFrameRenderer.java @@ -0,0 +1,130 @@ +package eu.crushedpixel.replaymod.video.frame; + +import eu.crushedpixel.replaymod.video.entity.TilingEntityRenderer; +import net.minecraft.crash.CrashReport; +import net.minecraft.util.ReportedException; +import net.minecraft.util.Timer; + +import java.awt.image.BufferedImage; +import java.util.Arrays; + +public class TilingFrameRenderer extends FrameRenderer { + private final TilingEntityRenderer entityRenderer = new TilingEntityRenderer(this); + private final int tileWidth; + private final int tileHeight; + + public TilingFrameRenderer(int videoWidth, int videoHeight) { + super(videoWidth, videoHeight); + this.tileWidth = mc.displayWidth; + this.tileHeight = mc.displayHeight; + setCustomEntityRenderer(entityRenderer); + } + + @Override + public boolean setRenderPreviewCallback(Runnable renderPreviewCallback) { + super.setRenderPreviewCallback(renderPreviewCallback); + return true; + } + + @Override + public BufferedImage captureFrame(Timer timer) { + BufferedImage image = new BufferedImage(getVideoWidth(), getVideoHeight(), BufferedImage.TYPE_INT_RGB); + int tilesX = getTilesX(); + int tilesY = getTilesY(); + for (int x = 0; x < tilesX; x++) { + for (int y = 0; y < tilesY; y++) { + try { + captureTile(timer, x, y, image); + } catch (Throwable t) { + CrashReport crash = CrashReport.makeCrashReport(t, "Rendering tile " + x + "/" + y); + throw new ReportedException(crash); + } + getRenderPreviewCallback().run(); + } + } + return image; + } + + protected void captureTile(Timer timer, int tileX, int tileY, BufferedImage into) { + // Render frame + entityRenderer.setTile(tileX, tileY); + renderFrame(timer); + + // Read pixels + readPixels(buffer); + + // Copy to image + copyPixelsToImage(buffer, tileWidth, into, tileX * tileWidth, tileY * tileHeight); + if (isPreviewActive()) { + int[] pixels = getPreviewTexture().getTextureData(); + copyPixelsToImage(buffer, tileWidth, pixels, tileX * tileWidth, tileY * tileHeight); + + // Draw red rectangle around next tile + tileY = (tileY + 1) % getTilesY(); + if (tileY == 0) { + tileX = (tileX + 1) % getTilesX(); + } + drawPreviewRectangle(pixels, tileX, tileY); + + getPreviewTexture().updateDynamicTexture(); + } + } + + private void drawPreviewRectangle(int[] pixels, int tileX, int tileY) { + final int RED = 0xffff0000; + + int videoWidth = getVideoWidth(); + int videoHeight = getVideoHeight(); + int border = videoWidth / tileWidth * 5; + int left = Math.min(tileX * tileWidth, videoWidth - border - 1); + int right = Math.min((left + tileWidth), videoWidth) - 1; + int top = Math.min(tileY * tileHeight, videoHeight - border - 1); + int bottom = Math.min((top + tileHeight), videoHeight - border) - 1; + + for (int i = 0; i < border; i++) { + // Top border + int offset = (top + i) * videoWidth; + Arrays.fill(pixels, offset + left, offset + right + 1, RED); + + // Bottom border + offset = (bottom - i) * videoWidth; + Arrays.fill(pixels, offset + left, offset + right + 1, RED); + + // Left border + for (int j = top; j <= bottom; j++) { + pixels[j * videoWidth + left + i] = RED; + } + + // Right border + for (int j = top; j <= bottom; j++) { + pixels[j * videoWidth + right - i] = RED; + } + } + } + + public int getTilesX() { + int videoWidth = getVideoWidth(); + if (videoWidth % tileWidth == 0) { + return videoWidth / tileWidth; + } else { + return videoWidth / tileWidth + 1; + } + } + + public int getTilesY() { + int videoHeight = getVideoHeight(); + if (videoHeight % tileHeight == 0) { + return videoHeight / tileHeight; + } else { + return videoHeight / tileHeight + 1; + } + } + + public int getTileWidth() { + return tileWidth; + } + + public int getTileHeight() { + return tileHeight; + } +} diff --git a/src/main/resources/META-INF/replaymod_at.cfg b/src/main/resources/META-INF/replaymod_at.cfg index 1174c284..6d654330 100644 --- a/src/main/resources/META-INF/replaymod_at.cfg +++ b/src/main/resources/META-INF/replaymod_at.cfg @@ -38,6 +38,35 @@ public net.minecraft.client.resources.ResourcePackRepository field_148534_e # di # EntityRenderer public net.minecraft.client.renderer.EntityRenderer field_147711_ac # resourceManager +public net.minecraft.client.renderer.EntityRenderer field_147707_d # theShaderGroup +public net.minecraft.client.renderer.EntityRenderer field_175083_ad # useShader +public net.minecraft.client.renderer.EntityRenderer field_78530_s # farPlaneDistance + +public net.minecraft.client.renderer.EntityRenderer func_78467_g(F)V # orientCamera +public net.minecraft.client.renderer.EntityRenderer func_78474_d(F)V # renderRainSnow +public net.minecraft.client.renderer.EntityRenderer func_78481_a(FZ)F # getFOVModifier +public net.minecraft.client.renderer.EntityRenderer func_78468_a(IF)V # setupFog +public net.minecraft.client.renderer.EntityRenderer func_78466_h(F)V # updateFogColor +public net.minecraft.client.renderer.EntityRenderer func_78472_g(F)V # updateLightmap + +# RenderGlobal +public net.minecraft.client.renderer.RenderGlobal field_174995_M # renderDispatcher +public net.minecraft.client.renderer.RenderGlobal field_147595_R # displayListEntitiesDirty +public net.minecraft.client.renderer.RenderGlobal field_175009_l # chunksToUpdate +public net.minecraft.client.renderer.RenderGlobal field_72755_R # renderInfos +public net.minecraft.client.renderer.RenderGlobal field_175009_l # chunksToUpdate +public net.minecraft.client.renderer.RenderGlobal func_174983_a(Lnet.minecraft.util.BlockPos;Lnet.minecraft.client.renderer.chunk.RenderChunk;)Z # isPositionInRenderChunk + +public net.minecraft.client.renderer.RenderGlobal$ContainerLocalRenderInformation +public net.minecraft.client.renderer.RenderGlobal$ContainerLocalRenderInformation field_178553_a # renderChunk + +# ChunkRenderDispatcher +public-f net.minecraft.client.renderer.chunk.ChunkRenderDispatcher field_178519_d # queueChunkUpdates +public net.minecraft.client.renderer.chunk.ChunkRenderDispatcher field_178524_h # queueChunkUploads +public net.minecraft.client.renderer.chunk.ChunkRenderDispatcher field_178522_c # listThreadedWorkers + +# TextureUtil +public net.minecraft.client.renderer.texture.TextureUtil func_147947_a(I[IIIIIZZZ)V # uploadTextureSub #RenderManager public net.minecraft.client.renderer.entity.RenderManager field_178636_l # skinMap diff --git a/src/main/resources/assets/replaymod/logo.jpg b/src/main/resources/assets/replaymod/logo.jpg new file mode 100755 index 00000000..fbe12762 Binary files /dev/null and b/src/main/resources/assets/replaymod/logo.jpg differ