diff --git a/src/main/java/eu/crushedpixel/replaymod/ReplayMod.java b/src/main/java/eu/crushedpixel/replaymod/ReplayMod.java index e4f02fa3..f3014612 100755 --- a/src/main/java/eu/crushedpixel/replaymod/ReplayMod.java +++ b/src/main/java/eu/crushedpixel/replaymod/ReplayMod.java @@ -22,11 +22,12 @@ import eu.crushedpixel.replaymod.settings.RenderOptions; import eu.crushedpixel.replaymod.settings.ReplaySettings; import eu.crushedpixel.replaymod.sound.SoundHandler; import eu.crushedpixel.replaymod.timer.ReplayTimer; +import eu.crushedpixel.replaymod.utils.OpenGLUtils; import eu.crushedpixel.replaymod.utils.ReplayFile; import eu.crushedpixel.replaymod.utils.ReplayFileIO; import eu.crushedpixel.replaymod.utils.TooltipRenderer; -import eu.crushedpixel.replaymod.video.frame.*; import lombok.Getter; +import eu.crushedpixel.replaymod.video.rendering.Pipelines; import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.entity.RenderPlayer; import net.minecraft.client.resources.IResourcePack; @@ -110,6 +111,10 @@ public class ReplayMod { e.printStackTrace(); } + // Initialize the static OpenGL info field from the minecraft main thread + // Unfortunately lwjgl uses static methods so we have to make use of magic init calls as well + OpenGLUtils.init(); + config = new Configuration(event.getSuggestedConfigurationFile()); config.load(); AuthenticationHandler.loadAuthkeyFromConfig(); @@ -280,31 +285,29 @@ public class ReplayMod { options.setExportCommandArgs(exportCommandArgs); } - FrameRenderer renderer; + Pipelines.Preset pipelinePreset = Pipelines.Preset.DEFAULT; if (type != null) { String[] parts = type.split(":"); type = parts[0].toUpperCase(); if ("NORMAL".equals(type) || "DEFAULT".equals(type)) { - renderer = new DefaultFrameRenderer(options); + pipelinePreset = Pipelines.Preset.DEFAULT; } else if ("STEREO".equals(type) || "STEREOSCOPIC".equals(type)) { - renderer = new StereoscopicFrameRenderer(options); + pipelinePreset = Pipelines.Preset.STEREOSCOPIC; } else if ("CUBIC".equals(type)) { if (parts.length < 2) { throw new IllegalArgumentException("Cubic renderer requires boolean for whether it's stable."); } - renderer = new CubicFrameRenderer(options, Boolean.parseBoolean(parts[1])); + pipelinePreset = Pipelines.Preset.CUBIC; } else if ("EQUIRECTANGULAR".equals(type)) { if (parts.length < 2) { throw new IllegalArgumentException("Equirectangular renderer requires boolean for whether it's stable."); } - renderer = new EquirectangularFrameRenderer(options, Boolean.parseBoolean(parts[1])); + pipelinePreset = Pipelines.Preset.EQUIRECTANGULAR; } else { throw new IllegalArgumentException("Unknown type: " + parts[0]); } - } else { - renderer = new DefaultFrameRenderer(options); } - options.setRenderer(renderer); + options.setMode(pipelinePreset); @SuppressWarnings("unchecked") Queue tasks = mc.scheduledTasks; diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/GuiRenderSettings.java b/src/main/java/eu/crushedpixel/replaymod/gui/GuiRenderSettings.java index fe355aed..1b06c72a 100644 --- a/src/main/java/eu/crushedpixel/replaymod/gui/GuiRenderSettings.java +++ b/src/main/java/eu/crushedpixel/replaymod/gui/GuiRenderSettings.java @@ -9,7 +9,7 @@ import eu.crushedpixel.replaymod.settings.RenderOptions; import eu.crushedpixel.replaymod.utils.MouseUtils; import eu.crushedpixel.replaymod.utils.ReplayFileIO; import eu.crushedpixel.replaymod.utils.StringUtils; -import eu.crushedpixel.replaymod.video.frame.*; +import eu.crushedpixel.replaymod.video.rendering.Pipelines; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiButton; import net.minecraft.client.gui.GuiErrorScreen; @@ -449,8 +449,6 @@ public class GuiRenderSettings extends GuiScreen { } private void startRendering() { - FrameRenderer renderer = null; - RendererSettings r = rendererDropdown.getElement(rendererDropdown.getSelectionIndex()); RenderOptions options = new RenderOptions(); @@ -477,16 +475,19 @@ public class GuiRenderSettings extends GuiScreen { options.setWidth(getWidthSetting()); options.setHeight(getHeightSetting()); + Pipelines.Preset pipePreset = Pipelines.Preset.DEFAULT; if(r == RendererSettings.DEFAULT) { - renderer = new DefaultFrameRenderer(options); + pipePreset = Pipelines.Preset.DEFAULT; } else if(r == RendererSettings.STEREOSCOPIC) { - renderer = new StereoscopicFrameRenderer(options); + pipePreset = Pipelines.Preset.STEREOSCOPIC; } else if(r == RendererSettings.CUBIC) { - renderer = new CubicFrameRenderer(options, ignoreCamDir.isChecked()); + pipePreset = Pipelines.Preset.CUBIC; } else if(r == RendererSettings.EQUIRECTANGULAR) { - renderer = new EquirectangularFrameRenderer(options, ignoreCamDir.isChecked()); + pipePreset = Pipelines.Preset.EQUIRECTANGULAR; } - options.setRenderer(renderer); + options.setMode(pipePreset); + + options.setIgnoreCameraRotation(ignoreCamDir.isChecked()); if(commandInput.getText().trim().length() > 0) { options.setExportCommand(commandInput.getText().trim()); @@ -525,6 +526,9 @@ public class GuiRenderSettings extends GuiScreen { ignoreCamDir.enabled = true; } + if (!ignoreCamDir.enabled) { + ignoreCamDir.setIsChecked(false); + } xRes.setCursorPositionEnd(); xRes.setSelectionPos(xRes.getCursorPosition()); diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/GuiVideoRenderer.java b/src/main/java/eu/crushedpixel/replaymod/gui/GuiVideoRenderer.java index f3371f02..ed61962e 100644 --- a/src/main/java/eu/crushedpixel/replaymod/gui/GuiVideoRenderer.java +++ b/src/main/java/eu/crushedpixel/replaymod/gui/GuiVideoRenderer.java @@ -1,18 +1,32 @@ package eu.crushedpixel.replaymod.gui; import eu.crushedpixel.replaymod.gui.elements.GuiProgressBar; +import eu.crushedpixel.replaymod.utils.BoundingUtils; import eu.crushedpixel.replaymod.utils.DurationUtils; import eu.crushedpixel.replaymod.video.VideoRenderer; -import eu.crushedpixel.replaymod.video.frame.FrameRenderer; +import eu.crushedpixel.replaymod.video.frame.ARGBFrame; +import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiButton; import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.renderer.texture.DynamicTexture; +import net.minecraft.client.renderer.texture.TextureUtil; import net.minecraft.client.resources.I18n; +import net.minecraft.util.ResourceLocation; import net.minecraftforge.fml.client.config.GuiCheckBox; +import org.lwjgl.util.Dimension; +import org.lwjgl.util.ReadableDimension; import java.awt.*; import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +import static net.minecraft.client.renderer.GlStateManager.bindTexture; +import static net.minecraft.client.renderer.GlStateManager.color; public class GuiVideoRenderer extends GuiScreen { + private static final ResourceLocation noPreviewTexture = new ResourceLocation("replaymod", "logo.jpg"); + private final VideoRenderer renderer; private final String SCREEN_TITLE = I18n.format("replaymod.gui.rendering.title"); @@ -27,6 +41,9 @@ public class GuiVideoRenderer extends GuiScreen { private GuiCheckBox previewCheckBox; private GuiProgressBar progressBar; + private DynamicTexture previewTexture; + private boolean previewTextureDirty; + private int renderTimeTaken = 0; private long prevTime = -1; private long prevRenderTime = -1; @@ -87,8 +104,6 @@ public class GuiVideoRenderer extends GuiScreen { } else if (CANCEL_CONFIRM.equals(cancelButton.displayString)) { renderer.cancel(); } - } else if (button == previewCheckBox) { - renderer.getFrameRenderer().setPreviewActive(previewCheckBox.isChecked()); } } @@ -139,7 +154,6 @@ public class GuiVideoRenderer extends GuiScreen { String takenString = I18n.format("replaymod.gui.rendering.timetaken")+": "+DurationUtils.convertSecondsToString(renderTimeTaken/1000); String leftString = I18n.format("replaymod.gui.rendering.timeleft")+": "+DurationUtils.convertSecondsToString(renderTimeLeft); - FrameRenderer frameRenderer = renderer.getFrameRenderer(); int centerX = width / 2; drawBackground(0); @@ -159,9 +173,9 @@ public class GuiVideoRenderer extends GuiScreen { int previewY = previewCheckBox.yPosition - 10 - previewHeight; if(previewCheckBox.isChecked()) { - frameRenderer.renderPreview(previewX, previewY, previewWidth, previewHeight); + renderPreview(previewX, previewY, previewWidth, previewHeight); } else { - FrameRenderer.renderNoPreview(previewX, previewY, previewWidth, previewHeight); + renderNoPreview(previewX, previewY, previewWidth, previewHeight); } drawString(fontRendererObj, takenString, 12, previewCheckBox.yPosition + 5 + 20, Color.WHITE.getRGB()); @@ -170,4 +184,75 @@ public class GuiVideoRenderer extends GuiScreen { super.drawScreen(mouseX, mouseY, partialTicks); } + + private synchronized void renderPreview(int x, int y, int width, int height) { + ReadableDimension videoSize = renderer.getFrameSize(); + final int videoWidth = videoSize.getWidth(); + final int videoHeight = videoSize.getHeight(); + + if (previewTexture == null) { + previewTexture = new DynamicTexture(videoWidth, videoHeight) { + @Override + public void updateDynamicTexture() { + bindTexture(getGlTextureId()); + TextureUtil.uploadTextureSub(0, getTextureData(), videoWidth, videoHeight, 0, 0, true, false, false); + } + }; + } + + if (previewTextureDirty) { + previewTexture.updateDynamicTexture(); + previewTextureDirty = false; + } + + Dimension dimension = BoundingUtils.fitIntoBounds(new Dimension(videoSize), new Dimension(width, height)); + + x += (width - dimension.getWidth()) / 2; + y += (height - dimension.getHeight()) / 2; + width = dimension.getWidth(); + height = dimension.getHeight(); + + color(1, 1, 1, 1); + bindTexture(previewTexture.getGlTextureId()); + drawScaledCustomSizeModalRect(x, y, 0, 0, videoWidth, videoHeight, width, height, videoWidth, videoHeight); + } + + private void renderNoPreview(int x, int y, int width, int height) { + int actualWidth = width; + int actualHeight = height; + if (width / height > 1280 / 720) { + actualWidth = height * 1280 / 720; + } else { + actualHeight = width * 720 / 1280; + } + + x += (width - actualWidth) / 2; + y += (height - actualHeight) / 2; + + Minecraft.getMinecraft().getTextureManager().bindTexture(noPreviewTexture); + color(1, 1, 1, 1); + drawScaledCustomSizeModalRect(x, y, 0, 0, 1280, 720, actualWidth, actualHeight, 1280, 720); + } + + public void updatePreview(ARGBFrame frame) { + if (previewCheckBox.isChecked() && previewTexture != null) { + ByteBuffer buffer = frame.getByteBuffer(); + buffer.mark(); + synchronized (this) { + swapEndianness(buffer); + buffer.asIntBuffer().get(previewTexture.getTextureData()); + swapEndianness(buffer); + previewTextureDirty = true; + } + buffer.reset(); + } + } + + private void swapEndianness(ByteBuffer buffer) { + if (buffer.order() == ByteOrder.BIG_ENDIAN) { + buffer.order(ByteOrder.LITTLE_ENDIAN); + } else { + buffer.order(ByteOrder.BIG_ENDIAN); + } + } } diff --git a/src/main/java/eu/crushedpixel/replaymod/settings/RenderOptions.java b/src/main/java/eu/crushedpixel/replaymod/settings/RenderOptions.java index 9a7901fe..75a13bce 100644 --- a/src/main/java/eu/crushedpixel/replaymod/settings/RenderOptions.java +++ b/src/main/java/eu/crushedpixel/replaymod/settings/RenderOptions.java @@ -1,25 +1,23 @@ package eu.crushedpixel.replaymod.settings; -import eu.crushedpixel.replaymod.utils.ReplayFileIO; -import eu.crushedpixel.replaymod.video.frame.FrameRenderer; +import eu.crushedpixel.replaymod.video.rendering.Pipelines; import lombok.Data; import net.minecraft.client.Minecraft; import java.io.File; -import java.text.SimpleDateFormat; -import java.util.Calendar; import static org.apache.commons.lang3.Validate.isTrue; -import static org.apache.commons.lang3.Validate.notNull; @Data public final class RenderOptions { - private FrameRenderer renderer; + private Pipelines.Preset mode = Pipelines.Preset.DEFAULT; private String bitrate = "10M"; private int fps = 30; private File outputFile; + private boolean ignoreCameraRotation; + // Advanced private boolean waitForChunks = true; private boolean isLinearMovement = false; @@ -32,11 +30,6 @@ public final class RenderOptions { private String exportCommandArgs = "-f rawvideo -pix_fmt argb -s %WIDTH%x%HEIGHT% -r %FPS% -i - " + "-an " + "-c:v libvpx -b:v %BITRATE% %FILENAME%"; - private int writerQueueSize = Integer.parseInt(System.getProperty("replaymod.render.writerQueueSize", "1")); - - public void setRenderer(FrameRenderer renderer) { - this.renderer = notNull(renderer); - } public int getFps() { return fps; @@ -53,9 +46,10 @@ public final class RenderOptions { public RenderOptions copy() { RenderOptions copy = new RenderOptions(); - copy.renderer = this.renderer; + copy.mode = this.mode; copy.bitrate = this.bitrate; copy.fps = this.fps; + copy.ignoreCameraRotation = this.ignoreCameraRotation; copy.waitForChunks = this.waitForChunks; copy.isLinearMovement = this.isLinearMovement; copy.skyColor = this.skyColor; @@ -63,7 +57,6 @@ public final class RenderOptions { copy.height = this.height; copy.exportCommand = this.exportCommand; copy.exportCommandArgs = this.exportCommandArgs; - copy.writerQueueSize = this.writerQueueSize; return copy; } } diff --git a/src/main/java/eu/crushedpixel/replaymod/utils/ByteBufferPool.java b/src/main/java/eu/crushedpixel/replaymod/utils/ByteBufferPool.java new file mode 100644 index 00000000..e5560407 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/utils/ByteBufferPool.java @@ -0,0 +1,48 @@ +package eu.crushedpixel.replaymod.utils; + +import com.google.common.collect.Maps; +import org.lwjgl.BufferUtils; + +import java.lang.ref.SoftReference; +import java.nio.ByteBuffer; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +public class ByteBufferPool { + private static Map>> bufferPool = Maps.newHashMap(); + + public static synchronized ByteBuffer allocate(int size) { + List> available = bufferPool.get(size); + if (available != null) { + Iterator> iter = available.iterator(); + try { + while (iter.hasNext()) { + SoftReference reference = iter.next(); + ByteBuffer buffer = reference.get(); + iter.remove(); + if (buffer != null) { + return buffer; + } + } + } finally { + if (!iter.hasNext()) { + bufferPool.remove(size); + } + } + } + return BufferUtils.createByteBuffer(size); + } + + public static synchronized void release(ByteBuffer buffer) { + buffer.clear(); + int size = buffer.capacity(); + List> available = bufferPool.get(size); + if (available == null) { + available = new LinkedList>(); + bufferPool.put(size, available); + } + available.add(new SoftReference(buffer)); + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/utils/OpenGLUtils.java b/src/main/java/eu/crushedpixel/replaymod/utils/OpenGLUtils.java index 6dc50254..ca6060d0 100644 --- a/src/main/java/eu/crushedpixel/replaymod/utils/OpenGLUtils.java +++ b/src/main/java/eu/crushedpixel/replaymod/utils/OpenGLUtils.java @@ -3,8 +3,6 @@ package eu.crushedpixel.replaymod.utils; 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.nio.IntBuffer; @@ -19,23 +17,28 @@ public class OpenGLUtils { VIEWPORT_MAX_HEIGHT = buffer.get(); } - public static void openGlBytesToBufferedImage(ByteBuffer buffer, int bufferWidth, BufferedImage image, int offsetX, int offsetY) { - int[] pixels = ((DataBufferInt) image.getRaster().getDataBuffer()).getData(); - int imageWidth = image.getWidth(); + /** + * Magic init method which has to be called from the OpenGL thread so the variables in this class + * can be initialized successfully. + * Does not perform any work on its own. + */ + public static void init() { + } + + public static void openGlBytesToARBG(ByteBuffer buffer, int bufferWidth, int xOffset, int yOffset, ByteBuffer to, int width) { + byte[] pixel = new byte[4]; + pixel[0] = (byte) 0xff; 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 >= pixels.length) { - buffer.position(buffer.position() + 3); // Pixel would end up outside of image - continue; - } + int x = xOffset + bufferWidth - i % bufferWidth - 1; // X coord of OpenGL image has to be flipped first + int y = yOffset + i / bufferWidth; // Write to image (row by row, left to right) - pixels[y * imageWidth + x] = 0xff << 24 | (buffer.get() & 0xff) << 16 | (buffer.get() & 0xff) << 8 | buffer.get() & 0xff; + buffer.get(pixel, 1, 3); + to.position((y * width + x) * 4); + to.put(pixel); } - buffer.rewind(); + to.rewind(); } - } diff --git a/src/main/java/eu/crushedpixel/replaymod/video/VideoRenderer.java b/src/main/java/eu/crushedpixel/replaymod/video/VideoRenderer.java index 79d5d5ca..7ddba530 100644 --- a/src/main/java/eu/crushedpixel/replaymod/video/VideoRenderer.java +++ b/src/main/java/eu/crushedpixel/replaymod/video/VideoRenderer.java @@ -16,14 +16,18 @@ import eu.crushedpixel.replaymod.replay.ReplaySender; import eu.crushedpixel.replaymod.settings.RenderOptions; import eu.crushedpixel.replaymod.timer.EnchantmentTimer; import eu.crushedpixel.replaymod.timer.ReplayTimer; -import eu.crushedpixel.replaymod.video.frame.FrameRenderer; +import eu.crushedpixel.replaymod.video.capturer.RenderInfo; +import eu.crushedpixel.replaymod.video.frame.ARGBFrame; +import eu.crushedpixel.replaymod.video.rendering.Pipeline; +import eu.crushedpixel.replaymod.video.rendering.Pipelines; 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 org.lwjgl.util.Dimension; +import org.lwjgl.util.ReadableDimension; -import java.awt.image.BufferedImage; import java.io.IOException; import java.util.concurrent.FutureTask; @@ -31,10 +35,8 @@ 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 { +public class VideoRenderer implements RenderInfo { private final Minecraft mc = Minecraft.getMinecraft(); - private final FrameRenderer frameRenderer; - private final VideoWriter videoWriter; private final ReplaySender replaySender; private final RenderOptions options; @@ -52,12 +54,19 @@ public class VideoRenderer { private boolean paused; private boolean cancelled; + private final Pipeline renderingPipeline; + public VideoRenderer(RenderOptions options) throws IOException { - this.frameRenderer = options.getRenderer(); - this.videoWriter = new VideoWriter(this, options); this.gui = new GuiVideoRenderer(this); this.replaySender = ReplayMod.replaySender; this.options = options; + this.renderingPipeline = Pipelines.newPipeline(options.getMode(), this, new VideoWriter(options) { + @Override + public void consume(ARGBFrame frame) { + gui.updatePreview(frame); + super.consume(frame); + } + }); } /** @@ -98,33 +107,33 @@ public class VideoRenderer { mc.renderGlobal.renderEntitiesStartupCounter = 0; - 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++; - - drawGui(); - System.gc(); - } + renderingPipeline.run(); finish(); return !cancelled; } + @Override + public float updateForNextFrame() { + drawGui(); + + updateTime(mc.timer, framesDone); + + int elapsedTicks = mc.timer.elapsedTicks; + while (elapsedTicks-- > 0) { + tick(); + } + + updateCam(); + framesDone++; + return mc.timer.renderPartialTicks; + } + + @Override + public RenderOptions getRenderOptions() { + return options; + } + private void setup() { if (Mouse.isGrabbed()) { mouseWasGrabbed = true; @@ -171,7 +180,6 @@ public class VideoRenderer { } time.prepare(); - frameRenderer.setup(); ScaledResolution scaled = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); gui.setWorldAndResolution(mc, scaled.getScaledWidth(), scaled.getScaledHeight()); @@ -182,18 +190,8 @@ public class VideoRenderer { } private void finish() { - if (cancelled) { - videoWriter.abortRecording(); - } else { - videoWriter.endRecording(); - } - try { - videoWriter.waitForFinish(); - } catch (InterruptedException ignored) { } - - frameRenderer.tearDown(); if (mouseWasGrabbed) { - Mouse.setGrabbed(true); + mc.mouseHelper.grabMouseCursor(); } ReplayTimer.get(mc).passive = false; mc.displayGuiScreen(null); @@ -202,6 +200,8 @@ public class VideoRenderer { } ReplayMod.soundHandler.playRenderSuccessSound(); + + mc.displayGuiScreen(null); } private void updateCam() { @@ -320,9 +320,7 @@ public class VideoRenderer { TimestampValue timestampValue = new TimestampValue(); time.applyPoint(Math.max(0, Math.min(1, timePos)), timestampValue); Integer replayTime = (int) timestampValue.value; - if(replayTime != null) { - replaySender.sendPacketsTill(replayTime); - } + replaySender.sendPacketsTill(replayTime); if (curSpeed >= 0) { replaySender.setReplaySpeed(curSpeed); @@ -337,7 +335,7 @@ public class VideoRenderer { timer.renderPartialTicks = timer.elapsedPartialTicks; } - private void tick() throws IOException { + private void tick() { synchronized (mc.scheduledTasks) { while (!mc.scheduledTasks.isEmpty()) { ((FutureTask) mc.scheduledTasks.poll()).run(); @@ -345,89 +343,70 @@ public class VideoRenderer { } 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; - } + try { + mc.runTick(); + } catch (IOException e) { + throw new RuntimeException(e); } } public void drawGui() { - pushMatrix(); - clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - enableTexture2D(); - mc.getFramebuffer().bindFramebuffer(true); - mc.entityRenderer.setupOverlayRendering(); + do { + 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); - } + 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); + 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(); + mc.getFramebuffer().unbindFramebuffer(); + popMatrix(); + pushMatrix(); + mc.getFramebuffer().framebufferRender(mc.displayWidth, mc.displayHeight); + popMatrix(); - Display.update(); - if (Mouse.isGrabbed()) { - Mouse.setGrabbed(false); - } + Display.update(); + if (Mouse.isGrabbed()) { + Mouse.setGrabbed(false); + } + if (paused) { + try { + Thread.sleep(50); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + } + } while (paused); } public int getFramesDone() { return framesDone; } + @Override + public ReadableDimension getFrameSize() { + return new Dimension(options.getWidth(), options.getHeight()); + } + public int getTotalFrames() { return totalFrames; } public int getVideoTime() { return framesDone * 1000 / fps; } - public FrameRenderer getFrameRenderer() { - return frameRenderer; - } - public void setPaused(boolean paused) { this.paused = paused; } @@ -438,5 +417,6 @@ public class VideoRenderer { public void cancel() { this.cancelled = true; + renderingPipeline.cancel(); } } diff --git a/src/main/java/eu/crushedpixel/replaymod/video/VideoWriter.java b/src/main/java/eu/crushedpixel/replaymod/video/VideoWriter.java index 87bd054b..686233b4 100755 --- a/src/main/java/eu/crushedpixel/replaymod/video/VideoWriter.java +++ b/src/main/java/eu/crushedpixel/replaymod/video/VideoWriter.java @@ -1,63 +1,44 @@ package eu.crushedpixel.replaymod.video; -import com.google.common.base.Preconditions; import eu.crushedpixel.replaymod.settings.RenderOptions; +import eu.crushedpixel.replaymod.utils.ByteBufferPool; import eu.crushedpixel.replaymod.utils.StreamPipe; import eu.crushedpixel.replaymod.utils.StringUtils; +import eu.crushedpixel.replaymod.video.frame.ARGBFrame; +import eu.crushedpixel.replaymod.video.rendering.FrameConsumer; import net.minecraft.client.Minecraft; import net.minecraft.crash.CrashReport; import net.minecraft.crash.CrashReportCategory; import org.apache.commons.io.IOUtils; +import org.lwjgl.util.ReadableDimension; -import java.awt.image.BufferedImage; -import java.awt.image.DataBuffer; -import java.awt.image.DataBufferByte; -import java.awt.image.DataBufferInt; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; -import java.nio.ByteBuffer; import java.nio.channels.Channels; import java.nio.channels.WritableByteChannel; import java.util.ArrayList; -import java.util.LinkedList; import java.util.List; -import java.util.Queue; -import java.util.concurrent.locks.Condition; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; +import java.util.concurrent.TimeUnit; import static org.apache.commons.lang3.Validate.isTrue; -public class VideoWriter { +public class VideoWriter implements FrameConsumer { private final RenderOptions options; private final Process process; private final OutputStream outputStream; private final WritableByteChannel channel; + private final String commandArgs; - private volatile boolean active = true; - private volatile boolean cancelled = false; - - 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(final VideoRenderer renderer, final RenderOptions options) throws IOException { + public VideoWriter(final RenderOptions options) throws IOException { this.options = options.copy(); - this.queueLimit = options.getWriterQueueSize(); - this.toWrite = new LinkedList(); File outputFolder = options.getOutputFile().getParentFile(); String fileName = options.getOutputFile().getName(); - final String args = options.getExportCommandArgs() + commandArgs = options.getExportCommandArgs() .replace("%WIDTH%", String.valueOf(options.getWidth())) .replace("%HEIGHT%", String.valueOf(options.getHeight())) .replace("%FPS%", String.valueOf(options.getFps())) @@ -66,127 +47,63 @@ public class VideoWriter { List command = new ArrayList(); command.add(options.getExportCommand()); - command.addAll(StringUtils.translateCommandline(args)); - System.out.println("Starting " + options.getExportCommand() + " with args: " + args); + command.addAll(StringUtils.translateCommandline(commandArgs)); + System.out.println("Starting " + options.getExportCommand() + " with args: " + commandArgs); process = new ProcessBuilder(command).directory(outputFolder).start(); OutputStream exportLogOut = new FileOutputStream("export.log"); new StreamPipe(process.getInputStream(), exportLogOut).start(); new StreamPipe(process.getErrorStream(), exportLogOut).start(); outputStream = process.getOutputStream(); channel = Channels.newChannel(outputStream); + } - writerThread = new Thread(new Runnable() { + @Override + public void close() throws IOException { + IOUtils.closeQuietly(outputStream); - @Override - public void run() { - try { - while (!cancelled && (active || !toWrite.isEmpty())) { - try { - lock.lockInterruptibly(); - BufferedImage img; - try { - img = toWrite.poll(); - if (img == null) { - noLongerEmptyCondition.await(); - img = toWrite.poll(); - } - noLongerFullCondition.signal(); - if (toWrite.isEmpty()) { - emptyCondition.signalAll(); - } - } finally { - lock.unlock(); - } - - DataBuffer imgBuffer = img.getRaster().getDataBuffer(); - if (imgBuffer instanceof DataBufferByte) { - outputStream.write(((DataBufferByte) imgBuffer).getData()); - } else if (imgBuffer instanceof DataBufferInt) { - ByteBuffer byteBuffer = ByteBuffer.allocate(img.getWidth() * img.getHeight() * 4); - byteBuffer.asIntBuffer().put(((DataBufferInt) imgBuffer).getData()); - channel.write(byteBuffer); - } else { - throw new RuntimeException("DataBuffer type not supported: " + imgBuffer.getClass()); - } - } catch (InterruptedException ignored) { - } + long startTime = System.nanoTime(); + long rem = TimeUnit.SECONDS.toNanos(30); + do { + try { + process.exitValue(); + break; + } catch(IllegalThreadStateException ex) { + if (rem > 0) { + try { + Thread.sleep(Math.min(TimeUnit.NANOSECONDS.toMillis(rem) + 1, 100)); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; } - toWrite.clear(); - IOUtils.closeQuietly(outputStream); - } catch (IOException e) { - if (active) { - CrashReport report = new CrashReport("Exporting frame", e); - CrashReportCategory exportDetails = report.makeCategory("Export details"); - exportDetails.addCrashSection("Export command", options.getExportCommand()); - exportDetails.addCrashSection("Export args", args); - Minecraft.getMinecraft().crashed(report); - renderer.cancel(); - } - } finally { - process.destroy(); } } - }, "replaymod-video-writer"); - writerThread.start(); + rem = TimeUnit.SECONDS.toNanos(30) - (System.nanoTime() - startTime); + } while (rem > 0); + + process.destroy(); } - /** - * 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."); - isTrue(image.getWidth() == options.getWidth(), "Width has to be " + options.getWidth() + " but was " + image.getWidth()); - isTrue(image.getHeight() == options.getHeight(), "Height has to be " + options.getHeight() + " but was " + image.getHeight()); - - lock.lock(); + @Override + public void consume(ARGBFrame frame) { try { - while (toWrite.size() >= queueLimit) { - if (waitIfFull) { - noLongerFullCondition.await(); - } else { - return false; - } - } - 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(); + checkSize(frame.getSize()); + channel.write(frame.getByteBuffer()); + ByteBufferPool.release(frame.getByteBuffer()); + } catch (Throwable t) { + CrashReport report = CrashReport.makeCrashReport(t, "Exporting frame"); + CrashReportCategory exportDetails = report.makeCategory("Export details"); + exportDetails.addCrashSection("Export command", options.getExportCommand()); + exportDetails.addCrashSection("Export args", commandArgs); + Minecraft.getMinecraft().crashed(report); } } - public void waitForFinish() throws InterruptedException { - Preconditions.checkState(!active, "Video writer still active."); - writerThread.join(); + private void checkSize(ReadableDimension size) { + checkSize(size.getWidth(), size.getHeight()); + } + + private void checkSize(int width, int height) { + isTrue(width == options.getWidth(), "Width has to be %d but was %d", options.getWidth(), width); + isTrue(height == options.getHeight(), "Height has to be %d but was %d", options.getHeight(), height); } } diff --git a/src/main/java/eu/crushedpixel/replaymod/video/capturer/CaptureData.java b/src/main/java/eu/crushedpixel/replaymod/video/capturer/CaptureData.java new file mode 100644 index 00000000..958193d2 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/video/capturer/CaptureData.java @@ -0,0 +1,4 @@ +package eu.crushedpixel.replaymod.video.capturer; + +public interface CaptureData { +} diff --git a/src/main/java/eu/crushedpixel/replaymod/video/capturer/CubicOpenGlFrameCapturer.java b/src/main/java/eu/crushedpixel/replaymod/video/capturer/CubicOpenGlFrameCapturer.java new file mode 100644 index 00000000..c8f8c3d1 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/video/capturer/CubicOpenGlFrameCapturer.java @@ -0,0 +1,37 @@ +package eu.crushedpixel.replaymod.video.capturer; + +import eu.crushedpixel.replaymod.video.frame.CubicOpenGlFrame; + +public class CubicOpenGlFrameCapturer extends OpenGlFrameCapturer { + public enum Data implements CaptureData { + LEFT, RIGHT, FRONT, BACK, TOP, BOTTOM + } + + private final int frameSize; + public CubicOpenGlFrameCapturer(WorldRenderer worldRenderer, RenderInfo renderInfo, int frameSize) { + super(worldRenderer, renderInfo); + this.frameSize = frameSize; + } + + @Override + protected int getFrameWidth() { + return frameSize; + } + + @Override + protected int getFrameHeight() { + return frameSize; + } + + @Override + public CubicOpenGlFrame process() { + float partialTicks = renderInfo.updateForNextFrame(); + int frameId = framesDone++; + return new CubicOpenGlFrame(renderFrame(frameId, partialTicks, Data.LEFT), + renderFrame(frameId, partialTicks, Data.RIGHT), + renderFrame(frameId, partialTicks, Data.FRONT), + renderFrame(frameId, partialTicks, Data.BACK), + renderFrame(frameId, partialTicks, Data.TOP), + renderFrame(frameId, partialTicks, Data.BOTTOM)); + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/video/capturer/OpenGlFrameCapturer.java b/src/main/java/eu/crushedpixel/replaymod/video/capturer/OpenGlFrameCapturer.java new file mode 100644 index 00000000..0028c829 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/video/capturer/OpenGlFrameCapturer.java @@ -0,0 +1,108 @@ +package eu.crushedpixel.replaymod.video.capturer; + +import eu.crushedpixel.replaymod.utils.ByteBufferPool; +import eu.crushedpixel.replaymod.video.frame.OpenGlFrame; +import eu.crushedpixel.replaymod.video.rendering.Frame; +import eu.crushedpixel.replaymod.video.rendering.FrameCapturer; +import net.minecraft.client.renderer.OpenGlHelper; +import net.minecraft.client.shader.Framebuffer; +import org.lwjgl.opengl.GL11; +import org.lwjgl.util.Dimension; +import org.lwjgl.util.ReadableDimension; +import org.lwjgl.util.WritableDimension; + +import java.io.IOException; +import java.nio.ByteBuffer; + +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 OpenGlFrameCapturer implements FrameCapturer { + protected final WorldRenderer worldRenderer; + protected final RenderInfo renderInfo; + protected int framesDone; + private Framebuffer frameBuffer; + + public OpenGlFrameCapturer(WorldRenderer worldRenderer, RenderInfo renderInfo) { + this.worldRenderer = worldRenderer; + this.renderInfo = renderInfo; + } + + protected final ReadableDimension frameSize = new ReadableDimension() { + @Override + public int getWidth() { + return getFrameWidth(); + } + + @Override + public int getHeight() { + return getFrameHeight(); + } + + @Override + public void getSize(WritableDimension dest) { + dest.setSize(getWidth(), getHeight()); + } + }; + + protected int getFrameWidth() { + return renderInfo.getFrameSize().getWidth(); + } + + protected int getFrameHeight() { + return renderInfo.getFrameSize().getHeight(); + } + + private Framebuffer frameBuffer() { + if (frameBuffer == null) { + frameBuffer = new Framebuffer(getFrameWidth(), getFrameHeight(), true); + } + return frameBuffer; + } + + @Override + public boolean isDone() { + return framesDone >= renderInfo.getTotalFrames(); + } + + protected OpenGlFrame renderFrame(int frameId, float partialTicks) { + return renderFrame(frameId, partialTicks, null); + } + + protected OpenGlFrame renderFrame(int frameId, float partialTicks, D captureData) { + pushMatrix(); + frameBuffer().bindFramebuffer(true); + + clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + enableTexture2D(); + + worldRenderer.renderWorld(frameSize, partialTicks, captureData); + + frameBuffer().unbindFramebuffer(); + popMatrix(); + + GL11.glPixelStorei(GL11.GL_PACK_ALIGNMENT, 1); + GL11.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 1); + + ByteBuffer buffer = ByteBufferPool.allocate(getFrameWidth() * getFrameHeight() * 3); + if (OpenGlHelper.isFramebufferEnabled()) { + frameBuffer().bindFramebufferTexture(); + GL11.glGetTexImage(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGB, GL11.GL_UNSIGNED_BYTE, buffer); + frameBuffer().unbindFramebufferTexture(); + } else { + GL11.glReadPixels(0, 0, getFrameWidth(), getFrameHeight(), GL11.GL_RGB, GL11.GL_UNSIGNED_BYTE, buffer); + } + buffer.rewind(); + + return new OpenGlFrame(frameId, new Dimension(getFrameWidth(), getFrameHeight()), buffer); + } + + @Override + public void close() throws IOException { + worldRenderer.close(); + if (frameBuffer != null) { + frameBuffer.deleteFramebuffer(); + } + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/video/capturer/RenderInfo.java b/src/main/java/eu/crushedpixel/replaymod/video/capturer/RenderInfo.java new file mode 100644 index 00000000..0b20de27 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/video/capturer/RenderInfo.java @@ -0,0 +1,14 @@ +package eu.crushedpixel.replaymod.video.capturer; + +import eu.crushedpixel.replaymod.settings.RenderOptions; +import org.lwjgl.util.ReadableDimension; + +public interface RenderInfo { + ReadableDimension getFrameSize(); + + int getTotalFrames(); + + float updateForNextFrame(); + + RenderOptions getRenderOptions(); +} diff --git a/src/main/java/eu/crushedpixel/replaymod/video/capturer/SimpleOpenGlFrameCapturer.java b/src/main/java/eu/crushedpixel/replaymod/video/capturer/SimpleOpenGlFrameCapturer.java new file mode 100644 index 00000000..bf5ac060 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/video/capturer/SimpleOpenGlFrameCapturer.java @@ -0,0 +1,16 @@ +package eu.crushedpixel.replaymod.video.capturer; + +import eu.crushedpixel.replaymod.video.frame.OpenGlFrame; + +public class SimpleOpenGlFrameCapturer extends OpenGlFrameCapturer { + + public SimpleOpenGlFrameCapturer(WorldRenderer worldRenderer, RenderInfo renderInfo) { + super(worldRenderer, renderInfo); + } + + @Override + public OpenGlFrame process() { + float partialTicks = renderInfo.updateForNextFrame(); + return renderFrame(framesDone++, partialTicks); + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/video/capturer/StereoscopicOpenGlFrameCapturer.java b/src/main/java/eu/crushedpixel/replaymod/video/capturer/StereoscopicOpenGlFrameCapturer.java new file mode 100644 index 00000000..3da16307 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/video/capturer/StereoscopicOpenGlFrameCapturer.java @@ -0,0 +1,29 @@ +package eu.crushedpixel.replaymod.video.capturer; + +import eu.crushedpixel.replaymod.video.frame.OpenGlFrame; +import eu.crushedpixel.replaymod.video.frame.StereoscopicOpenGlFrame; + +public class StereoscopicOpenGlFrameCapturer + extends OpenGlFrameCapturer { + public enum Data implements CaptureData { + LEFT_EYE, RIGHT_EYE + } + + public StereoscopicOpenGlFrameCapturer(WorldRenderer worldRenderer, RenderInfo renderInfo) { + super(worldRenderer, renderInfo); + } + + @Override + protected int getFrameWidth() { + return super.getFrameWidth() / 2; + } + + @Override + public StereoscopicOpenGlFrame process() { + float partialTicks = renderInfo.updateForNextFrame(); + int frameId = framesDone++; + OpenGlFrame left = renderFrame(frameId, partialTicks, Data.LEFT_EYE); + OpenGlFrame right = renderFrame(frameId, partialTicks, Data.RIGHT_EYE); + return new StereoscopicOpenGlFrame(left, right); + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/video/capturer/WorldRenderer.java b/src/main/java/eu/crushedpixel/replaymod/video/capturer/WorldRenderer.java new file mode 100644 index 00000000..295b3a4a --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/video/capturer/WorldRenderer.java @@ -0,0 +1,9 @@ +package eu.crushedpixel.replaymod.video.capturer; + +import org.lwjgl.util.ReadableDimension; + +import java.io.Closeable; + +public interface WorldRenderer extends Closeable { + void renderWorld(ReadableDimension displaySize, float partialTicks, D data); +} diff --git a/src/main/java/eu/crushedpixel/replaymod/video/entity/CubicEntityRenderer.java b/src/main/java/eu/crushedpixel/replaymod/video/entity/CubicEntityRenderer.java index 7420ab42..82a9698e 100644 --- a/src/main/java/eu/crushedpixel/replaymod/video/entity/CubicEntityRenderer.java +++ b/src/main/java/eu/crushedpixel/replaymod/video/entity/CubicEntityRenderer.java @@ -1,7 +1,7 @@ package eu.crushedpixel.replaymod.video.entity; -import eu.crushedpixel.replaymod.replay.ReplayHandler; import eu.crushedpixel.replaymod.settings.RenderOptions; +import eu.crushedpixel.replaymod.video.capturer.CubicOpenGlFrameCapturer; import net.minecraft.client.particle.EntityFX; import net.minecraft.client.renderer.GlStateManager; import net.minecraft.client.renderer.WorldRenderer; @@ -9,39 +9,13 @@ import net.minecraft.client.renderer.entity.RenderManager; import net.minecraft.entity.Entity; import net.minecraft.util.ResourceLocation; +import java.io.IOException; import java.lang.reflect.Field; -public class CubicEntityRenderer extends CustomEntityRenderer { +public class CubicEntityRenderer extends CustomEntityRenderer { - public 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(RenderOptions options, int frameSize, boolean stable) { - super(options, frameSize, frameSize); - this.stable = stable; + public CubicEntityRenderer(RenderOptions options) { + super(options); try { Field hookField = RenderManager.class.getField("hook"); @@ -54,8 +28,8 @@ public class CubicEntityRenderer extends CustomEntityRenderer { } @Override - public void cleanup() { - super.cleanup(); + public void close() throws IOException{ + super.close(); try { Field hookField = RenderManager.class.getField("hook"); hookField.set(mc.getRenderManager(), null); @@ -66,14 +40,6 @@ public class CubicEntityRenderer extends CustomEntityRenderer { } } - 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); @@ -88,32 +54,10 @@ public class CubicEntityRenderer extends CustomEntityRenderer { proxied.useShader = false; } - @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) { + switch (data) { case FRONT: GlStateManager.rotate(0, 0.0F, 1.0F, 0.0F); break; @@ -134,13 +78,6 @@ public class CubicEntityRenderer extends CustomEntityRenderer { 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 index cbe7779f..b6497bf5 100644 --- a/src/main/java/eu/crushedpixel/replaymod/video/entity/CustomEntityRenderer.java +++ b/src/main/java/eu/crushedpixel/replaymod/video/entity/CustomEntityRenderer.java @@ -3,8 +3,7 @@ package eu.crushedpixel.replaymod.video.entity; import eu.crushedpixel.replaymod.renderer.SpectatorRenderer; import eu.crushedpixel.replaymod.replay.ReplayHandler; import eu.crushedpixel.replaymod.settings.RenderOptions; -import eu.crushedpixel.replaymod.utils.OpenGLUtils; -import eu.crushedpixel.replaymod.video.entity.strategy.*; +import eu.crushedpixel.replaymod.video.capturer.CaptureData; import net.minecraft.client.Minecraft; import net.minecraft.client.particle.EffectRenderer; import net.minecraft.client.particle.EntityFX; @@ -19,16 +18,17 @@ import net.minecraft.util.AxisAlignedBB; import net.minecraft.util.EnumWorldBlockLayer; import net.minecraft.util.MathHelper; import net.minecraft.util.ResourceLocation; +import org.lwjgl.util.ReadableDimension; import org.lwjgl.util.glu.Project; -import java.awt.image.BufferedImage; +import java.io.IOException; import java.lang.reflect.Field; import java.util.List; import static net.minecraft.client.renderer.GlStateManager.*; import static org.lwjgl.opengl.GL11.*; -public abstract class CustomEntityRenderer { +public class CustomEntityRenderer implements eu.crushedpixel.replaymod.video.capturer.WorldRenderer { public final Minecraft mc = Minecraft.getMinecraft(); public final EntityRenderer proxied = mc.entityRenderer; protected final SpectatorRenderer spectatorRenderer = new SpectatorRenderer(){ @@ -37,43 +37,18 @@ public abstract class CustomEntityRenderer { CustomEntityRenderer.this.gluPerspective(fovY, aspect, zNear, zFar); } }; - protected final FrameRenderingStrategy renderingStrategy; + public GluPerspectiveHook gluPerspectiveHook; public LoadShaderHook loadShaderHook; protected int frameCount; + protected D data; + protected final RenderOptions options; - public final int resultWidth; - public final int resultHeight; public CustomEntityRenderer(RenderOptions options) { - this(options, options.getWidth(), options.getHeight()); - } - - public CustomEntityRenderer(RenderOptions options, int resultWidth, int resultHeight) { this.options = options; - this.resultWidth = resultWidth; - this.resultHeight = resultHeight; - - if (OpenGlHelper.isFramebufferEnabled()) { - if (resultWidth > OpenGLUtils.VIEWPORT_MAX_WIDTH || resultHeight > OpenGLUtils.VIEWPORT_MAX_HEIGHT) { - // Video resolution only limited by available RAM and disk size - renderingStrategy = new TiledFrameBufferRenderingStrategy(this); - } else { - // This strategy supports shader - renderingStrategy = new VanillaFrameBufferRenderingStrategy(this); - } - } else { - if (resultWidth > mc.displayHeight || resultHeight > mc.displayHeight) { - // Video resolution only limited by available RAM and disk size - renderingStrategy = new TiledReadPixelsRenderingStrategy(this); - } else { - // Simplest and fastest strategy if frame buffers aren't supported - renderingStrategy = new VanillaReadPixelsRenderingStrategy(this); - } - } - System.out.println("CustomEntityRenderer using " + renderingStrategy); // Install entity renderer hooks try { @@ -108,12 +83,6 @@ public abstract class CustomEntityRenderer { } } - public void renderFrame(float partialTicks, BufferedImage into, int x, int y) { - if (mc.theWorld != null) { - renderingStrategy.renderFrame(partialTicks, into, x, y); - } - } - public void withDisplaySize(int displayWidth, int displayHeight, Runnable runnable) { final int prevWidth = mc.displayWidth; final int prevHeight = mc.displayHeight; @@ -366,6 +335,13 @@ public abstract class CustomEntityRenderer { } 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(); + proxied.farPlaneDistance = (float)(this.mc.gameSettings.renderDistanceChunks * 16); matrixMode(GL_PROJECTION); @@ -377,6 +353,12 @@ public abstract class CustomEntityRenderer { loadIdentity(); orientCamera(partialTicks); + + entity.rotationYaw = orgYaw; + entity.rotationPitch = orgPitch; + entity.prevRotationYaw = orgPrevYaw; + entity.prevRotationPitch = orgPrevPitch; + ReplayHandler.setCameraTilt(orgRoll); } protected void setupFog(int fogDistanceFlag, float partialTicks) { @@ -386,6 +368,14 @@ public abstract class CustomEntityRenderer { } protected void orientCamera(float partialTicks) { + if (options.isIgnoreCameraRotation()) { + Entity entity = mc.getRenderViewEntity(); + // Stop the minecraft code from doing any rotation + entity.prevRotationPitch = entity.rotationPitch = 0; + entity.prevRotationYaw = entity.rotationYaw = 0; + ReplayHandler.setCameraTilt(0); + } + proxied.orientCamera(partialTicks); } @@ -407,7 +397,8 @@ public abstract class CustomEntityRenderer { } } - public void cleanup() { + @Override + public void close() throws IOException { try { Field hookField = EntityRenderer.class.getField("hook"); hookField.set(proxied, null); @@ -417,10 +408,30 @@ public abstract class CustomEntityRenderer { throw new Error(e); } - renderingStrategy.cleanup(); spectatorRenderer.cleanup(); } + @Override + public void renderWorld(ReadableDimension displaySize, final float partialTicks, D data) { + this.data = data; + withDisplaySize(displaySize.getWidth(), displaySize.getHeight(), new Runnable() { + @Override + public void run() { + renderWorld(partialTicks, 0); + if (OpenGlHelper.shadersSupported) { + if (proxied.theShaderGroup != null && proxied.useShader) { + matrixMode(GL_TEXTURE); + + pushMatrix(); + loadIdentity(); + proxied.theShaderGroup.loadShaderGroup(partialTicks); + popMatrix(); + } + } + } + }); + } + public static final class NoCullingCamera implements ICamera { @Override diff --git a/src/main/java/eu/crushedpixel/replaymod/video/entity/StereoscopicEntityRenderer.java b/src/main/java/eu/crushedpixel/replaymod/video/entity/StereoscopicEntityRenderer.java index 2329e5d6..d2587a65 100644 --- a/src/main/java/eu/crushedpixel/replaymod/video/entity/StereoscopicEntityRenderer.java +++ b/src/main/java/eu/crushedpixel/replaymod/video/entity/StereoscopicEntityRenderer.java @@ -1,32 +1,20 @@ package eu.crushedpixel.replaymod.video.entity; import eu.crushedpixel.replaymod.settings.RenderOptions; +import eu.crushedpixel.replaymod.video.capturer.StereoscopicOpenGlFrameCapturer; import net.minecraft.client.renderer.GlStateManager; import net.minecraft.util.MathHelper; import net.minecraft.util.ResourceLocation; -import java.awt.image.BufferedImage; - 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 class StereoscopicEntityRenderer extends CustomEntityRenderer { public StereoscopicEntityRenderer(RenderOptions options) { - super(options, options.getWidth() / 2, options.getHeight()); - } - - public void setEye(boolean leftEye) { - this.leftEye = leftEye; - } - - @Override - public void renderFrame(float partialTicks, BufferedImage into, int x, int y) { - super.renderFrame(partialTicks, into, x, y); + super(options); } @Override @@ -39,7 +27,7 @@ public class StereoscopicEntityRenderer extends CustomEntityRenderer { } protected void translateStereoscopic() { - GlStateManager.translate(leftEye ? 0.07 : -0.07, 0, 0); + GlStateManager.translate(data == StereoscopicOpenGlFrameCapturer.Data.LEFT_EYE ? 0.07 : -0.07, 0, 0); } @Override @@ -61,6 +49,6 @@ public class StereoscopicEntityRenderer extends CustomEntityRenderer { @Override protected void renderSpectatorHand(float partialTicks, int renderPass) { - super.renderSpectatorHand(partialTicks, leftEye ? 1 : 0); + super.renderSpectatorHand(partialTicks, data == StereoscopicOpenGlFrameCapturer.Data.LEFT_EYE ? 1 : 0); } } diff --git a/src/main/java/eu/crushedpixel/replaymod/video/entity/strategy/FrameRenderingStrategy.java b/src/main/java/eu/crushedpixel/replaymod/video/entity/strategy/FrameRenderingStrategy.java deleted file mode 100644 index 4bcf8d5e..00000000 --- a/src/main/java/eu/crushedpixel/replaymod/video/entity/strategy/FrameRenderingStrategy.java +++ /dev/null @@ -1,8 +0,0 @@ -package eu.crushedpixel.replaymod.video.entity.strategy; - -import java.awt.image.BufferedImage; - -public interface FrameRenderingStrategy { - void renderFrame(float partialTicks, BufferedImage into, int x, int y); - void cleanup(); -} diff --git a/src/main/java/eu/crushedpixel/replaymod/video/entity/strategy/TiledFrameBufferRenderingStrategy.java b/src/main/java/eu/crushedpixel/replaymod/video/entity/strategy/TiledFrameBufferRenderingStrategy.java deleted file mode 100644 index 40fea14c..00000000 --- a/src/main/java/eu/crushedpixel/replaymod/video/entity/strategy/TiledFrameBufferRenderingStrategy.java +++ /dev/null @@ -1,127 +0,0 @@ -package eu.crushedpixel.replaymod.video.entity.strategy; - -import eu.crushedpixel.replaymod.utils.OpenGLUtils; -import eu.crushedpixel.replaymod.video.entity.CustomEntityRenderer; -import net.minecraft.client.shader.Framebuffer; -import net.minecraft.crash.CrashReport; -import net.minecraft.util.ReportedException; -import org.lwjgl.BufferUtils; -import org.lwjgl.opengl.GL11; - -import java.awt.image.BufferedImage; -import java.nio.ByteBuffer; - -import static net.minecraft.client.renderer.GlStateManager.*; -import static org.lwjgl.opengl.GL11.*; - -public class TiledFrameBufferRenderingStrategy implements FrameRenderingStrategy, CustomEntityRenderer.GluPerspectiveHook { - - private final CustomEntityRenderer renderer; - private final int tileWidth; - private final int tileHeight; - private final ByteBuffer buffer; - private Framebuffer frameBuffer; - - private int tileX; - private int tileY; - - public TiledFrameBufferRenderingStrategy(CustomEntityRenderer renderer) { - this.renderer = renderer; - this.tileWidth = OpenGLUtils.VIEWPORT_MAX_WIDTH; - this.tileHeight = OpenGLUtils.VIEWPORT_MAX_HEIGHT; - this.buffer = BufferUtils.createByteBuffer(tileWidth * tileHeight * 3); - renderer.gluPerspectiveHook = this; - } - - @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; - - double tilesX = (double) renderer.resultWidth / tileWidth; - double tilesY = (double) renderer.resultHeight / 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); - } - - private Framebuffer frameBuffer() { - if (frameBuffer == null) { - frameBuffer = new Framebuffer(tileWidth, tileHeight, true); - } - return frameBuffer; - } - - @Override - public void renderFrame(float partialTicks, BufferedImage into, int x, int y) { - int tilesX = getTilesX(); - int tilesY = getTilesY(); - for (tileX = 0; tileX < tilesX; tileX++) { - for (tileY = 0; tileY < tilesY; tileY++) { - try { - renderTile(partialTicks, into, x + tileWidth * tileX, y + tileHeight * tileY); - } catch (Throwable t) { - CrashReport crash = CrashReport.makeCrashReport(t, "Rendering tile " + x + "/" + y); - throw new ReportedException(crash); - } - } - } - } - - public void renderTile(final float partialTicks, BufferedImage into, int x, int y) { - pushMatrix(); - frameBuffer().bindFramebuffer(true); - - clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - enableTexture2D(); - - renderer.withDisplaySize(tileWidth, tileHeight, new Runnable() { - @Override - public void run() { - renderer.renderWorld(partialTicks, 0); - } - }); - - frameBuffer().unbindFramebuffer(); - popMatrix(); - - GL11.glPixelStorei(GL11.GL_PACK_ALIGNMENT, 1); - GL11.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 1); - buffer.clear(); - - frameBuffer().bindFramebufferTexture(); - GL11.glGetTexImage(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGB, GL11.GL_UNSIGNED_BYTE, buffer); - frameBuffer().unbindFramebufferTexture(); - - OpenGLUtils.openGlBytesToBufferedImage(buffer, tileWidth, into, x, y); - } - - public int getTilesX() { - int videoWidth = renderer.resultWidth; - if (videoWidth % tileWidth == 0) { - return videoWidth / tileWidth; - } else { - return videoWidth / tileWidth + 1; - } - } - - public int getTilesY() { - int videoHeight = renderer.resultHeight; - if (videoHeight % tileHeight == 0) { - return videoHeight / tileHeight; - } else { - return videoHeight / tileHeight + 1; - } - } - - @Override - public void cleanup() { - if (frameBuffer != null) { - frameBuffer.deleteFramebuffer(); - } - } -} diff --git a/src/main/java/eu/crushedpixel/replaymod/video/entity/strategy/TiledReadPixelsRenderingStrategy.java b/src/main/java/eu/crushedpixel/replaymod/video/entity/strategy/TiledReadPixelsRenderingStrategy.java deleted file mode 100644 index f02b8e82..00000000 --- a/src/main/java/eu/crushedpixel/replaymod/video/entity/strategy/TiledReadPixelsRenderingStrategy.java +++ /dev/null @@ -1,112 +0,0 @@ -package eu.crushedpixel.replaymod.video.entity.strategy; - -import eu.crushedpixel.replaymod.utils.OpenGLUtils; -import eu.crushedpixel.replaymod.video.entity.CustomEntityRenderer; -import net.minecraft.crash.CrashReport; -import net.minecraft.util.ReportedException; -import org.lwjgl.BufferUtils; -import org.lwjgl.opengl.GL11; - -import java.awt.image.BufferedImage; -import java.nio.ByteBuffer; - -import static net.minecraft.client.renderer.GlStateManager.*; -import static org.lwjgl.opengl.GL11.*; - -public class TiledReadPixelsRenderingStrategy implements FrameRenderingStrategy, CustomEntityRenderer.GluPerspectiveHook { - - private final CustomEntityRenderer renderer; - private final int tileWidth; - private final int tileHeight; - private final ByteBuffer buffer; - - private int tileX; - private int tileY; - - public TiledReadPixelsRenderingStrategy(CustomEntityRenderer renderer) { - this.renderer = renderer; - this.tileWidth = renderer.mc.displayWidth; - this.tileHeight = renderer.mc.displayHeight; - this.buffer = BufferUtils.createByteBuffer(tileWidth * tileHeight * 3); - renderer.gluPerspectiveHook = this; - } - - @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; - - double tilesX = (double) renderer.resultWidth / tileWidth; - double tilesY = (double) renderer.resultHeight / 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); - } - - @Override - public void renderFrame(float partialTicks, BufferedImage into, int x, int y) { - int tilesX = getTilesX(); - int tilesY = getTilesY(); - for (tileX = 0; tileX < tilesX; tileX++) { - for (tileY = 0; tileY < tilesY; tileY++) { - try { - renderTile(partialTicks, into, x + tileWidth * tileX, y + tileHeight * tileY); - } catch (Throwable t) { - CrashReport crash = CrashReport.makeCrashReport(t, "Rendering tile " + x + "/" + y); - throw new ReportedException(crash); - } - } - } - } - - public void renderTile(final float partialTicks, BufferedImage into, int x, int y) { - pushMatrix(); - - clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - enableTexture2D(); - - renderer.withDisplaySize(tileWidth, tileHeight, new Runnable() { - @Override - public void run() { - renderer.renderWorld(partialTicks, 0); - } - }); - - popMatrix(); - - buffer.clear(); - GL11.glPixelStorei(GL11.GL_PACK_ALIGNMENT, 1); - GL11.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 1); - GL11.glReadPixels(0, 0, tileWidth, tileHeight, GL11.GL_RGB, GL11.GL_UNSIGNED_BYTE, buffer); - buffer.rewind(); - - OpenGLUtils.openGlBytesToBufferedImage(buffer, tileWidth, into, x, y); - } - - public int getTilesX() { - int videoWidth = renderer.resultWidth; - if (videoWidth % tileWidth == 0) { - return videoWidth / tileWidth; - } else { - return videoWidth / tileWidth + 1; - } - } - - public int getTilesY() { - int videoHeight = renderer.resultHeight; - if (videoHeight % tileHeight == 0) { - return videoHeight / tileHeight; - } else { - return videoHeight / tileHeight + 1; - } - } - - @Override - public void cleanup() { - - } -} diff --git a/src/main/java/eu/crushedpixel/replaymod/video/entity/strategy/VanillaFrameBufferRenderingStrategy.java b/src/main/java/eu/crushedpixel/replaymod/video/entity/strategy/VanillaFrameBufferRenderingStrategy.java deleted file mode 100644 index efac38eb..00000000 --- a/src/main/java/eu/crushedpixel/replaymod/video/entity/strategy/VanillaFrameBufferRenderingStrategy.java +++ /dev/null @@ -1,112 +0,0 @@ -package eu.crushedpixel.replaymod.video.entity.strategy; - -import com.google.gson.JsonSyntaxException; -import eu.crushedpixel.replaymod.utils.OpenGLUtils; -import eu.crushedpixel.replaymod.video.entity.CustomEntityRenderer; -import net.minecraft.client.renderer.EntityRenderer; -import net.minecraft.client.renderer.OpenGlHelper; -import net.minecraft.client.shader.Framebuffer; -import net.minecraft.client.shader.ShaderGroup; -import net.minecraft.util.ResourceLocation; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.lwjgl.BufferUtils; -import org.lwjgl.opengl.GL11; - -import java.awt.image.BufferedImage; -import java.io.IOException; -import java.nio.ByteBuffer; - -import static net.minecraft.client.renderer.GlStateManager.*; -import static org.lwjgl.opengl.GL11.*; - -public class VanillaFrameBufferRenderingStrategy implements FrameRenderingStrategy, CustomEntityRenderer.LoadShaderHook { - - private static final Logger logger = LogManager.getLogger(); - - private final CustomEntityRenderer renderer; - private final ByteBuffer buffer; - private Framebuffer frameBuffer; - - public VanillaFrameBufferRenderingStrategy(CustomEntityRenderer renderer) { - this.renderer = renderer; - this.buffer = BufferUtils.createByteBuffer(renderer.resultWidth * renderer.resultHeight * 3); - } - - @Override - public void loadShader(ResourceLocation resourceLocation) { - EntityRenderer proxied = renderer.proxied; - try { - ShaderGroup theShaderGroup = new ShaderGroup(renderer.mc.getTextureManager(), proxied.resourceManager, frameBuffer(), resourceLocation); - theShaderGroup.createBindFramebuffers(renderer.resultWidth, renderer.resultHeight); - proxied.theShaderGroup = theShaderGroup; - proxied.useShader = true; - } catch (IOException e) { - logger.warn("Failed to load shader: " + resourceLocation, e); - proxied.useShader = false; - } catch (JsonSyntaxException e) { - logger.warn("Failed to load shader: " + resourceLocation, e); - proxied.useShader = false; - } - } - - private Framebuffer frameBuffer() { - if (frameBuffer == null) { - frameBuffer = new Framebuffer(renderer.resultWidth, renderer.resultHeight, true); - - renderer.loadShaderHook = this; - renderer.proxied.loadEntityShader(renderer.mc.getRenderViewEntity()); - } - return frameBuffer; - } - - @Override - public void renderFrame(final float partialTicks, BufferedImage into, int x, int y) { - pushMatrix(); - frameBuffer().bindFramebuffer(true); - - clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - enableTexture2D(); - - renderer.withDisplaySize(renderer.resultWidth, renderer.resultHeight, new Runnable() { - @Override - public void run() { - renderer.renderWorld(partialTicks, 0); - if (OpenGlHelper.shadersSupported) { - EntityRenderer proxied = renderer.proxied; - if (proxied.theShaderGroup != null && proxied.useShader) { - matrixMode(GL_TEXTURE); - - pushMatrix(); - loadIdentity(); - proxied.theShaderGroup.loadShaderGroup(partialTicks); - popMatrix(); - } - } - } - }); - - frameBuffer().unbindFramebuffer(); - popMatrix(); - - GL11.glPixelStorei(GL11.GL_PACK_ALIGNMENT, 1); - GL11.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 1); - buffer.clear(); - - frameBuffer().bindFramebufferTexture(); - GL11.glGetTexImage(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGB, GL11.GL_UNSIGNED_BYTE, buffer); - frameBuffer().unbindFramebufferTexture(); - - OpenGLUtils.openGlBytesToBufferedImage(buffer, renderer.resultWidth, into, x, y); - } - - @Override - public void cleanup() { - if (frameBuffer != null) { - frameBuffer.deleteFramebuffer(); - } - if (renderer.proxied.theShaderGroup != null) { - renderer.proxied.theShaderGroup.createBindFramebuffers(renderer.mc.displayWidth, renderer.mc.displayHeight); - } - } -} diff --git a/src/main/java/eu/crushedpixel/replaymod/video/entity/strategy/VanillaReadPixelsRenderingStrategy.java b/src/main/java/eu/crushedpixel/replaymod/video/entity/strategy/VanillaReadPixelsRenderingStrategy.java deleted file mode 100644 index e9da9cae..00000000 --- a/src/main/java/eu/crushedpixel/replaymod/video/entity/strategy/VanillaReadPixelsRenderingStrategy.java +++ /dev/null @@ -1,54 +0,0 @@ -package eu.crushedpixel.replaymod.video.entity.strategy; - -import eu.crushedpixel.replaymod.utils.OpenGLUtils; -import eu.crushedpixel.replaymod.video.entity.CustomEntityRenderer; -import org.lwjgl.BufferUtils; -import org.lwjgl.opengl.GL11; - -import java.awt.image.BufferedImage; -import java.nio.ByteBuffer; - -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 VanillaReadPixelsRenderingStrategy implements FrameRenderingStrategy { - - private final CustomEntityRenderer renderer; - private final ByteBuffer buffer; - - public VanillaReadPixelsRenderingStrategy(CustomEntityRenderer renderer) { - this.renderer = renderer; - this.buffer = BufferUtils.createByteBuffer(renderer.resultWidth * renderer.resultHeight * 3); - } - - @Override - public void renderFrame(final float partialTicks, BufferedImage into, int x, int y) { - pushMatrix(); - - clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - enableTexture2D(); - - renderer.withDisplaySize(renderer.resultWidth, renderer.resultHeight, new Runnable() { - @Override - public void run() { - renderer.renderWorld(partialTicks, 0); - } - }); - - popMatrix(); - - buffer.clear(); - GL11.glPixelStorei(GL11.GL_PACK_ALIGNMENT, 1); - GL11.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 1); - GL11.glReadPixels(0, 0, renderer.resultWidth, renderer.resultHeight, GL11.GL_RGB, GL11.GL_UNSIGNED_BYTE, buffer); - buffer.rewind(); - - OpenGLUtils.openGlBytesToBufferedImage(buffer, renderer.resultWidth, into, x, y); - } - - @Override - public void cleanup() { - - } -} diff --git a/src/main/java/eu/crushedpixel/replaymod/video/frame/ARGBFrame.java b/src/main/java/eu/crushedpixel/replaymod/video/frame/ARGBFrame.java new file mode 100644 index 00000000..7cdda35d --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/video/frame/ARGBFrame.java @@ -0,0 +1,28 @@ +package eu.crushedpixel.replaymod.video.frame; + +import eu.crushedpixel.replaymod.video.rendering.Frame; +import lombok.Getter; +import org.apache.commons.lang3.Validate; +import org.lwjgl.util.ReadableDimension; + +import java.nio.ByteBuffer; + +public class ARGBFrame implements Frame { + @Getter + private final int frameId; + + @Getter + private final ReadableDimension size; + + @Getter + private final ByteBuffer byteBuffer; + + public ARGBFrame(int frameId, ReadableDimension size, ByteBuffer byteBuffer) { + Validate.isTrue(size.getWidth() * size.getHeight() * 4 == byteBuffer.remaining(), + "Buffer size is %d (cap: %d) but should be %d", + byteBuffer.remaining(), byteBuffer.capacity(), size.getWidth() * size.getHeight() * 4); + this.frameId = frameId; + this.size = size; + this.byteBuffer = byteBuffer; + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/video/frame/CubicFrameRenderer.java b/src/main/java/eu/crushedpixel/replaymod/video/frame/CubicFrameRenderer.java deleted file mode 100644 index 54eeddc3..00000000 --- a/src/main/java/eu/crushedpixel/replaymod/video/frame/CubicFrameRenderer.java +++ /dev/null @@ -1,46 +0,0 @@ -package eu.crushedpixel.replaymod.video.frame; - -import eu.crushedpixel.replaymod.settings.RenderOptions; -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; - -public class CubicFrameRenderer extends FrameRenderer { - - protected final int frameSize; - protected final CubicEntityRenderer entityRenderer; - - protected CubicFrameRenderer(RenderOptions options, int frameSize, boolean stable) { - super(options); - this.frameSize = frameSize; - this.entityRenderer = new CubicEntityRenderer(options, frameSize, stable); - setCustomEntityRenderer(entityRenderer); - } - - public CubicFrameRenderer(RenderOptions options, boolean stable) { - this(options, options.getWidth() / 4, 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 { - entityRenderer.setDirection(direction); - - int frame = direction.getCubicFrame(); - int xVideo = frame % 4 * frameSize; - int yVideo = frame / 4 * frameSize; - renderFrame(timer, image, xVideo, yVideo); - } catch (Throwable t) { - CrashReport crash = CrashReport.makeCrashReport(t, "Rendering frame " + direction + "."); - throw new ReportedException(crash); - } - } - updateDefaultPreview(image); - return image; - } -} diff --git a/src/main/java/eu/crushedpixel/replaymod/video/frame/CubicOpenGlFrame.java b/src/main/java/eu/crushedpixel/replaymod/video/frame/CubicOpenGlFrame.java new file mode 100644 index 00000000..2414b88f --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/video/frame/CubicOpenGlFrame.java @@ -0,0 +1,34 @@ +package eu.crushedpixel.replaymod.video.frame; + +import eu.crushedpixel.replaymod.video.rendering.Frame; +import lombok.Getter; +import org.apache.commons.lang3.Validate; + +public class CubicOpenGlFrame implements Frame { + @Getter + private final OpenGlFrame left, right, front, back, top, bottom; + + public CubicOpenGlFrame(OpenGlFrame left, OpenGlFrame right, OpenGlFrame front, OpenGlFrame back, OpenGlFrame top, OpenGlFrame bottom) { + Validate.isTrue(left.getFrameId() == right.getFrameId() + && right.getFrameId() == front.getFrameId() + && front.getFrameId() == back.getFrameId() + && back.getFrameId() == top.getFrameId() + && top.getFrameId() == bottom.getFrameId(), "Frame ids do not match."); + Validate.isTrue(left.getByteBuffer().remaining() == right.getByteBuffer().remaining() + && right.getByteBuffer().remaining() == front.getByteBuffer().remaining() + && front.getByteBuffer().remaining() == back.getByteBuffer().remaining() + && back.getByteBuffer().remaining() == top.getByteBuffer().remaining() + && top.getByteBuffer().remaining() == bottom.getByteBuffer().remaining(), "Buffer size does not match."); + this.left = left; + this.right = right; + this.front = front; + this.back = back; + this.top = top; + this.bottom = bottom; + } + + @Override + public int getFrameId() { + return left.getFrameId(); + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/video/frame/DefaultFrameRenderer.java b/src/main/java/eu/crushedpixel/replaymod/video/frame/DefaultFrameRenderer.java deleted file mode 100644 index 766ae8cd..00000000 --- a/src/main/java/eu/crushedpixel/replaymod/video/frame/DefaultFrameRenderer.java +++ /dev/null @@ -1,24 +0,0 @@ -package eu.crushedpixel.replaymod.video.frame; - -import eu.crushedpixel.replaymod.settings.RenderOptions; -import net.minecraft.util.Timer; - -import java.awt.image.BufferedImage; - -public class DefaultFrameRenderer extends FrameRenderer { - - public DefaultFrameRenderer(RenderOptions options) { - super(options); - } - - @Override - public BufferedImage captureFrame(Timer timer) { - int width = getVideoWidth(); - int height = getVideoHeight(); - - BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); - renderFrame(timer, image, 0, 0); - updateDefaultPreview(image); - 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 deleted file mode 100644 index 9ef280cc..00000000 --- a/src/main/java/eu/crushedpixel/replaymod/video/frame/EquirectangularFrameRenderer.java +++ /dev/null @@ -1,153 +0,0 @@ -package eu.crushedpixel.replaymod.video.frame; - -import eu.crushedpixel.replaymod.settings.RenderOptions; -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 FrameRenderer { - - private static final byte IMAGE_BACK = 0; - private static final byte IMAGE_FRONT = 1; - private static final byte IMAGE_LEFT = 2; - private static final byte IMAGE_RIGHT = 3; - private static final byte IMAGE_TOP = 4; - private static final byte IMAGE_BOTTOM = 5; - - private final int frameSize; - protected final CubicEntityRenderer entityRenderer; - - private byte[][] image; - private double[][] imageX; - private double[][] imageY; - - public EquirectangularFrameRenderer(RenderOptions options, boolean stable) { - super(options); - this.frameSize = options.getWidth() / 4; - this.entityRenderer = new CubicEntityRenderer(options, frameSize, stable); - setCustomEntityRenderer(entityRenderer); - } - - private void computeCubicToEquirectangularTables() { - int rWidth = getVideoWidth(); - int rHeight = getVideoHeight(); - image = new byte[rWidth][rHeight]; - imageX = new double[rWidth][rHeight]; - imageY = new double[rWidth][rHeight]; - for (int i = 0; i < rWidth; i++) { - double yaw = PI * 2 * i / rWidth; - int piQuarter = 8 * i / rWidth - 4; - byte target; - if (piQuarter < -3) { - target = IMAGE_BACK; - } else if (piQuarter < -1) { - target = IMAGE_LEFT; - } else if (piQuarter < 1) { - target = IMAGE_FRONT; - } else if (piQuarter < 3) { - target = IMAGE_RIGHT; - } else { - target = IMAGE_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; - byte 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 = IMAGE_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 = IMAGE_TOP; - } - - image[i][j] = pt; - imageX[i][j] = Math.min(frameSize - 1, (cXN * frameSize)); - imageY[i][j] = Math.min(frameSize - 1, (cYN * frameSize)); - } - } - } - - @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[][] images = { - ((DataBufferInt) back.getRaster().getDataBuffer()).getData(), - ((DataBufferInt) front.getRaster().getDataBuffer()).getData(), - ((DataBufferInt) left.getRaster().getDataBuffer()).getData(), - ((DataBufferInt) right.getRaster().getDataBuffer()).getData(), - ((DataBufferInt) top.getRaster().getDataBuffer()).getData(), - ((DataBufferInt) bottom.getRaster().getDataBuffer()).getData() - }; - - int fWidth = frameSize; - int rWidth = getVideoWidth(); - int rHeight = getVideoHeight(); - if (image == null || image.length != rWidth || image[0].length != rHeight) { - try { - computeCubicToEquirectangularTables(); - } catch (Throwable t) { - t.printStackTrace(); - } - } - - BufferedImage result = new BufferedImage(rWidth, rHeight, BufferedImage.TYPE_INT_RGB); - int[] resultPixels = ((DataBufferInt) result.getRaster().getDataBuffer()).getData(); - byte[] image; - double[] imageX, imageY; - for (int i = 0; i < rWidth; i++) { - image = this.image[i]; - imageX = this.imageX[i]; - imageY = this.imageY[i]; - for (int j = 0; j < rHeight; j++) { - resultPixels[i + j * rWidth] = images[image[j]][(int) imageX[j] + (int) imageY[j] * fWidth]; - } - } - - return result; - } - - protected BufferedImage captureFrame(Timer timer, CubicEntityRenderer.Direction direction) { - try { - BufferedImage image = new BufferedImage(frameSize, frameSize, BufferedImage.TYPE_INT_RGB); - entityRenderer.setDirection(direction); - renderFrame(timer, image, 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 deleted file mode 100644 index e083b98f..00000000 --- a/src/main/java/eu/crushedpixel/replaymod/video/frame/FrameRenderer.java +++ /dev/null @@ -1,149 +0,0 @@ -package eu.crushedpixel.replaymod.video.frame; - -import eu.crushedpixel.replaymod.settings.RenderOptions; -import eu.crushedpixel.replaymod.utils.BoundingUtils; -import eu.crushedpixel.replaymod.video.entity.CustomEntityRenderer; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.Gui; -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.util.Dimension; - -import java.awt.image.BufferedImage; -import java.awt.image.DataBufferInt; -import java.nio.ByteBuffer; -import java.util.Arrays; - -import static com.google.common.base.Preconditions.checkNotNull; -import static net.minecraft.client.renderer.GlStateManager.bindTexture; -import static net.minecraft.client.renderer.GlStateManager.color; - -public abstract class FrameRenderer { - private static final ResourceLocation noPreviewTexture = new ResourceLocation("replaymod", "logo.jpg"); - protected final Minecraft mc = Minecraft.getMinecraft(); - protected final RenderOptions options; - private CustomEntityRenderer customEntityRenderer; - private DynamicTexture previewTexture; - private boolean previewActive = false; - protected final ByteBuffer buffer; - - public FrameRenderer(RenderOptions options) { - this.options = options; - this.buffer = BufferUtils.createByteBuffer(options.getWidth() * options.getHeight() * 3); - } - - public final int getVideoWidth() { - return options.getWidth(); - } - - public final int getVideoHeight() { - return options.getHeight(); - } - - public void setPreviewActive(boolean previewActive) { - this.previewActive = previewActive; - if (previewActive) { - if (previewTexture != null) { - Arrays.fill(previewTexture.getTextureData(), 0xff000000); - previewTexture.updateDynamicTexture(); - } - } - } - - public boolean isPreviewActive() { - return previewActive; - } - - public void setCustomEntityRenderer(CustomEntityRenderer customEntityRenderer) { - this.customEntityRenderer = checkNotNull(customEntityRenderer); - } - - public abstract BufferedImage captureFrame(Timer timer); - - protected void updateDefaultPreview(BufferedImage image) { - if (isPreviewActive()) { - int[] pixels = ((DataBufferInt) image.getRaster().getDataBuffer()).getData(); - updateDefaultPreview(pixels); - } - } - - protected void updateDefaultPreview(int[] pixels) { - if (isPreviewActive() && previewTexture != null) { - System.arraycopy(pixels, 0, previewTexture.getTextureData(), 0, pixels.length); - previewTexture.updateDynamicTexture(); - } - } - - protected void renderFrame(Timer timer, BufferedImage into, int x, int y) { - customEntityRenderer.renderFrame(timer.renderPartialTicks, into, x, y); - } - - public void setup() { - if (customEntityRenderer == null) { - customEntityRenderer = new CustomEntityRenderer(options) {}; - } - - previewTexture = new DynamicTexture(getVideoWidth(), getVideoHeight()) { - @Override - public void updateDynamicTexture() { - bindTexture(getGlTextureId()); - TextureUtil.uploadTextureSub(0, getTextureData(), getVideoWidth(), getVideoHeight(), 0, 0, true, false, false); - } - }; - } - - public void tearDown() { - if (previewTexture != null) { - previewTexture.deleteGlTexture(); - } - if (customEntityRenderer != null) { - customEntityRenderer.cleanup(); - } - } - - public void drawPreviewTexture(int x, int y, int width, int height) { - int videoWidth = getVideoWidth(); - int videoHeight = getVideoHeight(); - color(1, 1, 1, 1); - if (previewTexture != null) { - bindTexture(previewTexture.getGlTextureId()); - Gui.drawScaledCustomSizeModalRect(x, y, 0, 0, videoWidth, videoHeight, width, height, videoWidth, videoHeight); - } - } - - /** - * Draw a preview of the current scene within the specified box. - * @param x Left border of the box - * @param y Upper border of the box - * @param width Width of the box - * @param height Height of the box - */ - public void renderPreview(int x, int y, int width, int height) { - Dimension dimension = BoundingUtils.fitIntoBounds(new Dimension(getVideoWidth(), getVideoHeight()), new Dimension(width, height)); - - x += (width - dimension.getWidth()) / 2; - y += (height - dimension.getHeight()) / 2; - - drawPreviewTexture(x, y, dimension.getWidth(), dimension.getHeight()); - } - - public static void renderNoPreview(int x, int y, int width, int height) { - int actualWidth = width; - int actualHeight = height; - if (width / height > 1280 / 720) { - actualWidth = height * 1280 / 720; - } else { - actualHeight = width * 720 / 1280; - } - - x += (width - actualWidth) / 2; - y += (height - actualHeight) / 2; - - Minecraft.getMinecraft().getTextureManager().bindTexture(noPreviewTexture); - color(1, 1, 1, 1); - Gui.drawScaledCustomSizeModalRect(x, y, 0, 0, 1280, 720, actualWidth, actualHeight, 1280, 720); - } -} diff --git a/src/main/java/eu/crushedpixel/replaymod/video/frame/OpenGlFrame.java b/src/main/java/eu/crushedpixel/replaymod/video/frame/OpenGlFrame.java new file mode 100644 index 00000000..df6fb1a9 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/video/frame/OpenGlFrame.java @@ -0,0 +1,20 @@ +package eu.crushedpixel.replaymod.video.frame; + +import eu.crushedpixel.replaymod.video.rendering.Frame; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.lwjgl.util.ReadableDimension; + +import java.nio.ByteBuffer; + +@RequiredArgsConstructor +public class OpenGlFrame implements Frame { + @Getter + private final int frameId; + + @Getter + private final ReadableDimension size; + + @Getter + private final ByteBuffer byteBuffer; +} diff --git a/src/main/java/eu/crushedpixel/replaymod/video/frame/StereoscopicFrameRenderer.java b/src/main/java/eu/crushedpixel/replaymod/video/frame/StereoscopicFrameRenderer.java deleted file mode 100644 index 206cb14d..00000000 --- a/src/main/java/eu/crushedpixel/replaymod/video/frame/StereoscopicFrameRenderer.java +++ /dev/null @@ -1,40 +0,0 @@ -package eu.crushedpixel.replaymod.video.frame; - -import eu.crushedpixel.replaymod.settings.RenderOptions; -import eu.crushedpixel.replaymod.video.entity.StereoscopicEntityRenderer; -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; - - public StereoscopicFrameRenderer(RenderOptions options) { - super(options); - entityRenderer = new StereoscopicEntityRenderer(options); - setCustomEntityRenderer(entityRenderer); - } - - @Override - public BufferedImage captureFrame(Timer timer) { - BufferedImage image = new BufferedImage(getVideoWidth(), getVideoHeight(), BufferedImage.TYPE_INT_RGB); - try { - entityRenderer.setEye(true); - renderFrame(timer, image, 0, 0); - } catch (Throwable t) { - CrashReport crash = CrashReport.makeCrashReport(t, "Rendering left eye."); - throw new ReportedException(crash); - } - try { - entityRenderer.setEye(false); - renderFrame(timer, image, getVideoWidth() / 2, 0); - } catch (Throwable t) { - CrashReport crash = CrashReport.makeCrashReport(t, "Rendering right eye."); - throw new ReportedException(crash); - } - updateDefaultPreview(image); - return image; - } -} diff --git a/src/main/java/eu/crushedpixel/replaymod/video/frame/StereoscopicOpenGlFrame.java b/src/main/java/eu/crushedpixel/replaymod/video/frame/StereoscopicOpenGlFrame.java new file mode 100644 index 00000000..0fbbbefe --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/video/frame/StereoscopicOpenGlFrame.java @@ -0,0 +1,22 @@ +package eu.crushedpixel.replaymod.video.frame; + +import eu.crushedpixel.replaymod.video.rendering.Frame; +import lombok.Getter; +import org.apache.commons.lang3.Validate; + +public class StereoscopicOpenGlFrame implements Frame { + @Getter + private final OpenGlFrame left, right; + + public StereoscopicOpenGlFrame(OpenGlFrame left, OpenGlFrame right) { + Validate.isTrue(left.getFrameId() == right.getFrameId(), "Frame ids do not match."); + Validate.isTrue(left.getByteBuffer().remaining() == right.getByteBuffer().remaining(), "Buffer size does not match."); + this.left = left; + this.right = right; + } + + @Override + public int getFrameId() { + return left.getFrameId(); + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/video/processor/AbstractFrameProcessor.java b/src/main/java/eu/crushedpixel/replaymod/video/processor/AbstractFrameProcessor.java new file mode 100644 index 00000000..d340a53c --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/video/processor/AbstractFrameProcessor.java @@ -0,0 +1,12 @@ +package eu.crushedpixel.replaymod.video.processor; + +import eu.crushedpixel.replaymod.video.rendering.Frame; +import eu.crushedpixel.replaymod.video.rendering.FrameProcessor; + +import java.io.IOException; + +public abstract class AbstractFrameProcessor implements FrameProcessor { + @Override + public void close() throws IOException { + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/video/processor/CubicToARGBProcessor.java b/src/main/java/eu/crushedpixel/replaymod/video/processor/CubicToARGBProcessor.java new file mode 100644 index 00000000..b0a7607f --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/video/processor/CubicToARGBProcessor.java @@ -0,0 +1,33 @@ +package eu.crushedpixel.replaymod.video.processor; + +import eu.crushedpixel.replaymod.utils.ByteBufferPool; +import eu.crushedpixel.replaymod.video.frame.ARGBFrame; +import eu.crushedpixel.replaymod.video.frame.CubicOpenGlFrame; +import org.lwjgl.util.Dimension; + +import java.nio.ByteBuffer; + +import static eu.crushedpixel.replaymod.utils.OpenGLUtils.openGlBytesToARBG; + +public class CubicToARGBProcessor extends AbstractFrameProcessor { + @Override + public ARGBFrame process(CubicOpenGlFrame rawFrame) { + int size = rawFrame.getLeft().getSize().getWidth(); + int width = size * 4; + int height = size * 3; + ByteBuffer result = ByteBufferPool.allocate(width * height * 4); + openGlBytesToARBG(rawFrame.getLeft().getByteBuffer(), size, 0, size, result, width); + openGlBytesToARBG(rawFrame.getFront().getByteBuffer(), size, size, size, result, width); + openGlBytesToARBG(rawFrame.getRight().getByteBuffer(), size, size * 2, size, result, width); + openGlBytesToARBG(rawFrame.getBack().getByteBuffer(), size, size * 3, size, result, width); + openGlBytesToARBG(rawFrame.getTop().getByteBuffer(), size, size, 0, result, width); + openGlBytesToARBG(rawFrame.getBottom().getByteBuffer(), size, size, size * 2, result, width); + ByteBufferPool.release(rawFrame.getLeft().getByteBuffer()); + ByteBufferPool.release(rawFrame.getRight().getByteBuffer()); + ByteBufferPool.release(rawFrame.getFront().getByteBuffer()); + ByteBufferPool.release(rawFrame.getBack().getByteBuffer()); + ByteBufferPool.release(rawFrame.getTop().getByteBuffer()); + ByteBufferPool.release(rawFrame.getBottom().getByteBuffer()); + return new ARGBFrame(rawFrame.getFrameId(), new Dimension(width, height), result); + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/video/processor/EquirectangularToARGBProcessor.java b/src/main/java/eu/crushedpixel/replaymod/video/processor/EquirectangularToARGBProcessor.java new file mode 100644 index 00000000..c03b5edf --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/video/processor/EquirectangularToARGBProcessor.java @@ -0,0 +1,119 @@ +package eu.crushedpixel.replaymod.video.processor; + +import eu.crushedpixel.replaymod.utils.ByteBufferPool; +import eu.crushedpixel.replaymod.video.frame.ARGBFrame; +import eu.crushedpixel.replaymod.video.frame.CubicOpenGlFrame; +import org.apache.commons.lang3.Validate; +import org.lwjgl.util.Dimension; + +import java.nio.ByteBuffer; + +import static java.lang.Math.PI; + +public class EquirectangularToARGBProcessor extends AbstractFrameProcessor { + private static final byte IMAGE_BACK = 0; + private static final byte IMAGE_FRONT = 1; + private static final byte IMAGE_LEFT = 2; + private static final byte IMAGE_RIGHT = 3; + private static final byte IMAGE_TOP = 4; + private static final byte IMAGE_BOTTOM = 5; + + private final int frameSize; + private final int width; + private final int height; + + private final byte[][] image; + private final int[][] imageX; + private final int[][] imageY; + + + public EquirectangularToARGBProcessor(int frameSize) { + this.frameSize = frameSize; + + width = frameSize * 4; + height = frameSize * 2; + image = new byte[height][width]; + imageX = new int[height][width]; + imageY = new int[height][width]; + for (int i = 0; i < width; i++) { + double yaw = PI * 2 * i / width; + int piQuarter = 8 * i / width - 4; + byte target; + if (piQuarter < -3) { + target = IMAGE_BACK; + } else if (piQuarter < -1) { + target = IMAGE_LEFT; + } else if (piQuarter < 1) { + target = IMAGE_FRONT; + } else if (piQuarter < 3) { + target = IMAGE_RIGHT; + } else { + target = IMAGE_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 < height; j++) { + double cXN = gcXN; + byte pt = target; + double pitch = PI * j / height - 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 = IMAGE_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 = IMAGE_TOP; + } + + int imgX = (int) Math.min(frameSize - 1, (cXN * frameSize)); + int imgY = (int) Math.min(frameSize - 1, (cYN * frameSize)); + image[j][i] = pt; + imageX[j][i] = imgX; + imageY[j][i] = frameSize - imgY - 1; // The OpenGl buffer contains data flipped vertically + } + } + } + + @Override + public ARGBFrame process(CubicOpenGlFrame rawFrame) { + Validate.isTrue(rawFrame.getLeft().getSize().getWidth() == frameSize, "Frame size must be %d but was %d", + frameSize, rawFrame.getLeft().getSize().getWidth()); + ByteBuffer result = ByteBufferPool.allocate(width * height * 4); + ByteBuffer[] images = { + rawFrame.getBack().getByteBuffer(), rawFrame.getFront().getByteBuffer(), + rawFrame.getLeft().getByteBuffer(), rawFrame.getRight().getByteBuffer(), + rawFrame.getTop().getByteBuffer(), rawFrame.getBottom().getByteBuffer() + }; + byte[] pixel = new byte[4]; + pixel[0] = (byte) 0xff; + byte[] image; + int[] imageX, imageY; + for (int y = 0; y < height; y++) { + image = this.image[y]; + imageX = this.imageX[y]; + imageY = this.imageY[y]; + for (int x = 0; x < width; x++) { + ByteBuffer source = images[image[x]]; + source.position((imageX[x] + imageY[x] * frameSize) * 3); + source.get(pixel, 1, 3); + result.put(pixel); + } + } + result.rewind(); + + ByteBufferPool.release(rawFrame.getLeft().getByteBuffer()); + ByteBufferPool.release(rawFrame.getRight().getByteBuffer()); + ByteBufferPool.release(rawFrame.getFront().getByteBuffer()); + ByteBufferPool.release(rawFrame.getBack().getByteBuffer()); + ByteBufferPool.release(rawFrame.getTop().getByteBuffer()); + ByteBufferPool.release(rawFrame.getBottom().getByteBuffer()); + return new ARGBFrame(rawFrame.getFrameId(), new Dimension(width, height), result); + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/video/processor/OpenGlToARGBProcessor.java b/src/main/java/eu/crushedpixel/replaymod/video/processor/OpenGlToARGBProcessor.java new file mode 100644 index 00000000..1205f97c --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/video/processor/OpenGlToARGBProcessor.java @@ -0,0 +1,22 @@ +package eu.crushedpixel.replaymod.video.processor; + +import eu.crushedpixel.replaymod.utils.ByteBufferPool; +import eu.crushedpixel.replaymod.video.frame.ARGBFrame; +import eu.crushedpixel.replaymod.video.frame.OpenGlFrame; +import org.lwjgl.util.ReadableDimension; + +import java.nio.ByteBuffer; + +import static eu.crushedpixel.replaymod.utils.OpenGLUtils.openGlBytesToARBG; + +public class OpenGlToARGBProcessor extends AbstractFrameProcessor { + @Override + public ARGBFrame process(OpenGlFrame rawFrame) { + ReadableDimension size = rawFrame.getSize(); + ByteBuffer buffer = rawFrame.getByteBuffer(); + ByteBuffer argb = ByteBufferPool.allocate(size.getWidth() * size.getHeight() * 4); + openGlBytesToARBG(buffer, size.getWidth(), 0, 0, argb, size.getWidth()); + ByteBufferPool.release(buffer); + return new ARGBFrame(rawFrame.getFrameId(), size, argb); + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/video/processor/StereoscopicToARGBProcessor.java b/src/main/java/eu/crushedpixel/replaymod/video/processor/StereoscopicToARGBProcessor.java new file mode 100644 index 00000000..7ca58496 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/video/processor/StereoscopicToARGBProcessor.java @@ -0,0 +1,27 @@ +package eu.crushedpixel.replaymod.video.processor; + +import eu.crushedpixel.replaymod.utils.ByteBufferPool; +import eu.crushedpixel.replaymod.video.frame.ARGBFrame; +import eu.crushedpixel.replaymod.video.frame.StereoscopicOpenGlFrame; +import org.lwjgl.util.Dimension; +import org.lwjgl.util.ReadableDimension; + +import java.nio.ByteBuffer; + +import static eu.crushedpixel.replaymod.utils.OpenGLUtils.openGlBytesToARBG; + +public class StereoscopicToARGBProcessor extends AbstractFrameProcessor { + @Override + public ARGBFrame process(StereoscopicOpenGlFrame rawFrame) { + ReadableDimension size = rawFrame.getLeft().getSize(); + int width = size.getWidth(); + ByteBuffer leftBuffer = rawFrame.getLeft().getByteBuffer(); + ByteBuffer rightBuffer = rawFrame.getRight().getByteBuffer(); + ByteBuffer result = ByteBufferPool.allocate(width * 2 * size.getHeight() * 4); + openGlBytesToARBG(leftBuffer, width, 0, 0, result, width * 2); + openGlBytesToARBG(rightBuffer, width, size.getWidth(), 0, result, width * 2); + ByteBufferPool.release(leftBuffer); + ByteBufferPool.release(rightBuffer); + return new ARGBFrame(rawFrame.getFrameId(), new Dimension(width * 2, size.getHeight()), result); + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/video/rendering/Frame.java b/src/main/java/eu/crushedpixel/replaymod/video/rendering/Frame.java new file mode 100644 index 00000000..491700f2 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/video/rendering/Frame.java @@ -0,0 +1,5 @@ +package eu.crushedpixel.replaymod.video.rendering; + +public interface Frame { + int getFrameId(); +} diff --git a/src/main/java/eu/crushedpixel/replaymod/video/rendering/FrameCapturer.java b/src/main/java/eu/crushedpixel/replaymod/video/rendering/FrameCapturer.java new file mode 100644 index 00000000..0bb9bd20 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/video/rendering/FrameCapturer.java @@ -0,0 +1,11 @@ +package eu.crushedpixel.replaymod.video.rendering; + +import java.io.Closeable; + +public interface FrameCapturer extends Closeable { + + boolean isDone(); + + R process(); + +} diff --git a/src/main/java/eu/crushedpixel/replaymod/video/rendering/FrameConsumer.java b/src/main/java/eu/crushedpixel/replaymod/video/rendering/FrameConsumer.java new file mode 100644 index 00000000..966dda2d --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/video/rendering/FrameConsumer.java @@ -0,0 +1,9 @@ +package eu.crushedpixel.replaymod.video.rendering; + +import java.io.Closeable; + +public interface FrameConsumer

extends Closeable { + + void consume(P frame); + +} diff --git a/src/main/java/eu/crushedpixel/replaymod/video/rendering/FrameProcessor.java b/src/main/java/eu/crushedpixel/replaymod/video/rendering/FrameProcessor.java new file mode 100644 index 00000000..4f3c5192 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/video/rendering/FrameProcessor.java @@ -0,0 +1,9 @@ +package eu.crushedpixel.replaymod.video.rendering; + +import java.io.Closeable; + +public interface FrameProcessor extends Closeable { + + P process(R rawFrame); + +} diff --git a/src/main/java/eu/crushedpixel/replaymod/video/rendering/Pipeline.java b/src/main/java/eu/crushedpixel/replaymod/video/rendering/Pipeline.java new file mode 100644 index 00000000..e50e8606 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/video/rendering/Pipeline.java @@ -0,0 +1,112 @@ +package eu.crushedpixel.replaymod.video.rendering; + +import lombok.RequiredArgsConstructor; +import net.minecraft.client.Minecraft; +import net.minecraft.crash.CrashReport; +import net.minecraft.util.ReportedException; +import org.lwjgl.opengl.Display; + +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +public class Pipeline implements Runnable { + + private final FrameCapturer capturer; + private final FrameProcessor processor; + private int consumerNextFrame; + private final Object consumerLock = new Object(); + private final FrameConsumer

consumer; + + private Thread runningThread; + + public Pipeline(FrameCapturer capturer, FrameProcessor processor, FrameConsumer

consumer) { + this.capturer = capturer; + this.processor = processor; + this.consumer = consumer; + } + + @Override + public synchronized void run() { + runningThread = Thread.currentThread(); + consumerNextFrame = 0; + int processors = Runtime.getRuntime().availableProcessors(); + int processThreads = Math.max(1, processors - 2); // One processor for the main thread and one for ffmpeg, sorry OS :( + ExecutorService processService = new ThreadPoolExecutor(processThreads, processThreads, + 0L, TimeUnit.MILLISECONDS, + new ArrayBlockingQueue(2) { + @Override + public boolean offer(Runnable runnable) { + try { + put(runnable); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return false; + } + return true; + } + }, new ThreadPoolExecutor.DiscardPolicy()); + + Minecraft mc = Minecraft.getMinecraft(); + while (!capturer.isDone() && !Thread.currentThread().isInterrupted()) { + if (Display.isCloseRequested() || mc.hasCrashed) { + Thread.currentThread().interrupt(); + return; + } + R rawFrame = capturer.process(); + if (rawFrame != null) { + processService.submit(new ProcessTask(rawFrame)); + } + } + + processService.shutdown(); + try { + processService.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + try { + capturer.close(); + processor.close(); + consumer.close(); + } catch (Throwable t) { + CrashReport crashReport = CrashReport.makeCrashReport(t, "Cleaning up rendering pipeline"); + throw new ReportedException(crashReport); + } + } + + public void cancel() { + if (runningThread != null) { + runningThread.interrupt(); + } + } + + @RequiredArgsConstructor + private class ProcessTask implements Runnable { + private final R rawFrame; + + @Override + public void run() { + try { + P processedFrame = processor.process(rawFrame); + synchronized (consumerLock) { + while (consumerNextFrame != processedFrame.getFrameId()) { + try { + consumerLock.wait(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + consumer.consume(processedFrame); + consumerNextFrame++; + consumerLock.notifyAll(); + } + } catch (Throwable t) { + CrashReport crashReport = CrashReport.makeCrashReport(t, "Processing frame"); + Minecraft.getMinecraft().crashed(crashReport); + } + } + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/video/rendering/Pipelines.java b/src/main/java/eu/crushedpixel/replaymod/video/rendering/Pipelines.java new file mode 100644 index 00000000..54e6530a --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/video/rendering/Pipelines.java @@ -0,0 +1,73 @@ +package eu.crushedpixel.replaymod.video.rendering; + +import eu.crushedpixel.replaymod.settings.RenderOptions; +import eu.crushedpixel.replaymod.video.capturer.*; +import eu.crushedpixel.replaymod.video.entity.CubicEntityRenderer; +import eu.crushedpixel.replaymod.video.entity.CustomEntityRenderer; +import eu.crushedpixel.replaymod.video.entity.StereoscopicEntityRenderer; +import eu.crushedpixel.replaymod.video.frame.ARGBFrame; +import eu.crushedpixel.replaymod.video.frame.CubicOpenGlFrame; +import eu.crushedpixel.replaymod.video.frame.OpenGlFrame; +import eu.crushedpixel.replaymod.video.frame.StereoscopicOpenGlFrame; +import eu.crushedpixel.replaymod.video.processor.CubicToARGBProcessor; +import eu.crushedpixel.replaymod.video.processor.EquirectangularToARGBProcessor; +import eu.crushedpixel.replaymod.video.processor.OpenGlToARGBProcessor; +import eu.crushedpixel.replaymod.video.processor.StereoscopicToARGBProcessor; +import lombok.experimental.UtilityClass; + +@UtilityClass +public class Pipelines { + public enum Preset { + DEFAULT, STEREOSCOPIC, CUBIC, EQUIRECTANGULAR + } + + public static Pipeline newPipeline(Preset preset, RenderInfo renderInfo, FrameConsumer consumer) { + switch (preset) { + case DEFAULT: + return newDefaultPipeline(renderInfo, consumer); + case STEREOSCOPIC: + return newStereoscopicPipeline(renderInfo, consumer); + case CUBIC: + return newCubicPipeline(renderInfo, consumer); + case EQUIRECTANGULAR: + return newEquirectangularPipeline(renderInfo, consumer); + } + throw new UnsupportedOperationException("Unknown preset: " + preset); + } + + public static Pipeline newDefaultPipeline(RenderInfo renderInfo, FrameConsumer consumer) { + RenderOptions options = renderInfo.getRenderOptions(); + return new Pipeline( + new SimpleOpenGlFrameCapturer(new CustomEntityRenderer(options), renderInfo), + new OpenGlToARGBProcessor(), + consumer + ); + } + + public static Pipeline newStereoscopicPipeline(RenderInfo renderInfo, FrameConsumer consumer) { + RenderOptions options = renderInfo.getRenderOptions(); + return new Pipeline( + new StereoscopicOpenGlFrameCapturer(new StereoscopicEntityRenderer(options), renderInfo), + new StereoscopicToARGBProcessor(), + consumer + ); + } + + public static Pipeline newCubicPipeline(RenderInfo renderInfo, FrameConsumer consumer) { + RenderOptions options = renderInfo.getRenderOptions(); + return new Pipeline( + new CubicOpenGlFrameCapturer(new CubicEntityRenderer(options), renderInfo, options.getWidth() / 4), + new CubicToARGBProcessor(), + consumer + ); + } + + public static Pipeline newEquirectangularPipeline(RenderInfo renderInfo, FrameConsumer consumer) { + RenderOptions options = renderInfo.getRenderOptions(); + return new Pipeline( + new CubicOpenGlFrameCapturer(new CubicEntityRenderer(options), renderInfo, options.getWidth() / 4), + new EquirectangularToARGBProcessor(options.getWidth() / 4), + consumer + ); + } +} diff --git a/src/main/resources/META-INF/replaymod_at.cfg b/src/main/resources/META-INF/replaymod_at.cfg index 8ac1115c..627b71a9 100644 --- a/src/main/resources/META-INF/replaymod_at.cfg +++ b/src/main/resources/META-INF/replaymod_at.cfg @@ -10,6 +10,7 @@ public net.minecraft.client.Minecraft field_71445_n # isGamePaused public net.minecraft.client.Minecraft field_152351_aB # scheduledTasks public net.minecraft.client.Minecraft field_110449_ao # defaultResourcePacks public net.minecraft.client.Minecraft field_110448_aq # mcResourcePackRepository +public net.minecraft.client.Minecraft field_71434_R # hasCrashed public net.minecraft.client.Minecraft func_71386_F()L # getSystemTime public net.minecraft.client.Minecraft func_71383_b(I)V # updateDebugProfilerName