Separate rendering into its own module

This commit is contained in:
johni0702
2016-06-14 17:01:32 +02:00
parent 65cfdd13ec
commit 25582b6a51
68 changed files with 1822 additions and 2635 deletions

View File

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

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

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

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

View File

@@ -0,0 +1,4 @@
package com.replaymod.render.capturer;
public interface CaptureData {
}

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View 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.");
}
}

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,5 @@
package com.replaymod.render.rendering;
public interface Frame {
int getFrameId();
}

View File

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

View File

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

View File

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

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

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

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

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

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

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

View File

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