Separate rendering into its own module
This commit is contained in:
@@ -14,13 +14,10 @@ import eu.crushedpixel.replaymod.registry.UploadedFileHandler;
|
||||
import eu.crushedpixel.replaymod.renderer.CustomObjectRenderer;
|
||||
import eu.crushedpixel.replaymod.renderer.PathPreviewRenderer;
|
||||
import eu.crushedpixel.replaymod.renderer.SpectatorRenderer;
|
||||
import eu.crushedpixel.replaymod.settings.EncodingPreset;
|
||||
import eu.crushedpixel.replaymod.settings.RenderOptions;
|
||||
import eu.crushedpixel.replaymod.settings.ReplaySettings;
|
||||
import eu.crushedpixel.replaymod.sound.SoundHandler;
|
||||
import eu.crushedpixel.replaymod.utils.OpenGLUtils;
|
||||
import eu.crushedpixel.replaymod.utils.TooltipRenderer;
|
||||
import eu.crushedpixel.replaymod.video.rendering.Pipelines;
|
||||
import lombok.Getter;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.settings.GameSettings;
|
||||
@@ -204,6 +201,7 @@ public class ReplayMod {
|
||||
String height = System.getProperty("replaymod.render.height");
|
||||
String exportCommand = System.getProperty("replaymod.render.exportcommand");
|
||||
String exportCommandArgs = System.getProperty("replaymod.render.exportcommandargs");
|
||||
/*
|
||||
final RenderOptions options = new RenderOptions();
|
||||
if (bitrate != null) {
|
||||
options.setBitrate(bitrate);
|
||||
@@ -265,6 +263,7 @@ public class ReplayMod {
|
||||
}
|
||||
}
|
||||
options.setMode(pipelinePreset);
|
||||
*/
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Queue<ListenableFutureTask> tasks = mc.scheduledTasks;
|
||||
|
||||
86
src/main/java/com/replaymod/render/RenderSettings.java
Normal file
86
src/main/java/com/replaymod/render/RenderSettings.java
Normal file
@@ -0,0 +1,86 @@
|
||||
package com.replaymod.render;
|
||||
|
||||
import lombok.Data;
|
||||
import net.minecraft.client.resources.I18n;
|
||||
import org.lwjgl.util.ReadableColor;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
@Data
|
||||
public class RenderSettings {
|
||||
public enum RenderMethod {
|
||||
DEFAULT, STEREOSCOPIC, CUBIC, EQUIRECTANGULAR;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return I18n.format("replaymod.gui.rendersettings.renderer." + name().toLowerCase());
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return I18n.format("replaymod.gui.rendersettings.renderer." + name().toLowerCase() + ".description");
|
||||
}
|
||||
}
|
||||
|
||||
public enum EncodingPreset {
|
||||
MP4_CUSTOM("-an -c:v libx264 -b:v %BITRATE% -pix_fmt yuv420p \"%FILENAME%\"", "mp4"),
|
||||
|
||||
MP4_HIGH("-an -c:v libx264 -preset ultrafast -qp 1 -pix_fmt yuv420p \"%FILENAME%\"", "mp4"),
|
||||
|
||||
MP4_DEFAULT("-an -c:v libx264 -preset ultrafast -pix_fmt yuv420p \"%FILENAME%\"", "mp4"),
|
||||
|
||||
MP4_POTATO("-an -c:v libx264 -preset ultrafast -crf 51 -pix_fmt yuv420p \"%FILENAME%\"", "mp4"),
|
||||
|
||||
WEBM_CUSTOM("-an -c:v libvpx -b:v %BITRATE% \"%FILENAME%\"", "webm"),
|
||||
|
||||
MKV_LOSSLESS("-an -c:v libx264 -preset ultrafast -qp 0 \"%FILENAME%\"", "mkv"),
|
||||
|
||||
PNG("\"%FILENAME%-%06d.png\"", "png");
|
||||
|
||||
private final String preset;
|
||||
private final String fileExtension;
|
||||
|
||||
EncodingPreset(String preset, String fileExtension) {
|
||||
this.preset = preset;
|
||||
this.fileExtension = fileExtension;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return "-y -f rawvideo -pix_fmt rgb24 -s %WIDTH%x%HEIGHT% -r %FPS% -i - " + preset;
|
||||
}
|
||||
|
||||
public String getFileExtension() {
|
||||
return fileExtension;
|
||||
}
|
||||
|
||||
public boolean hasBitrateSetting() {
|
||||
return preset.contains("%BITRATE%");
|
||||
}
|
||||
|
||||
public boolean isYuv420() { return preset.contains("-pix_fmt yuv420p"); }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return I18n.format("replaymod.gui.rendersettings.presets." + name().replace('_', '.').toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
private final RenderMethod renderMethod;
|
||||
private final EncodingPreset encodingPreset;
|
||||
private final int videoWidth;
|
||||
private final int videoHeight;
|
||||
private final int framesPerSecond;
|
||||
private final int bitRate;
|
||||
private final File outputFile;
|
||||
|
||||
private final boolean renderNameTags;
|
||||
private final boolean stabilizeYaw;
|
||||
private final boolean stabilizePitch;
|
||||
private final boolean stabilizeRoll;
|
||||
private final ReadableColor chromaKeyingColor;
|
||||
private final boolean inject360Metadata;
|
||||
|
||||
private final String exportCommand;
|
||||
private final String exportArguments;
|
||||
|
||||
private final boolean highPerformance;
|
||||
}
|
||||
32
src/main/java/com/replaymod/render/ReplayModRender.java
Normal file
32
src/main/java/com/replaymod/render/ReplayModRender.java
Normal file
@@ -0,0 +1,32 @@
|
||||
package com.replaymod.render;
|
||||
|
||||
import com.replaymod.core.ReplayMod;
|
||||
import net.minecraftforge.common.config.Configuration;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
import net.minecraftforge.fml.common.event.FMLPreInitializationEvent;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
@Mod(modid = ReplayModRender.MOD_ID, useMetadata = true)
|
||||
public class ReplayModRender {
|
||||
public static final String MOD_ID = "replaymod-render";
|
||||
|
||||
@Mod.Instance(MOD_ID)
|
||||
public static ReplayModRender instance;
|
||||
|
||||
@Mod.Instance(ReplayMod.MOD_ID)
|
||||
private static ReplayMod core;
|
||||
|
||||
private Logger logger;
|
||||
|
||||
private Configuration configuration;
|
||||
|
||||
@Mod.EventHandler
|
||||
public void preInit(FMLPreInitializationEvent event) {
|
||||
logger = event.getModLog();
|
||||
configuration = new Configuration(event.getSuggestedConfigurationFile());
|
||||
}
|
||||
|
||||
public Configuration getConfiguration() {
|
||||
return configuration;
|
||||
}
|
||||
}
|
||||
116
src/main/java/com/replaymod/render/VideoWriter.java
Executable file
116
src/main/java/com/replaymod/render/VideoWriter.java
Executable file
@@ -0,0 +1,116 @@
|
||||
package com.replaymod.render;
|
||||
|
||||
import com.replaymod.render.frame.RGBFrame;
|
||||
import com.replaymod.render.rendering.FrameConsumer;
|
||||
import com.replaymod.render.utils.ByteBufferPool;
|
||||
import eu.crushedpixel.replaymod.utils.StreamPipe;
|
||||
import eu.crushedpixel.replaymod.utils.StringUtils;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.crash.CrashReport;
|
||||
import net.minecraft.crash.CrashReportCategory;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.lwjgl.util.ReadableDimension;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.channels.Channels;
|
||||
import java.nio.channels.WritableByteChannel;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.apache.commons.lang3.Validate.isTrue;
|
||||
|
||||
public class VideoWriter implements FrameConsumer<RGBFrame> {
|
||||
|
||||
private final RenderSettings settings;
|
||||
private final Process process;
|
||||
private final OutputStream outputStream;
|
||||
private final WritableByteChannel channel;
|
||||
private final String commandArgs;
|
||||
private volatile boolean aborted;
|
||||
|
||||
public VideoWriter(final RenderSettings settings) throws IOException {
|
||||
this.settings = settings;
|
||||
|
||||
File outputFolder = settings.getOutputFile().getParentFile();
|
||||
String fileName = settings.getOutputFile().getName();
|
||||
|
||||
commandArgs = settings.getExportArguments()
|
||||
.replace("%WIDTH%", String.valueOf(settings.getVideoWidth()))
|
||||
.replace("%HEIGHT%", String.valueOf(settings.getVideoHeight()))
|
||||
.replace("%FPS%", String.valueOf(settings.getFramesPerSecond()))
|
||||
.replace("%FILENAME%", fileName)
|
||||
.replace("%BITRATE%", String.valueOf(settings.getBitRate()));
|
||||
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add(settings.getExportCommand().isEmpty() ? "ffmpeg" : settings.getExportCommand());
|
||||
command.addAll(StringUtils.translateCommandline(commandArgs));
|
||||
System.out.println("Starting " + settings.getExportCommand() + " with args: " + commandArgs);
|
||||
process = new ProcessBuilder(command).directory(outputFolder).start();
|
||||
OutputStream exportLogOut = new FileOutputStream("export.log");
|
||||
new StreamPipe(process.getInputStream(), exportLogOut).start();
|
||||
new StreamPipe(process.getErrorStream(), exportLogOut).start();
|
||||
outputStream = process.getOutputStream();
|
||||
channel = Channels.newChannel(outputStream);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
IOUtils.closeQuietly(outputStream);
|
||||
|
||||
long startTime = System.nanoTime();
|
||||
long rem = TimeUnit.SECONDS.toNanos(30);
|
||||
do {
|
||||
try {
|
||||
process.exitValue();
|
||||
break;
|
||||
} catch(IllegalThreadStateException ex) {
|
||||
if (rem > 0) {
|
||||
try {
|
||||
Thread.sleep(Math.min(TimeUnit.NANOSECONDS.toMillis(rem) + 1, 100));
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
rem = TimeUnit.SECONDS.toNanos(30) - (System.nanoTime() - startTime);
|
||||
} while (rem > 0);
|
||||
|
||||
process.destroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void consume(RGBFrame frame) {
|
||||
try {
|
||||
checkSize(frame.getSize());
|
||||
channel.write(frame.getByteBuffer());
|
||||
ByteBufferPool.release(frame.getByteBuffer());
|
||||
} catch (Throwable t) {
|
||||
if (aborted) {
|
||||
return;
|
||||
}
|
||||
CrashReport report = CrashReport.makeCrashReport(t, "Exporting frame");
|
||||
CrashReportCategory exportDetails = report.makeCategory("Export details");
|
||||
exportDetails.addCrashSection("Export command", settings.getExportCommand());
|
||||
exportDetails.addCrashSection("Export args", commandArgs);
|
||||
Minecraft.getMinecraft().crashed(report);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkSize(ReadableDimension size) {
|
||||
checkSize(size.getWidth(), size.getHeight());
|
||||
}
|
||||
|
||||
private void checkSize(int width, int height) {
|
||||
isTrue(width == settings.getVideoWidth(), "Width has to be %d but was %d", settings.getVideoWidth(), width);
|
||||
isTrue(height == settings.getVideoHeight(), "Height has to be %d but was %d", settings.getVideoHeight(), height);
|
||||
}
|
||||
|
||||
public void abort() {
|
||||
aborted = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package com.replaymod.render.capturer;
|
||||
|
||||
public interface CaptureData {
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.replaymod.render.capturer;
|
||||
|
||||
import com.replaymod.render.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 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,29 @@
|
||||
package com.replaymod.render.capturer;
|
||||
|
||||
import com.replaymod.render.frame.CubicOpenGlFrame;
|
||||
import com.replaymod.render.frame.OpenGlFrame;
|
||||
|
||||
public class CubicPboOpenGlFrameCapturer extends
|
||||
MultiFramePboOpenGlFrameCapturer<CubicOpenGlFrame, CubicOpenGlFrameCapturer.Data> {
|
||||
|
||||
private final int frameSize;
|
||||
public CubicPboOpenGlFrameCapturer(WorldRenderer worldRenderer, RenderInfo renderInfo, int frameSize) {
|
||||
super(worldRenderer, renderInfo, CubicOpenGlFrameCapturer.Data.class, frameSize * frameSize);
|
||||
this.frameSize = frameSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getFrameWidth() {
|
||||
return frameSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getFrameHeight() {
|
||||
return frameSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CubicOpenGlFrame create(OpenGlFrame[] from) {
|
||||
return new CubicOpenGlFrame(from[0], from[1], from[2], from[3], from[4], from[5]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package com.replaymod.render.capturer;
|
||||
|
||||
import com.replaymod.render.frame.OpenGlFrame;
|
||||
import com.replaymod.render.rendering.Frame;
|
||||
import com.replaymod.render.utils.ByteBufferPool;
|
||||
import com.replaymod.render.utils.PixelBufferObject;
|
||||
import net.minecraft.client.renderer.OpenGlHelper;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public abstract class MultiFramePboOpenGlFrameCapturer<F extends Frame, D extends Enum<D> & CaptureData>
|
||||
extends OpenGlFrameCapturer<F, D> {
|
||||
private final D[] data;
|
||||
private PixelBufferObject pbo, otherPBO;
|
||||
|
||||
public MultiFramePboOpenGlFrameCapturer(WorldRenderer worldRenderer, RenderInfo renderInfo, Class<D> type, int framePixels) {
|
||||
super(worldRenderer, renderInfo);
|
||||
|
||||
data = type.getEnumConstants();
|
||||
int bufferSize = framePixels * 3 * data.length;
|
||||
pbo = new PixelBufferObject(bufferSize, PixelBufferObject.Usage.READ);
|
||||
otherPBO = new PixelBufferObject(bufferSize, PixelBufferObject.Usage.READ);
|
||||
}
|
||||
|
||||
protected abstract F create(OpenGlFrame[] from);
|
||||
|
||||
private void swapPBOs() {
|
||||
PixelBufferObject old = pbo;
|
||||
pbo = otherPBO;
|
||||
otherPBO = old;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDone() {
|
||||
return framesDone >= renderInfo.getTotalFrames() + 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public F process() {
|
||||
F frame = null;
|
||||
|
||||
if (framesDone > 1) {
|
||||
// Read pbo to memory
|
||||
pbo.bind();
|
||||
ByteBuffer pboBuffer = pbo.mapReadOnly();
|
||||
|
||||
OpenGlFrame[] frames = new OpenGlFrame[data.length];
|
||||
int frameBufferSize = getFrameWidth() * getFrameHeight() * 3;
|
||||
for (int i = 0; i < frames.length; i++) {
|
||||
ByteBuffer frameBuffer = ByteBufferPool.allocate(frameBufferSize);
|
||||
pboBuffer.limit(pboBuffer.position() + frameBufferSize);
|
||||
frameBuffer.put(pboBuffer);
|
||||
frameBuffer.rewind();
|
||||
frames[i] = new OpenGlFrame(framesDone - 2, frameSize, frameBuffer);
|
||||
}
|
||||
|
||||
pbo.unmap();
|
||||
pbo.unbind();
|
||||
|
||||
frame = create(frames);
|
||||
}
|
||||
|
||||
if (framesDone < renderInfo.getTotalFrames()) {
|
||||
float partialTicks = renderInfo.updateForNextFrame();
|
||||
// Then fill it again
|
||||
for (D data : this.data) {
|
||||
renderFrame(framesDone, partialTicks, data);
|
||||
}
|
||||
}
|
||||
|
||||
framesDone++;
|
||||
swapPBOs();
|
||||
return frame;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected OpenGlFrame captureFrame(int frameId, D captureData) {
|
||||
pbo.bind();
|
||||
|
||||
GL11.glPixelStorei(GL11.GL_PACK_ALIGNMENT, 1);
|
||||
GL11.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 1);
|
||||
|
||||
int offset = captureData.ordinal() * getFrameWidth() * getFrameHeight() * 3;
|
||||
if (OpenGlHelper.isFramebufferEnabled()) {
|
||||
frameBuffer().bindFramebufferTexture();
|
||||
GL11.glGetTexImage(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGB, GL11.GL_UNSIGNED_BYTE, offset);
|
||||
frameBuffer().unbindFramebufferTexture();
|
||||
} else {
|
||||
GL11.glReadPixels(0, 0, getFrameWidth(), getFrameHeight(), GL11.GL_RGB, GL11.GL_UNSIGNED_BYTE, offset);
|
||||
}
|
||||
|
||||
pbo.unbind();
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
super.close();
|
||||
pbo.delete();
|
||||
otherPBO.delete();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package com.replaymod.render.capturer;
|
||||
|
||||
import com.replaymod.render.frame.OpenGlFrame;
|
||||
import com.replaymod.render.rendering.Frame;
|
||||
import com.replaymod.render.rendering.FrameCapturer;
|
||||
import com.replaymod.render.utils.ByteBufferPool;
|
||||
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 worldRenderer;
|
||||
protected final RenderInfo renderInfo;
|
||||
protected int framesDone;
|
||||
private Framebuffer frameBuffer;
|
||||
|
||||
public OpenGlFrameCapturer(WorldRenderer worldRenderer, RenderInfo renderInfo) {
|
||||
this.worldRenderer = worldRenderer;
|
||||
this.renderInfo = renderInfo;
|
||||
}
|
||||
|
||||
protected final ReadableDimension frameSize = new ReadableDimension() {
|
||||
@Override
|
||||
public int getWidth() {
|
||||
return getFrameWidth();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHeight() {
|
||||
return getFrameHeight();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getSize(WritableDimension dest) {
|
||||
dest.setSize(getWidth(), getHeight());
|
||||
}
|
||||
};
|
||||
|
||||
protected int getFrameWidth() {
|
||||
return renderInfo.getFrameSize().getWidth();
|
||||
}
|
||||
|
||||
protected int getFrameHeight() {
|
||||
return renderInfo.getFrameSize().getHeight();
|
||||
}
|
||||
|
||||
protected 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();
|
||||
|
||||
return captureFrame(frameId, captureData);
|
||||
}
|
||||
|
||||
protected OpenGlFrame captureFrame(int frameId, D captureData) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
14
src/main/java/com/replaymod/render/capturer/RenderInfo.java
Normal file
14
src/main/java/com/replaymod/render/capturer/RenderInfo.java
Normal file
@@ -0,0 +1,14 @@
|
||||
package com.replaymod.render.capturer;
|
||||
|
||||
import com.replaymod.render.RenderSettings;
|
||||
import org.lwjgl.util.ReadableDimension;
|
||||
|
||||
public interface RenderInfo {
|
||||
ReadableDimension getFrameSize();
|
||||
|
||||
int getTotalFrames();
|
||||
|
||||
float updateForNextFrame();
|
||||
|
||||
RenderSettings getRenderSettings();
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.replaymod.render.capturer;
|
||||
|
||||
import com.replaymod.render.frame.OpenGlFrame;
|
||||
|
||||
public class SimpleOpenGlFrameCapturer extends OpenGlFrameCapturer<OpenGlFrame, CaptureData> {
|
||||
|
||||
public SimpleOpenGlFrameCapturer(WorldRenderer worldRenderer, RenderInfo renderInfo) {
|
||||
super(worldRenderer, renderInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OpenGlFrame process() {
|
||||
float partialTicks = renderInfo.updateForNextFrame();
|
||||
return renderFrame(framesDone++, partialTicks);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package com.replaymod.render.capturer;
|
||||
|
||||
import com.replaymod.render.frame.OpenGlFrame;
|
||||
import com.replaymod.render.utils.ByteBufferPool;
|
||||
import com.replaymod.render.utils.PixelBufferObject;
|
||||
import net.minecraft.client.renderer.OpenGlHelper;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.util.ReadableDimension;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class SimplePboOpenGlFrameCapturer extends OpenGlFrameCapturer<OpenGlFrame, CaptureData> {
|
||||
private final int bufferSize;
|
||||
private PixelBufferObject pbo, otherPBO;
|
||||
|
||||
public SimplePboOpenGlFrameCapturer(WorldRenderer worldRenderer, RenderInfo renderInfo) {
|
||||
super(worldRenderer, renderInfo);
|
||||
|
||||
ReadableDimension size = renderInfo.getFrameSize();
|
||||
bufferSize = size.getHeight() * size.getWidth() * 3;
|
||||
pbo = new PixelBufferObject(bufferSize, PixelBufferObject.Usage.READ);
|
||||
otherPBO = new PixelBufferObject(bufferSize, PixelBufferObject.Usage.READ);
|
||||
}
|
||||
|
||||
private void swapPBOs() {
|
||||
PixelBufferObject old = pbo;
|
||||
pbo = otherPBO;
|
||||
otherPBO = old;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDone() {
|
||||
return framesDone >= renderInfo.getTotalFrames() + 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OpenGlFrame process() {
|
||||
OpenGlFrame frame = null;
|
||||
|
||||
if (framesDone > 1) {
|
||||
// Read pbo to memory
|
||||
pbo.bind();
|
||||
ByteBuffer pboBuffer = pbo.mapReadOnly();
|
||||
ByteBuffer buffer = ByteBufferPool.allocate(bufferSize);
|
||||
buffer.put(pboBuffer);
|
||||
buffer.rewind();
|
||||
pbo.unmap();
|
||||
pbo.unbind();
|
||||
frame = new OpenGlFrame(framesDone - 2, frameSize, buffer);
|
||||
}
|
||||
|
||||
if (framesDone < renderInfo.getTotalFrames()) {
|
||||
// Then fill it again
|
||||
renderFrame(framesDone, renderInfo.updateForNextFrame());
|
||||
}
|
||||
|
||||
framesDone++;
|
||||
swapPBOs();
|
||||
return frame;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected OpenGlFrame captureFrame(int frameId, CaptureData data) {
|
||||
pbo.bind();
|
||||
|
||||
GL11.glPixelStorei(GL11.GL_PACK_ALIGNMENT, 1);
|
||||
GL11.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 1);
|
||||
|
||||
if (OpenGlHelper.isFramebufferEnabled()) {
|
||||
frameBuffer().bindFramebufferTexture();
|
||||
GL11.glGetTexImage(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGB, GL11.GL_UNSIGNED_BYTE, 0);
|
||||
frameBuffer().unbindFramebufferTexture();
|
||||
} else {
|
||||
GL11.glReadPixels(0, 0, getFrameWidth(), getFrameHeight(), GL11.GL_RGB, GL11.GL_UNSIGNED_BYTE, 0);
|
||||
}
|
||||
|
||||
pbo.unbind();
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
super.close();
|
||||
pbo.delete();
|
||||
otherPBO.delete();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.replaymod.render.capturer;
|
||||
|
||||
import com.replaymod.render.frame.OpenGlFrame;
|
||||
import com.replaymod.render.frame.StereoscopicOpenGlFrame;
|
||||
|
||||
public class StereoscopicOpenGlFrameCapturer
|
||||
extends OpenGlFrameCapturer<StereoscopicOpenGlFrame, StereoscopicOpenGlFrameCapturer.Data> {
|
||||
public enum Data implements CaptureData {
|
||||
LEFT_EYE, RIGHT_EYE
|
||||
}
|
||||
|
||||
public StereoscopicOpenGlFrameCapturer(WorldRenderer worldRenderer, RenderInfo renderInfo) {
|
||||
super(worldRenderer, renderInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getFrameWidth() {
|
||||
return super.getFrameWidth() / 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StereoscopicOpenGlFrame process() {
|
||||
float partialTicks = renderInfo.updateForNextFrame();
|
||||
int frameId = framesDone++;
|
||||
OpenGlFrame left = renderFrame(frameId, partialTicks, Data.LEFT_EYE);
|
||||
OpenGlFrame right = renderFrame(frameId, partialTicks, Data.RIGHT_EYE);
|
||||
return new StereoscopicOpenGlFrame(left, right);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.replaymod.render.capturer;
|
||||
|
||||
import com.replaymod.render.frame.OpenGlFrame;
|
||||
import com.replaymod.render.frame.StereoscopicOpenGlFrame;
|
||||
|
||||
public class StereoscopicPboOpenGlFrameCapturer
|
||||
extends MultiFramePboOpenGlFrameCapturer<StereoscopicOpenGlFrame, StereoscopicOpenGlFrameCapturer.Data> {
|
||||
|
||||
public StereoscopicPboOpenGlFrameCapturer(WorldRenderer worldRenderer, RenderInfo renderInfo) {
|
||||
super(worldRenderer, renderInfo, StereoscopicOpenGlFrameCapturer.Data.class,
|
||||
renderInfo.getFrameSize().getWidth() / 2 * renderInfo.getFrameSize().getHeight());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getFrameWidth() {
|
||||
return super.getFrameWidth() / 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected StereoscopicOpenGlFrame create(OpenGlFrame[] from) {
|
||||
return new StereoscopicOpenGlFrame(from[0], from[1]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.replaymod.render.capturer;
|
||||
|
||||
import org.lwjgl.util.ReadableDimension;
|
||||
|
||||
import java.io.Closeable;
|
||||
|
||||
public interface WorldRenderer extends Closeable {
|
||||
void renderWorld(ReadableDimension displaySize, float partialTicks, CaptureData data);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.replaymod.render.frame;
|
||||
|
||||
import com.replaymod.render.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();
|
||||
}
|
||||
}
|
||||
20
src/main/java/com/replaymod/render/frame/OpenGlFrame.java
Normal file
20
src/main/java/com/replaymod/render/frame/OpenGlFrame.java
Normal file
@@ -0,0 +1,20 @@
|
||||
package com.replaymod.render.frame;
|
||||
|
||||
import com.replaymod.render.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;
|
||||
}
|
||||
28
src/main/java/com/replaymod/render/frame/RGBFrame.java
Normal file
28
src/main/java/com/replaymod/render/frame/RGBFrame.java
Normal file
@@ -0,0 +1,28 @@
|
||||
package com.replaymod.render.frame;
|
||||
|
||||
import com.replaymod.render.rendering.Frame;
|
||||
import lombok.Getter;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.lwjgl.util.ReadableDimension;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class RGBFrame implements Frame {
|
||||
@Getter
|
||||
private final int frameId;
|
||||
|
||||
@Getter
|
||||
private final ReadableDimension size;
|
||||
|
||||
@Getter
|
||||
private final ByteBuffer byteBuffer;
|
||||
|
||||
public RGBFrame(int frameId, ReadableDimension size, ByteBuffer byteBuffer) {
|
||||
Validate.isTrue(size.getWidth() * size.getHeight() * 3 == byteBuffer.remaining(),
|
||||
"Buffer size is %d (cap: %d) but should be %d",
|
||||
byteBuffer.remaining(), byteBuffer.capacity(), size.getWidth() * size.getHeight() * 3);
|
||||
this.frameId = frameId;
|
||||
this.size = size;
|
||||
this.byteBuffer = byteBuffer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.replaymod.render.frame;
|
||||
|
||||
import com.replaymod.render.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();
|
||||
}
|
||||
}
|
||||
414
src/main/java/com/replaymod/render/gui/GuiRenderSettings.java
Normal file
414
src/main/java/com/replaymod/render/gui/GuiRenderSettings.java
Normal file
@@ -0,0 +1,414 @@
|
||||
package com.replaymod.render.gui;
|
||||
|
||||
import com.google.common.util.concurrent.FutureCallback;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.InstanceCreator;
|
||||
import com.replaymod.pathing.path.Timeline;
|
||||
import com.replaymod.render.RenderSettings;
|
||||
import com.replaymod.render.ReplayModRender;
|
||||
import com.replaymod.render.rendering.VideoRenderer;
|
||||
import com.replaymod.replay.ReplayHandler;
|
||||
import de.johni0702.minecraft.gui.container.GuiContainer;
|
||||
import de.johni0702.minecraft.gui.container.GuiPanel;
|
||||
import de.johni0702.minecraft.gui.container.GuiScreen;
|
||||
import de.johni0702.minecraft.gui.container.GuiVerticalList;
|
||||
import de.johni0702.minecraft.gui.element.*;
|
||||
import de.johni0702.minecraft.gui.element.advanced.GuiColorPicker;
|
||||
import de.johni0702.minecraft.gui.element.advanced.GuiDropdownMenu;
|
||||
import de.johni0702.minecraft.gui.function.Closeable;
|
||||
import de.johni0702.minecraft.gui.layout.CustomLayout;
|
||||
import de.johni0702.minecraft.gui.layout.GridLayout;
|
||||
import de.johni0702.minecraft.gui.layout.HorizontalLayout;
|
||||
import de.johni0702.minecraft.gui.layout.VerticalLayout;
|
||||
import de.johni0702.minecraft.gui.popup.GuiFileChooserPopup;
|
||||
import de.johni0702.minecraft.gui.utils.Colors;
|
||||
import de.johni0702.minecraft.gui.utils.Consumer;
|
||||
import de.johni0702.minecraft.gui.utils.Utils;
|
||||
import net.minecraft.client.gui.GuiErrorScreen;
|
||||
import net.minecraft.client.resources.I18n;
|
||||
import net.minecraft.crash.CrashReport;
|
||||
import net.minecraft.util.ReportedException;
|
||||
import net.minecraftforge.common.config.Configuration;
|
||||
import net.minecraftforge.common.config.Property;
|
||||
import org.lwjgl.util.Dimension;
|
||||
import org.lwjgl.util.ReadableDimension;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Type;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
public class GuiRenderSettings extends GuiScreen implements Closeable {
|
||||
public final GuiPanel contentPanel = new GuiPanel(this).setBackgroundColor(Colors.DARK_TRANSPARENT);
|
||||
public final GuiVerticalList settingsList = new GuiVerticalList(contentPanel).setDrawSlider(true);
|
||||
|
||||
public final GuiDropdownMenu<RenderSettings.RenderMethod> renderMethodDropdown =
|
||||
new GuiDropdownMenu<RenderSettings.RenderMethod>().onSelection(new Consumer<Integer>() {
|
||||
@Override
|
||||
public void consume(Integer old) {
|
||||
updateInputs();
|
||||
}
|
||||
}).setMinSize(new Dimension(0, 20)).setValues(RenderSettings.RenderMethod.values());
|
||||
|
||||
{
|
||||
for (Map.Entry<RenderSettings.RenderMethod, IGuiClickable> entry :
|
||||
renderMethodDropdown.getDropdownEntries().entrySet()) {
|
||||
entry.getValue().setTooltip(new GuiTooltip().setText(entry.getKey().getDescription()));
|
||||
}
|
||||
}
|
||||
|
||||
public final GuiDropdownMenu<RenderSettings.EncodingPreset> encodingPresetDropdown =
|
||||
new GuiDropdownMenu<RenderSettings.EncodingPreset>().onSelection(new Consumer<Integer>() {
|
||||
@Override
|
||||
public void consume(Integer old) {
|
||||
RenderSettings.EncodingPreset newPreset = encodingPresetDropdown.getSelectedValue();
|
||||
// Update export arguments to match new Preset
|
||||
exportArguments.setText(newPreset.getValue());
|
||||
// If the user hasn't changed the output file by themselves,
|
||||
if (!outputFileManuallySet) {
|
||||
// generate a new output file name with updated file extension
|
||||
outputFile = generateOutputFile(newPreset);
|
||||
outputFileButton.setLabel(outputFile.getName());
|
||||
}
|
||||
updateInputs();
|
||||
}
|
||||
}).setMinSize(new Dimension(0, 20)).setValues(RenderSettings.EncodingPreset.values());
|
||||
|
||||
public final GuiNumberField videoWidth = new GuiNumberField().setSize(50, 20).setMinValue(1);
|
||||
public final GuiNumberField videoHeight = new GuiNumberField().setSize(50, 20).setMinValue(1);
|
||||
public final GuiSlider frameRateSlider = new GuiSlider().onValueChanged(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
frameRateSlider.setText(I18n.format("replaymod.gui.rendersettings.framerate")
|
||||
+ ": " + (frameRateSlider.getValue() + 10));
|
||||
}
|
||||
}).setSize(122, 20).setSteps(110);
|
||||
public final GuiPanel videoResolutionPanel = new GuiPanel()
|
||||
.setLayout(new HorizontalLayout(HorizontalLayout.Alignment.RIGHT).setSpacing(2))
|
||||
.addElements(null, videoWidth, new GuiLabel().setText("*"), videoHeight);
|
||||
|
||||
public final GuiNumberField bitRateField = new GuiNumberField().setValue(10).setSize(50, 20);
|
||||
public final GuiDropdownMenu<String> bitRateUnit = new GuiDropdownMenu<String>()
|
||||
.setSize(50, 20).setValues("bps", "kbps", "mbps").setSelected("mbps");
|
||||
|
||||
public final GuiButton outputFileButton = new GuiButton().setMinSize(new Dimension(0, 20)).onClick(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Futures.addCallback(
|
||||
GuiFileChooserPopup.openSaveGui(GuiRenderSettings.this, "replaymod.gui.save",
|
||||
encodingPresetDropdown.getSelectedValue().getFileExtension()).getFuture(),
|
||||
new FutureCallback<File>() {
|
||||
@Override
|
||||
public void onSuccess(@Nullable File result) {
|
||||
if (result != null) {
|
||||
outputFile = result;
|
||||
outputFileManuallySet = true;
|
||||
outputFileButton.setLabel(result.getName());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable t) {
|
||||
throw new RuntimeException(t);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
public final GuiPanel mainPanel = new GuiPanel()
|
||||
.addElements(new GridLayout.Data(1, 0.5),
|
||||
new GuiLabel().setI18nText("replaymod.gui.rendersettings.renderer"), renderMethodDropdown,
|
||||
new GuiLabel().setI18nText("replaymod.gui.rendersettings.presets"), encodingPresetDropdown,
|
||||
new GuiLabel().setI18nText("replaymod.gui.rendersettings.customresolution"), videoResolutionPanel,
|
||||
new GuiLabel().setI18nText("replaymod.gui.settings.bitrate"), new GuiPanel().addElements(null,
|
||||
new GuiPanel().addElements(null, bitRateField, bitRateUnit).setLayout(new HorizontalLayout()),
|
||||
frameRateSlider).setLayout(new HorizontalLayout(HorizontalLayout.Alignment.RIGHT).setSpacing(3)),
|
||||
new GuiLabel().setI18nText("replaymod.gui.rendersettings.outputfile"), outputFileButton)
|
||||
.setLayout(new GridLayout().setCellsEqualSize(false).setColumns(2).setSpacingX(5).setSpacingY(3));
|
||||
|
||||
public final GuiCheckbox nametagCheckbox = new GuiCheckbox()
|
||||
.setI18nLabel("replaymod.gui.rendersettings.nametags");
|
||||
|
||||
public final GuiPanel stabilizePanel = new GuiPanel().setLayout(new HorizontalLayout().setSpacing(10));
|
||||
public final GuiCheckbox stabilizeYaw = new GuiCheckbox(stabilizePanel)
|
||||
.setI18nLabel("replaymod.gui.yaw");
|
||||
public final GuiCheckbox stabilizePitch = new GuiCheckbox(stabilizePanel)
|
||||
.setI18nLabel("replaymod.gui.pitch");
|
||||
public final GuiCheckbox stabilizeRoll = new GuiCheckbox(stabilizePanel)
|
||||
.setI18nLabel("replaymod.gui.roll");
|
||||
|
||||
public final GuiCheckbox chromaKeyingCheckbox = new GuiCheckbox()
|
||||
.setI18nLabel("replaymod.gui.rendersettings.chromakey");
|
||||
public final GuiColorPicker chromaKeyingColor = new GuiColorPicker().setSize(30, 15);
|
||||
|
||||
public final GuiCheckbox inject360Metadata = new GuiCheckbox()
|
||||
.setI18nLabel("replaymod.gui.rendersettings.360metadata");
|
||||
|
||||
public final GuiPanel advancedPanel = new GuiPanel().setLayout(new VerticalLayout().setSpacing(15))
|
||||
.addElements(null, nametagCheckbox, new GuiPanel().setLayout(
|
||||
new GridLayout().setCellsEqualSize(false).setColumns(2).setSpacingX(5).setSpacingY(15))
|
||||
.addElements(new GridLayout.Data(0, 0.5),
|
||||
new GuiLabel().setI18nText("replaymod.gui.rendersettings.stabilizecamera"), stabilizePanel,
|
||||
chromaKeyingCheckbox, chromaKeyingColor,
|
||||
inject360Metadata));
|
||||
|
||||
public final GuiTextField exportCommand = new GuiTextField().setI18nHint("replaymod.gui.rendersettings.command")
|
||||
.setSize(55, 20).setMaxLength(100);
|
||||
public final GuiTextField exportArguments = new GuiTextField().setI18nHint("replaymod.gui.rendersettings.arguments")
|
||||
.setMinSize(new Dimension(245, 20)).setMaxLength(500);
|
||||
|
||||
public final GuiPanel commandlinePanel = new GuiPanel().setLayout(new VerticalLayout().setSpacing(10))
|
||||
.addElements(null,
|
||||
new GuiPanel().setLayout(new HorizontalLayout().setSpacing(5)).addElements(null, exportCommand, exportArguments),
|
||||
new GuiLabel(new GuiPanel().setLayout(new CustomLayout<GuiPanel>() {
|
||||
@Override
|
||||
protected void layout(GuiPanel container, int width, int height) {
|
||||
size(container.getChildren().iterator().next(), width, height);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadableDimension calcMinSize(GuiContainer<?> container) {
|
||||
return new Dimension(300, 50);
|
||||
}
|
||||
})).setI18nText("replaymod.gui.rendersettings.ffmpeg.description").getContainer());
|
||||
|
||||
|
||||
public final GuiPanel buttonPanel = new GuiPanel(contentPanel).setLayout(new HorizontalLayout().setSpacing(4));
|
||||
public final GuiButton renderButton = new GuiButton(buttonPanel).onClick(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
VideoRenderer videoRenderer = new VideoRenderer(save(true), replayHandler, timeline);
|
||||
videoRenderer.renderVideo();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
|
||||
GuiErrorScreen errorScreen = new GuiErrorScreen(I18n.format("replaymod.gui.rendering.error.title"),
|
||||
I18n.format("replaymod.gui.rendering.error.message"));
|
||||
getMinecraft().displayGuiScreen(errorScreen);
|
||||
} catch (Throwable t) {
|
||||
CrashReport crashReport = CrashReport.makeCrashReport(t, "Rendering video");
|
||||
throw new ReportedException(crashReport);
|
||||
}
|
||||
}
|
||||
}).setSize(148, 20).setI18nLabel("replaymod.gui.render");
|
||||
public final GuiButton cancelButton = new GuiButton(buttonPanel).onClick(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
getMinecraft().displayGuiScreen(null);
|
||||
}
|
||||
}).setSize(148, 20).setI18nLabel("replaymod.gui.cancel");
|
||||
|
||||
{
|
||||
setBackground(Background.NONE);
|
||||
Utils.link(videoWidth, videoHeight, bitRateField);
|
||||
setLayout(new CustomLayout<GuiScreen>() {
|
||||
@Override
|
||||
protected void layout(GuiScreen container, int width, int height) {
|
||||
pos(contentPanel, width / 2 - width(contentPanel) / 2, height / 2 - height(contentPanel) / 2);
|
||||
}
|
||||
});
|
||||
|
||||
contentPanel.setLayout(new CustomLayout<GuiPanel>() {
|
||||
@Override
|
||||
protected void layout(GuiPanel container, int width, int height) {
|
||||
size(settingsList, width, height - height(buttonPanel) - 25);
|
||||
pos(settingsList, width / 2 - width(settingsList) / 2, 5);
|
||||
pos(buttonPanel, width / 2 - width(buttonPanel) / 2, y(settingsList) + height(settingsList) + 10);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadableDimension calcMinSize(GuiContainer<?> container) {
|
||||
ReadableDimension screenSize = getMinSize();
|
||||
return new Dimension(screenSize.getWidth() - 40, screenSize.getHeight() - 40);
|
||||
}
|
||||
});
|
||||
|
||||
settingsList.getListPanel().setLayout(new VerticalLayout().setSpacing(10))
|
||||
.addElements(new VerticalLayout.Data(0.5),
|
||||
new GuiLabel().setI18nText("replaymod.gui.rendersettings.video"), mainPanel, new GuiPanel(),
|
||||
new GuiLabel().setI18nText("replaymod.gui.rendersettings.advanced"), advancedPanel, new GuiPanel(),
|
||||
new GuiLabel().setI18nText("replaymod.gui.rendersettings.commandline"), commandlinePanel);
|
||||
|
||||
videoWidth.onTextChanged(new Consumer<String>() {
|
||||
@Override
|
||||
public void consume(String old) {
|
||||
updateInputs();
|
||||
}
|
||||
});
|
||||
videoHeight.onTextChanged(new Consumer<String>() {
|
||||
@Override
|
||||
public void consume(String obj) {
|
||||
updateInputs();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private final ReplayHandler replayHandler;
|
||||
private final Timeline timeline;
|
||||
private File outputFile;
|
||||
private boolean outputFileManuallySet;
|
||||
|
||||
public GuiRenderSettings(ReplayHandler replayHandler, Timeline timeline) {
|
||||
this.replayHandler = replayHandler;
|
||||
this.timeline = timeline;
|
||||
|
||||
String json = getConfigProperty(ReplayModRender.instance.getConfiguration()).getString();
|
||||
RenderSettings settings = new GsonBuilder().registerTypeAdapter(RenderSettings.class, new InstanceCreator<RenderSettings>() {
|
||||
@Override
|
||||
public RenderSettings createInstance(Type type) {
|
||||
return getDefaultRenderSettings();
|
||||
}
|
||||
}).create().fromJson(json, RenderSettings.class);
|
||||
load(settings);
|
||||
}
|
||||
|
||||
protected void updateInputs() {
|
||||
// Validate video width and height
|
||||
String error = isResolutionValid();
|
||||
if (error == null) {
|
||||
renderButton.setEnabled().setTooltip(null);
|
||||
videoWidth.setTextColor(Colors.WHITE);
|
||||
videoHeight.setTextColor(Colors.WHITE);
|
||||
} else {
|
||||
renderButton.setDisabled().setTooltip(new GuiTooltip().setI18nText(error));
|
||||
videoWidth.setTextColor(Colors.RED);
|
||||
videoHeight.setTextColor(Colors.RED);
|
||||
}
|
||||
|
||||
// Enable/Disable camera stabilization checkboxes
|
||||
switch (renderMethodDropdown.getSelectedValue()) {
|
||||
case CUBIC:
|
||||
case EQUIRECTANGULAR:
|
||||
stabilizePanel.forEach(IGuiCheckbox.class).setEnabled();
|
||||
break;
|
||||
default:
|
||||
stabilizePanel.forEach(IGuiCheckbox.class).setDisabled();
|
||||
}
|
||||
|
||||
// Enable/Disable inject metadata checkbox
|
||||
if (encodingPresetDropdown.getSelectedValue().getFileExtension().equals("mp4")
|
||||
&& renderMethodDropdown.getSelectedValue() == RenderSettings.RenderMethod.EQUIRECTANGULAR) {
|
||||
inject360Metadata.setEnabled().setTooltip(null);
|
||||
} else {
|
||||
inject360Metadata.setDisabled().setTooltip(new GuiTooltip().setColor(Colors.RED)
|
||||
.setI18nText("replaymod.gui.rendersettings.360metadata.error"));
|
||||
}
|
||||
}
|
||||
|
||||
protected String isResolutionValid() {
|
||||
RenderSettings.EncodingPreset preset = encodingPresetDropdown.getSelectedValue();
|
||||
RenderSettings.RenderMethod method = renderMethodDropdown.getSelectedValue();
|
||||
int videoWidth = this.videoWidth.getInteger();
|
||||
int videoHeight = this.videoHeight.getInteger();
|
||||
|
||||
// Make sure the export arguments haven't been changed manually
|
||||
if (exportArguments.getText().equals(preset.getValue())) {
|
||||
// Yuv420 requires both dimensions to be even
|
||||
if (preset.isYuv420()
|
||||
&& (videoWidth % 2 != 0 || videoHeight % 2 != 0)) {
|
||||
return "replaymod.gui.rendersettings.customresolution.warning.yuv420";
|
||||
}
|
||||
}
|
||||
|
||||
if (method == RenderSettings.RenderMethod.CUBIC
|
||||
&& (videoWidth * 3 / 4 != videoHeight || videoWidth * 3 % 4 != 0)) {
|
||||
return "replaymod.gui.rendersettings.customresolution.warning.cubic";
|
||||
}
|
||||
if (method == RenderSettings.RenderMethod.EQUIRECTANGULAR
|
||||
&& (videoWidth / 2 != videoHeight || videoWidth % 2 != 0)) {
|
||||
return "replaymod.gui.rendersettings.customresolution.warning.equirectangular";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void load(RenderSettings settings) {
|
||||
renderMethodDropdown.setSelected(settings.getRenderMethod());
|
||||
encodingPresetDropdown.setSelected(settings.getEncodingPreset());
|
||||
videoWidth.setValue(settings.getVideoWidth());
|
||||
videoHeight.setValue(settings.getVideoHeight());
|
||||
frameRateSlider.setValue(settings.getFramesPerSecond() - 10);
|
||||
if (settings.getBitRate() % (1 << 20) == 0) {
|
||||
bitRateField.setValue(settings.getBitRate() >> 20);
|
||||
bitRateUnit.setSelected(2);
|
||||
} else if (settings.getBitRate() % (1 << 10) == 0) {
|
||||
bitRateField.setValue(settings.getBitRate() >> 10);
|
||||
bitRateUnit.setSelected(1);
|
||||
} else {
|
||||
bitRateField.setValue(settings.getBitRate());
|
||||
bitRateUnit.setSelected(0);
|
||||
}
|
||||
if (settings.getOutputFile() == null) {
|
||||
outputFile = generateOutputFile(settings.getEncodingPreset());
|
||||
outputFileManuallySet = false;
|
||||
} else {
|
||||
outputFile = settings.getOutputFile();
|
||||
outputFileManuallySet = true;
|
||||
}
|
||||
outputFileButton.setLabel(outputFile.getName());
|
||||
nametagCheckbox.setChecked(settings.isRenderNameTags());
|
||||
stabilizeYaw.setChecked(settings.isStabilizeYaw());
|
||||
stabilizePitch.setChecked(settings.isStabilizePitch());
|
||||
stabilizeRoll.setChecked(settings.isStabilizeRoll());
|
||||
if (settings.getChromaKeyingColor() == null) {
|
||||
chromaKeyingCheckbox.setChecked(false);
|
||||
chromaKeyingColor.setColor(Colors.GREEN);
|
||||
} else {
|
||||
chromaKeyingCheckbox.setChecked(true);
|
||||
chromaKeyingColor.setColor(settings.getChromaKeyingColor());
|
||||
}
|
||||
inject360Metadata.setChecked(settings.isInject360Metadata());
|
||||
exportCommand.setText(settings.getExportCommand());
|
||||
exportArguments.setText(settings.getExportArguments());
|
||||
}
|
||||
|
||||
public RenderSettings save(boolean saveOutputFile) {
|
||||
return new RenderSettings(
|
||||
renderMethodDropdown.getSelectedValue(),
|
||||
encodingPresetDropdown.getSelectedValue(),
|
||||
videoWidth.getInteger(),
|
||||
videoHeight.getInteger(),
|
||||
frameRateSlider.getValue() + 10,
|
||||
bitRateField.getInteger() << (10 * bitRateUnit.getSelected()),
|
||||
saveOutputFile ? outputFile : null,
|
||||
nametagCheckbox.isChecked(),
|
||||
stabilizeYaw.isChecked(),
|
||||
stabilizePitch.isChecked(),
|
||||
stabilizeRoll.isChecked(),
|
||||
chromaKeyingCheckbox.isChecked() ? chromaKeyingColor.getColor() : null,
|
||||
inject360Metadata.isChecked(),
|
||||
exportCommand.getText(),
|
||||
exportArguments.getText(),
|
||||
net.minecraft.client.gui.GuiScreen.isCtrlKeyDown()
|
||||
);
|
||||
}
|
||||
|
||||
private File generateOutputFile(RenderSettings.EncodingPreset encodingPreset) {
|
||||
String fileName = new SimpleDateFormat("yyyy_MM_dd_HH_mm_ss").format(new Date());
|
||||
return new File("replay_videos/" + fileName + "." + encodingPreset.getFileExtension());
|
||||
}
|
||||
|
||||
private RenderSettings getDefaultRenderSettings() {
|
||||
return new RenderSettings(RenderSettings.RenderMethod.DEFAULT, RenderSettings.EncodingPreset.MP4_DEFAULT, 1920, 1080, 60, 10 << 20, null,
|
||||
true, false, false, false, null, false, "", RenderSettings.EncodingPreset.MP4_DEFAULT.getValue(), false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
RenderSettings settings = save(false);
|
||||
String json = new Gson().toJson(settings);
|
||||
Configuration config = ReplayModRender.instance.getConfiguration();
|
||||
getConfigProperty(config).set(json);
|
||||
config.save();
|
||||
}
|
||||
|
||||
private Property getConfigProperty(Configuration configuration) {
|
||||
return configuration.get("rendersettings", "settings", "{}",
|
||||
"Last state of the render settings GUI. Internal use only.");
|
||||
}
|
||||
}
|
||||
284
src/main/java/com/replaymod/render/gui/GuiVideoRenderer.java
Normal file
284
src/main/java/com/replaymod/render/gui/GuiVideoRenderer.java
Normal file
@@ -0,0 +1,284 @@
|
||||
package com.replaymod.render.gui;
|
||||
|
||||
import com.replaymod.render.frame.RGBFrame;
|
||||
import com.replaymod.render.rendering.VideoRenderer;
|
||||
import de.johni0702.minecraft.gui.GuiRenderer;
|
||||
import de.johni0702.minecraft.gui.RenderInfo;
|
||||
import de.johni0702.minecraft.gui.container.GuiPanel;
|
||||
import de.johni0702.minecraft.gui.container.GuiScreen;
|
||||
import de.johni0702.minecraft.gui.element.GuiButton;
|
||||
import de.johni0702.minecraft.gui.element.GuiCheckbox;
|
||||
import de.johni0702.minecraft.gui.element.GuiLabel;
|
||||
import de.johni0702.minecraft.gui.element.advanced.GuiProgressBar;
|
||||
import de.johni0702.minecraft.gui.layout.CustomLayout;
|
||||
import de.johni0702.minecraft.gui.layout.HorizontalLayout;
|
||||
import eu.crushedpixel.replaymod.utils.BoundingUtils;
|
||||
import net.minecraft.client.renderer.texture.DynamicTexture;
|
||||
import net.minecraft.client.renderer.texture.TextureUtil;
|
||||
import net.minecraft.client.resources.I18n;
|
||||
import net.minecraft.util.ResourceLocation;
|
||||
import org.lwjgl.util.Dimension;
|
||||
import org.lwjgl.util.ReadableDimension;
|
||||
import org.lwjgl.util.ReadablePoint;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import static net.minecraft.client.renderer.GlStateManager.bindTexture;
|
||||
|
||||
public class GuiVideoRenderer extends GuiScreen {
|
||||
private static final ResourceLocation NO_PREVIEW_TEXTURE = new ResourceLocation("replaymod", "logo.jpg");
|
||||
|
||||
private final VideoRenderer renderer;
|
||||
|
||||
public final GuiLabel title = new GuiLabel().setI18nText("replaymod.gui.rendering.title");
|
||||
public final GuiPanel imagePanel = new GuiPanel(){
|
||||
@Override
|
||||
public void draw(GuiRenderer renderer, ReadableDimension size, RenderInfo renderInfo) {
|
||||
if (previewCheckbox.isChecked()) {
|
||||
renderPreview(renderer, size);
|
||||
} else {
|
||||
renderNoPreview(renderer, size);
|
||||
}
|
||||
}
|
||||
}.setSize(Integer.MAX_VALUE, Integer.MAX_VALUE);
|
||||
public final GuiCheckbox previewCheckbox = new GuiCheckbox().setI18nLabel("replaymod.gui.rendering.preview");
|
||||
public final GuiLabel renderTime = new GuiLabel();
|
||||
public final GuiLabel remainingTime = new GuiLabel();
|
||||
public final GuiProgressBar progressBar = new GuiProgressBar();
|
||||
public final GuiPanel buttonPanel = new GuiPanel().setLayout(new HorizontalLayout().setSpacing(4));
|
||||
public final GuiButton pauseButton = new GuiButton(buttonPanel).onClick(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (renderer.isPaused()) {
|
||||
pauseButton.setI18nLabel("replaymod.gui.rendering.pause");
|
||||
renderer.setPaused(false);
|
||||
} else {
|
||||
pauseButton.setI18nLabel("replaymod.gui.rendering.resume");
|
||||
renderer.setPaused(true);
|
||||
}
|
||||
}
|
||||
}).setI18nLabel("replaymod.gui.rendering.pause").setSize(150, 20);
|
||||
|
||||
public final GuiButton cancelButton = new GuiButton(buttonPanel){
|
||||
boolean waitingForConfirmation;
|
||||
|
||||
@Override
|
||||
public boolean mouseClick(ReadablePoint position, int button) {
|
||||
boolean result = super.mouseClick(position, button);
|
||||
if (waitingForConfirmation && !result) {
|
||||
setI18nLabel("replaymod.gui.rendering.cancel");
|
||||
waitingForConfirmation = false;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick() {
|
||||
super.onClick();
|
||||
if (!waitingForConfirmation) {
|
||||
setI18nLabel("replaymod.gui.rendering.cancel.callback");
|
||||
waitingForConfirmation = true;
|
||||
} else {
|
||||
renderer.cancel();
|
||||
}
|
||||
}
|
||||
}.setI18nLabel("replaymod.gui.rendering.cancel").setSize(150, 20);
|
||||
|
||||
private DynamicTexture previewTexture;
|
||||
private boolean previewTextureDirty;
|
||||
|
||||
{
|
||||
final GuiPanel contentPanel = new GuiPanel(this).setLayout(new CustomLayout<GuiPanel>() {
|
||||
@Override
|
||||
protected void layout(GuiPanel container, int width, int height) {
|
||||
size(progressBar, width, 20);
|
||||
pos(title, width / 2 - width(title) / 2, 0);
|
||||
pos(imagePanel, 0, y(title) + height(title) + 5);
|
||||
pos(buttonPanel, width / 2 - width(buttonPanel) / 2, height - height(buttonPanel));
|
||||
pos(progressBar, width / 2 - width(progressBar) / 2, y(buttonPanel) - 5 - height(progressBar));
|
||||
pos(renderTime, 0, y(progressBar) - 2 - height(renderTime));
|
||||
pos(remainingTime, width - width(remainingTime), y(progressBar) - 2 - height(renderTime));
|
||||
pos(previewCheckbox, width / 2 - width(previewCheckbox) / 2, y(renderTime) - 10 - height(previewCheckbox));
|
||||
size(imagePanel, width, y(previewCheckbox) - 5 - y(imagePanel));
|
||||
}
|
||||
}).addElements(null, title, imagePanel, previewCheckbox, renderTime, remainingTime, progressBar, buttonPanel);
|
||||
setLayout(new CustomLayout<GuiScreen>() {
|
||||
@Override
|
||||
protected void layout(GuiScreen container, int width, int height) {
|
||||
// Make sure we have some space around our central GUI components
|
||||
pos(contentPanel, 5, 3);
|
||||
size(contentPanel, width - 10, height - 10);
|
||||
}
|
||||
});
|
||||
setBackground(Background.DIRT);
|
||||
}
|
||||
|
||||
public GuiVideoRenderer(VideoRenderer renderer) {
|
||||
this.renderer = renderer;
|
||||
}
|
||||
|
||||
//the total render time
|
||||
private int renderTimeTaken = 0;
|
||||
|
||||
//the time at which the Screen was last updated
|
||||
private long prevTime = -1;
|
||||
|
||||
//the time at which the rendering of the current frame started
|
||||
private long frameStartTime = -1;
|
||||
|
||||
//the amount of frames that were rendered when the time left was last updated
|
||||
private int prevRenderedFrames = 0;
|
||||
|
||||
//the estimated render time that is left (in seconds)
|
||||
private int renderTimeLeft = 0;
|
||||
|
||||
//each of the durations the rendering process took for the last 50 frames
|
||||
private int[] renderTimes = new int[50];
|
||||
//the algorithm's current position in the renderTimes array
|
||||
private int currentIndex = 0;
|
||||
|
||||
@Override
|
||||
public void draw(GuiRenderer guiRenderer, ReadableDimension size, RenderInfo renderInfo) {
|
||||
long current = System.currentTimeMillis();
|
||||
|
||||
//first, update the total render time (only if rendering is not paused and has already started)
|
||||
if(!renderer.isPaused() && renderer.getFramesDone() > 0 && prevTime > -1) {
|
||||
renderTimeTaken += (current - prevTime);
|
||||
} else {
|
||||
//if the rendering process is paused, we have to update the frame start time to prevent huge render times
|
||||
//for the currently rendered frame(s)
|
||||
frameStartTime = current;
|
||||
}
|
||||
|
||||
//always update prevTime, so we don't get huge time jumps when unpausing the rendering process
|
||||
prevTime = current;
|
||||
|
||||
|
||||
//calculate estimated time left
|
||||
|
||||
//if the amount of rendered frames has increased since the last update
|
||||
if(prevRenderedFrames < renderer.getFramesDone()) {
|
||||
//we don't include the first frame in our calculations,
|
||||
//as setting up the rendering process takes a few seconds
|
||||
if(prevRenderedFrames > 0) {
|
||||
|
||||
//calculate the amount of frames that have been rendered since the last update
|
||||
int framesRendered = renderer.getFramesDone() - prevRenderedFrames;
|
||||
|
||||
//calculate the time it took to render these frames
|
||||
int renderTime = (int)(current - frameStartTime);
|
||||
|
||||
//calculate the average time it took for each of these frames to render
|
||||
int avgRenderTime = renderTime/framesRendered;
|
||||
|
||||
//add all of the average render times to the render times array
|
||||
for(int i=0; i<framesRendered; i++) {
|
||||
renderTimes[currentIndex] = avgRenderTime;
|
||||
currentIndex++;
|
||||
if(currentIndex >= renderTimes.length) currentIndex = 0;
|
||||
}
|
||||
|
||||
//the renderTimes array initially contains lots of zeros,
|
||||
//so we count the amount of valid timespans
|
||||
int validValues = 0;
|
||||
|
||||
int totalTime = 0;
|
||||
for(int i : renderTimes) {
|
||||
if(i > 0) {
|
||||
totalTime += i;
|
||||
validValues++;
|
||||
}
|
||||
}
|
||||
|
||||
//calculate the average render time for the previous [up to 50] frames
|
||||
float averageRenderTime = validValues > 0 ? totalTime / validValues : 0;
|
||||
|
||||
//calculate the remaining render time in seconds
|
||||
renderTimeLeft = Math.round((averageRenderTime * (renderer.getTotalFrames() - renderer.getFramesDone())) / 1000);
|
||||
}
|
||||
|
||||
//set the render start time of the next frame(s) to the current timestamp
|
||||
frameStartTime = current;
|
||||
//update the amount of rendered frames for the last calculation
|
||||
prevRenderedFrames = renderer.getFramesDone();
|
||||
}
|
||||
|
||||
renderTime.setText(I18n.format("replaymod.gui.rendering.timetaken") + ": " + secToString(renderTimeTaken/1000));
|
||||
remainingTime.setText(I18n.format("replaymod.gui.rendering.timeleft") + ": " + secToString(renderTimeLeft));
|
||||
|
||||
int framesDone = renderer.getFramesDone(), framesTotal = renderer.getTotalFrames();
|
||||
progressBar.setI18nLabel("replaymod.gui.rendering.progress", framesDone, framesTotal);
|
||||
progressBar.setProgress((float) framesDone / framesTotal);
|
||||
|
||||
super.draw(guiRenderer, size, renderInfo);
|
||||
}
|
||||
|
||||
private String secToString(int seconds) {
|
||||
int hours = seconds/(60*60);
|
||||
int min = seconds/60 - hours*60;
|
||||
int sec = seconds - ((min*60) + (hours*60*60));
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
if(hours > 0) builder.append(hours).append(I18n.format("replaymod.gui.hours"));
|
||||
if(min > 0 || hours > 0) builder.append(min).append(I18n.format("replaymod.gui.minutes"));
|
||||
builder.append(sec).append(I18n.format("replaymod.gui.seconds"));
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private synchronized void renderPreview(GuiRenderer guiRenderer, ReadableDimension size) {
|
||||
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;
|
||||
}
|
||||
|
||||
guiRenderer.bindTexture(previewTexture);
|
||||
renderPreviewTexture(guiRenderer, size, videoWidth, videoHeight);
|
||||
}
|
||||
|
||||
private void renderNoPreview(GuiRenderer guiRenderer, ReadableDimension size) {
|
||||
guiRenderer.bindTexture(NO_PREVIEW_TEXTURE);
|
||||
renderPreviewTexture(guiRenderer, size, 1280, 720);
|
||||
}
|
||||
|
||||
private void renderPreviewTexture(GuiRenderer guiRenderer, ReadableDimension size,
|
||||
int videoWidth, int videoHeight) {
|
||||
Dimension dimension = BoundingUtils.fitIntoBounds(new Dimension(videoWidth, videoHeight), size);
|
||||
|
||||
int width = dimension.getWidth();
|
||||
int height = dimension.getHeight();
|
||||
int x = (size.getWidth() - width) / 2;
|
||||
int y = (size.getHeight() - height) / 2;
|
||||
|
||||
guiRenderer.drawTexturedRect(x, y, 0, 0, width, height, videoWidth, videoHeight, videoWidth, videoHeight);
|
||||
}
|
||||
|
||||
public void updatePreview(RGBFrame frame) {
|
||||
if (previewCheckbox.isChecked() && previewTexture != null) {
|
||||
ByteBuffer buffer = frame.getByteBuffer();
|
||||
buffer.mark();
|
||||
synchronized (this) {
|
||||
int[] data = previewTexture.getTextureData();
|
||||
for (int i = 0; i < data.length; i++) {
|
||||
data[i] = 0xff << 24 | (buffer.get() & 0xff) << 16 | (buffer.get() & 0xff) << 8 | (buffer.get() & 0xff);
|
||||
}
|
||||
previewTextureDirty = true;
|
||||
}
|
||||
buffer.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package com.replaymod.render.hooks;
|
||||
|
||||
import eu.crushedpixel.replaymod.utils.JailingQueue;
|
||||
import net.minecraft.client.renderer.RegionRenderCacheBuilder;
|
||||
import net.minecraft.client.renderer.RenderGlobal;
|
||||
import net.minecraft.client.renderer.chunk.ChunkCompileTaskGenerator;
|
||||
import net.minecraft.client.renderer.chunk.ChunkRenderDispatcher;
|
||||
import net.minecraft.client.renderer.chunk.ChunkRenderWorker;
|
||||
import net.minecraft.client.renderer.chunk.RenderChunk;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Iterator;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
|
||||
public class ChunkLoadingRenderGlobal {
|
||||
|
||||
private final RenderGlobal hooked;
|
||||
private final ChunkRenderDispatcher renderDispatcher;
|
||||
private final JailingQueue<?> workerJailingQueue;
|
||||
private final CustomChunkRenderWorker renderWorker;
|
||||
private int frame;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public ChunkLoadingRenderGlobal(RenderGlobal renderGlobal) {
|
||||
this.hooked = renderGlobal;
|
||||
this.renderDispatcher = renderGlobal.renderDispatcher;
|
||||
this.renderWorker = new CustomChunkRenderWorker(renderDispatcher, new RegionRenderCacheBuilder());
|
||||
|
||||
int workerThreads = renderDispatcher.listThreadedWorkers.size();
|
||||
BlockingQueue<Object> queueChunkUpdates = renderDispatcher.queueChunkUpdates;
|
||||
workerJailingQueue = new JailingQueue<>(queueChunkUpdates);
|
||||
renderDispatcher.queueChunkUpdates = workerJailingQueue;
|
||||
ChunkCompileTaskGenerator element = new ChunkCompileTaskGenerator(null, null);
|
||||
element.finish();
|
||||
for (int i = 0; i < workerThreads; i++) {
|
||||
queueChunkUpdates.add(element);
|
||||
}
|
||||
|
||||
// Temporary workaround for dead lock, will be replaced by a new (ShaderMod compatible) mechanism later
|
||||
//noinspection StatementWithEmptyBody
|
||||
while (renderDispatcher.runChunkUploads(0)) {}
|
||||
|
||||
workerJailingQueue.jail(workerThreads);
|
||||
renderDispatcher.queueChunkUpdates = queueChunkUpdates;
|
||||
|
||||
try {
|
||||
Field hookField = RenderGlobal.class.getField("replayModRender_hook");
|
||||
hookField.set(hooked, this);
|
||||
} catch (NoSuchFieldException | IllegalAccessException e) {
|
||||
throw new Error(e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public void updateChunks() {
|
||||
while (renderDispatcher.runChunkUploads(0)) {
|
||||
hooked.displayListEntitiesDirty = true;
|
||||
}
|
||||
|
||||
while (!renderDispatcher.queueChunkUpdates.isEmpty()) {
|
||||
try {
|
||||
renderWorker.processTask((ChunkCompileTaskGenerator) renderDispatcher.queueChunkUpdates.poll());
|
||||
} catch (InterruptedException ignored) { }
|
||||
}
|
||||
|
||||
Iterator<RenderChunk> iterator = hooked.chunksToUpdate.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
RenderChunk renderchunk = iterator.next();
|
||||
|
||||
renderDispatcher.updateChunkNow(renderchunk);
|
||||
|
||||
renderchunk.setNeedsUpdate(false);
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
|
||||
public void uninstall() {
|
||||
workerJailingQueue.freeAll();
|
||||
|
||||
try {
|
||||
Field hookField = RenderGlobal.class.getField("replayModRender_hook");
|
||||
hookField.set(hooked, null);
|
||||
} catch (NoSuchFieldException | IllegalAccessException e) {
|
||||
throw new Error(e);
|
||||
}
|
||||
}
|
||||
|
||||
public int nextFrameId() {
|
||||
return frame++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom ChunkRenderWorker class providing access to the protected processTask method
|
||||
*/
|
||||
private static class CustomChunkRenderWorker extends ChunkRenderWorker {
|
||||
public CustomChunkRenderWorker(ChunkRenderDispatcher p_i46202_1_, RegionRenderCacheBuilder p_i46202_2_) {
|
||||
super(p_i46202_1_, p_i46202_2_);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processTask(ChunkCompileTaskGenerator p_178474_1_) throws InterruptedException {
|
||||
super.processTask(p_178474_1_);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.replaymod.render.hooks;
|
||||
|
||||
import com.replaymod.render.RenderSettings;
|
||||
import com.replaymod.render.capturer.CaptureData;
|
||||
import com.replaymod.render.capturer.WorldRenderer;
|
||||
import lombok.Getter;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.culling.ClippingHelper;
|
||||
import org.lwjgl.util.ReadableDimension;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import static net.minecraft.client.renderer.GlStateManager.*;
|
||||
|
||||
public class EntityRendererHandler implements WorldRenderer {
|
||||
public final Minecraft mc = Minecraft.getMinecraft();
|
||||
|
||||
@Getter
|
||||
protected final RenderSettings settings;
|
||||
|
||||
public CaptureData data;
|
||||
|
||||
public EntityRendererHandler(RenderSettings settings) {
|
||||
this.settings = settings;
|
||||
|
||||
((IEntityRenderer) mc.entityRenderer).replayModRender_setHandler(this);
|
||||
}
|
||||
|
||||
public void withDisplaySize(int displayWidth, int displayHeight, Runnable runnable) {
|
||||
final int prevWidth = mc.displayWidth;
|
||||
final int prevHeight = mc.displayHeight;
|
||||
mc.displayWidth = displayWidth;
|
||||
mc.displayHeight = displayHeight;
|
||||
|
||||
runnable.run();
|
||||
|
||||
mc.displayWidth = prevWidth;
|
||||
mc.displayHeight = prevHeight;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void renderWorld(ReadableDimension displaySize, final float partialTicks, CaptureData data) {
|
||||
this.data = data;
|
||||
withDisplaySize(displaySize.getWidth(), displaySize.getHeight(), new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
renderWorld(partialTicks, 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void renderWorld(float partialTicks, long finishTimeNano) {
|
||||
mc.entityRenderer.updateLightmap(partialTicks);
|
||||
|
||||
enableDepth();
|
||||
enableAlpha();
|
||||
alphaFunc(516, 0.5F);
|
||||
|
||||
mc.entityRenderer.renderWorldPass(2, partialTicks, finishTimeNano);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
((IEntityRenderer) mc.entityRenderer).replayModRender_setHandler(null);
|
||||
}
|
||||
|
||||
public static final class NoCullingClippingHelper extends ClippingHelper {
|
||||
@Override
|
||||
public boolean isBoxInFrustum(double p_78553_1_, double p_78553_3_, double p_78553_5_, double p_78553_7_, double p_78553_9_, double p_78553_11_) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public interface GluPerspective {
|
||||
void replayModRender_gluPerspective(float fovY, float aspect, float zNear, float zFar);
|
||||
}
|
||||
|
||||
public interface IEntityRenderer {
|
||||
void replayModRender_setHandler(EntityRendererHandler handler);
|
||||
EntityRendererHandler replayModRender_getHandler();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.replaymod.render.hooks;
|
||||
|
||||
import com.replaymod.core.utils.WrappedTimer;
|
||||
import net.minecraft.util.Timer;
|
||||
|
||||
/**
|
||||
* Wrapper around the current timer that prevents the timer from advancing by itself.
|
||||
*/
|
||||
public class RenderReplayTimer extends WrappedTimer {
|
||||
private final Timer state = new Timer(0);
|
||||
|
||||
public RenderReplayTimer(Timer wrapped) {
|
||||
super(wrapped);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateTimer() {
|
||||
copy(this, state); // Save our current state
|
||||
super.updateTimer(); // Update current state
|
||||
copy(state, this); // Restore our old state
|
||||
}
|
||||
|
||||
public Timer getWrapped() {
|
||||
return wrapped;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package com.replaymod.render.metadata;
|
||||
|
||||
import com.coremedia.iso.IsoFile;
|
||||
import com.coremedia.iso.boxes.*;
|
||||
import com.google.common.primitives.Bytes;
|
||||
import com.googlecode.mp4parser.BasicContainer;
|
||||
import net.minecraftforge.fml.common.FMLLog;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class MetadataInjector {
|
||||
|
||||
private static final String STITCHING_SOFTWARE = "Minecraft ReplayMod";
|
||||
|
||||
/*
|
||||
* The Spherical XML is a modified version of https://github.com/google/spatial-media/tree/master/360-Videos-Metadata
|
||||
*/
|
||||
private static final String SPHERICAL_XML_HEADER =
|
||||
"<?xml version=\"1.0\"?> " +
|
||||
"<rdf:SphericalVideo xmlns:rdf=" +
|
||||
"\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\" xmlns:GSpherical=\"http://ns.google.com/videos/1.0/spherical/\"> ";
|
||||
private static final String SPHERICAL_XML_CONTENTS =
|
||||
"<GSpherical:Spherical>true</GSpherical:Spherical> " +
|
||||
"<GSpherical:Stitched>true</GSpherical:Stitched> " +
|
||||
"<GSpherical:StitchingSoftware>"+STITCHING_SOFTWARE+"</GSpherical:StitchingSoftware> " +
|
||||
"<GSpherical:ProjectionType>equirectangular</GSpherical:ProjectionType> ";
|
||||
private static final String SPHERICAL_XML_FOOTER = "</rdf:SphericalVideo>";
|
||||
|
||||
private static final String XML_360_METADATA = SPHERICAL_XML_HEADER + SPHERICAL_XML_CONTENTS + SPHERICAL_XML_FOOTER;
|
||||
|
||||
/**
|
||||
* These bytes are taken from the variable 'spherical_uuid_id'
|
||||
* in https://github.com/google/spatial-media/tree/master/360-Videos-Metadata
|
||||
*/
|
||||
private static final byte[] UUID_BYTES = new byte[] {
|
||||
(byte)0xff, (byte)0xcc, (byte)0x82, (byte)0x63,
|
||||
(byte)0xf8, (byte)0x55, (byte)0x4a, (byte)0x93,
|
||||
(byte)0x88, (byte)0x14, (byte)0x58, (byte)0x7a,
|
||||
(byte)0x02, (byte)0x52, (byte)0x1f, (byte)0xdd
|
||||
};
|
||||
|
||||
private static final byte[] BYTES_360_METADATA = Bytes.concat(UUID_BYTES, XML_360_METADATA.getBytes());
|
||||
|
||||
public static void inject360Metadata(File videoFile) {
|
||||
writeMetadata(videoFile, BYTES_360_METADATA);
|
||||
}
|
||||
|
||||
private static void writeMetadata(File videoFile, byte[] metadata) {
|
||||
File tempFile = null;
|
||||
FileOutputStream videoFileOutputStream = null;
|
||||
IsoFile tempIsoFile = null;
|
||||
|
||||
try {
|
||||
tempFile = File.createTempFile("videoCopy", "mp4");
|
||||
FileUtils.copyFile(videoFile, tempFile);
|
||||
|
||||
tempIsoFile = new IsoFile(tempFile.getAbsolutePath());
|
||||
|
||||
//first, get the "moov" box from the mp4
|
||||
MovieBox moovBox = (MovieBox)getBoxByName(tempIsoFile, "moov");
|
||||
if(moovBox == null) throw new IOException("Could not find moov box inside IsoFile");
|
||||
|
||||
//get the Movie Track to which we will add the Metadata
|
||||
TrackBox trackBox = (TrackBox)getBoxByName(moovBox, "trak");
|
||||
if(trackBox == null) throw new IOException("Could not find trak box inside moov box");
|
||||
|
||||
//create a new UserBox, which actually contains the Metadata bytes
|
||||
UserBox metadataBox = new UserBox(new byte[0]);
|
||||
metadataBox.setData(metadata);
|
||||
|
||||
//add the Metadata UserBox to the Movie Track
|
||||
trackBox.addBox(metadataBox);
|
||||
|
||||
//finally, reduce the Video's FreeBox, whose size we will need
|
||||
//to reduce by the Metadata Box's size to preserve the Video's File size
|
||||
FreeBox freeBox = (FreeBox)getBoxByName(tempIsoFile, "free");
|
||||
if(freeBox == null) throw new IOException("Could not find free box inside IsoFile");
|
||||
|
||||
int freeSize = Math.max(0, freeBox.getData().capacity() - (int)metadataBox.getSize());
|
||||
freeBox.setData(ByteBuffer.allocate(freeSize));
|
||||
|
||||
//save the ISO file to disk
|
||||
videoFileOutputStream = new FileOutputStream(videoFile);
|
||||
tempIsoFile.getBox(videoFileOutputStream.getChannel());
|
||||
} catch(Exception e) {
|
||||
FMLLog.getLogger().error("360 Degree Metadata couldn't be injected", e);
|
||||
} finally {
|
||||
IOUtils.closeQuietly(tempIsoFile);
|
||||
IOUtils.closeQuietly(videoFileOutputStream);
|
||||
FileUtils.deleteQuietly(tempFile);
|
||||
}
|
||||
}
|
||||
|
||||
private static Box getBoxByName(BasicContainer container, String boxName) {
|
||||
for (Box box : container.getBoxes()) {
|
||||
if(box.getType().equals(boxName)) return box;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.replaymod.render.mixin;
|
||||
|
||||
import com.replaymod.render.capturer.CubicOpenGlFrameCapturer;
|
||||
import com.replaymod.render.hooks.EntityRendererHandler;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.particle.EffectRenderer;
|
||||
import net.minecraft.client.particle.EntityFX;
|
||||
import net.minecraft.client.renderer.WorldRenderer;
|
||||
import net.minecraft.entity.Entity;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Redirect;
|
||||
|
||||
@Mixin(EffectRenderer.class)
|
||||
public abstract class MixinEffectRenderer {
|
||||
@Redirect(method = "renderParticles", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/particle/EntityFX;func_180434_a(Lnet/minecraft/client/renderer/WorldRenderer;Lnet/minecraft/entity/Entity;FFFFFF)V"))
|
||||
private void renderNormalParticle(EntityFX fx, WorldRenderer worldRenderer, Entity view, float partialTicks,
|
||||
float rotX, float rotXZ, float rotZ, float rotYZ, float rotXY) {
|
||||
renderParticle(fx, worldRenderer, view, partialTicks, rotX, rotXZ, rotZ, rotYZ, rotXY);
|
||||
}
|
||||
|
||||
@Redirect(method = "renderLitParticles", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/particle/EntityFX;func_180434_a(Lnet/minecraft/client/renderer/WorldRenderer;Lnet/minecraft/entity/Entity;FFFFFF)V"))
|
||||
private void renderLitParticle(EntityFX fx, WorldRenderer worldRenderer, Entity view, float partialTicks,
|
||||
float rotX, float rotXZ, float rotZ, float rotYZ, float rotXY) {
|
||||
renderParticle(fx, worldRenderer, view, partialTicks, rotX, rotXZ, rotZ, rotYZ, rotXY);
|
||||
}
|
||||
|
||||
private void renderParticle(EntityFX fx, WorldRenderer worldRenderer, Entity view, float partialTicks,
|
||||
float rotX, float rotXZ, float rotZ, float rotYZ, float rotXY) {
|
||||
EntityRendererHandler handler = ((EntityRendererHandler.IEntityRenderer) Minecraft.getMinecraft().entityRenderer).replayModRender_getHandler();
|
||||
if (handler != null && handler.data instanceof CubicOpenGlFrameCapturer.Data) {
|
||||
// Align all particles towards the camera
|
||||
double dx = fx.prevPosX + (fx.posX - fx.prevPosX) * partialTicks - view.posX;
|
||||
double dy = fx.prevPosY + (fx.posY - fx.prevPosY) * partialTicks - view.posY;
|
||||
double dz = fx.prevPosZ + (fx.posZ - fx.prevPosZ) * partialTicks - view.posZ;
|
||||
double pitch = -Math.atan2(dy, Math.sqrt(dx * dx + dz * dz));
|
||||
double yaw = -Math.atan2(dx, dz);
|
||||
|
||||
rotX = (float) Math.cos(yaw);
|
||||
rotZ = (float) Math.sin(yaw);
|
||||
rotXZ = (float) Math.cos(pitch);
|
||||
|
||||
rotYZ = (float) (-rotZ * Math.sin(pitch));
|
||||
rotXY = (float) (rotX * Math.sin(pitch));
|
||||
}
|
||||
fx.func_180434_a(worldRenderer, view, partialTicks, rotX, rotXZ, rotZ, rotYZ, rotXY);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
package com.replaymod.render.mixin;
|
||||
|
||||
import com.replaymod.render.RenderSettings;
|
||||
import com.replaymod.render.capturer.CubicOpenGlFrameCapturer;
|
||||
import com.replaymod.render.capturer.StereoscopicOpenGlFrameCapturer;
|
||||
import com.replaymod.render.hooks.EntityRendererHandler;
|
||||
import com.replaymod.replay.camera.CameraEntity;
|
||||
import eu.crushedpixel.replaymod.renderer.SpectatorRenderer;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.EntityRenderer;
|
||||
import net.minecraft.client.renderer.GlStateManager;
|
||||
import net.minecraft.client.renderer.RenderGlobal;
|
||||
import net.minecraft.client.renderer.culling.Frustum;
|
||||
import net.minecraft.client.settings.GameSettings;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.entity.player.EntityPlayer;
|
||||
import net.minecraft.util.MovingObjectPosition;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.util.ReadableColor;
|
||||
import org.lwjgl.util.glu.Project;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.Redirect;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
@Mixin(value = EntityRenderer.class)
|
||||
public abstract class MixinEntityRenderer implements EntityRendererHandler.IEntityRenderer, EntityRendererHandler.GluPerspective {
|
||||
@Shadow
|
||||
public Minecraft mc;
|
||||
|
||||
private EntityRendererHandler replayModRender_handler;
|
||||
|
||||
@Shadow(remap = false)
|
||||
private SpectatorRenderer spectatorRenderer;
|
||||
|
||||
@Override
|
||||
public void replayModRender_setHandler(EntityRendererHandler handler) {
|
||||
this.replayModRender_handler = handler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EntityRendererHandler replayModRender_getHandler() {
|
||||
return replayModRender_handler;
|
||||
}
|
||||
|
||||
@Inject(method = "setupFog", at = @At("HEAD"), cancellable = true)
|
||||
private void replayModRender_onSetupFog(int fogDistanceFlag, float partialTicks, CallbackInfo ci) {
|
||||
if (replayModRender_handler == null) return;
|
||||
if (replayModRender_handler.getSettings().getChromaKeyingColor() != null) {
|
||||
ci.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@Redirect(method = "renderWorldPass", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/culling/Frustum;setPosition(DDD)V"))
|
||||
public void replayModRender_createNoCullingFrustum(Frustum frustum, double x, double y, double z) {
|
||||
if (replayModRender_handler != null) {
|
||||
frustum.clippingHelper = new EntityRendererHandler.NoCullingClippingHelper();
|
||||
}
|
||||
frustum.setPosition(x, y, z);
|
||||
}
|
||||
|
||||
@Inject(method = "orientCamera", at = @At("HEAD"))
|
||||
private void replayModRender_resetRotationIfNeeded(float partialTicks, CallbackInfo ci) {
|
||||
if (replayModRender_handler != null) {
|
||||
Entity entity = Minecraft.getMinecraft().getRenderViewEntity();
|
||||
RenderSettings settings = replayModRender_handler.getSettings();
|
||||
if (settings.isStabilizeYaw()) {
|
||||
entity.prevRotationYaw = entity.rotationYaw = 0;
|
||||
}
|
||||
if (settings.isStabilizePitch()) {
|
||||
entity.prevRotationPitch = entity.rotationPitch = 0;
|
||||
}
|
||||
if (settings.isStabilizeRoll() && entity instanceof CameraEntity) {
|
||||
((CameraEntity) entity).roll = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private float orgYaw;
|
||||
private float orgPitch;
|
||||
private float orgPrevYaw;
|
||||
private float orgPrevPitch;
|
||||
private float orgRoll;
|
||||
|
||||
@Inject(method = "setupCameraTransform", at = @At("HEAD"))
|
||||
private void replayModRender_beforeSetupCameraTransform(float partialTicks, int renderPass, CallbackInfo ci) {
|
||||
if (replayModRender_handler != null) {
|
||||
Entity entity = Minecraft.getMinecraft().getRenderViewEntity();
|
||||
orgYaw = entity.rotationYaw;
|
||||
orgPitch = entity.rotationPitch;
|
||||
orgPrevYaw = entity.prevRotationYaw;
|
||||
orgPrevPitch = entity.prevRotationPitch;
|
||||
orgRoll = entity instanceof CameraEntity ? ((CameraEntity) entity).roll : 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Inject(method = "setupCameraTransform", at = @At("RETURN"))
|
||||
private void replayModRender_afterSetupCameraTransform(float partialTicks, int renderPass, CallbackInfo ci) {
|
||||
if (replayModRender_handler != null) {
|
||||
Entity entity = Minecraft.getMinecraft().getRenderViewEntity();
|
||||
entity.rotationYaw = orgYaw;
|
||||
entity.rotationPitch = orgPitch;
|
||||
entity.prevRotationYaw = orgPrevYaw;
|
||||
entity.prevRotationPitch = orgPrevPitch;
|
||||
if (entity instanceof CameraEntity) {
|
||||
((CameraEntity) entity).roll = orgRoll;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Inject(method = "renderHand", at = @At("HEAD"), cancellable = true)
|
||||
private void replayModRender_renderSpectatorHand(float partialTicks, int renderPass, CallbackInfo ci) {
|
||||
if (replayModRender_handler != null) {
|
||||
if (replayModRender_handler.data instanceof CubicOpenGlFrameCapturer.Data) {
|
||||
ci.cancel();
|
||||
return; // No spectator hands during 360° view, we wouldn't even know where to put it
|
||||
}
|
||||
Entity currentEntity = Minecraft.getMinecraft().getRenderViewEntity();
|
||||
if (currentEntity instanceof EntityPlayer && !(currentEntity instanceof CameraEntity)) {
|
||||
renderPass = replayModRender_handler.data == StereoscopicOpenGlFrameCapturer.Data.LEFT_EYE ? 1 : 0;
|
||||
spectatorRenderer.renderSpectatorHand((EntityPlayer) currentEntity, partialTicks, renderPass);
|
||||
ci.cancel(); // No further hand rendering needed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Redirect(method = "renderWorldPass", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/RenderGlobal;drawSelectionBox(Lnet/minecraft/entity/player/EntityPlayer;Lnet/minecraft/util/MovingObjectPosition;IF)V"))
|
||||
private void replayModRender_drawSelectionBox(RenderGlobal instance, EntityPlayer player, MovingObjectPosition mop, int alwaysZero, float partialTicks) {
|
||||
if (replayModRender_handler == null) {
|
||||
instance.drawSelectionBox(player, mop, alwaysZero, partialTicks);
|
||||
}
|
||||
}
|
||||
|
||||
private int orgRenderDistanceChunks;
|
||||
|
||||
@Inject(method = "renderWorldPass", at = @At(value = "JUMP", ordinal = 0))
|
||||
private void replayModRender_beforeRenderSky(CallbackInfo ci) {
|
||||
if (replayModRender_handler != null && replayModRender_handler.getSettings().getChromaKeyingColor() != null) {
|
||||
GameSettings settings = Minecraft.getMinecraft().gameSettings;
|
||||
orgRenderDistanceChunks = settings.renderDistanceChunks;
|
||||
settings.renderDistanceChunks = 5; // Set render distance to 5 so we're always rendering sky when chroma keying
|
||||
}
|
||||
}
|
||||
|
||||
@Redirect(method = "renderWorldPass", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/RenderGlobal;renderSky(FI)V"))
|
||||
private void replayModRender_renderSky(RenderGlobal instance, float partialTicks, int renderPass) {
|
||||
if (replayModRender_handler != null && replayModRender_handler.getSettings().getChromaKeyingColor() != null) {
|
||||
Minecraft.getMinecraft().gameSettings.renderDistanceChunks = orgRenderDistanceChunks;
|
||||
ReadableColor color = replayModRender_handler.getSettings().getChromaKeyingColor();
|
||||
GlStateManager.clearColor(color.getRed() / 255f, color.getGreen() / 255f, color.getBlue() / 255f, 1);
|
||||
GlStateManager.clear(GL11.GL_COLOR_BUFFER_BIT);
|
||||
} else {
|
||||
instance.renderSky(partialTicks, renderPass);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Stereoscopic Renderer
|
||||
*/
|
||||
|
||||
@Inject(method = "setupCameraTransform", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/GlStateManager;loadIdentity()V", shift = At.Shift.AFTER, ordinal = 0))
|
||||
private void replayModRender_setupStereoscopicProjection(float partialTicks, int renderPass, CallbackInfo ci) {
|
||||
if (replayModRender_getHandler() != null) {
|
||||
if (replayModRender_getHandler().data == StereoscopicOpenGlFrameCapturer.Data.LEFT_EYE) {
|
||||
GlStateManager.translate(0.07, 0, 0);
|
||||
} else if (replayModRender_getHandler().data == StereoscopicOpenGlFrameCapturer.Data.RIGHT_EYE) {
|
||||
GlStateManager.translate(-0.07, 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Inject(method = "setupCameraTransform", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/GlStateManager;loadIdentity()V", shift = At.Shift.AFTER, ordinal = 1))
|
||||
private void replayModRender_setupStereoscopicModelView(float partialTicks, int renderPass, CallbackInfo ci) {
|
||||
if (replayModRender_getHandler() != null) {
|
||||
if (replayModRender_getHandler().data == StereoscopicOpenGlFrameCapturer.Data.LEFT_EYE) {
|
||||
GlStateManager.translate(0.1, 0, 0);
|
||||
} else if (replayModRender_getHandler().data == StereoscopicOpenGlFrameCapturer.Data.RIGHT_EYE) {
|
||||
GlStateManager.translate(-0.1, 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Cubic Renderer
|
||||
*/
|
||||
|
||||
@Redirect(method = "setupCameraTransform", at = @At(value = "INVOKE", target = "Lorg/lwjgl/util/glu/Project;gluPerspective(FFFF)V", remap = false))
|
||||
private void replayModRender_gluPerspective$0(float fovY, float aspect, float zNear, float zFar) {
|
||||
replayModRender_gluPerspective(fovY, aspect, zNear, zFar);
|
||||
}
|
||||
|
||||
@Redirect(method = "renderWorldPass", at = @At(value = "INVOKE", target = "Lorg/lwjgl/util/glu/Project;gluPerspective(FFFF)V", remap = false))
|
||||
private void replayModRender_gluPerspective$1(float fovY, float aspect, float zNear, float zFar) {
|
||||
replayModRender_gluPerspective(fovY, aspect, zNear, zFar);
|
||||
}
|
||||
|
||||
@Redirect(method = "renderCloudsCheck", at = @At(value = "INVOKE", target = "Lorg/lwjgl/util/glu/Project;gluPerspective(FFFF)V", remap = false))
|
||||
private void replayModRender_gluPerspective$2(float fovY, float aspect, float zNear, float zFar) {
|
||||
replayModRender_gluPerspective(fovY, aspect, zNear, zFar);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void replayModRender_gluPerspective(float fovY, float aspect, float zNear, float zFar) {
|
||||
if (replayModRender_getHandler() != null && replayModRender_getHandler().data instanceof CubicOpenGlFrameCapturer.Data) {
|
||||
fovY = 90;
|
||||
aspect = 1;
|
||||
}
|
||||
Project.gluPerspective(fovY, aspect, zNear, zFar);
|
||||
}
|
||||
|
||||
@Inject(method = "orientCamera", at = @At("HEAD"))
|
||||
private void replayModRender_setupCubicFrameRotation(float partialTicks, CallbackInfo ci) {
|
||||
if (replayModRender_getHandler() != null && replayModRender_getHandler().data instanceof CubicOpenGlFrameCapturer.Data) {
|
||||
switch ((CubicOpenGlFrameCapturer.Data) replayModRender_getHandler().data) {
|
||||
case FRONT:
|
||||
GlStateManager.rotate(0, 0.0F, 1.0F, 0.0F);
|
||||
break;
|
||||
case RIGHT:
|
||||
GlStateManager.rotate(90, 0.0F, 1.0F, 0.0F);
|
||||
break;
|
||||
case BACK:
|
||||
GlStateManager.rotate(180, 0.0F, 1.0F, 0.0F);
|
||||
break;
|
||||
case LEFT:
|
||||
GlStateManager.rotate(-90, 0.0F, 1.0F, 0.0F);
|
||||
break;
|
||||
case BOTTOM:
|
||||
GlStateManager.rotate(90, 1.0F, 0.0F, 0.0F);
|
||||
break;
|
||||
case TOP:
|
||||
GlStateManager.rotate(-90, 1.0F, 0.0F, 0.0F);
|
||||
break;
|
||||
}
|
||||
|
||||
// Minecraft goes back a little so we have to revert that
|
||||
GlStateManager.translate(0.0F, 0.0F, 0.1F);
|
||||
}
|
||||
}
|
||||
}
|
||||
22
src/main/java/com/replaymod/render/mixin/MixinRender.java
Normal file
22
src/main/java/com/replaymod/render/mixin/MixinRender.java
Normal file
@@ -0,0 +1,22 @@
|
||||
package com.replaymod.render.mixin;
|
||||
|
||||
import com.replaymod.render.hooks.EntityRendererHandler;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.entity.Render;
|
||||
import net.minecraft.entity.Entity;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
@Mixin(Render.class)
|
||||
public abstract class MixinRender {
|
||||
@Inject(method = "canRenderName", at = @At("HEAD"), cancellable = true)
|
||||
private void replayModRender_areAllNamesHidden(Entity entity, CallbackInfoReturnable<Boolean> ci) {
|
||||
EntityRendererHandler handler = ((EntityRendererHandler.IEntityRenderer) Minecraft.getMinecraft().entityRenderer).replayModRender_getHandler();
|
||||
if (handler != null && !handler.getSettings().isRenderNameTags()) {
|
||||
ci.setReturnValue(false);
|
||||
ci.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.replaymod.render.mixin;
|
||||
|
||||
import com.replaymod.render.hooks.ChunkLoadingRenderGlobal;
|
||||
import net.minecraft.client.renderer.RenderGlobal;
|
||||
import net.minecraft.client.renderer.chunk.ChunkRenderDispatcher;
|
||||
import net.minecraft.client.renderer.chunk.RenderChunk;
|
||||
import net.minecraft.client.renderer.culling.ICamera;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.util.BlockPos;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
@Mixin(RenderGlobal.class)
|
||||
public abstract class MixinRenderGlobal {
|
||||
public ChunkLoadingRenderGlobal replayModRender_hook;
|
||||
private boolean replayModRender_passThroughSetupTerrain;
|
||||
|
||||
@Shadow
|
||||
public boolean displayListEntitiesDirty;
|
||||
|
||||
@Shadow
|
||||
public ChunkRenderDispatcher renderDispatcher;
|
||||
|
||||
@Shadow
|
||||
public abstract void setupTerrain(Entity viewEntity, double partialTicks, ICamera camera,
|
||||
int frameCount, boolean playerSpectator);
|
||||
|
||||
@Inject(method = "setupTerrain", at = @At("HEAD"), cancellable = true)
|
||||
public void replayModRender_setupTerrain(Entity viewEntity, double partialTicks, ICamera camera,
|
||||
int frameCount, boolean playerSpectator, CallbackInfo ci) {
|
||||
if (replayModRender_hook != null && !replayModRender_passThroughSetupTerrain) {
|
||||
replayModRender_passThroughSetupTerrain = true;
|
||||
|
||||
do {
|
||||
setupTerrain(viewEntity, partialTicks, camera, replayModRender_hook.nextFrameId(), playerSpectator);
|
||||
} while (displayListEntitiesDirty);
|
||||
|
||||
replayModRender_passThroughSetupTerrain = false;
|
||||
ci.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@Inject(method = "isPositionInRenderChunk", at = @At("HEAD"), cancellable = true)
|
||||
public void replayModRender_isPositionInRenderChunk(BlockPos pos, RenderChunk chunk, CallbackInfoReturnable<Boolean> ci) {
|
||||
if (replayModRender_hook != null) {
|
||||
ci.setReturnValue(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Inject(method = "updateChunks", at = @At("HEAD"), cancellable = true)
|
||||
public void replayModRender_updateChunks(long finishTimeNano, CallbackInfo ci) {
|
||||
if (replayModRender_hook != null) {
|
||||
replayModRender_hook.updateChunks();
|
||||
ci.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.replaymod.render.mixin;
|
||||
|
||||
import com.replaymod.render.capturer.CubicOpenGlFrameCapturer;
|
||||
import com.replaymod.render.hooks.EntityRendererHandler;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.entity.RenderManager;
|
||||
import net.minecraft.entity.Entity;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
@Mixin(RenderManager.class)
|
||||
public abstract class MixinRenderManager {
|
||||
@Shadow
|
||||
private float playerViewX;
|
||||
|
||||
@Shadow
|
||||
private float playerViewY;
|
||||
|
||||
@Inject(method = "doRenderEntity", at = @At("HEAD"))
|
||||
private void replayModRender_reorientForCubicRendering(Entity entity, double dx, double dy, double dz, float iDoNotKnow, float partialTicks, boolean iDoNotCare, CallbackInfoReturnable ci) {
|
||||
EntityRendererHandler handler = ((EntityRendererHandler.IEntityRenderer) Minecraft.getMinecraft().entityRenderer).replayModRender_getHandler();
|
||||
if (handler != null && handler.data instanceof CubicOpenGlFrameCapturer.Data) {
|
||||
double pitch = -Math.atan2(dy, Math.sqrt(dx * dx + dz * dz));
|
||||
double yaw = -Math.atan2(dx, dz);
|
||||
playerViewX = (float) Math.toDegrees(pitch);
|
||||
playerViewY = (float) Math.toDegrees(yaw);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.replaymod.render.mixin;
|
||||
|
||||
import com.replaymod.render.hooks.EntityRendererHandler;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.entity.RendererLivingEntity;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.entity.player.EntityPlayer;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.Redirect;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
@Mixin(RendererLivingEntity.class)
|
||||
public abstract class MixinRendererLivingEntity {
|
||||
@Inject(method = "canRenderName", at = @At("HEAD"), cancellable = true)
|
||||
private void replayModRender_areAllNamesHidden(EntityLivingBase entity, CallbackInfoReturnable<Boolean> ci) {
|
||||
EntityRendererHandler handler = ((EntityRendererHandler.IEntityRenderer) Minecraft.getMinecraft().entityRenderer).replayModRender_getHandler();
|
||||
if (handler != null && !handler.getSettings().isRenderNameTags()) {
|
||||
ci.setReturnValue(false); //this calls the cancel method
|
||||
}
|
||||
|
||||
// TODO
|
||||
// if(ReplayHandler.isInReplay() && entity.isInvisible()
|
||||
// && ReplaySettings.ReplayOptions.renderInvisible.getValue() == Boolean.FALSE) {
|
||||
// ci.setReturnValue(false);
|
||||
// }
|
||||
}
|
||||
|
||||
@Redirect(method = "renderModel", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/EntityLivingBase;isInvisibleToPlayer(Lnet/minecraft/entity/player/EntityPlayer;)Z"))
|
||||
private boolean replayModRender_shouldInvisibleNotBeRendered(EntityLivingBase entity, EntityPlayer thePlayer) {
|
||||
// TODO
|
||||
// if(ReplaySettings.ReplayOptions.renderInvisible.getValue() == Boolean.TRUE|| !ReplayHandler.isInReplay()) {
|
||||
// return entity.isInvisibleToPlayer(thePlayer);
|
||||
// }
|
||||
return true; //the original method inverts the return value
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.replaymod.render.processor;
|
||||
|
||||
import com.replaymod.render.rendering.Frame;
|
||||
import com.replaymod.render.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 com.replaymod.render.processor;
|
||||
|
||||
import com.replaymod.render.frame.CubicOpenGlFrame;
|
||||
import com.replaymod.render.frame.RGBFrame;
|
||||
import com.replaymod.render.utils.ByteBufferPool;
|
||||
import org.lwjgl.util.Dimension;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import static com.replaymod.render.utils.Utils.openGlBytesToRBG;
|
||||
|
||||
public class CubicToRGBProcessor extends AbstractFrameProcessor<CubicOpenGlFrame, RGBFrame> {
|
||||
@Override
|
||||
public RGBFrame process(CubicOpenGlFrame rawFrame) {
|
||||
int size = rawFrame.getLeft().getSize().getWidth();
|
||||
int width = size * 4;
|
||||
int height = size * 3;
|
||||
ByteBuffer result = ByteBufferPool.allocate(width * height * 3);
|
||||
openGlBytesToRBG(rawFrame.getLeft().getByteBuffer(), size, 0, size, result, width);
|
||||
openGlBytesToRBG(rawFrame.getFront().getByteBuffer(), size, size, size, result, width);
|
||||
openGlBytesToRBG(rawFrame.getRight().getByteBuffer(), size, size * 2, size, result, width);
|
||||
openGlBytesToRBG(rawFrame.getBack().getByteBuffer(), size, size * 3, size, result, width);
|
||||
openGlBytesToRBG(rawFrame.getTop().getByteBuffer(), size, size, 0, result, width);
|
||||
openGlBytesToRBG(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 RGBFrame(rawFrame.getFrameId(), new Dimension(width, height), result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package com.replaymod.render.processor;
|
||||
|
||||
import com.replaymod.render.frame.CubicOpenGlFrame;
|
||||
import com.replaymod.render.frame.RGBFrame;
|
||||
import com.replaymod.render.utils.ByteBufferPool;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.lwjgl.util.Dimension;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import static java.lang.Math.PI;
|
||||
|
||||
public class EquirectangularToRGBProcessor extends AbstractFrameProcessor<CubicOpenGlFrame, RGBFrame> {
|
||||
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 EquirectangularToRGBProcessor(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 RGBFrame 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 * 3);
|
||||
ByteBuffer[] images = {
|
||||
rawFrame.getBack().getByteBuffer(), rawFrame.getFront().getByteBuffer(),
|
||||
rawFrame.getLeft().getByteBuffer(), rawFrame.getRight().getByteBuffer(),
|
||||
rawFrame.getTop().getByteBuffer(), rawFrame.getBottom().getByteBuffer()
|
||||
};
|
||||
byte[] pixel = new byte[3];
|
||||
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);
|
||||
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 RGBFrame(rawFrame.getFrameId(), new Dimension(width, height), result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.replaymod.render.processor;
|
||||
|
||||
import com.replaymod.render.frame.OpenGlFrame;
|
||||
import com.replaymod.render.frame.RGBFrame;
|
||||
import org.lwjgl.util.ReadableDimension;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class OpenGlToRGBProcessor extends AbstractFrameProcessor<OpenGlFrame, RGBFrame> {
|
||||
|
||||
private byte[] row, rowSwap;
|
||||
|
||||
@Override
|
||||
public RGBFrame process(OpenGlFrame rawFrame) {
|
||||
// Flip whole image in place
|
||||
|
||||
ReadableDimension size = rawFrame.getSize();
|
||||
int rowSize = size.getWidth() * 3;
|
||||
if (row == null || row.length < rowSize) {
|
||||
row = new byte[rowSize];
|
||||
rowSwap = new byte[rowSize];
|
||||
}
|
||||
ByteBuffer buffer = rawFrame.getByteBuffer();
|
||||
int rows = size.getHeight();
|
||||
byte[] row = this.row;
|
||||
byte[] rowSwap = this.rowSwap;
|
||||
for (int i = 0; i < rows / 2; i++) {
|
||||
int from = rowSize * i;
|
||||
int to = rowSize * (rows - i - 1);
|
||||
buffer.position(from);
|
||||
buffer.get(row);
|
||||
buffer.position(to);
|
||||
buffer.get(rowSwap);
|
||||
buffer.position(to);
|
||||
buffer.put(row);
|
||||
buffer.position(from);
|
||||
buffer.put(rowSwap);
|
||||
}
|
||||
buffer.rewind();
|
||||
return new RGBFrame(rawFrame.getFrameId(), size, buffer);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.replaymod.render.processor;
|
||||
|
||||
import com.replaymod.render.frame.RGBFrame;
|
||||
import com.replaymod.render.frame.StereoscopicOpenGlFrame;
|
||||
import com.replaymod.render.utils.ByteBufferPool;
|
||||
import org.lwjgl.util.Dimension;
|
||||
import org.lwjgl.util.ReadableDimension;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import static com.replaymod.render.utils.Utils.openGlBytesToRBG;
|
||||
|
||||
public class StereoscopicToRGBProcessor extends AbstractFrameProcessor<StereoscopicOpenGlFrame, RGBFrame> {
|
||||
@Override
|
||||
public RGBFrame 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() * 3);
|
||||
openGlBytesToRBG(leftBuffer, width, 0, 0, result, width * 2);
|
||||
openGlBytesToRBG(rightBuffer, width, size.getWidth(), 0, result, width * 2);
|
||||
ByteBufferPool.release(leftBuffer);
|
||||
ByteBufferPool.release(rightBuffer);
|
||||
return new RGBFrame(rawFrame.getFrameId(), new Dimension(width * 2, size.getHeight()), result);
|
||||
}
|
||||
}
|
||||
5
src/main/java/com/replaymod/render/rendering/Frame.java
Normal file
5
src/main/java/com/replaymod/render/rendering/Frame.java
Normal file
@@ -0,0 +1,5 @@
|
||||
package com.replaymod.render.rendering;
|
||||
|
||||
public interface Frame {
|
||||
int getFrameId();
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.replaymod.render.rendering;
|
||||
|
||||
import java.io.Closeable;
|
||||
|
||||
public interface FrameCapturer<R extends Frame> extends Closeable {
|
||||
|
||||
boolean isDone();
|
||||
|
||||
R process();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.replaymod.render.rendering;
|
||||
|
||||
import java.io.Closeable;
|
||||
|
||||
public interface FrameConsumer<P extends Frame> extends Closeable {
|
||||
|
||||
void consume(P frame);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.replaymod.render.rendering;
|
||||
|
||||
import java.io.Closeable;
|
||||
|
||||
public interface FrameProcessor<R extends Frame, P extends Frame> extends Closeable {
|
||||
|
||||
P process(R rawFrame);
|
||||
|
||||
}
|
||||
112
src/main/java/com/replaymod/render/rendering/Pipeline.java
Normal file
112
src/main/java/com/replaymod/render/rendering/Pipeline.java
Normal file
@@ -0,0 +1,112 @@
|
||||
package com.replaymod.render.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
76
src/main/java/com/replaymod/render/rendering/Pipelines.java
Normal file
76
src/main/java/com/replaymod/render/rendering/Pipelines.java
Normal file
@@ -0,0 +1,76 @@
|
||||
package com.replaymod.render.rendering;
|
||||
|
||||
import com.replaymod.render.RenderSettings;
|
||||
import com.replaymod.render.capturer.*;
|
||||
import com.replaymod.render.frame.CubicOpenGlFrame;
|
||||
import com.replaymod.render.frame.OpenGlFrame;
|
||||
import com.replaymod.render.frame.RGBFrame;
|
||||
import com.replaymod.render.frame.StereoscopicOpenGlFrame;
|
||||
import com.replaymod.render.hooks.EntityRendererHandler;
|
||||
import com.replaymod.render.processor.CubicToRGBProcessor;
|
||||
import com.replaymod.render.processor.EquirectangularToRGBProcessor;
|
||||
import com.replaymod.render.processor.OpenGlToRGBProcessor;
|
||||
import com.replaymod.render.processor.StereoscopicToRGBProcessor;
|
||||
import com.replaymod.render.utils.PixelBufferObject;
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
@UtilityClass
|
||||
public class Pipelines {
|
||||
public static Pipeline newPipeline(RenderSettings.RenderMethod method, RenderInfo renderInfo, FrameConsumer<RGBFrame> consumer) {
|
||||
switch (method) {
|
||||
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 method: " + method);
|
||||
}
|
||||
|
||||
public static Pipeline<OpenGlFrame, RGBFrame> newDefaultPipeline(RenderInfo renderInfo, FrameConsumer<RGBFrame> consumer) {
|
||||
RenderSettings settings = renderInfo.getRenderSettings();
|
||||
FrameCapturer<OpenGlFrame> capturer;
|
||||
if (PixelBufferObject.SUPPORTED) {
|
||||
capturer = new SimplePboOpenGlFrameCapturer(new EntityRendererHandler(settings), renderInfo);
|
||||
} else {
|
||||
capturer = new SimpleOpenGlFrameCapturer(new EntityRendererHandler(settings), renderInfo);
|
||||
}
|
||||
return new Pipeline<>(capturer, new OpenGlToRGBProcessor(), consumer);
|
||||
}
|
||||
|
||||
public static Pipeline<StereoscopicOpenGlFrame, RGBFrame> newStereoscopicPipeline(RenderInfo renderInfo, FrameConsumer<RGBFrame> consumer) {
|
||||
RenderSettings settings = renderInfo.getRenderSettings();
|
||||
FrameCapturer<StereoscopicOpenGlFrame> capturer;
|
||||
if (PixelBufferObject.SUPPORTED) {
|
||||
capturer = new StereoscopicPboOpenGlFrameCapturer(new EntityRendererHandler(settings), renderInfo);
|
||||
} else {
|
||||
capturer = new StereoscopicOpenGlFrameCapturer(new EntityRendererHandler(settings), renderInfo);
|
||||
}
|
||||
return new Pipeline<>(capturer, new StereoscopicToRGBProcessor(), consumer);
|
||||
}
|
||||
|
||||
public static Pipeline<CubicOpenGlFrame, RGBFrame> newCubicPipeline(RenderInfo renderInfo, FrameConsumer<RGBFrame> consumer) {
|
||||
RenderSettings settings = renderInfo.getRenderSettings();
|
||||
FrameCapturer<CubicOpenGlFrame> capturer;
|
||||
if (PixelBufferObject.SUPPORTED) {
|
||||
capturer = new CubicPboOpenGlFrameCapturer(new EntityRendererHandler(settings), renderInfo, settings.getVideoWidth() / 4);
|
||||
} else {
|
||||
capturer = new CubicOpenGlFrameCapturer(new EntityRendererHandler(settings), renderInfo, settings.getVideoWidth() / 4);
|
||||
}
|
||||
return new Pipeline<>(capturer, new CubicToRGBProcessor(), consumer);
|
||||
}
|
||||
|
||||
public static Pipeline<CubicOpenGlFrame, RGBFrame> newEquirectangularPipeline(RenderInfo renderInfo, FrameConsumer<RGBFrame> consumer) {
|
||||
RenderSettings settings = renderInfo.getRenderSettings();
|
||||
FrameCapturer<CubicOpenGlFrame> capturer;
|
||||
if (PixelBufferObject.SUPPORTED) {
|
||||
capturer = new CubicPboOpenGlFrameCapturer(new EntityRendererHandler(settings), renderInfo, settings.getVideoWidth() / 4);
|
||||
} else {
|
||||
capturer = new CubicOpenGlFrameCapturer(new EntityRendererHandler(settings), renderInfo, settings.getVideoWidth() / 4);
|
||||
}
|
||||
return new Pipeline<>(capturer, new EquirectangularToRGBProcessor(settings.getVideoWidth() / 4), consumer);
|
||||
}
|
||||
}
|
||||
308
src/main/java/com/replaymod/render/rendering/VideoRenderer.java
Normal file
308
src/main/java/com/replaymod/render/rendering/VideoRenderer.java
Normal file
@@ -0,0 +1,308 @@
|
||||
package com.replaymod.render.rendering;
|
||||
|
||||
import com.google.common.base.Optional;
|
||||
import com.replaymod.core.ReplayMod;
|
||||
import com.replaymod.pathing.path.Keyframe;
|
||||
import com.replaymod.pathing.path.Path;
|
||||
import com.replaymod.pathing.path.Timeline;
|
||||
import com.replaymod.pathing.properties.TimestampProperty;
|
||||
import com.replaymod.render.RenderSettings;
|
||||
import com.replaymod.render.VideoWriter;
|
||||
import com.replaymod.render.capturer.RenderInfo;
|
||||
import com.replaymod.render.frame.RGBFrame;
|
||||
import com.replaymod.render.gui.GuiVideoRenderer;
|
||||
import com.replaymod.render.hooks.ChunkLoadingRenderGlobal;
|
||||
import com.replaymod.render.hooks.RenderReplayTimer;
|
||||
import com.replaymod.render.metadata.MetadataInjector;
|
||||
import com.replaymod.replay.ReplayHandler;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.audio.SoundCategory;
|
||||
import net.minecraft.client.gui.ScaledResolution;
|
||||
import net.minecraft.client.renderer.OpenGlHelper;
|
||||
import net.minecraft.util.Timer;
|
||||
import org.lwjgl.input.Mouse;
|
||||
import org.lwjgl.opengl.Display;
|
||||
import org.lwjgl.util.Dimension;
|
||||
import org.lwjgl.util.ReadableDimension;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.EnumMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.FutureTask;
|
||||
|
||||
import static com.google.common.collect.Iterables.getLast;
|
||||
import static net.minecraft.client.renderer.GlStateManager.*;
|
||||
import static org.lwjgl.opengl.GL11.GL_COLOR_BUFFER_BIT;
|
||||
import static org.lwjgl.opengl.GL11.GL_DEPTH_BUFFER_BIT;
|
||||
|
||||
public class VideoRenderer implements RenderInfo {
|
||||
private final Minecraft mc = Minecraft.getMinecraft();
|
||||
private final RenderSettings settings;
|
||||
private final ReplayHandler replayHandler;
|
||||
private final Timeline timeline;
|
||||
private final Pipeline renderingPipeline;
|
||||
private final VideoWriter videoWriter;
|
||||
|
||||
private int fps;
|
||||
private boolean mouseWasGrabbed;
|
||||
private boolean debugInfoWasShown;
|
||||
private Map originalSoundLevels;
|
||||
|
||||
private ChunkLoadingRenderGlobal chunkLoadingRenderGlobal;
|
||||
|
||||
private int framesDone;
|
||||
private int totalFrames;
|
||||
|
||||
private final GuiVideoRenderer gui;
|
||||
private boolean paused;
|
||||
private boolean cancelled;
|
||||
|
||||
public VideoRenderer(RenderSettings settings, ReplayHandler replayHandler, Timeline timeline) throws IOException {
|
||||
this.settings = settings;
|
||||
this.replayHandler = replayHandler;
|
||||
this.timeline = timeline;
|
||||
this.gui = new GuiVideoRenderer(this);
|
||||
this.renderingPipeline = Pipelines.newPipeline(settings.getRenderMethod(), this,
|
||||
videoWriter = new VideoWriter(settings) {
|
||||
@Override
|
||||
public void consume(RGBFrame frame) {
|
||||
gui.updatePreview(frame);
|
||||
super.consume(frame);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Render this video.
|
||||
* @return {@code true} if rendering was successful, {@code false} if the user aborted rendering (or the window was closed)
|
||||
*/
|
||||
public boolean renderVideo() {
|
||||
setup();
|
||||
|
||||
// Because this might take some time to prepare we'll render the GUI at least once to not confuse the user
|
||||
drawGui();
|
||||
|
||||
Timer timer = mc.timer;
|
||||
|
||||
// Play up to one second before starting to render
|
||||
// This is necessary in order to ensure that all entities have at least two position packets
|
||||
// and their first position in the recording is correct.
|
||||
// Note that it is impossible to also get the interpolation between their latest position
|
||||
// and the one in the recording correct as there's no reliable way to tell when the server ticks
|
||||
// or when we should be done with the interpolation of the entity
|
||||
Optional<Integer> optionalVideoStartTime = timeline.getValue(TimestampProperty.PROPERTY, 0);
|
||||
if (optionalVideoStartTime.isPresent()) {
|
||||
int videoStart = optionalVideoStartTime.get();
|
||||
|
||||
if (videoStart > 1000) {
|
||||
int replayTime = videoStart - 1000;
|
||||
timer.elapsedPartialTicks = timer.renderPartialTicks = 0;
|
||||
timer.timerSpeed = 1;
|
||||
while (replayTime < videoStart) {
|
||||
timer.elapsedTicks = 1;
|
||||
replayTime += 50;
|
||||
replayHandler.getReplaySender().sendPacketsTill(replayTime);
|
||||
tick();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mc.renderGlobal.renderEntitiesStartupCounter = 0;
|
||||
|
||||
renderingPipeline.run();
|
||||
|
||||
if (settings.isInject360Metadata()) {
|
||||
MetadataInjector.inject360Metadata(settings.getOutputFile());
|
||||
}
|
||||
|
||||
finish();
|
||||
return !cancelled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float updateForNextFrame() {
|
||||
if (!settings.isHighPerformance() || framesDone % fps == 0) {
|
||||
drawGui();
|
||||
}
|
||||
|
||||
timeline.applyToGame(getVideoTime(), replayHandler);
|
||||
|
||||
int elapsedTicks = mc.timer.elapsedTicks;
|
||||
while (elapsedTicks-- > 0) {
|
||||
tick();
|
||||
}
|
||||
|
||||
framesDone++;
|
||||
return mc.timer.renderPartialTicks;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RenderSettings getRenderSettings() {
|
||||
return settings;
|
||||
}
|
||||
|
||||
private void setup() {
|
||||
replayHandler.getReplaySender().setSyncModeAndWait();
|
||||
|
||||
if (!OpenGlHelper.isFramebufferEnabled()) {
|
||||
Display.setResizable(false);
|
||||
}
|
||||
if (mc.gameSettings.showDebugInfo) {
|
||||
debugInfoWasShown = true;
|
||||
mc.gameSettings.showDebugInfo = false;
|
||||
}
|
||||
if (Mouse.isGrabbed()) {
|
||||
mouseWasGrabbed = true;
|
||||
}
|
||||
Mouse.setGrabbed(false);
|
||||
mc.timer = new RenderReplayTimer(mc.timer);
|
||||
mc.timer.timerSpeed = 1;
|
||||
|
||||
// Mute all sounds except GUI sounds (buttons, etc.)
|
||||
Map<SoundCategory, Float> mutedSounds = new EnumMap<>(SoundCategory.class);
|
||||
for (SoundCategory category : SoundCategory.values()) {
|
||||
mutedSounds.put(category, 0f);
|
||||
}
|
||||
originalSoundLevels = mc.gameSettings.mapSoundLevels;
|
||||
mutedSounds.put(SoundCategory.MASTER, (Float) originalSoundLevels.get(SoundCategory.MASTER));
|
||||
mc.gameSettings.mapSoundLevels = mutedSounds;
|
||||
|
||||
fps = settings.getFramesPerSecond();
|
||||
|
||||
long duration = 0;
|
||||
for (Path path : timeline.getPaths()) {
|
||||
if (!path.isActive()) continue;
|
||||
|
||||
// Prepare path interpolations
|
||||
path.updateAll();
|
||||
// Find end time
|
||||
Collection<Keyframe> keyframes = path.getKeyframes();
|
||||
if (keyframes.size() > 0) {
|
||||
duration = Math.max(duration, getLast(keyframes).getTime());
|
||||
}
|
||||
}
|
||||
|
||||
totalFrames = (int) (duration*fps/1000);
|
||||
|
||||
ScaledResolution scaled = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight);
|
||||
gui.toMinecraft().setWorldAndResolution(mc, scaled.getScaledWidth(), scaled.getScaledHeight());
|
||||
|
||||
chunkLoadingRenderGlobal = new ChunkLoadingRenderGlobal(mc.renderGlobal);
|
||||
}
|
||||
|
||||
private void finish() {
|
||||
replayHandler.getReplaySender().setAsyncMode(true);
|
||||
|
||||
if (!OpenGlHelper.isFramebufferEnabled()) {
|
||||
Display.setResizable(true);
|
||||
}
|
||||
mc.gameSettings.showDebugInfo = debugInfoWasShown;
|
||||
if (mouseWasGrabbed) {
|
||||
mc.mouseHelper.grabMouseCursor();
|
||||
}
|
||||
mc.timer = ((RenderReplayTimer) mc.timer).getWrapped();
|
||||
mc.gameSettings.mapSoundLevels = originalSoundLevels;
|
||||
mc.displayGuiScreen(null);
|
||||
if (chunkLoadingRenderGlobal != null) {
|
||||
chunkLoadingRenderGlobal.uninstall();
|
||||
}
|
||||
|
||||
ReplayMod.soundHandler.playRenderSuccessSound();
|
||||
|
||||
mc.displayGuiScreen(null);
|
||||
}
|
||||
|
||||
private void tick() {
|
||||
synchronized (mc.scheduledTasks) {
|
||||
while (!mc.scheduledTasks.isEmpty()) {
|
||||
((FutureTask) mc.scheduledTasks.poll()).run();
|
||||
}
|
||||
}
|
||||
|
||||
mc.currentScreen = gui.toMinecraft();
|
||||
try {
|
||||
mc.runTick();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void drawGui() {
|
||||
do {
|
||||
pushMatrix();
|
||||
clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||
enableTexture2D();
|
||||
mc.getFramebuffer().bindFramebuffer(true);
|
||||
mc.entityRenderer.setupOverlayRendering();
|
||||
|
||||
try {
|
||||
gui.toMinecraft().handleInput();
|
||||
} catch (IOException e) {
|
||||
// That's a strange exception from this kind of method O_o
|
||||
// It isn't actually thrown here, so we'll deal with it the easy way
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
ScaledResolution scaled = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight);
|
||||
int mouseX = Mouse.getX() * scaled.getScaledWidth() / mc.displayWidth;
|
||||
int mouseY = scaled.getScaledHeight() - Mouse.getY() * scaled.getScaledHeight() / mc.displayHeight - 1;
|
||||
gui.toMinecraft().drawScreen(mouseX, mouseY, 0);
|
||||
|
||||
mc.getFramebuffer().unbindFramebuffer();
|
||||
popMatrix();
|
||||
pushMatrix();
|
||||
mc.getFramebuffer().framebufferRender(mc.displayWidth, mc.displayHeight);
|
||||
popMatrix();
|
||||
|
||||
|
||||
// if not in high performance mode, update the gui size if screen size changed
|
||||
// otherwise just swap the progress gui to screen
|
||||
if (settings.isHighPerformance()) {
|
||||
Display.update();
|
||||
} else {
|
||||
mc.updateDisplay();
|
||||
}
|
||||
if (Mouse.isGrabbed()) {
|
||||
Mouse.setGrabbed(false);
|
||||
}
|
||||
if (paused) {
|
||||
try {
|
||||
Thread.sleep(50);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
}
|
||||
}
|
||||
} while (paused);
|
||||
}
|
||||
|
||||
public int getFramesDone() {
|
||||
return framesDone;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadableDimension getFrameSize() {
|
||||
return new Dimension(settings.getVideoWidth(), settings.getVideoHeight());
|
||||
}
|
||||
|
||||
public int getTotalFrames() {
|
||||
return totalFrames;
|
||||
}
|
||||
|
||||
public int getVideoTime() { return framesDone * 1000 / fps; }
|
||||
|
||||
public void setPaused(boolean paused) {
|
||||
this.paused = paused;
|
||||
}
|
||||
|
||||
public boolean isPaused() {
|
||||
return paused;
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
videoWriter.abort();
|
||||
this.cancelled = true;
|
||||
renderingPipeline.cancel();
|
||||
}
|
||||
}
|
||||
48
src/main/java/com/replaymod/render/utils/ByteBufferPool.java
Normal file
48
src/main/java/com/replaymod/render/utils/ByteBufferPool.java
Normal file
@@ -0,0 +1,48 @@
|
||||
package com.replaymod.render.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<>();
|
||||
bufferPool.put(size, available);
|
||||
}
|
||||
available.add(new SoftReference<>(buffer));
|
||||
}
|
||||
}
|
||||
176
src/main/java/com/replaymod/render/utils/PixelBufferObject.java
Normal file
176
src/main/java/com/replaymod/render/utils/PixelBufferObject.java
Normal file
@@ -0,0 +1,176 @@
|
||||
package com.replaymod.render.utils;
|
||||
|
||||
import com.google.common.base.Objects;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.lwjgl.opengl.ARBBufferObject;
|
||||
import org.lwjgl.opengl.GLContext;
|
||||
import org.lwjgl.opengl.Util;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import static org.lwjgl.opengl.ARBPixelBufferObject.*;
|
||||
import static org.lwjgl.opengl.GL15.*;
|
||||
import static org.lwjgl.opengl.GL21.GL_PIXEL_PACK_BUFFER;
|
||||
|
||||
public class PixelBufferObject {
|
||||
@RequiredArgsConstructor
|
||||
public enum Usage {
|
||||
COPY(GL_STREAM_COPY_ARB, GL_STREAM_COPY),
|
||||
DRAW(GL_STREAM_DRAW_ARB, GL_STREAM_DRAW),
|
||||
READ(GL_STREAM_READ_ARB, GL_STREAM_READ);
|
||||
|
||||
private final int arb, gl15;
|
||||
}
|
||||
|
||||
public static final boolean SUPPORTED = GLContext.getCapabilities().GL_ARB_pixel_buffer_object || GLContext.getCapabilities().OpenGL15;
|
||||
private static final boolean arb = !GLContext.getCapabilities().OpenGL15;
|
||||
|
||||
private static ThreadLocal<Integer> bound = new ThreadLocal<>();
|
||||
private static ThreadLocal<Integer> mapped = new ThreadLocal<>();
|
||||
|
||||
private final long size;
|
||||
private long handle;
|
||||
|
||||
public PixelBufferObject(long size, Usage usage) {
|
||||
if (!SUPPORTED) {
|
||||
throw new UnsupportedOperationException("PBOs not supported.");
|
||||
}
|
||||
|
||||
this.size = size;
|
||||
this.handle = arb ? ARBBufferObject.glGenBuffersARB() : glGenBuffers();
|
||||
|
||||
bind();
|
||||
|
||||
if (arb) {
|
||||
ARBBufferObject.glBufferDataARB(GL_PIXEL_PACK_BUFFER_ARB, size, usage.arb);
|
||||
} else {
|
||||
glBufferData(GL_PIXEL_PACK_BUFFER, size, usage.gl15);
|
||||
}
|
||||
}
|
||||
|
||||
private int getHandle() {
|
||||
if (handle == -1) {
|
||||
throw new IllegalStateException("PBO not allocated.");
|
||||
}
|
||||
return (int) handle;
|
||||
}
|
||||
|
||||
public void bind() {
|
||||
if (arb) {
|
||||
ARBBufferObject.glBindBufferARB(GL_PIXEL_PACK_BUFFER_ARB, getHandle());
|
||||
} else {
|
||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, getHandle());
|
||||
}
|
||||
bound.set(getHandle());
|
||||
}
|
||||
|
||||
public void unbind() {
|
||||
checkBound();
|
||||
if (arb) {
|
||||
ARBBufferObject.glBindBufferARB(GL_PIXEL_PACK_BUFFER_ARB, 0);
|
||||
} else {
|
||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
||||
}
|
||||
bound.set(0);
|
||||
}
|
||||
|
||||
private void checkBound() {
|
||||
if (!Objects.equal(getHandle(), bound.get())) {
|
||||
throw new IllegalStateException("Buffer not bound.");
|
||||
}
|
||||
}
|
||||
|
||||
private void checkNotMapped() {
|
||||
if (Objects.equal(getHandle(), mapped.get())) {
|
||||
throw new IllegalStateException("Buffer already mapped.");
|
||||
}
|
||||
}
|
||||
|
||||
public ByteBuffer mapReadOnly() {
|
||||
checkBound();
|
||||
checkNotMapped();
|
||||
ByteBuffer buffer;
|
||||
if (arb) {
|
||||
buffer = ARBBufferObject.glMapBufferARB(GL_PIXEL_PACK_BUFFER_ARB, GL_READ_ONLY_ARB, size, null);
|
||||
} else {
|
||||
buffer = glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY, size, null);
|
||||
}
|
||||
if (buffer == null) {
|
||||
Util.checkGLError();
|
||||
}
|
||||
mapped.set(getHandle());
|
||||
return buffer;
|
||||
}
|
||||
|
||||
public ByteBuffer mapWriteOnly() {
|
||||
checkBound();
|
||||
checkNotMapped();
|
||||
ByteBuffer buffer;
|
||||
if (arb) {
|
||||
buffer = ARBBufferObject.glMapBufferARB(GL_PIXEL_PACK_BUFFER_ARB, GL_WRITE_ONLY_ARB, size, null);
|
||||
} else {
|
||||
buffer = glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_WRITE_ONLY, size, null);
|
||||
}
|
||||
if (buffer == null) {
|
||||
Util.checkGLError();
|
||||
}
|
||||
mapped.set(getHandle());
|
||||
return buffer;
|
||||
}
|
||||
|
||||
public ByteBuffer mapReadWrite() {
|
||||
checkBound();
|
||||
checkNotMapped();
|
||||
ByteBuffer buffer;
|
||||
if (arb) {
|
||||
buffer = ARBBufferObject.glMapBufferARB(GL_PIXEL_PACK_BUFFER_ARB, GL_READ_WRITE_ARB, size, null);
|
||||
} else {
|
||||
buffer = glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_WRITE, size, null);
|
||||
}
|
||||
if (buffer == null) {
|
||||
Util.checkGLError();
|
||||
}
|
||||
mapped.set(getHandle());
|
||||
return buffer;
|
||||
}
|
||||
|
||||
public void unmap() {
|
||||
checkBound();
|
||||
if (!Objects.equal(mapped.get(), getHandle())) {
|
||||
throw new IllegalStateException("Buffer not mapped.");
|
||||
}
|
||||
if (arb) {
|
||||
ARBBufferObject.glUnmapBufferARB(GL_PIXEL_PACK_BUFFER_ARB);
|
||||
} else {
|
||||
glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
|
||||
}
|
||||
mapped.set(0);
|
||||
}
|
||||
|
||||
public void delete() {
|
||||
if (handle != -1) {
|
||||
if (arb) {
|
||||
ARBBufferObject.glDeleteBuffersARB(getHandle());
|
||||
} else {
|
||||
glDeleteBuffers(getHandle());
|
||||
}
|
||||
handle = -1;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void finalize() throws Throwable {
|
||||
super.finalize();
|
||||
if (handle != -1) {
|
||||
LogManager.getLogger().warn("PBO garbage collected before deleted!");
|
||||
Minecraft.getMinecraft().addScheduledTask(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
delete();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
26
src/main/java/com/replaymod/render/utils/Utils.java
Normal file
26
src/main/java/com/replaymod/render/utils/Utils.java
Normal file
@@ -0,0 +1,26 @@
|
||||
package com.replaymod.render.utils;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class Utils {
|
||||
/**
|
||||
* Copies the rgb image (flipped vertically) to the specified position in the target buffer
|
||||
* @param buffer Source image
|
||||
* @param bufferWidth Source image width
|
||||
* @param xOffset X offset in target image
|
||||
* @param yOffset Y offset in target image
|
||||
* @param to Target image
|
||||
* @param width Target image width
|
||||
*/
|
||||
public static void openGlBytesToRBG(ByteBuffer buffer, int bufferWidth, int xOffset, int yOffset, ByteBuffer to, int width) {
|
||||
byte[] rowBuf = new byte[bufferWidth * 3];
|
||||
// Copy image flipped vertically to target buffer
|
||||
int rows = buffer.remaining() / 3 / bufferWidth;
|
||||
for (int i = 0; i < rows; i++) {
|
||||
buffer.get(rowBuf);
|
||||
to.position(((yOffset + rows - i - 1) * width + xOffset) * 3);
|
||||
to.put(rowBuf);
|
||||
}
|
||||
to.rewind();
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import com.replaymod.pathing.path.Timeline;
|
||||
import com.replaymod.pathing.player.RealtimeTimelinePlayer;
|
||||
import com.replaymod.pathing.properties.CameraProperties;
|
||||
import com.replaymod.pathing.properties.TimestampProperty;
|
||||
import com.replaymod.render.gui.GuiRenderSettings;
|
||||
import com.replaymod.replay.ReplayHandler;
|
||||
import com.replaymod.replay.camera.CameraEntity;
|
||||
import com.replaymod.replay.gui.overlay.GuiReplayOverlay;
|
||||
@@ -28,6 +29,7 @@ import de.johni0702.minecraft.gui.element.advanced.IGuiTimeline;
|
||||
import de.johni0702.minecraft.gui.layout.CustomLayout;
|
||||
import de.johni0702.minecraft.gui.layout.HorizontalLayout;
|
||||
import de.johni0702.minecraft.gui.layout.VerticalLayout;
|
||||
import net.minecraftforge.fml.common.Loader;
|
||||
import org.apache.commons.lang3.tuple.Triple;
|
||||
import org.lwjgl.input.Keyboard;
|
||||
import org.lwjgl.util.Dimension;
|
||||
@@ -48,6 +50,14 @@ public class GuiPathing {
|
||||
public final GuiTexturedButton playPauseButton = new GuiTexturedButton().setSize(20, 20)
|
||||
.setTexture(ReplayMod.TEXTURE, ReplayMod.TEXTURE_SIZE);
|
||||
|
||||
public final GuiTexturedButton renderButton = new GuiTexturedButton().onClick(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
preparePathsForPlayback();
|
||||
new GuiRenderSettings(replayHandler, mod.getCurrentTimeline()).display();
|
||||
}
|
||||
}).setSize(20, 20).setTexture(ReplayMod.TEXTURE, ReplayMod.TEXTURE_SIZE).setTexturePosH(40, 0);
|
||||
|
||||
public final GuiTexturedButton positionKeyframeButton = new GuiTexturedButton().setSize(20, 20)
|
||||
.setTexture(ReplayMod.TEXTURE, ReplayMod.TEXTURE_SIZE);
|
||||
|
||||
@@ -117,9 +127,15 @@ public class GuiPathing {
|
||||
}).addElements(null, timelineTime, timeline, scrollbar, zoomButtonPanel);
|
||||
|
||||
public final GuiPanel panel = new GuiPanel()
|
||||
.setLayout(new HorizontalLayout(HorizontalLayout.Alignment.CENTER).setSpacing(5))
|
||||
.addElements(new HorizontalLayout.Data(0.5), playPauseButton, positionKeyframeButton, timeKeyframeButton,
|
||||
timelinePanel);
|
||||
.setLayout(new HorizontalLayout(HorizontalLayout.Alignment.CENTER).setSpacing(5));
|
||||
|
||||
{
|
||||
panel.addElements(new HorizontalLayout.Data(0.5), playPauseButton);
|
||||
if (Loader.isModLoaded("replaymod-render")) {
|
||||
panel.addElements(new HorizontalLayout.Data(0.5), renderButton);
|
||||
}
|
||||
panel.addElements(new HorizontalLayout.Data(0.5), positionKeyframeButton, timeKeyframeButton, timelinePanel);
|
||||
}
|
||||
|
||||
/**
|
||||
* IGuiClickable dummy component that is inserted at a high level.
|
||||
@@ -187,22 +203,8 @@ public class GuiPathing {
|
||||
} else {
|
||||
Timeline timeline = mod.getCurrentTimeline();
|
||||
Path timePath = timeline.getPaths().get(TIME_PATH);
|
||||
Path positionPath = timeline.getPaths().get(POSITION_PATH);
|
||||
|
||||
// TODO Change interpolator via Change class
|
||||
AbstractInterpolator interpolator = new LinearInterpolator();
|
||||
interpolator.registerProperty(TimestampProperty.PROPERTY);
|
||||
for (PathSegment segment : timePath.getSegments()) {
|
||||
segment.setInterpolator(interpolator);
|
||||
}
|
||||
interpolator = new CubicSplineInterpolator();
|
||||
interpolator.registerProperty(CameraProperties.POSITION);
|
||||
interpolator.registerProperty(CameraProperties.ROTATION);
|
||||
for (PathSegment segment : positionPath.getSegments()) {
|
||||
segment.setInterpolator(interpolator);
|
||||
}
|
||||
timePath.updateAll();
|
||||
positionPath.updateAll();
|
||||
preparePathsForPlayback();
|
||||
|
||||
timePath.setActive(!Keyboard.isKeyDown(Keyboard.KEY_LSHIFT));
|
||||
player.start(timeline);
|
||||
@@ -298,6 +300,27 @@ public class GuiPathing {
|
||||
});
|
||||
}
|
||||
|
||||
private void preparePathsForPlayback() {
|
||||
Timeline timeline = mod.getCurrentTimeline();
|
||||
Path timePath = timeline.getPaths().get(TIME_PATH);
|
||||
Path positionPath = timeline.getPaths().get(POSITION_PATH);
|
||||
|
||||
// TODO Change interpolator via Change class
|
||||
AbstractInterpolator interpolator = new LinearInterpolator();
|
||||
interpolator.registerProperty(TimestampProperty.PROPERTY);
|
||||
for (PathSegment segment : timePath.getSegments()) {
|
||||
segment.setInterpolator(interpolator);
|
||||
}
|
||||
interpolator = new CubicSplineInterpolator();
|
||||
interpolator.registerProperty(CameraProperties.POSITION);
|
||||
interpolator.registerProperty(CameraProperties.ROTATION);
|
||||
for (PathSegment segment : positionPath.getSegments()) {
|
||||
segment.setInterpolator(interpolator);
|
||||
}
|
||||
timePath.updateAll();
|
||||
positionPath.updateAll();
|
||||
}
|
||||
|
||||
public void zoomTimeline(double factor) {
|
||||
scrollbar.setZoom(scrollbar.getZoom() * factor);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user