From 7fd429e25fb84e2395e81a36eb5fb7c19fc3405a Mon Sep 17 00:00:00 2001 From: johni0702 Date: Wed, 15 Jul 2015 14:21:18 +0200 Subject: [PATCH] Rework rendering pipeline to make better use of multithreading Move OpenGL frame to ARGB conversion to processing threads Move video exporting to processing threads Skip creation of BufferedImage and instead use ByteBuffer with ARGB content directly --- .../eu/crushedpixel/replaymod/ReplayMod.java | 21 +- .../replaymod/gui/GuiRenderSettings.java | 20 +- .../replaymod/gui/GuiVideoRenderer.java | 97 ++++++++- .../replaymod/settings/RenderOptions.java | 19 +- .../replaymod/utils/ByteBufferPool.java | 48 +++++ .../replaymod/utils/OpenGLUtils.java | 31 +-- .../replaymod/video/VideoRenderer.java | 196 ++++++++---------- .../replaymod/video/VideoWriter.java | 183 +++++----------- .../replaymod/video/capturer/CaptureData.java | 4 + .../capturer/CubicOpenGlFrameCapturer.java | 37 ++++ .../video/capturer/OpenGlFrameCapturer.java | 108 ++++++++++ .../replaymod/video/capturer/RenderInfo.java | 14 ++ .../capturer/SimpleOpenGlFrameCapturer.java | 16 ++ .../StereoscopicOpenGlFrameCapturer.java | 29 +++ .../video/capturer/WorldRenderer.java | 9 + .../video/entity/CubicEntityRenderer.java | 79 +------ .../video/entity/CustomEntityRenderer.java | 91 ++++---- .../entity/StereoscopicEntityRenderer.java | 22 +- .../strategy/FrameRenderingStrategy.java | 8 - .../TiledFrameBufferRenderingStrategy.java | 127 ------------ .../TiledReadPixelsRenderingStrategy.java | 112 ---------- .../VanillaFrameBufferRenderingStrategy.java | 112 ---------- .../VanillaReadPixelsRenderingStrategy.java | 54 ----- .../replaymod/video/frame/ARGBFrame.java | 28 +++ .../video/frame/CubicFrameRenderer.java | 46 ---- .../video/frame/CubicOpenGlFrame.java | 34 +++ .../video/frame/DefaultFrameRenderer.java | 24 --- .../frame/EquirectangularFrameRenderer.java | 153 -------------- .../replaymod/video/frame/FrameRenderer.java | 149 ------------- .../replaymod/video/frame/OpenGlFrame.java | 20 ++ .../frame/StereoscopicFrameRenderer.java | 40 ---- .../video/frame/StereoscopicOpenGlFrame.java | 22 ++ .../processor/AbstractFrameProcessor.java | 12 ++ .../video/processor/CubicToARGBProcessor.java | 33 +++ .../EquirectangularToARGBProcessor.java | 119 +++++++++++ .../processor/OpenGlToARGBProcessor.java | 22 ++ .../StereoscopicToARGBProcessor.java | 27 +++ .../replaymod/video/rendering/Frame.java | 5 + .../video/rendering/FrameCapturer.java | 11 + .../video/rendering/FrameConsumer.java | 9 + .../video/rendering/FrameProcessor.java | 9 + .../replaymod/video/rendering/Pipeline.java | 112 ++++++++++ .../replaymod/video/rendering/Pipelines.java | 73 +++++++ src/main/resources/META-INF/replaymod_at.cfg | 1 + 44 files changed, 1142 insertions(+), 1244 deletions(-) create mode 100644 src/main/java/eu/crushedpixel/replaymod/utils/ByteBufferPool.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/video/capturer/CaptureData.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/video/capturer/CubicOpenGlFrameCapturer.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/video/capturer/OpenGlFrameCapturer.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/video/capturer/RenderInfo.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/video/capturer/SimpleOpenGlFrameCapturer.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/video/capturer/StereoscopicOpenGlFrameCapturer.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/video/capturer/WorldRenderer.java delete mode 100644 src/main/java/eu/crushedpixel/replaymod/video/entity/strategy/FrameRenderingStrategy.java delete mode 100644 src/main/java/eu/crushedpixel/replaymod/video/entity/strategy/TiledFrameBufferRenderingStrategy.java delete mode 100644 src/main/java/eu/crushedpixel/replaymod/video/entity/strategy/TiledReadPixelsRenderingStrategy.java delete mode 100644 src/main/java/eu/crushedpixel/replaymod/video/entity/strategy/VanillaFrameBufferRenderingStrategy.java delete mode 100644 src/main/java/eu/crushedpixel/replaymod/video/entity/strategy/VanillaReadPixelsRenderingStrategy.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/video/frame/ARGBFrame.java delete mode 100644 src/main/java/eu/crushedpixel/replaymod/video/frame/CubicFrameRenderer.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/video/frame/CubicOpenGlFrame.java delete mode 100644 src/main/java/eu/crushedpixel/replaymod/video/frame/DefaultFrameRenderer.java delete mode 100644 src/main/java/eu/crushedpixel/replaymod/video/frame/EquirectangularFrameRenderer.java delete mode 100644 src/main/java/eu/crushedpixel/replaymod/video/frame/FrameRenderer.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/video/frame/OpenGlFrame.java delete mode 100644 src/main/java/eu/crushedpixel/replaymod/video/frame/StereoscopicFrameRenderer.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/video/frame/StereoscopicOpenGlFrame.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/video/processor/AbstractFrameProcessor.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/video/processor/CubicToARGBProcessor.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/video/processor/EquirectangularToARGBProcessor.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/video/processor/OpenGlToARGBProcessor.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/video/processor/StereoscopicToARGBProcessor.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/video/rendering/Frame.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/video/rendering/FrameCapturer.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/video/rendering/FrameConsumer.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/video/rendering/FrameProcessor.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/video/rendering/Pipeline.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/video/rendering/Pipelines.java 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