Rework rendering

Adds default, stereoscopic, tiling, cubic and equirectangular frame rendering
This commit is contained in:
johni0702
2015-05-23 14:56:11 +02:00
parent 361ba73978
commit 18f22f57d0
30 changed files with 2167 additions and 207 deletions

View File

@@ -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<ListenableFutureTask> 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() {

View File

@@ -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<AbstractInsnNode> 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) {

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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());

View File

@@ -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);
}
}

View File

@@ -0,0 +1,7 @@
package eu.crushedpixel.replaymod.interpolation;
public interface Interpolation<T> {
void prepare();
T getPoint(float position);
void addPoint(T pos);
}

View File

@@ -5,7 +5,7 @@ import akka.japi.Pair;
import java.util.ArrayList;
import java.util.List;
public abstract class LinearInterpolation<K> {
public abstract class LinearInterpolation<K> implements Interpolation<K> {
protected List<K> points = new ArrayList<K>();
@@ -13,6 +13,11 @@ public abstract class LinearInterpolation<K> {
points = new ArrayList<K>();
}
@Override
public void prepare() {
}
public abstract K getPoint(float position);
public void addPoint(K point) {

View File

@@ -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<Position> {
private static final Object[] EMPTYOBJ = new Object[]{};
private Vector<Position> points;
private Vector<Cubic> 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);

View File

@@ -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<Object> queueChunkUpdates = renderDispatcher.queueChunkUpdates;
workerJailingQueue = new JailingQueue<Object>(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<RenderChunk> 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_);
}
}
}

View File

@@ -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();

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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<T> extends AbstractQueue<T> implements BlockingQueue<T> {
private final BlockingQueue<T> delegate;
private final Set<Thread> jailed = new HashSet<Thread>();
public JailingQueue(BlockingQueue<T> 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<T> 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<? super T> c) {
tryAccess();
return delegate.drainTo(c);
}
@Override
public int drainTo(Collection<? super T> 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();
}
}

View File

@@ -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();
}
}

View File

@@ -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<Position> movement;
private Interpolation<Integer> 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;
}
}

View File

@@ -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<BufferedImage> toWrite = new LinkedBlockingQueue<BufferedImage>();
private volatile boolean active = true;
private volatile boolean cancelled = false;
public static boolean isRecording() {
return isRecording;
private int queueLimit;
private final Queue<BufferedImage> 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<BufferedImage>();
toWrite = new LinkedBlockingQueue<BufferedImage>();
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<BufferedImage>();
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();
}
}

View File

@@ -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);
}
}

View File

@@ -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_) {
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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> 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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB