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
This commit is contained in:
@@ -22,11 +22,12 @@ import eu.crushedpixel.replaymod.settings.RenderOptions;
|
|||||||
import eu.crushedpixel.replaymod.settings.ReplaySettings;
|
import eu.crushedpixel.replaymod.settings.ReplaySettings;
|
||||||
import eu.crushedpixel.replaymod.sound.SoundHandler;
|
import eu.crushedpixel.replaymod.sound.SoundHandler;
|
||||||
import eu.crushedpixel.replaymod.timer.ReplayTimer;
|
import eu.crushedpixel.replaymod.timer.ReplayTimer;
|
||||||
|
import eu.crushedpixel.replaymod.utils.OpenGLUtils;
|
||||||
import eu.crushedpixel.replaymod.utils.ReplayFile;
|
import eu.crushedpixel.replaymod.utils.ReplayFile;
|
||||||
import eu.crushedpixel.replaymod.utils.ReplayFileIO;
|
import eu.crushedpixel.replaymod.utils.ReplayFileIO;
|
||||||
import eu.crushedpixel.replaymod.utils.TooltipRenderer;
|
import eu.crushedpixel.replaymod.utils.TooltipRenderer;
|
||||||
import eu.crushedpixel.replaymod.video.frame.*;
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
import eu.crushedpixel.replaymod.video.rendering.Pipelines;
|
||||||
import net.minecraft.client.Minecraft;
|
import net.minecraft.client.Minecraft;
|
||||||
import net.minecraft.client.renderer.entity.RenderPlayer;
|
import net.minecraft.client.renderer.entity.RenderPlayer;
|
||||||
import net.minecraft.client.resources.IResourcePack;
|
import net.minecraft.client.resources.IResourcePack;
|
||||||
@@ -110,6 +111,10 @@ public class ReplayMod {
|
|||||||
e.printStackTrace();
|
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 = new Configuration(event.getSuggestedConfigurationFile());
|
||||||
config.load();
|
config.load();
|
||||||
AuthenticationHandler.loadAuthkeyFromConfig();
|
AuthenticationHandler.loadAuthkeyFromConfig();
|
||||||
@@ -280,31 +285,29 @@ public class ReplayMod {
|
|||||||
options.setExportCommandArgs(exportCommandArgs);
|
options.setExportCommandArgs(exportCommandArgs);
|
||||||
}
|
}
|
||||||
|
|
||||||
FrameRenderer renderer;
|
Pipelines.Preset pipelinePreset = Pipelines.Preset.DEFAULT;
|
||||||
if (type != null) {
|
if (type != null) {
|
||||||
String[] parts = type.split(":");
|
String[] parts = type.split(":");
|
||||||
type = parts[0].toUpperCase();
|
type = parts[0].toUpperCase();
|
||||||
if ("NORMAL".equals(type) || "DEFAULT".equals(type)) {
|
if ("NORMAL".equals(type) || "DEFAULT".equals(type)) {
|
||||||
renderer = new DefaultFrameRenderer(options);
|
pipelinePreset = Pipelines.Preset.DEFAULT;
|
||||||
} else if ("STEREO".equals(type) || "STEREOSCOPIC".equals(type)) {
|
} else if ("STEREO".equals(type) || "STEREOSCOPIC".equals(type)) {
|
||||||
renderer = new StereoscopicFrameRenderer(options);
|
pipelinePreset = Pipelines.Preset.STEREOSCOPIC;
|
||||||
} else if ("CUBIC".equals(type)) {
|
} else if ("CUBIC".equals(type)) {
|
||||||
if (parts.length < 2) {
|
if (parts.length < 2) {
|
||||||
throw new IllegalArgumentException("Cubic renderer requires boolean for whether it's stable.");
|
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)) {
|
} else if ("EQUIRECTANGULAR".equals(type)) {
|
||||||
if (parts.length < 2) {
|
if (parts.length < 2) {
|
||||||
throw new IllegalArgumentException("Equirectangular renderer requires boolean for whether it's stable.");
|
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 {
|
} else {
|
||||||
throw new IllegalArgumentException("Unknown type: " + parts[0]);
|
throw new IllegalArgumentException("Unknown type: " + parts[0]);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
renderer = new DefaultFrameRenderer(options);
|
|
||||||
}
|
}
|
||||||
options.setRenderer(renderer);
|
options.setMode(pipelinePreset);
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
Queue<ListenableFutureTask> tasks = mc.scheduledTasks;
|
Queue<ListenableFutureTask> tasks = mc.scheduledTasks;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import eu.crushedpixel.replaymod.settings.RenderOptions;
|
|||||||
import eu.crushedpixel.replaymod.utils.MouseUtils;
|
import eu.crushedpixel.replaymod.utils.MouseUtils;
|
||||||
import eu.crushedpixel.replaymod.utils.ReplayFileIO;
|
import eu.crushedpixel.replaymod.utils.ReplayFileIO;
|
||||||
import eu.crushedpixel.replaymod.utils.StringUtils;
|
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.Minecraft;
|
||||||
import net.minecraft.client.gui.GuiButton;
|
import net.minecraft.client.gui.GuiButton;
|
||||||
import net.minecraft.client.gui.GuiErrorScreen;
|
import net.minecraft.client.gui.GuiErrorScreen;
|
||||||
@@ -449,8 +449,6 @@ public class GuiRenderSettings extends GuiScreen {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void startRendering() {
|
private void startRendering() {
|
||||||
FrameRenderer renderer = null;
|
|
||||||
|
|
||||||
RendererSettings r = rendererDropdown.getElement(rendererDropdown.getSelectionIndex());
|
RendererSettings r = rendererDropdown.getElement(rendererDropdown.getSelectionIndex());
|
||||||
|
|
||||||
RenderOptions options = new RenderOptions();
|
RenderOptions options = new RenderOptions();
|
||||||
@@ -477,16 +475,19 @@ public class GuiRenderSettings extends GuiScreen {
|
|||||||
options.setWidth(getWidthSetting());
|
options.setWidth(getWidthSetting());
|
||||||
options.setHeight(getHeightSetting());
|
options.setHeight(getHeightSetting());
|
||||||
|
|
||||||
|
Pipelines.Preset pipePreset = Pipelines.Preset.DEFAULT;
|
||||||
if(r == RendererSettings.DEFAULT) {
|
if(r == RendererSettings.DEFAULT) {
|
||||||
renderer = new DefaultFrameRenderer(options);
|
pipePreset = Pipelines.Preset.DEFAULT;
|
||||||
} else if(r == RendererSettings.STEREOSCOPIC) {
|
} else if(r == RendererSettings.STEREOSCOPIC) {
|
||||||
renderer = new StereoscopicFrameRenderer(options);
|
pipePreset = Pipelines.Preset.STEREOSCOPIC;
|
||||||
} else if(r == RendererSettings.CUBIC) {
|
} else if(r == RendererSettings.CUBIC) {
|
||||||
renderer = new CubicFrameRenderer(options, ignoreCamDir.isChecked());
|
pipePreset = Pipelines.Preset.CUBIC;
|
||||||
} else if(r == RendererSettings.EQUIRECTANGULAR) {
|
} 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) {
|
if(commandInput.getText().trim().length() > 0) {
|
||||||
options.setExportCommand(commandInput.getText().trim());
|
options.setExportCommand(commandInput.getText().trim());
|
||||||
@@ -525,6 +526,9 @@ public class GuiRenderSettings extends GuiScreen {
|
|||||||
ignoreCamDir.enabled = true;
|
ignoreCamDir.enabled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!ignoreCamDir.enabled) {
|
||||||
|
ignoreCamDir.setIsChecked(false);
|
||||||
|
}
|
||||||
|
|
||||||
xRes.setCursorPositionEnd();
|
xRes.setCursorPositionEnd();
|
||||||
xRes.setSelectionPos(xRes.getCursorPosition());
|
xRes.setSelectionPos(xRes.getCursorPosition());
|
||||||
|
|||||||
@@ -1,18 +1,32 @@
|
|||||||
package eu.crushedpixel.replaymod.gui;
|
package eu.crushedpixel.replaymod.gui;
|
||||||
|
|
||||||
import eu.crushedpixel.replaymod.gui.elements.GuiProgressBar;
|
import eu.crushedpixel.replaymod.gui.elements.GuiProgressBar;
|
||||||
|
import eu.crushedpixel.replaymod.utils.BoundingUtils;
|
||||||
import eu.crushedpixel.replaymod.utils.DurationUtils;
|
import eu.crushedpixel.replaymod.utils.DurationUtils;
|
||||||
import eu.crushedpixel.replaymod.video.VideoRenderer;
|
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.GuiButton;
|
||||||
import net.minecraft.client.gui.GuiScreen;
|
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.client.resources.I18n;
|
||||||
|
import net.minecraft.util.ResourceLocation;
|
||||||
import net.minecraftforge.fml.client.config.GuiCheckBox;
|
import net.minecraftforge.fml.client.config.GuiCheckBox;
|
||||||
|
import org.lwjgl.util.Dimension;
|
||||||
|
import org.lwjgl.util.ReadableDimension;
|
||||||
|
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.io.IOException;
|
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 {
|
public class GuiVideoRenderer extends GuiScreen {
|
||||||
|
private static final ResourceLocation noPreviewTexture = new ResourceLocation("replaymod", "logo.jpg");
|
||||||
|
|
||||||
private final VideoRenderer renderer;
|
private final VideoRenderer renderer;
|
||||||
|
|
||||||
private final String SCREEN_TITLE = I18n.format("replaymod.gui.rendering.title");
|
private final String SCREEN_TITLE = I18n.format("replaymod.gui.rendering.title");
|
||||||
@@ -27,6 +41,9 @@ public class GuiVideoRenderer extends GuiScreen {
|
|||||||
private GuiCheckBox previewCheckBox;
|
private GuiCheckBox previewCheckBox;
|
||||||
private GuiProgressBar progressBar;
|
private GuiProgressBar progressBar;
|
||||||
|
|
||||||
|
private DynamicTexture previewTexture;
|
||||||
|
private boolean previewTextureDirty;
|
||||||
|
|
||||||
private int renderTimeTaken = 0;
|
private int renderTimeTaken = 0;
|
||||||
private long prevTime = -1;
|
private long prevTime = -1;
|
||||||
private long prevRenderTime = -1;
|
private long prevRenderTime = -1;
|
||||||
@@ -87,8 +104,6 @@ public class GuiVideoRenderer extends GuiScreen {
|
|||||||
} else if (CANCEL_CONFIRM.equals(cancelButton.displayString)) {
|
} else if (CANCEL_CONFIRM.equals(cancelButton.displayString)) {
|
||||||
renderer.cancel();
|
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 takenString = I18n.format("replaymod.gui.rendering.timetaken")+": "+DurationUtils.convertSecondsToString(renderTimeTaken/1000);
|
||||||
String leftString = I18n.format("replaymod.gui.rendering.timeleft")+": "+DurationUtils.convertSecondsToString(renderTimeLeft);
|
String leftString = I18n.format("replaymod.gui.rendering.timeleft")+": "+DurationUtils.convertSecondsToString(renderTimeLeft);
|
||||||
|
|
||||||
FrameRenderer frameRenderer = renderer.getFrameRenderer();
|
|
||||||
int centerX = width / 2;
|
int centerX = width / 2;
|
||||||
|
|
||||||
drawBackground(0);
|
drawBackground(0);
|
||||||
@@ -159,9 +173,9 @@ public class GuiVideoRenderer extends GuiScreen {
|
|||||||
int previewY = previewCheckBox.yPosition - 10 - previewHeight;
|
int previewY = previewCheckBox.yPosition - 10 - previewHeight;
|
||||||
|
|
||||||
if(previewCheckBox.isChecked()) {
|
if(previewCheckBox.isChecked()) {
|
||||||
frameRenderer.renderPreview(previewX, previewY, previewWidth, previewHeight);
|
renderPreview(previewX, previewY, previewWidth, previewHeight);
|
||||||
} else {
|
} else {
|
||||||
FrameRenderer.renderNoPreview(previewX, previewY, previewWidth, previewHeight);
|
renderNoPreview(previewX, previewY, previewWidth, previewHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
drawString(fontRendererObj, takenString, 12, previewCheckBox.yPosition + 5 + 20, Color.WHITE.getRGB());
|
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);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,23 @@
|
|||||||
package eu.crushedpixel.replaymod.settings;
|
package eu.crushedpixel.replaymod.settings;
|
||||||
|
|
||||||
import eu.crushedpixel.replaymod.utils.ReplayFileIO;
|
import eu.crushedpixel.replaymod.video.rendering.Pipelines;
|
||||||
import eu.crushedpixel.replaymod.video.frame.FrameRenderer;
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import net.minecraft.client.Minecraft;
|
import net.minecraft.client.Minecraft;
|
||||||
|
|
||||||
import java.io.File;
|
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.isTrue;
|
||||||
import static org.apache.commons.lang3.Validate.notNull;
|
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public final class RenderOptions {
|
public final class RenderOptions {
|
||||||
private FrameRenderer renderer;
|
private Pipelines.Preset mode = Pipelines.Preset.DEFAULT;
|
||||||
private String bitrate = "10M";
|
private String bitrate = "10M";
|
||||||
private int fps = 30;
|
private int fps = 30;
|
||||||
|
|
||||||
private File outputFile;
|
private File outputFile;
|
||||||
|
|
||||||
|
private boolean ignoreCameraRotation;
|
||||||
|
|
||||||
// Advanced
|
// Advanced
|
||||||
private boolean waitForChunks = true;
|
private boolean waitForChunks = true;
|
||||||
private boolean isLinearMovement = false;
|
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 - " +
|
private String exportCommandArgs = "-f rawvideo -pix_fmt argb -s %WIDTH%x%HEIGHT% -r %FPS% -i - " +
|
||||||
"-an " +
|
"-an " +
|
||||||
"-c:v libvpx -b:v %BITRATE% %FILENAME%";
|
"-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() {
|
public int getFps() {
|
||||||
return fps;
|
return fps;
|
||||||
@@ -53,9 +46,10 @@ public final class RenderOptions {
|
|||||||
|
|
||||||
public RenderOptions copy() {
|
public RenderOptions copy() {
|
||||||
RenderOptions copy = new RenderOptions();
|
RenderOptions copy = new RenderOptions();
|
||||||
copy.renderer = this.renderer;
|
copy.mode = this.mode;
|
||||||
copy.bitrate = this.bitrate;
|
copy.bitrate = this.bitrate;
|
||||||
copy.fps = this.fps;
|
copy.fps = this.fps;
|
||||||
|
copy.ignoreCameraRotation = this.ignoreCameraRotation;
|
||||||
copy.waitForChunks = this.waitForChunks;
|
copy.waitForChunks = this.waitForChunks;
|
||||||
copy.isLinearMovement = this.isLinearMovement;
|
copy.isLinearMovement = this.isLinearMovement;
|
||||||
copy.skyColor = this.skyColor;
|
copy.skyColor = this.skyColor;
|
||||||
@@ -63,7 +57,6 @@ public final class RenderOptions {
|
|||||||
copy.height = this.height;
|
copy.height = this.height;
|
||||||
copy.exportCommand = this.exportCommand;
|
copy.exportCommand = this.exportCommand;
|
||||||
copy.exportCommandArgs = this.exportCommandArgs;
|
copy.exportCommandArgs = this.exportCommandArgs;
|
||||||
copy.writerQueueSize = this.writerQueueSize;
|
|
||||||
return copy;
|
return copy;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<Integer, List<SoftReference<ByteBuffer>>> bufferPool = Maps.newHashMap();
|
||||||
|
|
||||||
|
public static synchronized ByteBuffer allocate(int size) {
|
||||||
|
List<SoftReference<ByteBuffer>> available = bufferPool.get(size);
|
||||||
|
if (available != null) {
|
||||||
|
Iterator<SoftReference<ByteBuffer>> iter = available.iterator();
|
||||||
|
try {
|
||||||
|
while (iter.hasNext()) {
|
||||||
|
SoftReference<ByteBuffer> 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<SoftReference<ByteBuffer>> available = bufferPool.get(size);
|
||||||
|
if (available == null) {
|
||||||
|
available = new LinkedList<SoftReference<ByteBuffer>>();
|
||||||
|
bufferPool.put(size, available);
|
||||||
|
}
|
||||||
|
available.add(new SoftReference<ByteBuffer>(buffer));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,8 +3,6 @@ package eu.crushedpixel.replaymod.utils;
|
|||||||
import org.lwjgl.BufferUtils;
|
import org.lwjgl.BufferUtils;
|
||||||
import org.lwjgl.opengl.GL11;
|
import org.lwjgl.opengl.GL11;
|
||||||
|
|
||||||
import java.awt.image.BufferedImage;
|
|
||||||
import java.awt.image.DataBufferInt;
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.IntBuffer;
|
import java.nio.IntBuffer;
|
||||||
|
|
||||||
@@ -19,23 +17,28 @@ public class OpenGLUtils {
|
|||||||
VIEWPORT_MAX_HEIGHT = buffer.get();
|
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();
|
* Magic init method which has to be called from the OpenGL thread so the variables in this class
|
||||||
int imageWidth = image.getWidth();
|
* 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;
|
int bufferSize = buffer.remaining() / 3;
|
||||||
// Read the OpenGL image row by row from right to left (flipped horizontally)
|
// Read the OpenGL image row by row from right to left (flipped horizontally)
|
||||||
for (int i = bufferSize - 1; i >= 0; i--) {
|
for (int i = bufferSize - 1; i >= 0; i--) {
|
||||||
// Coordinates in the final image
|
// Coordinates in the final image
|
||||||
int x = offsetX + bufferWidth - i % bufferWidth - 1; // X coord of OpenGL image has to be flipped first
|
int x = xOffset + bufferWidth - i % bufferWidth - 1; // X coord of OpenGL image has to be flipped first
|
||||||
int y = offsetY + i / bufferWidth;
|
int y = yOffset + i / bufferWidth;
|
||||||
if (x >= imageWidth || y * imageWidth >= pixels.length) {
|
|
||||||
buffer.position(buffer.position() + 3); // Pixel would end up outside of image
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// Write to image (row by row, left to right)
|
// 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,14 +16,18 @@ import eu.crushedpixel.replaymod.replay.ReplaySender;
|
|||||||
import eu.crushedpixel.replaymod.settings.RenderOptions;
|
import eu.crushedpixel.replaymod.settings.RenderOptions;
|
||||||
import eu.crushedpixel.replaymod.timer.EnchantmentTimer;
|
import eu.crushedpixel.replaymod.timer.EnchantmentTimer;
|
||||||
import eu.crushedpixel.replaymod.timer.ReplayTimer;
|
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.Minecraft;
|
||||||
import net.minecraft.client.gui.ScaledResolution;
|
import net.minecraft.client.gui.ScaledResolution;
|
||||||
import net.minecraft.util.Timer;
|
import net.minecraft.util.Timer;
|
||||||
import org.lwjgl.input.Mouse;
|
import org.lwjgl.input.Mouse;
|
||||||
import org.lwjgl.opengl.Display;
|
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.io.IOException;
|
||||||
import java.util.concurrent.FutureTask;
|
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_COLOR_BUFFER_BIT;
|
||||||
import static org.lwjgl.opengl.GL11.GL_DEPTH_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 Minecraft mc = Minecraft.getMinecraft();
|
||||||
private final FrameRenderer frameRenderer;
|
|
||||||
private final VideoWriter videoWriter;
|
|
||||||
private final ReplaySender replaySender;
|
private final ReplaySender replaySender;
|
||||||
private final RenderOptions options;
|
private final RenderOptions options;
|
||||||
|
|
||||||
@@ -52,12 +54,19 @@ public class VideoRenderer {
|
|||||||
private boolean paused;
|
private boolean paused;
|
||||||
private boolean cancelled;
|
private boolean cancelled;
|
||||||
|
|
||||||
|
private final Pipeline renderingPipeline;
|
||||||
|
|
||||||
public VideoRenderer(RenderOptions options) throws IOException {
|
public VideoRenderer(RenderOptions options) throws IOException {
|
||||||
this.frameRenderer = options.getRenderer();
|
|
||||||
this.videoWriter = new VideoWriter(this, options);
|
|
||||||
this.gui = new GuiVideoRenderer(this);
|
this.gui = new GuiVideoRenderer(this);
|
||||||
this.replaySender = ReplayMod.replaySender;
|
this.replaySender = ReplayMod.replaySender;
|
||||||
this.options = options;
|
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;
|
mc.renderGlobal.renderEntitiesStartupCounter = 0;
|
||||||
|
|
||||||
framesDone = 0;
|
renderingPipeline.run();
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
finish();
|
finish();
|
||||||
return !cancelled;
|
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() {
|
private void setup() {
|
||||||
if (Mouse.isGrabbed()) {
|
if (Mouse.isGrabbed()) {
|
||||||
mouseWasGrabbed = true;
|
mouseWasGrabbed = true;
|
||||||
@@ -171,7 +180,6 @@ public class VideoRenderer {
|
|||||||
}
|
}
|
||||||
time.prepare();
|
time.prepare();
|
||||||
|
|
||||||
frameRenderer.setup();
|
|
||||||
|
|
||||||
ScaledResolution scaled = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight);
|
ScaledResolution scaled = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight);
|
||||||
gui.setWorldAndResolution(mc, scaled.getScaledWidth(), scaled.getScaledHeight());
|
gui.setWorldAndResolution(mc, scaled.getScaledWidth(), scaled.getScaledHeight());
|
||||||
@@ -182,18 +190,8 @@ public class VideoRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void finish() {
|
private void finish() {
|
||||||
if (cancelled) {
|
|
||||||
videoWriter.abortRecording();
|
|
||||||
} else {
|
|
||||||
videoWriter.endRecording();
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
videoWriter.waitForFinish();
|
|
||||||
} catch (InterruptedException ignored) { }
|
|
||||||
|
|
||||||
frameRenderer.tearDown();
|
|
||||||
if (mouseWasGrabbed) {
|
if (mouseWasGrabbed) {
|
||||||
Mouse.setGrabbed(true);
|
mc.mouseHelper.grabMouseCursor();
|
||||||
}
|
}
|
||||||
ReplayTimer.get(mc).passive = false;
|
ReplayTimer.get(mc).passive = false;
|
||||||
mc.displayGuiScreen(null);
|
mc.displayGuiScreen(null);
|
||||||
@@ -202,6 +200,8 @@ public class VideoRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ReplayMod.soundHandler.playRenderSuccessSound();
|
ReplayMod.soundHandler.playRenderSuccessSound();
|
||||||
|
|
||||||
|
mc.displayGuiScreen(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateCam() {
|
private void updateCam() {
|
||||||
@@ -320,9 +320,7 @@ public class VideoRenderer {
|
|||||||
TimestampValue timestampValue = new TimestampValue();
|
TimestampValue timestampValue = new TimestampValue();
|
||||||
time.applyPoint(Math.max(0, Math.min(1, timePos)), timestampValue);
|
time.applyPoint(Math.max(0, Math.min(1, timePos)), timestampValue);
|
||||||
Integer replayTime = (int) timestampValue.value;
|
Integer replayTime = (int) timestampValue.value;
|
||||||
if(replayTime != null) {
|
replaySender.sendPacketsTill(replayTime);
|
||||||
replaySender.sendPacketsTill(replayTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (curSpeed >= 0) {
|
if (curSpeed >= 0) {
|
||||||
replaySender.setReplaySpeed(curSpeed);
|
replaySender.setReplaySpeed(curSpeed);
|
||||||
@@ -337,7 +335,7 @@ public class VideoRenderer {
|
|||||||
timer.renderPartialTicks = timer.elapsedPartialTicks;
|
timer.renderPartialTicks = timer.elapsedPartialTicks;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void tick() throws IOException {
|
private void tick() {
|
||||||
synchronized (mc.scheduledTasks) {
|
synchronized (mc.scheduledTasks) {
|
||||||
while (!mc.scheduledTasks.isEmpty()) {
|
while (!mc.scheduledTasks.isEmpty()) {
|
||||||
((FutureTask) mc.scheduledTasks.poll()).run();
|
((FutureTask) mc.scheduledTasks.poll()).run();
|
||||||
@@ -345,89 +343,70 @@ public class VideoRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mc.currentScreen = gui;
|
mc.currentScreen = gui;
|
||||||
mc.runTick();
|
try {
|
||||||
}
|
mc.runTick();
|
||||||
|
} catch (IOException e) {
|
||||||
private void frame(Timer timer) {
|
throw new RuntimeException(e);
|
||||||
updateCam();
|
|
||||||
|
|
||||||
BufferedImage frame = null;
|
|
||||||
while (frame == null) {
|
|
||||||
try {
|
|
||||||
frame = frameRenderer.captureFrame(timer);
|
|
||||||
} catch (OutOfMemoryError e) {
|
|
||||||
System.out.println("Caught oom error-> calling garbage collector, decreasing queue size and waiting for video writer");
|
|
||||||
System.gc();
|
|
||||||
videoWriter.waitTillQueueEmpty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
while (!videoWriter.writeImage(frame, false)) {
|
|
||||||
try {
|
|
||||||
Thread.sleep(10);
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
drawGui();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void pauseIfNeeded() {
|
|
||||||
while (paused && !cancelled && !Display.isCloseRequested()) {
|
|
||||||
drawGui();
|
|
||||||
try {
|
|
||||||
Thread.sleep(20);
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void drawGui() {
|
public void drawGui() {
|
||||||
pushMatrix();
|
do {
|
||||||
clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
pushMatrix();
|
||||||
enableTexture2D();
|
clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||||
mc.getFramebuffer().bindFramebuffer(true);
|
enableTexture2D();
|
||||||
mc.entityRenderer.setupOverlayRendering();
|
mc.getFramebuffer().bindFramebuffer(true);
|
||||||
|
mc.entityRenderer.setupOverlayRendering();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
gui.handleInput();
|
gui.handleInput();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
// That's a strange exception from this kind of method O_o
|
// 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
|
// It isn't actually thrown here, so we'll deal with it the easy way
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
ScaledResolution scaled = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight);
|
ScaledResolution scaled = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight);
|
||||||
int mouseX = Mouse.getX() * scaled.getScaledWidth() / mc.displayWidth;
|
int mouseX = Mouse.getX() * scaled.getScaledWidth() / mc.displayWidth;
|
||||||
int mouseY = scaled.getScaledHeight() - Mouse.getY() * scaled.getScaledHeight() / mc.displayHeight - 1;
|
int mouseY = scaled.getScaledHeight() - Mouse.getY() * scaled.getScaledHeight() / mc.displayHeight - 1;
|
||||||
gui.drawScreen(mouseX, mouseY, 0);
|
gui.drawScreen(mouseX, mouseY, 0);
|
||||||
|
|
||||||
mc.getFramebuffer().unbindFramebuffer();
|
mc.getFramebuffer().unbindFramebuffer();
|
||||||
popMatrix();
|
popMatrix();
|
||||||
pushMatrix();
|
pushMatrix();
|
||||||
mc.getFramebuffer().framebufferRender(mc.displayWidth, mc.displayHeight);
|
mc.getFramebuffer().framebufferRender(mc.displayWidth, mc.displayHeight);
|
||||||
popMatrix();
|
popMatrix();
|
||||||
|
|
||||||
Display.update();
|
Display.update();
|
||||||
if (Mouse.isGrabbed()) {
|
if (Mouse.isGrabbed()) {
|
||||||
Mouse.setGrabbed(false);
|
Mouse.setGrabbed(false);
|
||||||
}
|
}
|
||||||
|
if (paused) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(50);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (paused);
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getFramesDone() {
|
public int getFramesDone() {
|
||||||
return framesDone;
|
return framesDone;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ReadableDimension getFrameSize() {
|
||||||
|
return new Dimension(options.getWidth(), options.getHeight());
|
||||||
|
}
|
||||||
|
|
||||||
public int getTotalFrames() {
|
public int getTotalFrames() {
|
||||||
return totalFrames;
|
return totalFrames;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getVideoTime() { return framesDone * 1000 / fps; }
|
public int getVideoTime() { return framesDone * 1000 / fps; }
|
||||||
|
|
||||||
public FrameRenderer getFrameRenderer() {
|
|
||||||
return frameRenderer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setPaused(boolean paused) {
|
public void setPaused(boolean paused) {
|
||||||
this.paused = paused;
|
this.paused = paused;
|
||||||
}
|
}
|
||||||
@@ -438,5 +417,6 @@ public class VideoRenderer {
|
|||||||
|
|
||||||
public void cancel() {
|
public void cancel() {
|
||||||
this.cancelled = true;
|
this.cancelled = true;
|
||||||
|
renderingPipeline.cancel();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,63 +1,44 @@
|
|||||||
package eu.crushedpixel.replaymod.video;
|
package eu.crushedpixel.replaymod.video;
|
||||||
|
|
||||||
import com.google.common.base.Preconditions;
|
|
||||||
import eu.crushedpixel.replaymod.settings.RenderOptions;
|
import eu.crushedpixel.replaymod.settings.RenderOptions;
|
||||||
|
import eu.crushedpixel.replaymod.utils.ByteBufferPool;
|
||||||
import eu.crushedpixel.replaymod.utils.StreamPipe;
|
import eu.crushedpixel.replaymod.utils.StreamPipe;
|
||||||
import eu.crushedpixel.replaymod.utils.StringUtils;
|
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.client.Minecraft;
|
||||||
import net.minecraft.crash.CrashReport;
|
import net.minecraft.crash.CrashReport;
|
||||||
import net.minecraft.crash.CrashReportCategory;
|
import net.minecraft.crash.CrashReportCategory;
|
||||||
import org.apache.commons.io.IOUtils;
|
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.File;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.nio.channels.Channels;
|
import java.nio.channels.Channels;
|
||||||
import java.nio.channels.WritableByteChannel;
|
import java.nio.channels.WritableByteChannel;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Queue;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.locks.Condition;
|
|
||||||
import java.util.concurrent.locks.Lock;
|
|
||||||
import java.util.concurrent.locks.ReentrantLock;
|
|
||||||
|
|
||||||
import static org.apache.commons.lang3.Validate.isTrue;
|
import static org.apache.commons.lang3.Validate.isTrue;
|
||||||
|
|
||||||
public class VideoWriter {
|
public class VideoWriter implements FrameConsumer<ARGBFrame> {
|
||||||
|
|
||||||
private final RenderOptions options;
|
private final RenderOptions options;
|
||||||
private final Process process;
|
private final Process process;
|
||||||
private final OutputStream outputStream;
|
private final OutputStream outputStream;
|
||||||
private final WritableByteChannel channel;
|
private final WritableByteChannel channel;
|
||||||
|
private final String commandArgs;
|
||||||
|
|
||||||
private volatile boolean active = true;
|
public VideoWriter(final RenderOptions options) throws IOException {
|
||||||
private volatile boolean cancelled = false;
|
|
||||||
|
|
||||||
private int queueLimit;
|
|
||||||
private final Queue<BufferedImage> toWrite;
|
|
||||||
private final Thread writerThread;
|
|
||||||
|
|
||||||
private final Lock lock = new ReentrantLock();
|
|
||||||
private final Condition emptyCondition = lock.newCondition();
|
|
||||||
private final Condition noLongerEmptyCondition = lock.newCondition();
|
|
||||||
private final Condition noLongerFullCondition = lock.newCondition();
|
|
||||||
|
|
||||||
public VideoWriter(final VideoRenderer renderer, final RenderOptions options) throws IOException {
|
|
||||||
this.options = options.copy();
|
this.options = options.copy();
|
||||||
this.queueLimit = options.getWriterQueueSize();
|
|
||||||
this.toWrite = new LinkedList<BufferedImage>();
|
|
||||||
|
|
||||||
File outputFolder = options.getOutputFile().getParentFile();
|
File outputFolder = options.getOutputFile().getParentFile();
|
||||||
String fileName = options.getOutputFile().getName();
|
String fileName = options.getOutputFile().getName();
|
||||||
|
|
||||||
final String args = options.getExportCommandArgs()
|
commandArgs = options.getExportCommandArgs()
|
||||||
.replace("%WIDTH%", String.valueOf(options.getWidth()))
|
.replace("%WIDTH%", String.valueOf(options.getWidth()))
|
||||||
.replace("%HEIGHT%", String.valueOf(options.getHeight()))
|
.replace("%HEIGHT%", String.valueOf(options.getHeight()))
|
||||||
.replace("%FPS%", String.valueOf(options.getFps()))
|
.replace("%FPS%", String.valueOf(options.getFps()))
|
||||||
@@ -66,127 +47,63 @@ public class VideoWriter {
|
|||||||
|
|
||||||
List<String> command = new ArrayList<String>();
|
List<String> command = new ArrayList<String>();
|
||||||
command.add(options.getExportCommand());
|
command.add(options.getExportCommand());
|
||||||
command.addAll(StringUtils.translateCommandline(args));
|
command.addAll(StringUtils.translateCommandline(commandArgs));
|
||||||
System.out.println("Starting " + options.getExportCommand() + " with args: " + args);
|
System.out.println("Starting " + options.getExportCommand() + " with args: " + commandArgs);
|
||||||
process = new ProcessBuilder(command).directory(outputFolder).start();
|
process = new ProcessBuilder(command).directory(outputFolder).start();
|
||||||
OutputStream exportLogOut = new FileOutputStream("export.log");
|
OutputStream exportLogOut = new FileOutputStream("export.log");
|
||||||
new StreamPipe(process.getInputStream(), exportLogOut).start();
|
new StreamPipe(process.getInputStream(), exportLogOut).start();
|
||||||
new StreamPipe(process.getErrorStream(), exportLogOut).start();
|
new StreamPipe(process.getErrorStream(), exportLogOut).start();
|
||||||
outputStream = process.getOutputStream();
|
outputStream = process.getOutputStream();
|
||||||
channel = Channels.newChannel(outputStream);
|
channel = Channels.newChannel(outputStream);
|
||||||
|
}
|
||||||
|
|
||||||
writerThread = new Thread(new Runnable() {
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
IOUtils.closeQuietly(outputStream);
|
||||||
|
|
||||||
@Override
|
long startTime = System.nanoTime();
|
||||||
public void run() {
|
long rem = TimeUnit.SECONDS.toNanos(30);
|
||||||
try {
|
do {
|
||||||
while (!cancelled && (active || !toWrite.isEmpty())) {
|
try {
|
||||||
try {
|
process.exitValue();
|
||||||
lock.lockInterruptibly();
|
break;
|
||||||
BufferedImage img;
|
} catch(IllegalThreadStateException ex) {
|
||||||
try {
|
if (rem > 0) {
|
||||||
img = toWrite.poll();
|
try {
|
||||||
if (img == null) {
|
Thread.sleep(Math.min(TimeUnit.NANOSECONDS.toMillis(rem) + 1, 100));
|
||||||
noLongerEmptyCondition.await();
|
} catch (InterruptedException e) {
|
||||||
img = toWrite.poll();
|
Thread.currentThread().interrupt();
|
||||||
}
|
break;
|
||||||
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) {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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");
|
rem = TimeUnit.SECONDS.toNanos(30) - (System.nanoTime() - startTime);
|
||||||
writerThread.start();
|
} while (rem > 0);
|
||||||
|
|
||||||
|
process.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Override
|
||||||
* Add the image to the writer queue.
|
public void consume(ARGBFrame frame) {
|
||||||
* @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();
|
|
||||||
try {
|
try {
|
||||||
while (toWrite.size() >= queueLimit) {
|
checkSize(frame.getSize());
|
||||||
if (waitIfFull) {
|
channel.write(frame.getByteBuffer());
|
||||||
noLongerFullCondition.await();
|
ByteBufferPool.release(frame.getByteBuffer());
|
||||||
} else {
|
} catch (Throwable t) {
|
||||||
return false;
|
CrashReport report = CrashReport.makeCrashReport(t, "Exporting frame");
|
||||||
}
|
CrashReportCategory exportDetails = report.makeCategory("Export details");
|
||||||
}
|
exportDetails.addCrashSection("Export command", options.getExportCommand());
|
||||||
toWrite.offer(image);
|
exportDetails.addCrashSection("Export args", commandArgs);
|
||||||
noLongerEmptyCondition.signal();
|
Minecraft.getMinecraft().crashed(report);
|
||||||
return true;
|
|
||||||
} catch (InterruptedException ignored) {
|
|
||||||
} finally {
|
|
||||||
lock.unlock();
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void endRecording() {
|
|
||||||
active = false;
|
|
||||||
writerThread.interrupt();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void abortRecording() {
|
|
||||||
cancelled = true;
|
|
||||||
active = false;
|
|
||||||
writerThread.interrupt();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void waitTillQueueEmpty() {
|
|
||||||
lock.lock();
|
|
||||||
try {
|
|
||||||
if (!toWrite.isEmpty()) {
|
|
||||||
emptyCondition.await();
|
|
||||||
}
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
} finally {
|
|
||||||
lock.unlock();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void waitForFinish() throws InterruptedException {
|
private void checkSize(ReadableDimension size) {
|
||||||
Preconditions.checkState(!active, "Video writer still active.");
|
checkSize(size.getWidth(), size.getHeight());
|
||||||
writerThread.join();
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package eu.crushedpixel.replaymod.video.capturer;
|
||||||
|
|
||||||
|
public interface CaptureData {
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package eu.crushedpixel.replaymod.video.capturer;
|
||||||
|
|
||||||
|
import eu.crushedpixel.replaymod.video.frame.CubicOpenGlFrame;
|
||||||
|
|
||||||
|
public class CubicOpenGlFrameCapturer extends OpenGlFrameCapturer<CubicOpenGlFrame, CubicOpenGlFrameCapturer.Data> {
|
||||||
|
public enum Data implements CaptureData {
|
||||||
|
LEFT, RIGHT, FRONT, BACK, TOP, BOTTOM
|
||||||
|
}
|
||||||
|
|
||||||
|
private final int frameSize;
|
||||||
|
public CubicOpenGlFrameCapturer(WorldRenderer<Data> 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<F extends Frame, D extends CaptureData> implements FrameCapturer<F> {
|
||||||
|
protected final WorldRenderer<D> worldRenderer;
|
||||||
|
protected final RenderInfo renderInfo;
|
||||||
|
protected int framesDone;
|
||||||
|
private Framebuffer frameBuffer;
|
||||||
|
|
||||||
|
public OpenGlFrameCapturer(WorldRenderer<D> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package eu.crushedpixel.replaymod.video.capturer;
|
||||||
|
|
||||||
|
import eu.crushedpixel.replaymod.video.frame.OpenGlFrame;
|
||||||
|
|
||||||
|
public class SimpleOpenGlFrameCapturer extends OpenGlFrameCapturer<OpenGlFrame, CaptureData> {
|
||||||
|
|
||||||
|
public SimpleOpenGlFrameCapturer(WorldRenderer<CaptureData> worldRenderer, RenderInfo renderInfo) {
|
||||||
|
super(worldRenderer, renderInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OpenGlFrame process() {
|
||||||
|
float partialTicks = renderInfo.updateForNextFrame();
|
||||||
|
return renderFrame(framesDone++, partialTicks);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<StereoscopicOpenGlFrame, StereoscopicOpenGlFrameCapturer.Data> {
|
||||||
|
public enum Data implements CaptureData {
|
||||||
|
LEFT_EYE, RIGHT_EYE
|
||||||
|
}
|
||||||
|
|
||||||
|
public StereoscopicOpenGlFrameCapturer(WorldRenderer<Data> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package eu.crushedpixel.replaymod.video.capturer;
|
||||||
|
|
||||||
|
import org.lwjgl.util.ReadableDimension;
|
||||||
|
|
||||||
|
import java.io.Closeable;
|
||||||
|
|
||||||
|
public interface WorldRenderer<D extends CaptureData> extends Closeable {
|
||||||
|
void renderWorld(ReadableDimension displaySize, float partialTicks, D data);
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package eu.crushedpixel.replaymod.video.entity;
|
package eu.crushedpixel.replaymod.video.entity;
|
||||||
|
|
||||||
import eu.crushedpixel.replaymod.replay.ReplayHandler;
|
|
||||||
import eu.crushedpixel.replaymod.settings.RenderOptions;
|
import eu.crushedpixel.replaymod.settings.RenderOptions;
|
||||||
|
import eu.crushedpixel.replaymod.video.capturer.CubicOpenGlFrameCapturer;
|
||||||
import net.minecraft.client.particle.EntityFX;
|
import net.minecraft.client.particle.EntityFX;
|
||||||
import net.minecraft.client.renderer.GlStateManager;
|
import net.minecraft.client.renderer.GlStateManager;
|
||||||
import net.minecraft.client.renderer.WorldRenderer;
|
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.entity.Entity;
|
||||||
import net.minecraft.util.ResourceLocation;
|
import net.minecraft.util.ResourceLocation;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
|
|
||||||
public class CubicEntityRenderer extends CustomEntityRenderer {
|
public class CubicEntityRenderer extends CustomEntityRenderer<CubicOpenGlFrameCapturer.Data> {
|
||||||
|
|
||||||
public enum Direction {
|
public CubicEntityRenderer(RenderOptions options) {
|
||||||
TOP(1), BOTTOM(9), LEFT(4), FRONT(5), RIGHT(6), BACK(7);
|
super(options);
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Field hookField = RenderManager.class.getField("hook");
|
Field hookField = RenderManager.class.getField("hook");
|
||||||
@@ -54,8 +28,8 @@ public class CubicEntityRenderer extends CustomEntityRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void cleanup() {
|
public void close() throws IOException{
|
||||||
super.cleanup();
|
super.close();
|
||||||
try {
|
try {
|
||||||
Field hookField = RenderManager.class.getField("hook");
|
Field hookField = RenderManager.class.getField("hook");
|
||||||
hookField.set(mc.getRenderManager(), null);
|
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
|
@Override
|
||||||
protected void gluPerspective(float fovY, float aspect, float zNear, float zFar) {
|
protected void gluPerspective(float fovY, float aspect, float zNear, float zFar) {
|
||||||
super.gluPerspective(90, 1, zNear, zFar);
|
super.gluPerspective(90, 1, zNear, zFar);
|
||||||
@@ -88,32 +54,10 @@ public class CubicEntityRenderer extends CustomEntityRenderer {
|
|||||||
proxied.useShader = false;
|
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
|
@Override
|
||||||
protected void orientCamera(float partialTicks) {
|
protected void orientCamera(float partialTicks) {
|
||||||
Entity entity = mc.getRenderViewEntity();
|
|
||||||
|
|
||||||
// Rotate
|
// Rotate
|
||||||
switch(direction) {
|
switch (data) {
|
||||||
case FRONT:
|
case FRONT:
|
||||||
GlStateManager.rotate(0, 0.0F, 1.0F, 0.0F);
|
GlStateManager.rotate(0, 0.0F, 1.0F, 0.0F);
|
||||||
break;
|
break;
|
||||||
@@ -134,13 +78,6 @@ public class CubicEntityRenderer extends CustomEntityRenderer {
|
|||||||
break;
|
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
|
// Minecraft goes back a little so we have to invert that
|
||||||
GlStateManager.translate(0.0F, 0.0F, 0.1F);
|
GlStateManager.translate(0.0F, 0.0F, 0.1F);
|
||||||
super.orientCamera(partialTicks);
|
super.orientCamera(partialTicks);
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ package eu.crushedpixel.replaymod.video.entity;
|
|||||||
import eu.crushedpixel.replaymod.renderer.SpectatorRenderer;
|
import eu.crushedpixel.replaymod.renderer.SpectatorRenderer;
|
||||||
import eu.crushedpixel.replaymod.replay.ReplayHandler;
|
import eu.crushedpixel.replaymod.replay.ReplayHandler;
|
||||||
import eu.crushedpixel.replaymod.settings.RenderOptions;
|
import eu.crushedpixel.replaymod.settings.RenderOptions;
|
||||||
import eu.crushedpixel.replaymod.utils.OpenGLUtils;
|
import eu.crushedpixel.replaymod.video.capturer.CaptureData;
|
||||||
import eu.crushedpixel.replaymod.video.entity.strategy.*;
|
|
||||||
import net.minecraft.client.Minecraft;
|
import net.minecraft.client.Minecraft;
|
||||||
import net.minecraft.client.particle.EffectRenderer;
|
import net.minecraft.client.particle.EffectRenderer;
|
||||||
import net.minecraft.client.particle.EntityFX;
|
import net.minecraft.client.particle.EntityFX;
|
||||||
@@ -19,16 +18,17 @@ import net.minecraft.util.AxisAlignedBB;
|
|||||||
import net.minecraft.util.EnumWorldBlockLayer;
|
import net.minecraft.util.EnumWorldBlockLayer;
|
||||||
import net.minecraft.util.MathHelper;
|
import net.minecraft.util.MathHelper;
|
||||||
import net.minecraft.util.ResourceLocation;
|
import net.minecraft.util.ResourceLocation;
|
||||||
|
import org.lwjgl.util.ReadableDimension;
|
||||||
import org.lwjgl.util.glu.Project;
|
import org.lwjgl.util.glu.Project;
|
||||||
|
|
||||||
import java.awt.image.BufferedImage;
|
import java.io.IOException;
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import static net.minecraft.client.renderer.GlStateManager.*;
|
import static net.minecraft.client.renderer.GlStateManager.*;
|
||||||
import static org.lwjgl.opengl.GL11.*;
|
import static org.lwjgl.opengl.GL11.*;
|
||||||
|
|
||||||
public abstract class CustomEntityRenderer {
|
public class CustomEntityRenderer<D extends CaptureData> implements eu.crushedpixel.replaymod.video.capturer.WorldRenderer<D> {
|
||||||
public final Minecraft mc = Minecraft.getMinecraft();
|
public final Minecraft mc = Minecraft.getMinecraft();
|
||||||
public final EntityRenderer proxied = mc.entityRenderer;
|
public final EntityRenderer proxied = mc.entityRenderer;
|
||||||
protected final SpectatorRenderer spectatorRenderer = new SpectatorRenderer(){
|
protected final SpectatorRenderer spectatorRenderer = new SpectatorRenderer(){
|
||||||
@@ -37,43 +37,18 @@ public abstract class CustomEntityRenderer {
|
|||||||
CustomEntityRenderer.this.gluPerspective(fovY, aspect, zNear, zFar);
|
CustomEntityRenderer.this.gluPerspective(fovY, aspect, zNear, zFar);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
protected final FrameRenderingStrategy renderingStrategy;
|
|
||||||
public GluPerspectiveHook gluPerspectiveHook;
|
public GluPerspectiveHook gluPerspectiveHook;
|
||||||
public LoadShaderHook loadShaderHook;
|
public LoadShaderHook loadShaderHook;
|
||||||
protected int frameCount;
|
protected int frameCount;
|
||||||
|
|
||||||
|
protected D data;
|
||||||
|
|
||||||
protected final RenderOptions options;
|
protected final RenderOptions options;
|
||||||
|
|
||||||
public final int resultWidth;
|
|
||||||
public final int resultHeight;
|
|
||||||
|
|
||||||
public CustomEntityRenderer(RenderOptions options) {
|
public CustomEntityRenderer(RenderOptions options) {
|
||||||
this(options, options.getWidth(), options.getHeight());
|
|
||||||
}
|
|
||||||
|
|
||||||
public CustomEntityRenderer(RenderOptions options, int resultWidth, int resultHeight) {
|
|
||||||
this.options = options;
|
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
|
// Install entity renderer hooks
|
||||||
try {
|
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) {
|
public void withDisplaySize(int displayWidth, int displayHeight, Runnable runnable) {
|
||||||
final int prevWidth = mc.displayWidth;
|
final int prevWidth = mc.displayWidth;
|
||||||
final int prevHeight = mc.displayHeight;
|
final int prevHeight = mc.displayHeight;
|
||||||
@@ -366,6 +335,13 @@ public abstract class CustomEntityRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected void setupCameraTransform(float partialTicks) {
|
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);
|
proxied.farPlaneDistance = (float)(this.mc.gameSettings.renderDistanceChunks * 16);
|
||||||
|
|
||||||
matrixMode(GL_PROJECTION);
|
matrixMode(GL_PROJECTION);
|
||||||
@@ -377,6 +353,12 @@ public abstract class CustomEntityRenderer {
|
|||||||
loadIdentity();
|
loadIdentity();
|
||||||
|
|
||||||
orientCamera(partialTicks);
|
orientCamera(partialTicks);
|
||||||
|
|
||||||
|
entity.rotationYaw = orgYaw;
|
||||||
|
entity.rotationPitch = orgPitch;
|
||||||
|
entity.prevRotationYaw = orgPrevYaw;
|
||||||
|
entity.prevRotationPitch = orgPrevPitch;
|
||||||
|
ReplayHandler.setCameraTilt(orgRoll);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void setupFog(int fogDistanceFlag, float partialTicks) {
|
protected void setupFog(int fogDistanceFlag, float partialTicks) {
|
||||||
@@ -386,6 +368,14 @@ public abstract class CustomEntityRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected void orientCamera(float partialTicks) {
|
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);
|
proxied.orientCamera(partialTicks);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -407,7 +397,8 @@ public abstract class CustomEntityRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void cleanup() {
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
try {
|
try {
|
||||||
Field hookField = EntityRenderer.class.getField("hook");
|
Field hookField = EntityRenderer.class.getField("hook");
|
||||||
hookField.set(proxied, null);
|
hookField.set(proxied, null);
|
||||||
@@ -417,10 +408,30 @@ public abstract class CustomEntityRenderer {
|
|||||||
throw new Error(e);
|
throw new Error(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderingStrategy.cleanup();
|
|
||||||
spectatorRenderer.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 {
|
public static final class NoCullingCamera implements ICamera {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -1,32 +1,20 @@
|
|||||||
package eu.crushedpixel.replaymod.video.entity;
|
package eu.crushedpixel.replaymod.video.entity;
|
||||||
|
|
||||||
import eu.crushedpixel.replaymod.settings.RenderOptions;
|
import eu.crushedpixel.replaymod.settings.RenderOptions;
|
||||||
|
import eu.crushedpixel.replaymod.video.capturer.StereoscopicOpenGlFrameCapturer;
|
||||||
import net.minecraft.client.renderer.GlStateManager;
|
import net.minecraft.client.renderer.GlStateManager;
|
||||||
import net.minecraft.util.MathHelper;
|
import net.minecraft.util.MathHelper;
|
||||||
import net.minecraft.util.ResourceLocation;
|
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.loadIdentity;
|
||||||
import static net.minecraft.client.renderer.GlStateManager.matrixMode;
|
import static net.minecraft.client.renderer.GlStateManager.matrixMode;
|
||||||
import static org.lwjgl.opengl.GL11.GL_MODELVIEW;
|
import static org.lwjgl.opengl.GL11.GL_MODELVIEW;
|
||||||
import static org.lwjgl.opengl.GL11.GL_PROJECTION;
|
import static org.lwjgl.opengl.GL11.GL_PROJECTION;
|
||||||
|
|
||||||
public class StereoscopicEntityRenderer extends CustomEntityRenderer {
|
public class StereoscopicEntityRenderer extends CustomEntityRenderer<StereoscopicOpenGlFrameCapturer.Data> {
|
||||||
|
|
||||||
private boolean leftEye;
|
|
||||||
|
|
||||||
public StereoscopicEntityRenderer(RenderOptions options) {
|
public StereoscopicEntityRenderer(RenderOptions options) {
|
||||||
super(options, options.getWidth() / 2, options.getHeight());
|
super(options);
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -39,7 +27,7 @@ public class StereoscopicEntityRenderer extends CustomEntityRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected void translateStereoscopic() {
|
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
|
@Override
|
||||||
@@ -61,6 +49,6 @@ public class StereoscopicEntityRenderer extends CustomEntityRenderer {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void renderSpectatorHand(float partialTicks, int renderPass) {
|
protected void renderSpectatorHand(float partialTicks, int renderPass) {
|
||||||
super.renderSpectatorHand(partialTicks, leftEye ? 1 : 0);
|
super.renderSpectatorHand(partialTicks, data == StereoscopicOpenGlFrameCapturer.Data.LEFT_EYE ? 1 : 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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() {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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() {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<R extends Frame, P extends Frame> implements FrameProcessor<R, P> {
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<CubicOpenGlFrame, ARGBFrame> {
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<CubicOpenGlFrame, ARGBFrame> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<OpenGlFrame, ARGBFrame> {
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<StereoscopicOpenGlFrame, ARGBFrame> {
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package eu.crushedpixel.replaymod.video.rendering;
|
||||||
|
|
||||||
|
public interface Frame {
|
||||||
|
int getFrameId();
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package eu.crushedpixel.replaymod.video.rendering;
|
||||||
|
|
||||||
|
import java.io.Closeable;
|
||||||
|
|
||||||
|
public interface FrameCapturer<R extends Frame> extends Closeable {
|
||||||
|
|
||||||
|
boolean isDone();
|
||||||
|
|
||||||
|
R process();
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package eu.crushedpixel.replaymod.video.rendering;
|
||||||
|
|
||||||
|
import java.io.Closeable;
|
||||||
|
|
||||||
|
public interface FrameConsumer<P extends Frame> extends Closeable {
|
||||||
|
|
||||||
|
void consume(P frame);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package eu.crushedpixel.replaymod.video.rendering;
|
||||||
|
|
||||||
|
import java.io.Closeable;
|
||||||
|
|
||||||
|
public interface FrameProcessor<R extends Frame, P extends Frame> extends Closeable {
|
||||||
|
|
||||||
|
P process(R rawFrame);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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<R extends Frame, P extends Frame> implements Runnable {
|
||||||
|
|
||||||
|
private final FrameCapturer<R> capturer;
|
||||||
|
private final FrameProcessor<R, P> processor;
|
||||||
|
private int consumerNextFrame;
|
||||||
|
private final Object consumerLock = new Object();
|
||||||
|
private final FrameConsumer<P> consumer;
|
||||||
|
|
||||||
|
private Thread runningThread;
|
||||||
|
|
||||||
|
public Pipeline(FrameCapturer<R> capturer, FrameProcessor<R, P> processor, FrameConsumer<P> 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<Runnable>(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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<ARGBFrame> 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<OpenGlFrame, ARGBFrame> newDefaultPipeline(RenderInfo renderInfo, FrameConsumer<ARGBFrame> consumer) {
|
||||||
|
RenderOptions options = renderInfo.getRenderOptions();
|
||||||
|
return new Pipeline<OpenGlFrame, ARGBFrame>(
|
||||||
|
new SimpleOpenGlFrameCapturer(new CustomEntityRenderer<CaptureData>(options), renderInfo),
|
||||||
|
new OpenGlToARGBProcessor(),
|
||||||
|
consumer
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Pipeline<StereoscopicOpenGlFrame, ARGBFrame> newStereoscopicPipeline(RenderInfo renderInfo, FrameConsumer<ARGBFrame> consumer) {
|
||||||
|
RenderOptions options = renderInfo.getRenderOptions();
|
||||||
|
return new Pipeline<StereoscopicOpenGlFrame, ARGBFrame>(
|
||||||
|
new StereoscopicOpenGlFrameCapturer(new StereoscopicEntityRenderer(options), renderInfo),
|
||||||
|
new StereoscopicToARGBProcessor(),
|
||||||
|
consumer
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Pipeline<CubicOpenGlFrame, ARGBFrame> newCubicPipeline(RenderInfo renderInfo, FrameConsumer<ARGBFrame> consumer) {
|
||||||
|
RenderOptions options = renderInfo.getRenderOptions();
|
||||||
|
return new Pipeline<CubicOpenGlFrame, ARGBFrame>(
|
||||||
|
new CubicOpenGlFrameCapturer(new CubicEntityRenderer(options), renderInfo, options.getWidth() / 4),
|
||||||
|
new CubicToARGBProcessor(),
|
||||||
|
consumer
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Pipeline<CubicOpenGlFrame, ARGBFrame> newEquirectangularPipeline(RenderInfo renderInfo, FrameConsumer<ARGBFrame> consumer) {
|
||||||
|
RenderOptions options = renderInfo.getRenderOptions();
|
||||||
|
return new Pipeline<CubicOpenGlFrame, ARGBFrame>(
|
||||||
|
new CubicOpenGlFrameCapturer(new CubicEntityRenderer(options), renderInfo, options.getWidth() / 4),
|
||||||
|
new EquirectangularToARGBProcessor(options.getWidth() / 4),
|
||||||
|
consumer
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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_152351_aB # scheduledTasks
|
||||||
public net.minecraft.client.Minecraft field_110449_ao # defaultResourcePacks
|
public net.minecraft.client.Minecraft field_110449_ao # defaultResourcePacks
|
||||||
public net.minecraft.client.Minecraft field_110448_aq # mcResourcePackRepository
|
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_71386_F()L # getSystemTime
|
||||||
public net.minecraft.client.Minecraft func_71383_b(I)V # updateDebugProfilerName
|
public net.minecraft.client.Minecraft func_71383_b(I)V # updateDebugProfilerName
|
||||||
|
|||||||
Reference in New Issue
Block a user