Allow custom resolution for all video rendering modes

Remove tiled rendering
Choose different rendering strategy based on video size and framebuffer support
Make size of video writer queue configurable via system properties
This commit is contained in:
johni0702
2015-06-03 21:19:41 +02:00
parent 6b038adaa1
commit d171ff4346
25 changed files with 855 additions and 452 deletions

View File

@@ -176,6 +176,8 @@ public class ReplayMod {
String waitForChunks = System.getProperty("replaymod.render.waitforchunks");
String linearMovement = System.getProperty("replaymod.render.linearmovement");
String skyColor = System.getProperty("replaymod.render.skycolor");
String width = System.getProperty("replaymod.render.width");
String height = System.getProperty("replaymod.render.height");
final RenderOptions options = new RenderOptions();
if (quality != null) {
options.setQuality(Float.parseFloat(quality));
@@ -196,6 +198,12 @@ public class ReplayMod {
options.setSkyColor(Integer.parseInt(skyColor));
}
}
if (width != null) {
options.setWidth(Integer.parseInt(width));
}
if (height != null) {
options.setHeight(Integer.parseInt(height));
}
FrameRenderer renderer;
if (type != null) {
@@ -203,13 +211,6 @@ public class ReplayMod {
type = parts[0].toUpperCase();
if ("NORMAL".equals(type) || "DEFAULT".equals(type)) {
renderer = new DefaultFrameRenderer(options);
} else if ("TILED".equals(type)) {
if (parts.length < 3) {
throw new IllegalArgumentException("Tiled renderer requires width and height.");
}
int width = Integer.parseInt(parts[1]);
int height = Integer.parseInt(parts[2]);
renderer = new TilingFrameRenderer(options, width, height);
} else if ("STEREO".equals(type) || "STEREOSCOPIC".equals(type)) {
renderer = new StereoscopicFrameRenderer(options);
} else if ("CUBIC".equals(type)) {

View File

@@ -0,0 +1,127 @@
package eu.crushedpixel.replaymod.coremod;
import net.minecraft.launchwrapper.IClassTransformer;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.tree.*;
import static org.objectweb.asm.Opcodes.*;
/**
* Transforms the EntityRenderer class:
* - Adds a new field 'CustomEntityRenderer hook'
* - Moves code of 'loadShader' into 'loadShader$Original'
* - Creates 'loadShader' method (see {@link #createLoadShader(ClassNode, MethodNode)} ()}
*
* Transforms the CustomEntityRenderer class:
* - Method 'original_loadShader' calls 'loadShader$Original'
*/
public class EntityRendererCT implements IClassTransformer {
private static final String HOOK = "eu.crushedpixel.replaymod.video.entity.CustomEntityRenderer";
private static final String HOOK_JVM = HOOK.replace('.', '/');
private static final String HOOK_TYPE = "L" + HOOK_JVM + ";";
private static final String ENTITY_RENDERER = "net.minecraft.client.renderer.EntityRenderer";
private static final String ENTITY_RENDERER_JVM = ENTITY_RENDERER.replace('.', '/');
private static final String ENTITY_RENDERER_TYPE = "L" + ENTITY_RENDERER_JVM + ";";
@Override
public byte[] transform(String name, String transformedName, byte[] bytes) {
if (ENTITY_RENDERER.equals(transformedName)) {
if (name.equals(transformedName)) {
return transformEntityRenderer(bytes, "loadShader", "(Lnet/minecraft/util/ResourceLocation;)V");
} else {
return transformEntityRenderer(bytes, "a", "(Loa;)V");
}
} else if (HOOK.equals(transformedName)) {
return transformHook(bytes);
}
return bytes;
}
private byte[] transformHook(byte[] bytes) {
ClassReader classReader = new ClassReader(bytes);
ClassNode classNode = new ClassNode();
classReader.accept(classNode, 0);
// Find method
for (MethodNode m : classNode.methods) {
if ("original_loadShader".equals(m.name)) {
m.instructions.clear();
InsnList iter = m.instructions;
iter.add(new VarInsnNode(ALOAD, 0)); // this
iter.add(new FieldInsnNode(GETFIELD, classNode.name, "proxied", ENTITY_RENDERER_TYPE));
iter.add(new VarInsnNode(ALOAD, 1)); // resourceLocation
iter.add(new MethodInsnNode(INVOKEVIRTUAL, ENTITY_RENDERER_JVM, "loadShader$Original", m.desc, false));
iter.add(new InsnNode(RETURN));
}
}
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
classNode.accept(classWriter);
return classWriter.toByteArray();
}
private byte[] transformEntityRenderer(byte[] bytes, String name_loadShader, String desc_loadShader) {
ClassReader classReader = new ClassReader(bytes);
ClassNode classNode = new ClassNode();
classReader.accept(classNode, 0);
// Add field
classNode.visitField(ACC_PUBLIC, "hook", HOOK_TYPE, null, null);
// Find method
MethodNode setupTerrain = null;
for (MethodNode m : classNode.methods) {
if (name_loadShader.equals(m.name) && desc_loadShader.equals(m.desc)) {
setupTerrain = m;
}
}
if (setupTerrain == null) {
throw new NoSuchMethodError("loadShader");
}
// Generate new method
classNode.methods.add(createLoadShader(classNode, setupTerrain));
// Rename original method
setupTerrain.name = "loadShader$Original";
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
classNode.accept(classWriter);
return classWriter.toByteArray();
}
/**
* public void loadShader(ResourceLocation resourceLocation) {
* if (this.hook == null) {
* loadShader$Original(resourceLocation);
* return;
* }
* hook.loadShader(resourceLocation);
* }
*/
private MethodNode createLoadShader(ClassNode classNode, MethodNode org) {
MethodNode method = new MethodNode(org.access, org.name, org.desc, org.signature, new String[0]);
InsnList list = method.instructions;
list.add(new VarInsnNode(ALOAD, 0));
list.add(new FieldInsnNode(GETFIELD, classNode.name, "hook", HOOK_TYPE));
LabelNode labelNotNull = new LabelNode();
list.add(new JumpInsnNode(IFNONNULL, labelNotNull));
list.add(new VarInsnNode(ALOAD, 0)); // this
list.add(new VarInsnNode(ALOAD, 1)); // resourceLocation
list.add(new MethodInsnNode(INVOKEVIRTUAL, classNode.name, "loadShader$Original", org.desc, false));
list.add(new InsnNode(RETURN));
list.add(labelNotNull);
list.add(new FrameNode(F_SAME, 0, null, 0, null));
list.add(new VarInsnNode(ALOAD, 0)); // this
list.add(new FieldInsnNode(GETFIELD, classNode.name, "hook", HOOK_TYPE));
list.add(new VarInsnNode(ALOAD, 1)); // resourceLocation
list.add(new MethodInsnNode(INVOKEVIRTUAL, HOOK_JVM, "loadShader", org.desc, false));
list.add(new InsnNode(RETURN));
return method;
}
}

View File

@@ -11,6 +11,7 @@ public class LoadingPlugin implements IFMLLoadingPlugin {
return new String[]{
CameraRollCT.class.getName(),
ForceChunkLoadingCT.class.getName(),
EntityRendererCT.class.getName(),
SoundManagerCT.class.getName(),
EnchantmentTimerCT.class.getName()
};

View File

@@ -59,15 +59,51 @@ public class GuiRenderSettings extends GuiScreen {
fontRendererObj, 0, 0, 200, 5);
rendererDropdown.addSelectionListener(new RendererDropdownListener());
int i = 0;
for (RendererSettings r : RendererSettings.values()) {
rendererDropdown.addElement(r);
rendererDropdown.setHoverText(i, r.getDescription());
i++;
}
renderButton = new GuiButton(GuiConstants.RENDER_SETTINGS_RENDER_BUTTON, 0, 0, I18n.format("replaymod.gui.render"));
cancelButton = new GuiButton(GuiConstants.RENDER_SETTINGS_CANCEL_BUTTON, 0, 0, I18n.format("replaymod.gui.cancel"));
advancedButton = new GuiButton(GuiConstants.RENDER_SETTINGS_ADVANCED_BUTTON, 0, 0, I18n.format("replaymod.gui.rendersettings.advanced"));
customResolution = new GuiCheckBox(GuiConstants.RENDER_SETTINGS_RESOLUTION_CHECKBOX, 0, 0, I18n.format("replaymod.gui.rendersettings.customresolution"), false);
customResolution.enabled = false;
xRes = new GuiNumberInput(GuiConstants.RENDER_SETTINGS_RESOLUTION_X, fontRendererObj, 0, 0, 50, 1, 100000, mc.displayWidth, false);
yRes = new GuiNumberInput(GuiConstants.RENDER_SETTINGS_RESOLUTION_Y, fontRendererObj, 0, 0, 50, 1, 100000, mc.displayHeight, false);
xRes = new GuiNumberInput(GuiConstants.RENDER_SETTINGS_RESOLUTION_X, fontRendererObj, 0, 0, 50, 1, 100000, mc.displayWidth, false) {
@Override
public void setCursorPosition(int p_146190_1_) {
super.setCursorPosition(p_146190_1_);
RendererSettings renderer = rendererDropdown.getElement(rendererDropdown.getSelectionIndex());
Integer value = getIntValueNullable();
if (value != null) {
if (renderer == RendererSettings.CUBIC) {
yRes.text = Integer.toString(Math.max(1, value * 3 / 4));
}
if (renderer == RendererSettings.EQUIRECTANGULAR) {
yRes.text = Integer.toString(Math.max(1, value / 2));
}
}
}
};
yRes = new GuiNumberInput(GuiConstants.RENDER_SETTINGS_RESOLUTION_Y, fontRendererObj, 0, 0, 50, 1, 100000, mc.displayHeight, false) {
@Override
public void setCursorPosition(int p_146190_1_) {
super.setCursorPosition(p_146190_1_);
RendererSettings renderer = rendererDropdown.getElement(rendererDropdown.getSelectionIndex());
Integer value = getIntValueNullable();
if (value != null) {
if (renderer == RendererSettings.CUBIC) {
xRes.text = Integer.toString(value * 4 / 3);
}
if (renderer == RendererSettings.EQUIRECTANGULAR) {
xRes.text = Integer.toString(value * 2);
}
}
}
};
xRes.setEnabled(false);
yRes.setEnabled(false);
@@ -101,13 +137,6 @@ public class GuiRenderSettings extends GuiScreen {
youtubeExport = new GuiCheckBox(GuiConstants.RENDER_SETTINGS_YOUTUBE_READY, 0, 0, I18n.format("replaymod.gui.rendersettings.exportyoutube"), true);
ignoreCamDir.enabled = youtubeExport.enabled = false;
int i = 0;
for(RendererSettings r : RendererSettings.values()) {
rendererDropdown.addElement(r);
rendererDropdown.setHoverText(i, r.getDescription());
i++;
}
permanentButtons.add(advancedButton);
permanentButtons.add(renderButton);
permanentButtons.add(cancelButton);
@@ -320,7 +349,6 @@ public class GuiRenderSettings extends GuiScreen {
private enum RendererSettings {
DEFAULT("default"),
TILED("tiled"),
STEREOSCOPIC("stereoscopic"),
CUBIC("cubic"),
EQUIRECTANGULAR("equirectangular");
@@ -363,10 +391,11 @@ public class GuiRenderSettings extends GuiScreen {
options.setSkyColor(colorPicker.getPickedColor());
}
options.setWidth(getWidthSetting());
options.setHeight(getHeightSetting());
if(r == RendererSettings.DEFAULT) {
renderer = new DefaultFrameRenderer(options);
} else if(r == RendererSettings.TILED) {
renderer = new TilingFrameRenderer(options, getWidthSetting(), getHeightSetting());
} else if(r == RendererSettings.STEREOSCOPIC) {
renderer = new StereoscopicFrameRenderer(options);
} else if(r == RendererSettings.CUBIC) {
@@ -393,32 +422,18 @@ public class GuiRenderSettings extends GuiScreen {
RendererSettings s = rendererDropdown.getElement(selectionIndex);
if(s == RendererSettings.DEFAULT) {
customResolution.enabled = false;
xRes.setEnabled(false);
yRes.setEnabled(false);
youtubeExport.enabled = ignoreCamDir.enabled = false;
} else if(s == RendererSettings.TILED) {
customResolution.enabled = true;
xRes.setEnabled(customResolution.isChecked());
yRes.setEnabled(customResolution.isChecked());
youtubeExport.enabled = ignoreCamDir.enabled = false;
} else if(s == RendererSettings.STEREOSCOPIC) {
customResolution.enabled = false;
xRes.setEnabled(false);
yRes.setEnabled(false);
youtubeExport.enabled = ignoreCamDir.enabled = false;
} else if(s == RendererSettings.CUBIC) {
customResolution.enabled = false;
xRes.setEnabled(false);
yRes.setEnabled(false);
youtubeExport.enabled = true;
ignoreCamDir.enabled = false;
} else if(s == RendererSettings.EQUIRECTANGULAR) {
customResolution.enabled = false;
xRes.setEnabled(false);
yRes.setEnabled(false);
youtubeExport.enabled = ignoreCamDir.enabled = true;
}
xRes.setCursorPositionEnd();
yRes.setCursorPositionEnd();
}
}
}

View File

@@ -60,7 +60,7 @@ public class SpectatorRenderer {
float f1 = 0.07F;
if(this.mc.gameSettings.anaglyph) {
if (renderPass != 2) {
GlStateManager.translate((float)(-(renderPass * 2 - 1)) * f1, 0.0F, 0.0F);
}
@@ -70,7 +70,7 @@ public class SpectatorRenderer {
GlStateManager.matrixMode(GL11.GL_MODELVIEW);
GlStateManager.loadIdentity();
if(this.mc.gameSettings.anaglyph) {
if (renderPass != 2) {
GlStateManager.translate((float)(renderPass * 2 - 1) * 0.1F, 0.0F, 0.0F);
}
@@ -540,4 +540,9 @@ public class SpectatorRenderer {
GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F);
GlStateManager.disableBlend();
}
public void cleanup() {
MinecraftForge.EVENT_BUS.unregister(this);
FMLCommonHandler.instance().bus().unregister(this);
}
}

View File

@@ -1,6 +1,7 @@
package eu.crushedpixel.replaymod.settings;
import eu.crushedpixel.replaymod.video.frame.FrameRenderer;
import net.minecraft.client.Minecraft;
import static org.apache.commons.lang3.Validate.*;
@@ -8,9 +9,16 @@ public final class RenderOptions {
private FrameRenderer renderer;
private float quality = 0.5f;
private int fps = 30;
// Advanced
private boolean waitForChunks = true;
private boolean isLinearMovement = false;
private int skyColor = -1;
private int width = Minecraft.getMinecraft().displayWidth;
private int height = Minecraft.getMinecraft().displayHeight;
// Highly advanced
private int writerQueueSize = Integer.parseInt(System.getProperty("replaymod.render.writerQueueSize", "1"));
public FrameRenderer getRenderer() {
return renderer;
@@ -66,6 +74,30 @@ public final class RenderOptions {
this.skyColor = skyColor;
}
public int getWidth() {
return width;
}
public void setWidth(int width) {
this.width = width;
}
public int getHeight() {
return height;
}
public void setHeight(int height) {
this.height = height;
}
public int getWriterQueueSize() {
return writerQueueSize;
}
public void setWriterQueueSize(int writerQueueSize) {
this.writerQueueSize = writerQueueSize;
}
public RenderOptions copy() {
RenderOptions copy = new RenderOptions();
copy.renderer = this.renderer;
@@ -74,6 +106,9 @@ public final class RenderOptions {
copy.waitForChunks = this.waitForChunks;
copy.isLinearMovement = this.isLinearMovement;
copy.skyColor = this.skyColor;
copy.width = this.width;
copy.height = this.height;
copy.writerQueueSize = this.writerQueueSize;
return copy;
}
}

View File

@@ -0,0 +1,41 @@
package eu.crushedpixel.replaymod.utils;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
public class OpenGLUtils {
public static final int VIEWPORT_MAX_WIDTH;
public static final int VIEWPORT_MAX_HEIGHT;
static {
IntBuffer buffer = BufferUtils.createIntBuffer(16);
GL11.glGetInteger(GL11.GL_MAX_VIEWPORT_DIMS, buffer);
VIEWPORT_MAX_WIDTH = buffer.get();
VIEWPORT_MAX_HEIGHT = buffer.get();
}
public static void openGlBytesToBufferedImage(ByteBuffer buffer, int bufferWidth, BufferedImage image, int offsetX, int offsetY) {
int[] pixels = ((DataBufferInt) image.getRaster().getDataBuffer()).getData();
int imageWidth = image.getWidth();
int bufferSize = buffer.remaining() / 3;
// Read the OpenGL image row by row from right to left (flipped horizontally)
for (int i = bufferSize - 1; i >= 0; i--) {
// Coordinates in the final image
int x = offsetX + bufferWidth - i % bufferWidth - 1; // X coord of OpenGL image has to be flipped first
int y = offsetY + i / bufferWidth;
if (x >= imageWidth || y * imageWidth >= pixels.length) {
buffer.position(buffer.position() + 3); // Pixel would end up outside of image
continue;
}
// Write to image (row by row, left to right)
pixels[y * imageWidth + x] = 0xff << 24 | (buffer.get() & 0xff) << 16 | (buffer.get() & 0xff) << 8 | buffer.get() & 0xff;
}
buffer.rewind();
}
}

View File

@@ -50,14 +50,12 @@ public class VideoRenderer {
private int totalFrames;
private final GuiVideoRenderer gui;
private boolean frameRendererUpdatesGui;
private boolean paused;
private boolean cancelled;
public VideoRenderer(RenderOptions options) throws IOException {
this.frameRenderer = options.getRenderer();
this.videoWriter = new VideoWriter(frameRenderer.getVideoWidth(), frameRenderer.getVideoHeight(),
options.getFps(), options.getQuality(), 10);
this.videoWriter = new VideoWriter(options);
this.gui = new GuiVideoRenderer(this);
this.replaySender = ReplayMod.replaySender;
this.options = options;
@@ -100,9 +98,7 @@ public class VideoRenderer {
frame(timer);
framesDone++;
if (!frameRendererUpdatesGui) {
drawGui();
}
drawGui();
System.gc();
}
@@ -149,12 +145,6 @@ public class VideoRenderer {
}
time.prepare();
frameRendererUpdatesGui = frameRenderer.setRenderPreviewCallback(new Runnable() {
@Override
public void run() {
drawGui();
}
});
frameRenderer.setup();
ScaledResolution scaled = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight);

View File

@@ -1,6 +1,7 @@
package eu.crushedpixel.replaymod.video;
import com.google.common.base.Preconditions;
import eu.crushedpixel.replaymod.settings.RenderOptions;
import eu.crushedpixel.replaymod.utils.ReplayFileIO;
import org.monte.media.*;
import org.monte.media.FormatKeys.MediaType;
@@ -42,12 +43,8 @@ public class VideoWriter {
private final Condition noLongerEmptyCondition = lock.newCondition();
private final Condition noLongerFullCondition = lock.newCondition();
public VideoWriter(int width, int height, int fps, float quality) throws IOException {
this(width, height, fps, quality, Integer.MAX_VALUE);
}
public VideoWriter(int width, int height, int fps, float quality, int queueLimit) throws IOException {
this.queueLimit = queueLimit;
public VideoWriter(RenderOptions options) throws IOException {
this.queueLimit = options.getWriterQueueSize();
this.toWrite = new LinkedList<BufferedImage>();
File folder = ReplayFileIO.getRenderFolder();
@@ -58,11 +55,11 @@ public class VideoWriter {
out = Registry.getInstance().getWriter(file);
Format format = new Format(FormatKeys.MediaTypeKey, MediaType.VIDEO,
FormatKeys.EncodingKey, VideoFormatKeys.ENCODING_AVI_MJPG,
FormatKeys.FrameRateKey, new Rational(fps, 1),
VideoFormatKeys.WidthKey, width,
VideoFormatKeys.HeightKey, height,
FormatKeys.FrameRateKey, new Rational(options.getFps(), 1),
VideoFormatKeys.WidthKey, options.getWidth(),
VideoFormatKeys.HeightKey, options.getHeight(),
VideoFormatKeys.DepthKey, 24,
VideoFormatKeys.QualityKey, quality);
VideoFormatKeys.QualityKey, options.getQuality());
track = out.addTrack(format);

View File

@@ -33,8 +33,8 @@ public class CubicEntityRenderer extends CustomEntityRenderer {
private final boolean stable;
private Direction direction;
public CubicEntityRenderer(RenderOptions options, boolean stable) {
super(options);
public CubicEntityRenderer(RenderOptions options, int frameSize, boolean stable) {
super(options, frameSize, frameSize);
this.stable = stable;
}

View File

@@ -3,6 +3,8 @@ package eu.crushedpixel.replaymod.video.entity;
import eu.crushedpixel.replaymod.renderer.SpectatorRenderer;
import eu.crushedpixel.replaymod.replay.ReplayHandler;
import eu.crushedpixel.replaymod.settings.RenderOptions;
import eu.crushedpixel.replaymod.utils.OpenGLUtils;
import eu.crushedpixel.replaymod.video.entity.strategy.*;
import net.minecraft.client.Minecraft;
import net.minecraft.client.particle.EffectRenderer;
import net.minecraft.client.renderer.*;
@@ -14,53 +16,103 @@ import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.util.AxisAlignedBB;
import net.minecraft.util.EnumWorldBlockLayer;
import net.minecraft.util.MathHelper;
import net.minecraft.util.ResourceLocation;
import org.lwjgl.util.glu.Project;
import java.awt.image.BufferedImage;
import static net.minecraft.client.renderer.GlStateManager.*;
import static org.lwjgl.opengl.GL11.*;
public abstract class CustomEntityRenderer {
protected final Minecraft mc = Minecraft.getMinecraft();
protected final EntityRenderer proxied = mc.entityRenderer;
public final Minecraft mc = Minecraft.getMinecraft();
public final EntityRenderer proxied = mc.entityRenderer;
protected final SpectatorRenderer spectatorRenderer = new SpectatorRenderer(){
@Override
protected void gluPerspective(float fovY, float aspect, float zNear, float zFar) {
CustomEntityRenderer.this.gluPerspective(fovY, aspect, zNear, zFar);
}
};
protected final FrameRenderingStrategy renderingStrategy;
public GluPerspectiveHook gluPerspectiveHook;
public LoadShaderHook loadShaderHook;
protected int frameCount;
protected final RenderOptions options;
public final int resultWidth;
public final int resultHeight;
public CustomEntityRenderer(RenderOptions options) {
this(options, options.getWidth(), options.getHeight());
}
public CustomEntityRenderer(RenderOptions options, int resultWidth, int resultHeight) {
this.options = options;
}
this.resultWidth = resultWidth;
this.resultHeight = resultHeight;
protected void gluPerspective(float fovY, float aspect, float zNear, float zFar) {
Project.gluPerspective(fovY, aspect, zNear, zFar);
}
public void updateCameraAndRender(float renderPartialTicks) {
if (mc.theWorld != null) {
renderWorld(renderPartialTicks, 0);
if (OpenGlHelper.shadersSupported) {
mc.renderGlobal.renderEntityOutlineFramebuffer();
if (proxied.theShaderGroup != null && proxied.useShader) {
matrixMode(GL_TEXTURE);
pushMatrix();
loadIdentity();
proxied.theShaderGroup.loadShaderGroup(renderPartialTicks);
popMatrix();
}
mc.getFramebuffer().bindFramebuffer(true);
if (OpenGlHelper.isFramebufferEnabled()) {
if (resultWidth > OpenGLUtils.VIEWPORT_MAX_WIDTH || resultHeight > OpenGLUtils.VIEWPORT_MAX_HEIGHT) {
// Video resolution only limited by available RAM and disk size
renderingStrategy = new TiledFrameBufferRenderingStrategy(this);
} else {
// This strategy supports shader
renderingStrategy = new VanillaFrameBufferRenderingStrategy(this);
}
} else {
if (resultWidth > mc.displayHeight || resultHeight > mc.displayHeight) {
// Video resolution only limited by available RAM and disk size
renderingStrategy = new TiledReadPixelsRenderingStrategy(this);
} else {
// Simplest and fastest strategy if frame buffers aren't supported
renderingStrategy = new VanillaReadPixelsRenderingStrategy(this);
}
}
System.out.println("CustomEntityRenderer using " + renderingStrategy);
}
@SuppressWarnings("unused") // Method called by ASM hook
protected void loadShader(ResourceLocation resourceLocation) {
if (loadShaderHook != null) {
loadShaderHook.loadShader(resourceLocation);
} else {
original_loadShader(resourceLocation);
}
}
protected void renderWorld(float partialTicks, long finishTimeNano) {
@SuppressWarnings("unused")
protected final void original_loadShader(ResourceLocation resourceLocation) {
// Method body generated by ASM
}
protected void gluPerspective(float fovY, float aspect, float zNear, float zFar) {
if (gluPerspectiveHook == null) {
Project.gluPerspective(fovY, aspect, zNear, zFar);
} else {
gluPerspectiveHook.gluPerspective(fovY, aspect, zNear, zFar);
}
}
public void renderFrame(float partialTicks, BufferedImage into, int x, int y) {
if (mc.theWorld != null) {
renderingStrategy.renderFrame(partialTicks, into, x, y);
}
}
public void withDisplaySize(int displayWidth, int displayHeight, Runnable runnable) {
final int prevWidth = mc.displayWidth;
final int prevHeight = mc.displayHeight;
mc.displayWidth = displayWidth;
mc.displayHeight = displayHeight;
runnable.run();
mc.displayWidth = prevWidth;
mc.displayHeight = prevHeight;
}
public void renderWorld(float partialTicks, long finishTimeNano) {
proxied.updateLightmap(partialTicks);
enableDepth();
@@ -247,6 +299,11 @@ public abstract class CustomEntityRenderer {
}
}
public void cleanup() {
renderingStrategy.cleanup();
spectatorRenderer.cleanup();
}
public static final class NoCullingCamera implements ICamera {
@Override
@@ -259,4 +316,12 @@ public abstract class CustomEntityRenderer {
}
}
public static interface GluPerspectiveHook {
void gluPerspective(float fovY, float aspect, float zNear, float zFar);
}
public static interface LoadShaderHook {
void loadShader(ResourceLocation resourceLocation);
}
}

View File

@@ -4,6 +4,8 @@ import eu.crushedpixel.replaymod.settings.RenderOptions;
import net.minecraft.client.renderer.GlStateManager;
import net.minecraft.util.MathHelper;
import java.awt.image.BufferedImage;
import static net.minecraft.client.renderer.GlStateManager.loadIdentity;
import static net.minecraft.client.renderer.GlStateManager.matrixMode;
import static org.lwjgl.opengl.GL11.GL_MODELVIEW;
@@ -14,7 +16,7 @@ public class StereoscopicEntityRenderer extends CustomEntityRenderer {
private boolean leftEye;
public StereoscopicEntityRenderer(RenderOptions options) {
super(options);
super(options, options.getWidth() / 2, options.getHeight());
}
public void setEye(boolean leftEye) {
@@ -22,11 +24,8 @@ public class StereoscopicEntityRenderer extends CustomEntityRenderer {
}
@Override
public void updateCameraAndRender(float renderPartialTicks) {
boolean wasAnaglyph = mc.gameSettings.anaglyph;
mc.gameSettings.anaglyph = true;
super.updateCameraAndRender(renderPartialTicks);
mc.gameSettings.anaglyph = wasAnaglyph;
public void renderFrame(float partialTicks, BufferedImage into, int x, int y) {
super.renderFrame(partialTicks, into, x, y);
}
protected void translateStereoscopic() {

View File

@@ -1,43 +0,0 @@
package eu.crushedpixel.replaymod.video.entity;
import eu.crushedpixel.replaymod.settings.RenderOptions;
import eu.crushedpixel.replaymod.video.frame.TilingFrameRenderer;
import static net.minecraft.client.renderer.GlStateManager.scale;
import static net.minecraft.client.renderer.GlStateManager.translate;
import static org.lwjgl.opengl.GL11.glFrustum;
public class TilingEntityRenderer extends CustomEntityRenderer {
private final TilingFrameRenderer renderer;
private int tileX, tileY;
public TilingEntityRenderer(RenderOptions options, TilingFrameRenderer renderer) {
super(options);
this.renderer = renderer;
}
public void setTile(int tileX, int tileY) {
this.tileX = tileX;
this.tileY = tileY;
}
@Override
public void gluPerspective(float fovY, float aspect, float zNear, float zFar) {
double height = Math.tan(fovY / 360 * Math.PI) * zNear;
double width = height * aspect;
int videoWidth = renderer.getVideoWidth();
int videoHeight = renderer.getVideoHeight();
int tileWidth = renderer.getTileWidth();
int tileHeight = renderer.getTileHeight();
double tilesX = (double) videoWidth / tileWidth;
double tilesY = (double) videoHeight / tileHeight;
double tilesMin = Math.min(tilesX, tilesY);
scale(tilesMin, tilesMin, 1);
double xOffset = (2 * tileX - tilesX + 1) / tilesMin;
double yOffset = (2 * tileY - tilesY + 1) / tilesMin;
translate(-xOffset, yOffset, 0);
glFrustum(-width, width, -height, height, zNear, zFar);
}
}

View File

@@ -0,0 +1,8 @@
package eu.crushedpixel.replaymod.video.entity.strategy;
import java.awt.image.BufferedImage;
public interface FrameRenderingStrategy {
void renderFrame(float partialTicks, BufferedImage into, int x, int y);
void cleanup();
}

View File

@@ -0,0 +1,127 @@
package eu.crushedpixel.replaymod.video.entity.strategy;
import eu.crushedpixel.replaymod.utils.OpenGLUtils;
import eu.crushedpixel.replaymod.video.entity.CustomEntityRenderer;
import net.minecraft.client.shader.Framebuffer;
import net.minecraft.crash.CrashReport;
import net.minecraft.util.ReportedException;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import java.awt.image.BufferedImage;
import java.nio.ByteBuffer;
import static net.minecraft.client.renderer.GlStateManager.*;
import static org.lwjgl.opengl.GL11.*;
public class TiledFrameBufferRenderingStrategy implements FrameRenderingStrategy, CustomEntityRenderer.GluPerspectiveHook {
private final CustomEntityRenderer renderer;
private final int tileWidth;
private final int tileHeight;
private final ByteBuffer buffer;
private Framebuffer frameBuffer;
private int tileX;
private int tileY;
public TiledFrameBufferRenderingStrategy(CustomEntityRenderer renderer) {
this.renderer = renderer;
this.tileWidth = OpenGLUtils.VIEWPORT_MAX_WIDTH;
this.tileHeight = OpenGLUtils.VIEWPORT_MAX_HEIGHT;
this.buffer = BufferUtils.createByteBuffer(tileWidth * tileHeight * 3);
renderer.gluPerspectiveHook = this;
}
@Override
public void gluPerspective(float fovY, float aspect, float zNear, float zFar) {
double height = Math.tan(fovY / 360 * Math.PI) * zNear;
double width = height * aspect;
double tilesX = (double) renderer.resultWidth / tileWidth;
double tilesY = (double) renderer.resultHeight / tileHeight;
double tilesMin = Math.min(tilesX, tilesY);
scale(tilesMin, tilesMin, 1);
double xOffset = (2 * tileX - tilesX + 1) / tilesMin;
double yOffset = (2 * tileY - tilesY + 1) / tilesMin;
translate(-xOffset, yOffset, 0);
glFrustum(-width, width, -height, height, zNear, zFar);
}
private Framebuffer frameBuffer() {
if (frameBuffer == null) {
frameBuffer = new Framebuffer(tileWidth, tileHeight, true);
}
return frameBuffer;
}
@Override
public void renderFrame(float partialTicks, BufferedImage into, int x, int y) {
int tilesX = getTilesX();
int tilesY = getTilesY();
for (tileX = 0; tileX < tilesX; tileX++) {
for (tileY = 0; tileY < tilesY; tileY++) {
try {
renderTile(partialTicks, into, x + tileWidth * tileX, y + tileHeight * tileY);
} catch (Throwable t) {
CrashReport crash = CrashReport.makeCrashReport(t, "Rendering tile " + x + "/" + y);
throw new ReportedException(crash);
}
}
}
}
public void renderTile(final float partialTicks, BufferedImage into, int x, int y) {
pushMatrix();
frameBuffer().bindFramebuffer(true);
clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
enableTexture2D();
renderer.withDisplaySize(tileWidth, tileHeight, new Runnable() {
@Override
public void run() {
renderer.renderWorld(partialTicks, 0);
}
});
frameBuffer().unbindFramebuffer();
popMatrix();
GL11.glPixelStorei(GL11.GL_PACK_ALIGNMENT, 1);
GL11.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 1);
buffer.clear();
frameBuffer().bindFramebufferTexture();
GL11.glGetTexImage(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGB, GL11.GL_UNSIGNED_BYTE, buffer);
frameBuffer().unbindFramebufferTexture();
OpenGLUtils.openGlBytesToBufferedImage(buffer, tileWidth, into, x, y);
}
public int getTilesX() {
int videoWidth = renderer.resultWidth;
if (videoWidth % tileWidth == 0) {
return videoWidth / tileWidth;
} else {
return videoWidth / tileWidth + 1;
}
}
public int getTilesY() {
int videoHeight = renderer.resultHeight;
if (videoHeight % tileHeight == 0) {
return videoHeight / tileHeight;
} else {
return videoHeight / tileHeight + 1;
}
}
@Override
public void cleanup() {
if (frameBuffer != null) {
frameBuffer.deleteFramebuffer();
}
}
}

View File

@@ -0,0 +1,112 @@
package eu.crushedpixel.replaymod.video.entity.strategy;
import eu.crushedpixel.replaymod.utils.OpenGLUtils;
import eu.crushedpixel.replaymod.video.entity.CustomEntityRenderer;
import net.minecraft.crash.CrashReport;
import net.minecraft.util.ReportedException;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import java.awt.image.BufferedImage;
import java.nio.ByteBuffer;
import static net.minecraft.client.renderer.GlStateManager.*;
import static org.lwjgl.opengl.GL11.*;
public class TiledReadPixelsRenderingStrategy implements FrameRenderingStrategy, CustomEntityRenderer.GluPerspectiveHook {
private final CustomEntityRenderer renderer;
private final int tileWidth;
private final int tileHeight;
private final ByteBuffer buffer;
private int tileX;
private int tileY;
public TiledReadPixelsRenderingStrategy(CustomEntityRenderer renderer) {
this.renderer = renderer;
this.tileWidth = renderer.mc.displayWidth;
this.tileHeight = renderer.mc.displayHeight;
this.buffer = BufferUtils.createByteBuffer(tileWidth * tileHeight * 3);
renderer.gluPerspectiveHook = this;
}
@Override
public void gluPerspective(float fovY, float aspect, float zNear, float zFar) {
double height = Math.tan(fovY / 360 * Math.PI) * zNear;
double width = height * aspect;
double tilesX = (double) renderer.resultWidth / tileWidth;
double tilesY = (double) renderer.resultHeight / tileHeight;
double tilesMin = Math.min(tilesX, tilesY);
scale(tilesMin, tilesMin, 1);
double xOffset = (2 * tileX - tilesX + 1) / tilesMin;
double yOffset = (2 * tileY - tilesY + 1) / tilesMin;
translate(-xOffset, yOffset, 0);
glFrustum(-width, width, -height, height, zNear, zFar);
}
@Override
public void renderFrame(float partialTicks, BufferedImage into, int x, int y) {
int tilesX = getTilesX();
int tilesY = getTilesY();
for (tileX = 0; tileX < tilesX; tileX++) {
for (tileY = 0; tileY < tilesY; tileY++) {
try {
renderTile(partialTicks, into, x + tileWidth * tileX, y + tileHeight * tileY);
} catch (Throwable t) {
CrashReport crash = CrashReport.makeCrashReport(t, "Rendering tile " + x + "/" + y);
throw new ReportedException(crash);
}
}
}
}
public void renderTile(final float partialTicks, BufferedImage into, int x, int y) {
pushMatrix();
clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
enableTexture2D();
renderer.withDisplaySize(tileWidth, tileHeight, new Runnable() {
@Override
public void run() {
renderer.renderWorld(partialTicks, 0);
}
});
popMatrix();
buffer.clear();
GL11.glPixelStorei(GL11.GL_PACK_ALIGNMENT, 1);
GL11.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 1);
GL11.glReadPixels(0, 0, tileWidth, tileHeight, GL11.GL_RGB, GL11.GL_UNSIGNED_BYTE, buffer);
buffer.rewind();
OpenGLUtils.openGlBytesToBufferedImage(buffer, tileWidth, into, x, y);
}
public int getTilesX() {
int videoWidth = renderer.resultWidth;
if (videoWidth % tileWidth == 0) {
return videoWidth / tileWidth;
} else {
return videoWidth / tileWidth + 1;
}
}
public int getTilesY() {
int videoHeight = renderer.resultHeight;
if (videoHeight % tileHeight == 0) {
return videoHeight / tileHeight;
} else {
return videoHeight / tileHeight + 1;
}
}
@Override
public void cleanup() {
}
}

View File

@@ -0,0 +1,115 @@
package eu.crushedpixel.replaymod.video.entity.strategy;
import com.google.gson.JsonSyntaxException;
import eu.crushedpixel.replaymod.utils.OpenGLUtils;
import eu.crushedpixel.replaymod.video.entity.CustomEntityRenderer;
import net.minecraft.client.renderer.EntityRenderer;
import net.minecraft.client.renderer.OpenGlHelper;
import net.minecraft.client.shader.Framebuffer;
import net.minecraft.client.shader.ShaderGroup;
import net.minecraft.util.ResourceLocation;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.nio.ByteBuffer;
import static net.minecraft.client.renderer.GlStateManager.*;
import static org.lwjgl.opengl.GL11.*;
public class VanillaFrameBufferRenderingStrategy implements FrameRenderingStrategy, CustomEntityRenderer.LoadShaderHook {
private static final Logger logger = LogManager.getLogger();
private final CustomEntityRenderer renderer;
private final ByteBuffer buffer;
private Framebuffer frameBuffer;
public VanillaFrameBufferRenderingStrategy(CustomEntityRenderer renderer) {
this.renderer = renderer;
this.buffer = BufferUtils.createByteBuffer(renderer.resultWidth * renderer.resultHeight * 3);
}
@Override
public void loadShader(ResourceLocation resourceLocation) {
EntityRenderer proxied = renderer.proxied;
try {
ShaderGroup theShaderGroup = new ShaderGroup(renderer.mc.getTextureManager(), proxied.resourceManager, frameBuffer(), resourceLocation);
theShaderGroup.createBindFramebuffers(renderer.resultWidth, renderer.resultHeight);
proxied.theShaderGroup = theShaderGroup;
proxied.useShader = true;
} catch (IOException e) {
logger.warn("Failed to load shader: " + resourceLocation, e);
proxied.useShader = false;
} catch (JsonSyntaxException e) {
logger.warn("Failed to load shader: " + resourceLocation, e);
proxied.useShader = false;
}
}
private Framebuffer frameBuffer() {
if (frameBuffer == null) {
frameBuffer = new Framebuffer(renderer.resultWidth, renderer.resultHeight, true);
ShaderGroup theShaderGroup = renderer.proxied.theShaderGroup;
if (theShaderGroup != null) {
theShaderGroup.deleteShaderGroup();
renderer.proxied.theShaderGroup = null;
}
}
return frameBuffer;
}
@Override
public void renderFrame(final float partialTicks, BufferedImage into, int x, int y) {
pushMatrix();
frameBuffer().bindFramebuffer(true);
clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
enableTexture2D();
renderer.withDisplaySize(renderer.resultWidth, renderer.resultHeight, new Runnable() {
@Override
public void run() {
renderer.renderWorld(partialTicks, 0);
if (OpenGlHelper.shadersSupported) {
EntityRenderer proxied = renderer.proxied;
if (proxied.theShaderGroup != null && proxied.useShader) {
matrixMode(GL_TEXTURE);
pushMatrix();
loadIdentity();
proxied.theShaderGroup.loadShaderGroup(partialTicks);
popMatrix();
}
}
}
});
frameBuffer().unbindFramebuffer();
popMatrix();
GL11.glPixelStorei(GL11.GL_PACK_ALIGNMENT, 1);
GL11.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 1);
buffer.clear();
frameBuffer().bindFramebufferTexture();
GL11.glGetTexImage(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGB, GL11.GL_UNSIGNED_BYTE, buffer);
frameBuffer().unbindFramebufferTexture();
OpenGLUtils.openGlBytesToBufferedImage(buffer, renderer.resultWidth, into, x, y);
}
@Override
public void cleanup() {
if (frameBuffer != null) {
frameBuffer.deleteFramebuffer();
}
if (renderer.proxied.theShaderGroup != null) {
renderer.proxied.theShaderGroup.createBindFramebuffers(renderer.mc.displayWidth, renderer.mc.displayHeight);
}
}
}

View File

@@ -0,0 +1,54 @@
package eu.crushedpixel.replaymod.video.entity.strategy;
import eu.crushedpixel.replaymod.utils.OpenGLUtils;
import eu.crushedpixel.replaymod.video.entity.CustomEntityRenderer;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import java.awt.image.BufferedImage;
import java.nio.ByteBuffer;
import static net.minecraft.client.renderer.GlStateManager.*;
import static org.lwjgl.opengl.GL11.GL_COLOR_BUFFER_BIT;
import static org.lwjgl.opengl.GL11.GL_DEPTH_BUFFER_BIT;
public class VanillaReadPixelsRenderingStrategy implements FrameRenderingStrategy {
private final CustomEntityRenderer renderer;
private final ByteBuffer buffer;
public VanillaReadPixelsRenderingStrategy(CustomEntityRenderer renderer) {
this.renderer = renderer;
this.buffer = BufferUtils.createByteBuffer(renderer.resultWidth * renderer.resultHeight * 3);
}
@Override
public void renderFrame(final float partialTicks, BufferedImage into, int x, int y) {
pushMatrix();
clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
enableTexture2D();
renderer.withDisplaySize(renderer.resultWidth, renderer.resultHeight, new Runnable() {
@Override
public void run() {
renderer.renderWorld(partialTicks, 0);
}
});
popMatrix();
buffer.clear();
GL11.glPixelStorei(GL11.GL_PACK_ALIGNMENT, 1);
GL11.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 1);
GL11.glReadPixels(0, 0, renderer.resultWidth, renderer.resultHeight, GL11.GL_RGB, GL11.GL_UNSIGNED_BYTE, buffer);
buffer.rewind();
OpenGLUtils.openGlBytesToBufferedImage(buffer, renderer.resultWidth, into, x, y);
}
@Override
public void cleanup() {
}
}

View File

@@ -2,7 +2,6 @@ package eu.crushedpixel.replaymod.video.frame;
import eu.crushedpixel.replaymod.settings.RenderOptions;
import eu.crushedpixel.replaymod.video.entity.CubicEntityRenderer;
import net.minecraft.client.Minecraft;
import net.minecraft.crash.CrashReport;
import net.minecraft.util.ReportedException;
import net.minecraft.util.Timer;
@@ -10,23 +9,19 @@ import net.minecraft.util.Timer;
import java.awt.image.BufferedImage;
public class CubicFrameRenderer extends FrameRenderer {
protected static int getDisplaySize() {
Minecraft mc = Minecraft.getMinecraft();
return Math.min(mc.displayWidth, mc.displayHeight);
}
protected final int frameSize;
protected final CubicEntityRenderer entityRenderer;
protected final int displaySize;
protected CubicFrameRenderer(RenderOptions options, int videoWidth, int videoHeight, boolean stable) {
super(options, videoWidth, videoHeight, getDisplaySize() * getDisplaySize() * 3);
this.displaySize = getDisplaySize();
this.entityRenderer = new CubicEntityRenderer(options, stable);
protected CubicFrameRenderer(RenderOptions options, int frameSize, boolean stable) {
super(options);
this.frameSize = frameSize;
this.entityRenderer = new CubicEntityRenderer(options, frameSize, stable);
setCustomEntityRenderer(entityRenderer);
}
public CubicFrameRenderer(RenderOptions options, boolean stable) {
this(options, getDisplaySize() * 4, getDisplaySize() * 3, stable);
this(options, options.getWidth() / 4, stable);
}
@Override
@@ -34,7 +29,12 @@ public class CubicFrameRenderer extends FrameRenderer {
BufferedImage image = new BufferedImage(getVideoWidth(), getVideoHeight(), BufferedImage.TYPE_INT_RGB);
for (CubicEntityRenderer.Direction direction : CubicEntityRenderer.Direction.values()) {
try {
captureFrame(timer, image, direction);
entityRenderer.setDirection(direction);
int frame = direction.getCubicFrame();
int xVideo = frame % 4 * frameSize;
int yVideo = frame / 4 * frameSize;
renderFrame(timer, image, xVideo, yVideo);
} catch (Throwable t) {
CrashReport crash = CrashReport.makeCrashReport(t, "Rendering frame " + direction + ".");
throw new ReportedException(crash);
@@ -43,38 +43,4 @@ public class CubicFrameRenderer extends FrameRenderer {
updateDefaultPreview(image);
return image;
}
protected void captureFrame(Timer timer, BufferedImage into, CubicEntityRenderer.Direction direction) {
renderFrameToBuffer(timer, direction);
// Copy to image
int frame = direction.getCubicFrame();
int xVideo = frame % 4 * displaySize;
int yVideo = frame / 4 * displaySize;
copyPixelsToImage(buffer, displaySize, into, xVideo, yVideo);
}
protected void renderFrameToBuffer(Timer timer, CubicEntityRenderer.Direction direction) {
// Setup aspect ratio
int originalWidth = mc.displayWidth;
int originalHeight = mc.displayHeight;
mc.displayWidth = mc.displayHeight = displaySize;
updateFrameBufferSize();
// Render frame
entityRenderer.setDirection(direction);
renderFrame(timer);
// Read pixels
readPixels(buffer);
mc.displayWidth = originalWidth;
mc.displayHeight = originalHeight;
updateFrameBufferSize();
}
private void updateFrameBufferSize() {
mc.getFramebuffer().createBindFramebuffer(mc.displayWidth, mc.displayHeight);
mc.entityRenderer.updateShaderGroupSize(mc.displayWidth, mc.displayHeight);
}
}

View File

@@ -1,7 +1,6 @@
package eu.crushedpixel.replaymod.video.frame;
import eu.crushedpixel.replaymod.settings.RenderOptions;
import net.minecraft.client.Minecraft;
import net.minecraft.util.Timer;
import java.awt.image.BufferedImage;
@@ -9,20 +8,16 @@ import java.awt.image.BufferedImage;
public class DefaultFrameRenderer extends FrameRenderer {
public DefaultFrameRenderer(RenderOptions options) {
super(options, Minecraft.getMinecraft().displayWidth, Minecraft.getMinecraft().displayHeight);
super(options);
}
@Override
public BufferedImage captureFrame(Timer timer) {
renderFrame(timer);
int width = getVideoWidth();
int height = getVideoHeight();
readPixels(buffer);
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
copyPixelsToImage(buffer, width, image, 0, 0);
renderFrame(timer, image, 0, 0);
updateDefaultPreview(image);
return image;
}

View File

@@ -11,10 +11,16 @@ import java.awt.image.DataBufferInt;
import static java.lang.Math.PI;
public class EquirectangularFrameRenderer extends CubicFrameRenderer {
public class EquirectangularFrameRenderer extends FrameRenderer {
private final int frameSize;
protected final CubicEntityRenderer entityRenderer;
public EquirectangularFrameRenderer(RenderOptions options, boolean stable) {
super(options, getDisplaySize() * 4, getDisplaySize() * 2, stable);
super(options);
this.frameSize = options.getWidth() / 4;
this.entityRenderer = new CubicEntityRenderer(options, frameSize, stable);
setCustomEntityRenderer(entityRenderer);
}
@Override
@@ -77,8 +83,8 @@ public class EquirectangularFrameRenderer extends CubicFrameRenderer {
pt = top;
}
int cX = Math.min(displaySize - 1, (int) (cXN * displaySize));
int cY = Math.min(displaySize - 1, (int) (cYN * displaySize));
int cX = Math.min(frameSize - 1, (int) (cXN * frameSize));
int cY = Math.min(frameSize - 1, (int) (cYN * frameSize));
resultPixels[i + j * rWidth] = 0xff000000 | pt.getRGB(cX, cY); // Make sure we got alpha value as well
}
}
@@ -88,11 +94,9 @@ public class EquirectangularFrameRenderer extends CubicFrameRenderer {
protected BufferedImage captureFrame(Timer timer, CubicEntityRenderer.Direction direction) {
try {
renderFrameToBuffer(timer, direction);
// Copy to image
BufferedImage image = new BufferedImage(displaySize, displaySize, BufferedImage.TYPE_INT_RGB);
copyPixelsToImage(buffer, displaySize, image, displaySize, 0, 0);
BufferedImage image = new BufferedImage(frameSize, frameSize, BufferedImage.TYPE_INT_RGB);
entityRenderer.setDirection(direction);
renderFrame(timer, image, 0, 0);
return image;
} catch (Throwable t) {
CrashReport crash = CrashReport.makeCrashReport(t, "Rendering frame " + direction);

View File

@@ -1,7 +1,5 @@
package eu.crushedpixel.replaymod.video.frame;
import com.google.common.base.Preconditions;
import com.google.common.util.concurrent.Runnables;
import eu.crushedpixel.replaymod.settings.RenderOptions;
import eu.crushedpixel.replaymod.video.entity.CustomEntityRenderer;
import net.minecraft.client.Minecraft;
@@ -11,7 +9,6 @@ import net.minecraft.client.renderer.texture.TextureUtil;
import net.minecraft.util.ResourceLocation;
import net.minecraft.util.Timer;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
@@ -19,51 +16,38 @@ import java.nio.ByteBuffer;
import java.util.Arrays;
import static com.google.common.base.Preconditions.checkNotNull;
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;
import static net.minecraft.client.renderer.GlStateManager.bindTexture;
import static net.minecraft.client.renderer.GlStateManager.color;
public abstract class FrameRenderer {
private static final ResourceLocation noPreviewTexture = new ResourceLocation("replaymod", "logo.jpg");
protected final Minecraft mc = Minecraft.getMinecraft();
protected final RenderOptions options;
private final int videoWidth;
private final int videoHeight;
private CustomEntityRenderer customEntityRenderer;
private DynamicTexture previewTexture;
private Runnable renderPreviewCallback;
private boolean previewActive = false;
protected final ByteBuffer buffer;
public FrameRenderer(RenderOptions options, int videoWidth, int videoHeight) {
public FrameRenderer(RenderOptions options) {
this.options = options;
this.videoWidth = videoWidth;
this.videoHeight = videoHeight;
this.buffer = BufferUtils.createByteBuffer(mc.displayWidth * mc.displayHeight * 3);
this.customEntityRenderer = new CustomEntityRenderer(options) {};
}
public FrameRenderer(RenderOptions options, int videoWidth, int videoHeight, int bufferSize) {
this.options = options;
this.videoWidth = videoWidth;
this.videoHeight = videoHeight;
this.buffer = BufferUtils.createByteBuffer(bufferSize);
this.customEntityRenderer = new CustomEntityRenderer(options) {};
this.buffer = BufferUtils.createByteBuffer(options.getWidth() * options.getHeight() * 3);
}
public final int getVideoWidth() {
return videoWidth;
return options.getWidth();
}
public final int getVideoHeight() {
return videoHeight;
return options.getHeight();
}
public void setPreviewActive(boolean previewActive) {
this.previewActive = previewActive;
if (previewActive) {
Arrays.fill(previewTexture.getTextureData(), 0xff000000);
previewTexture.updateDynamicTexture();
if (previewTexture != null) {
Arrays.fill(previewTexture.getTextureData(), 0xff000000);
previewTexture.updateDynamicTexture();
}
}
}
@@ -71,30 +55,12 @@ public abstract class FrameRenderer {
return previewActive;
}
public boolean setRenderPreviewCallback(Runnable renderPreviewCallback) {
Preconditions.checkState(this.renderPreviewCallback == null, "Render preview callback already set.");
this.renderPreviewCallback = renderPreviewCallback;
return false;
}
public Runnable getRenderPreviewCallback() {
return renderPreviewCallback != null ? renderPreviewCallback : Runnables.doNothing();
}
public void setCustomEntityRenderer(CustomEntityRenderer customEntityRenderer) {
this.customEntityRenderer = checkNotNull(customEntityRenderer);
}
public abstract BufferedImage captureFrame(Timer timer);
protected void readPixels(ByteBuffer buffer) {
buffer.clear();
GL11.glPixelStorei(GL11.GL_PACK_ALIGNMENT, 1);
GL11.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 1);
GL11.glReadPixels(0, 0, mc.displayWidth, mc.displayHeight, GL11.GL_RGB, GL11.GL_UNSIGNED_BYTE, buffer);
buffer.rewind();
}
protected void updateDefaultPreview(BufferedImage image) {
if (isPreviewActive()) {
int[] pixels = ((DataBufferInt) image.getRaster().getDataBuffer()).getData();
@@ -103,62 +69,22 @@ public abstract class FrameRenderer {
}
protected void updateDefaultPreview(int[] pixels) {
if (isPreviewActive()) {
System.arraycopy(pixels, 0, getPreviewTexture().getTextureData(), 0, pixels.length);
getPreviewTexture().updateDynamicTexture();
if (isPreviewActive() && previewTexture != null) {
System.arraycopy(pixels, 0, previewTexture.getTextureData(), 0, pixels.length);
previewTexture.updateDynamicTexture();
}
}
protected void copyPixelsToImage(ByteBuffer buffer, int bufferWidth, BufferedImage image, int offsetX, int offsetY) {
int[] pixels = ((DataBufferInt) image.getRaster().getDataBuffer()).getData();
copyPixelsToImage(buffer, bufferWidth, pixels, offsetX, offsetY);
}
protected void copyPixelsToImage(ByteBuffer buffer, int bufferWidth, BufferedImage image, int imageWidth, int offsetX, int offsetY) {
int[] pixels = ((DataBufferInt) image.getRaster().getDataBuffer()).getData();
copyPixelsToImage(buffer, bufferWidth, pixels, imageWidth, offsetX, offsetY);
}
protected void copyPixelsToImage(ByteBuffer buffer, int bufferWidth, int[] image, int offsetX, int offsetY) {
copyPixelsToImage(buffer, bufferWidth, image, videoWidth, offsetX, offsetY);
}
protected void copyPixelsToImage(ByteBuffer buffer, int bufferWidth, int[] image, int imageWidth, int offsetX, int offsetY) {
int bufferSize = buffer.remaining() / 3;
// Read the OpenGL image row by row from right to left (flipped horizontally)
for (int i = bufferSize - 1; i >= 0; i--) {
// Coordinates in the final image
int x = offsetX + bufferWidth - i % bufferWidth - 1; // X coord of OpenGL image has to be flipped first
int y = offsetY + i / bufferWidth;
if (x >= imageWidth || y * imageWidth >= image.length) {
buffer.position(buffer.position() + 3); // Pixel would end up outside of image
continue;
}
// Write to image (row by row, left to right)
image[y * imageWidth + x] = 0xff << 24 | (buffer.get() & 0xff) << 16 | (buffer.get() & 0xff) << 8 | buffer.get() & 0xff;
}
buffer.rewind();
}
protected void renderFrame(Timer timer) {
pushMatrix();
clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
mc.getFramebuffer().bindFramebuffer(true);
enableTexture2D();
customEntityRenderer.updateCameraAndRender(timer.renderPartialTicks);
mc.getFramebuffer().unbindFramebuffer();
popMatrix();
pushMatrix();
mc.getFramebuffer().framebufferRender(mc.displayWidth, mc.displayHeight);
popMatrix();
protected void renderFrame(Timer timer, BufferedImage into, int x, int y) {
customEntityRenderer.renderFrame(timer.renderPartialTicks, into, x, y);
}
public void setup() {
this.previewTexture = new DynamicTexture(videoWidth, videoHeight) {
if (customEntityRenderer == null) {
customEntityRenderer = new CustomEntityRenderer(options) {};
}
previewTexture = new DynamicTexture(getVideoWidth(), getVideoHeight()) {
@Override
public void updateDynamicTexture() {
bindTexture(getGlTextureId());
@@ -168,11 +94,22 @@ public abstract class FrameRenderer {
}
public void tearDown() {
this.previewTexture.deleteGlTexture();
if (previewTexture != null) {
previewTexture.deleteGlTexture();
}
if (customEntityRenderer != null) {
customEntityRenderer.cleanup();
}
}
public DynamicTexture getPreviewTexture() {
return previewTexture;
public void drawPreviewTexture(int x, int y, int width, int height) {
int videoWidth = getVideoWidth();
int videoHeight = getVideoHeight();
color(1, 1, 1, 1);
if (previewTexture != null) {
bindTexture(previewTexture.getGlTextureId());
Gui.drawScaledCustomSizeModalRect(x, y, 0, 0, videoWidth, videoHeight, width, height, videoWidth, videoHeight);
}
}
/**
@@ -194,9 +131,7 @@ public abstract class FrameRenderer {
x += (width - actualWidth) / 2;
y += (height - actualHeight) / 2;
bindTexture(previewTexture.getGlTextureId());
color(1, 1, 1, 1);
Gui.drawScaledCustomSizeModalRect(x, y, 0, 0, videoWidth, videoHeight, actualWidth, actualHeight, videoWidth, videoHeight);
drawPreviewTexture(x, y, actualWidth, actualHeight);
}
public static void renderNoPreview(int x, int y, int width, int height) {

View File

@@ -2,7 +2,6 @@ package eu.crushedpixel.replaymod.video.frame;
import eu.crushedpixel.replaymod.settings.RenderOptions;
import eu.crushedpixel.replaymod.video.entity.StereoscopicEntityRenderer;
import net.minecraft.client.Minecraft;
import net.minecraft.crash.CrashReport;
import net.minecraft.util.ReportedException;
import net.minecraft.util.Timer;
@@ -13,7 +12,7 @@ public class StereoscopicFrameRenderer extends FrameRenderer {
private final StereoscopicEntityRenderer entityRenderer;
public StereoscopicFrameRenderer(RenderOptions options) {
super(options, Minecraft.getMinecraft().displayWidth * 2, Minecraft.getMinecraft().displayHeight);
super(options);
entityRenderer = new StereoscopicEntityRenderer(options);
setCustomEntityRenderer(entityRenderer);
}
@@ -22,32 +21,20 @@ public class StereoscopicFrameRenderer extends FrameRenderer {
public BufferedImage captureFrame(Timer timer) {
BufferedImage image = new BufferedImage(getVideoWidth(), getVideoHeight(), BufferedImage.TYPE_INT_RGB);
try {
captureFrame(timer, image, true);
entityRenderer.setEye(true);
renderFrame(timer, image, 0, 0);
} catch (Throwable t) {
CrashReport crash = CrashReport.makeCrashReport(t, "Rendering left eye.");
throw new ReportedException(crash);
}
try {
captureFrame(timer, image, false);
entityRenderer.setEye(false);
renderFrame(timer, image, getVideoWidth() / 2, 0);
} catch (Throwable t) {
CrashReport crash = CrashReport.makeCrashReport(t, "Rendering right eye.");
throw new ReportedException(crash);
}
updateDefaultPreview(image);
return image;
}
protected void captureFrame(Timer timer, BufferedImage into, boolean leftEye) {
int displayWidth = getVideoWidth() / 2;
// Render frame
entityRenderer.setEye(leftEye);
renderFrame(timer);
// Read pixels
readPixels(buffer);
// Copy to image
copyPixelsToImage(buffer, displayWidth, into, leftEye ? 0 : displayWidth, 0);
updateDefaultPreview(into);
}
}

View File

@@ -1,132 +0,0 @@
package eu.crushedpixel.replaymod.video.frame;
import eu.crushedpixel.replaymod.settings.RenderOptions;
import eu.crushedpixel.replaymod.video.entity.TilingEntityRenderer;
import net.minecraft.crash.CrashReport;
import net.minecraft.util.ReportedException;
import net.minecraft.util.Timer;
import java.awt.image.BufferedImage;
import java.util.Arrays;
public class TilingFrameRenderer extends FrameRenderer {
private final TilingEntityRenderer entityRenderer;
private final int tileWidth;
private final int tileHeight;
public TilingFrameRenderer(RenderOptions options, int videoWidth, int videoHeight) {
super(options, videoWidth, videoHeight);
this.entityRenderer = new TilingEntityRenderer(options, this);
this.tileWidth = mc.displayWidth;
this.tileHeight = mc.displayHeight;
setCustomEntityRenderer(entityRenderer);
}
@Override
public boolean setRenderPreviewCallback(Runnable renderPreviewCallback) {
super.setRenderPreviewCallback(renderPreviewCallback);
return true;
}
@Override
public BufferedImage captureFrame(Timer timer) {
BufferedImage image = new BufferedImage(getVideoWidth(), getVideoHeight(), BufferedImage.TYPE_INT_RGB);
int tilesX = getTilesX();
int tilesY = getTilesY();
for (int x = 0; x < tilesX; x++) {
for (int y = 0; y < tilesY; y++) {
try {
captureTile(timer, x, y, image);
} catch (Throwable t) {
CrashReport crash = CrashReport.makeCrashReport(t, "Rendering tile " + x + "/" + y);
throw new ReportedException(crash);
}
getRenderPreviewCallback().run();
}
}
return image;
}
protected void captureTile(Timer timer, int tileX, int tileY, BufferedImage into) {
// Render frame
entityRenderer.setTile(tileX, tileY);
renderFrame(timer);
// Read pixels
readPixels(buffer);
// Copy to image
copyPixelsToImage(buffer, tileWidth, into, tileX * tileWidth, tileY * tileHeight);
if (isPreviewActive()) {
int[] pixels = getPreviewTexture().getTextureData();
copyPixelsToImage(buffer, tileWidth, pixels, tileX * tileWidth, tileY * tileHeight);
// Draw red rectangle around next tile
tileY = (tileY + 1) % getTilesY();
if (tileY == 0) {
tileX = (tileX + 1) % getTilesX();
}
drawPreviewRectangle(pixels, tileX, tileY);
getPreviewTexture().updateDynamicTexture();
}
}
private void drawPreviewRectangle(int[] pixels, int tileX, int tileY) {
final int RED = 0xffff0000;
int videoWidth = getVideoWidth();
int videoHeight = getVideoHeight();
int border = videoWidth / tileWidth * 5;
int left = Math.min(tileX * tileWidth, videoWidth - border - 1);
int right = Math.min((left + tileWidth), videoWidth) - 1;
int top = Math.min(tileY * tileHeight, videoHeight - border - 1);
int bottom = Math.min((top + tileHeight), videoHeight - border) - 1;
for (int i = 0; i < border; i++) {
// Top border
int offset = (top + i) * videoWidth;
Arrays.fill(pixels, offset + left, offset + right + 1, RED);
// Bottom border
offset = (bottom - i) * videoWidth;
Arrays.fill(pixels, offset + left, offset + right + 1, RED);
// Left border
for (int j = top; j <= bottom; j++) {
pixels[j * videoWidth + left + i] = RED;
}
// Right border
for (int j = top; j <= bottom; j++) {
pixels[j * videoWidth + right - i] = RED;
}
}
}
public int getTilesX() {
int videoWidth = getVideoWidth();
if (videoWidth % tileWidth == 0) {
return videoWidth / tileWidth;
} else {
return videoWidth / tileWidth + 1;
}
}
public int getTilesY() {
int videoHeight = getVideoHeight();
if (videoHeight % tileHeight == 0) {
return videoHeight / tileHeight;
} else {
return videoHeight / tileHeight + 1;
}
}
public int getTileWidth() {
return tileWidth;
}
public int getTileHeight() {
return tileHeight;
}
}

View File

@@ -254,9 +254,8 @@ replaymod.gui.rendersettings.renderer.stereoscopic=Stereoscopic Rendering
replaymod.gui.rendersettings.renderer.cubic=Cubic Rendering
replaymod.gui.rendersettings.renderer.equirectangular=Equirectangular Rendering
replaymod.gui.rendersettings.renderer.default.description=Renders the video with your current resolution. Fastest Rendering Option.
replaymod.gui.rendersettings.renderer.tiled.description=Renders the video with a custom resolution, even if it's larger than your screen.
replaymod.gui.rendersettings.renderer.stereoscopic.description=Renders the video as a steroscopic (side-by-side) 3D movie, useable by different 3D technologies.
replaymod.gui.rendersettings.renderer.default.description=Renders the video in the specified resolution. Fastest Rendering Option.
replaymod.gui.rendersettings.renderer.stereoscopic.description=Renders the video as a steroscopic (side-by-side) 3D movie, useable by different 3D technologies. The image for one eye is half the width of the video
replaymod.gui.rendersettings.renderer.cubic.description=Renders the video with a 360 degree panoramic view, using Cubic Projection. This is useable by several 360 degree video players (and the Oculus Rift), for example VR Player.
replaymod.gui.rendersettings.renderer.equirectangular.description=Renders the video with a 360 degree panoramic view, using Equirectangular Projection. This is useable by YouTube's new 360 degree video function, and several video players (and the Oculus Rift), for example VR Player.