Rework rendering

Adds default, stereoscopic, tiling, cubic and equirectangular frame rendering
This commit is contained in:
johni0702
2015-05-23 14:56:11 +02:00
parent 361ba73978
commit 18f22f57d0
30 changed files with 2167 additions and 207 deletions

View File

@@ -1,6 +1,5 @@
package eu.crushedpixel.replaymod.video;
import eu.crushedpixel.replaymod.replay.ReplayProcess;
import net.minecraft.util.Timer;
public class ReplayTimer extends Timer {
@@ -11,7 +10,5 @@ public class ReplayTimer extends Timer {
@Override
public void updateTimer() {
if(ReplayProcess.isVideoRecording()) return;
super.updateTimer();
}
}

View File

@@ -0,0 +1,391 @@
package eu.crushedpixel.replaymod.video;
import eu.crushedpixel.replaymod.ReplayMod;
import eu.crushedpixel.replaymod.entities.CameraEntity;
import eu.crushedpixel.replaymod.gui.GuiVideoRenderer;
import eu.crushedpixel.replaymod.holders.Keyframe;
import eu.crushedpixel.replaymod.holders.Position;
import eu.crushedpixel.replaymod.holders.PositionKeyframe;
import eu.crushedpixel.replaymod.holders.TimeKeyframe;
import eu.crushedpixel.replaymod.interpolation.Interpolation;
import eu.crushedpixel.replaymod.interpolation.LinearPoint;
import eu.crushedpixel.replaymod.interpolation.LinearTimestamp;
import eu.crushedpixel.replaymod.interpolation.SplinePoint;
import eu.crushedpixel.replaymod.renderer.ChunkLoadingRenderGlobal;
import eu.crushedpixel.replaymod.replay.ReplayHandler;
import eu.crushedpixel.replaymod.replay.ReplaySender;
import eu.crushedpixel.replaymod.settings.RenderOptions;
import eu.crushedpixel.replaymod.timer.EnchantmentTimer;
import eu.crushedpixel.replaymod.timer.MCTimerHandler;
import eu.crushedpixel.replaymod.video.frame.FrameRenderer;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.ScaledResolution;
import net.minecraft.util.Timer;
import org.lwjgl.input.Mouse;
import org.lwjgl.opengl.Display;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.concurrent.FutureTask;
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 {
private final Minecraft mc = Minecraft.getMinecraft();
private final FrameRenderer frameRenderer;
private final VideoWriter videoWriter;
private final ReplaySender replaySender;
private final RenderOptions options;
private int fps;
private boolean mouseWasGrabbed;
private Interpolation<Position> movement;
private Interpolation<Integer> time;
private int framesDone;
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.gui = new GuiVideoRenderer(this);
this.replaySender = ReplayMod.replaySender;
this.options = options;
}
/**
* Render this video.
* @return {@code true} if rendering was successful, {@code false} if the user aborted rendering (or the window was closed)
* @throws IOException {@link Minecraft#runTick()} can apparently throw these, don't question it!
*/
public boolean renderVideo() throws IOException {
setup();
Timer timer = MCTimerHandler.getTimer();
replaySender.sendPacketsTill(time.getPoint(0));
// Pre-tick twice, once to process all the packets
tick();
// the second time to prevent interpolation of the player position
tick();
framesDone = 0;
while (framesDone < totalFrames && !cancelled) {
pauseIfNeeded();
if (Display.isActive() && Display.isCloseRequested()) {
mc.shutdown();
return false;
}
updateTime(timer, framesDone);
int elapsedTicks = timer.elapsedTicks;
while (elapsedTicks-- > 0) {
tick();
}
frame(timer);
framesDone++;
if (!frameRendererUpdatesGui) {
drawGui();
}
System.gc();
}
finish();
return !cancelled;
}
private void setup() {
if (Mouse.isGrabbed()) {
mouseWasGrabbed = true;
}
Mouse.setGrabbed(false);
MCTimerHandler.setTimerSpeed(1f);
MCTimerHandler.setPassiveTimer();
fps = options.getFps();
if (options.isLinearMovement()) {
movement = new LinearPoint();
} else {
movement = new SplinePoint();
}
time = new LinearTimestamp();
int duration = 0;
int posKeyframes = 0;
for (Keyframe keyframe : ReplayHandler.getKeyframes()) {
if (keyframe.getRealTimestamp() > duration) {
duration = keyframe.getRealTimestamp();
}
if(keyframe instanceof PositionKeyframe) {
movement.addPoint(((PositionKeyframe) keyframe).getPosition());
posKeyframes++;
}
if (keyframe instanceof TimeKeyframe) {
time.addPoint(((TimeKeyframe) keyframe).getTimestamp());
}
}
totalFrames = duration*fps/1000;
if (posKeyframes >= 2) {
movement.prepare();
} else {
movement = null; // No movement occurring
}
time.prepare();
frameRendererUpdatesGui = frameRenderer.setRenderPreviewCallback(new Runnable() {
@Override
public void run() {
drawGui();
}
});
frameRenderer.setup();
ScaledResolution scaled = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight);
gui.setWorldAndResolution(mc, scaled.getScaledWidth(), scaled.getScaledHeight());
if (options.isWaitForChunks()) {
ChunkLoadingRenderGlobal.install(mc);
}
}
private void finish() {
if (cancelled) {
videoWriter.abortRecording();
} else {
videoWriter.endRecording();
}
try {
videoWriter.waitForFinish();
} catch (InterruptedException ignored) { }
frameRenderer.tearDown();
if (mouseWasGrabbed) {
Mouse.setGrabbed(true);
}
mc.displayGuiScreen(null);
if (mc.renderGlobal instanceof ChunkLoadingRenderGlobal) {
((ChunkLoadingRenderGlobal) mc.renderGlobal).uninstall();
}
}
private void updateCam() {
if (ReplayHandler.getCameraEntity() == null) {
if (mc.theWorld == null) {
return; // World hasn't been sent yet
}
ReplayHandler.setCameraEntity(new CameraEntity(mc.theWorld));
}
int videoTime = framesDone * 1000 / fps;
int posCount = ReplayHandler.getPosKeyframeCount();
Position pos;
PositionKeyframe lastPos = ReplayHandler.getPreviousPositionKeyframe(videoTime);
if (movement == null || lastPos == null) {
// Stay at one position, no movement
pos = ReplayHandler.getNextPositionKeyframe(-1).getPosition();
} else {
// Position interpolation
PositionKeyframe nextPos = ReplayHandler.getNextPositionKeyframe(videoTime);
int lastPosStamp = lastPos.getRealTimestamp();
int nextPosStamp = (nextPos == null ? lastPos : nextPos).getRealTimestamp();
int diffLength = nextPosStamp - lastPosStamp;
float diffPct = (float) (videoTime - lastPosStamp) / diffLength;
if(Float.isInfinite(diffPct) || Float.isNaN(diffPct)) diffPct = 0;
float totalPct = (ReplayHandler.getKeyframeIndex(lastPos) + diffPct) / (posCount-1);
pos = movement.getPoint(Math.max(0, Math.min(1, totalPct)));
}
ReplayHandler.setCameraTilt(pos.getRoll());
ReplayHandler.getCameraEntity().movePath(pos);
// Make sure we're spectating the camera entity
// We do not use .spectateCamera() as that method sets the position of the camera to the previous
// entity, overriding our calculations
ReplayHandler.spectateEntity(ReplayHandler.getCameraEntity());
}
private void updateTime(Timer timer, int framesDone) {
int videoTime = framesDone * 1000 / fps;
int timeCount = ReplayHandler.getTimeKeyframeCount();
// WARNING: The rest of this method contains some magic for which Marius is responsible
// Time interpolation
TimeKeyframe lastTime = ReplayHandler.getPreviousTimeKeyframe(videoTime);
TimeKeyframe nextTime = ReplayHandler.getNextTimeKeyframe(videoTime);
int lastTimeStamp = 0;
int nextTimeStamp = 0;
double curSpeed = 0;
if(nextTime != null || lastTime != null) {
if(nextTime != null && lastTime != null && nextTime.getRealTimestamp() == lastTime.getRealTimestamp()) {
curSpeed = 0;
} else {
if(nextTime != null) {
nextTimeStamp = nextTime.getRealTimestamp();
} else {
nextTimeStamp = lastTime.getRealTimestamp();
}
if(lastTime != null) {
lastTimeStamp = lastTime.getRealTimestamp();
} else {
lastTimeStamp = nextTime.getRealTimestamp();
}
if(!(nextTime == null || lastTime == null)) {
if(lastTimeStamp == nextTimeStamp) curSpeed = 0f;
else curSpeed = ((double)((nextTime.getTimestamp()-lastTime.getTimestamp())))/((double)((nextTimeStamp-lastTimeStamp)));
}
if(lastTimeStamp == nextTimeStamp) {
curSpeed = 0f;
}
}
}
int currentTimeDiff = nextTimeStamp - lastTimeStamp;
int currentTime = videoTime - lastTimeStamp;
float currentTimeStepPerc = (float)currentTime/(float)currentTimeDiff; //The percentage of the travelled path between the current timestamps
if(Float.isInfinite(currentTimeStepPerc) || Float.isNaN(currentTimeStepPerc)) currentTimeStepPerc = 0;
float timePos = (ReplayHandler.getKeyframeIndex(lastTime) + currentTimeStepPerc) / (timeCount-1f);
Integer replayTime = time.getPoint(Math.max(0, Math.min(1, timePos)));
if(replayTime != null) {
replaySender.sendPacketsTill(replayTime);
}
if(curSpeed > 0) {
replaySender.setReplaySpeed(curSpeed);
}
// Update Timer
EnchantmentTimer.increaseRecordingTime(1000 / fps);
timer.elapsedPartialTicks += timer.timerSpeed * 20 / fps;
timer.elapsedTicks = (int) timer.elapsedPartialTicks;
timer.elapsedPartialTicks -= timer.elapsedTicks;
timer.renderPartialTicks = timer.elapsedPartialTicks;
}
private void tick() throws IOException {
synchronized (mc.scheduledTasks) {
while (!mc.scheduledTasks.isEmpty()) {
((FutureTask) mc.scheduledTasks.poll()).run();
}
}
mc.currentScreen = gui;
mc.runTick();
}
private void frame(Timer timer) {
updateCam();
BufferedImage frame = null;
while (frame == null) {
try {
frame = frameRenderer.captureFrame(timer);
} catch (OutOfMemoryError e) {
System.out.println("Caught oom error-> calling garbage collector, decreasing queue size and waiting for video writer");
System.gc();
videoWriter.waitTillQueueEmpty();
}
}
while (!videoWriter.writeImage(frame, false)) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
return;
}
drawGui();
}
}
private void pauseIfNeeded() {
while (paused && !cancelled && !Display.isCloseRequested()) {
drawGui();
try {
Thread.sleep(20);
} catch (InterruptedException e) {
return;
}
}
}
public void drawGui() {
pushMatrix();
clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
enableTexture2D();
mc.getFramebuffer().bindFramebuffer(true);
mc.entityRenderer.setupOverlayRendering();
try {
gui.handleInput();
} catch (IOException e) {
// That's a strange exception from this kind of method O_o
// It isn't actually thrown here, so we'll deal with it the easy way
throw new RuntimeException(e);
}
ScaledResolution scaled = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight);
int mouseX = Mouse.getX() * scaled.getScaledWidth() / mc.displayWidth;
int mouseY = scaled.getScaledHeight() - Mouse.getY() * scaled.getScaledHeight() / mc.displayHeight - 1;
gui.drawScreen(mouseX, mouseY, 0);
mc.getFramebuffer().unbindFramebuffer();
popMatrix();
pushMatrix();
mc.getFramebuffer().framebufferRender(mc.displayWidth, mc.displayHeight);
popMatrix();
Display.update();
if (Mouse.isGrabbed()) {
Mouse.setGrabbed(false);
}
}
public int getFramesDone() {
return framesDone;
}
public int getTotalFrames() {
return totalFrames;
}
public FrameRenderer getFrameRenderer() {
return frameRenderer;
}
public void setPaused(boolean paused) {
this.paused = paused;
}
public boolean isPaused() {
return paused;
}
public void cancel() {
this.cancelled = true;
}
}

View File

@@ -1,6 +1,6 @@
package eu.crushedpixel.replaymod.video;
import eu.crushedpixel.replaymod.ReplayMod;
import com.google.common.base.Preconditions;
import eu.crushedpixel.replaymod.utils.ReplayFileIO;
import org.monte.media.*;
import org.monte.media.FormatKeys.MediaType;
@@ -9,132 +9,170 @@ import org.monte.media.math.Rational;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class VideoWriter {
private static final String DATE_FORMAT = "yyyy_MM_dd_HH_mm_ss";
private static final SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT);
private static final SimpleDateFormat FILE_FORMAT = new SimpleDateFormat(DATE_FORMAT);
private static final String VIDEO_EXTENSION = ".avi";
private static MovieWriter out;
private static File file;
private static boolean isRecording = false;
private static boolean requestFinish = false;
private static boolean abort = false;
private final File file;
private final MovieWriter out;
private final Buffer buf;
private final int track;
private static Buffer buf;
private static int track;
private static Queue<BufferedImage> toWrite = new LinkedBlockingQueue<BufferedImage>();
private volatile boolean active = true;
private volatile boolean cancelled = false;
public static boolean isRecording() {
return isRecording;
private int queueLimit;
private final Queue<BufferedImage> toWrite;
private final Thread writerThread;
private final Lock lock = new ReentrantLock();
private final Condition emptyCondition = lock.newCondition();
private final Condition noLongerEmptyCondition = lock.newCondition();
private final Condition noLongerFullCondition = lock.newCondition();
public VideoWriter(int width, int height, int fps, float quality) throws IOException {
this(width, height, fps, quality, Integer.MAX_VALUE);
}
public static void startRecording(int width, int height) {
if(isRecording) {
IllegalStateException up = new IllegalStateException("VideoWriter is already recording!");
throw up; //lolololo
}
isRecording = true;
public VideoWriter(int width, int height, int fps, float quality, int queueLimit) throws IOException {
this.queueLimit = queueLimit;
this.toWrite = new LinkedList<BufferedImage>();
toWrite = new LinkedBlockingQueue<BufferedImage>();
File folder = ReplayFileIO.getRenderFolder();
String fileName = FILE_FORMAT.format(Calendar.getInstance().getTime());
file = new File(folder, fileName + VIDEO_EXTENSION);
Files.createFile(file.toPath());
try {
File folder = ReplayFileIO.getRenderFolder();
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,
VideoFormatKeys.DepthKey, 24,
VideoFormatKeys.QualityKey, quality);
String fileName = sdf.format(Calendar.getInstance().getTime());
track = out.addTrack(format);
file = new File(folder, fileName + VIDEO_EXTENSION);
file.createNewFile();
buf = new Buffer();
buf.format = new Format(VideoFormatKeys.DataClassKey, BufferedImage.class);
buf.sampleDuration = out.getFormat(track).get(VideoFormatKeys.FrameRateKey).inverse();
out = Registry.getInstance().getWriter(file);
Format format = new Format(FormatKeys.MediaTypeKey, MediaType.VIDEO,
FormatKeys.EncodingKey, VideoFormatKeys.ENCODING_AVI_MJPG,
FormatKeys.FrameRateKey, new Rational(ReplayMod.replaySettings.getVideoFramerate(), 1),
VideoFormatKeys.WidthKey, width,
VideoFormatKeys.HeightKey, height,
VideoFormatKeys.DepthKey, 24,
VideoFormatKeys.QualityKey, (float) ReplayMod.replaySettings.getVideoQuality());
track = out.addTrack(format);
buf = new Buffer();
buf.format = new Format(VideoFormatKeys.DataClassKey, BufferedImage.class);
buf.sampleDuration = out.getFormat(track).get(VideoFormatKeys.FrameRateKey).inverse();
} catch(IOException e) {
e.printStackTrace();
}
Thread t = new Thread(new Runnable() {
writerThread = new Thread(new Runnable() {
@Override
public void run() {
while(true) {
if(toWrite.isEmpty() || abort) {
if(requestFinish) {
requestFinish = false;
isRecording = false;
try {
out.close();
if(abort) {
file.delete();
}
} catch(IOException e) {
e.printStackTrace();
}
abort = false;
toWrite = new LinkedBlockingQueue<BufferedImage>();
return;
}
while(!cancelled && (active || !toWrite.isEmpty())) {
try {
lock.lockInterruptibly();
BufferedImage img;
try {
Thread.sleep(10);
} catch(Exception e) {
img = toWrite.poll();
if (img == null) {
noLongerEmptyCondition.await();
img = toWrite.poll();
}
noLongerFullCondition.signal();
if (toWrite.isEmpty()) {
emptyCondition.signalAll();
}
} finally {
lock.unlock();
}
buf.data = img;
try {
out.write(track, buf);
} catch (IOException e) {
e.printStackTrace();
}
} else {
write();
} catch (InterruptedException ignored) {
}
}
try {
toWrite.clear();
out.close();
if (cancelled) {
Files.delete(file.toPath());
}
} catch (IOException e) {
e.printStackTrace();
}
}
});
t.start();
writerThread.start();
}
public static void writeImage(BufferedImage image) {
if(requestFinish || !isRecording) {
IllegalStateException up = new IllegalStateException(
"The VideoWriter is currently not available. Please try again later.");
throw up; //lolololo^2
}
toWrite.add(image);
public void setQueueLimit(int limit) {
this.queueLimit = limit;
}
public static void endRecording() {
if(!isRecording) return;
requestFinish = true;
}
/**
* Add the image to the writer queue.
* @param image The image
* @param waitIfFull Whether to wait if the queue is full or to return immediately
* @return {@code} true if the image was added, {@code false} if it could not be added due to the queue being full
* and {@code waitIfFull} being {@code false}
*/
public boolean writeImage(BufferedImage image, boolean waitIfFull) {
Preconditions.checkState(active, "This VideoWriter has already been closed.");
private static void write() {
lock.lock();
try {
BufferedImage img = toWrite.poll();
if(img != null) {
buf.data = img;
out.write(track, buf);
while (toWrite.size() >= queueLimit) {
if (waitIfFull) {
noLongerFullCondition.await();
} else {
return false;
}
}
} catch(IOException e) {
toWrite.offer(image);
noLongerEmptyCondition.signal();
return true;
} catch (InterruptedException ignored) {
} finally {
lock.unlock();
}
return false;
}
public void endRecording() {
active = false;
writerThread.interrupt();
}
public void abortRecording() {
cancelled = true;
active = false;
writerThread.interrupt();
}
public void waitTillQueueEmpty() {
lock.lock();
try {
if (!toWrite.isEmpty()) {
emptyCondition.await();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void abortRecording() {
requestFinish = true;
abort = true;
public void waitForFinish() throws InterruptedException {
Preconditions.checkState(!active, "Video writer still active.");
writerThread.join();
}
}

View File

@@ -0,0 +1,109 @@
package eu.crushedpixel.replaymod.video.entity;
import eu.crushedpixel.replaymod.replay.ReplayHandler;
import net.minecraft.client.renderer.GlStateManager;
import net.minecraft.entity.Entity;
public class CubicEntityRenderer extends CustomEntityRenderer {
public static enum Direction {
TOP(1), BOTTOM(9), LEFT(4), FRONT(5), RIGHT(6), BACK(7);
private final int frame;
Direction(int frame) {
this.frame = frame;
}
public static Direction forFrame(int frame) {
for (Direction d : values()) {
if (d.frame == frame) {
return d;
}
}
return null;
}
public int getCubicFrame() {
return frame;
}
}
private final boolean stable;
private Direction direction;
public CubicEntityRenderer(boolean stable) {
this.stable = stable;
}
public void setFrame(int id) {
setDirection(Direction.forFrame(id));
}
public void setDirection(Direction direction) {
this.direction = direction;
}
@Override
protected void gluPerspective(float fovY, float aspect, float zNear, float zFar) {
super.gluPerspective(90, 1, zNear, zFar);
}
@Override
protected void setupCameraTransform(float partialTicks) {
Entity entity = mc.getRenderViewEntity();
float orgYaw = entity.rotationYaw;
float orgPitch = entity.rotationPitch;
float orgPrevYaw = entity.prevRotationYaw;
float orgPrevPitch = entity.prevRotationPitch;
float orgRoll = ReplayHandler.getCameraTilt();
super.setupCameraTransform(partialTicks);
entity.rotationYaw = orgYaw;
entity.rotationPitch = orgPitch;
entity.prevRotationYaw = orgPrevYaw;
entity.prevRotationPitch = orgPrevPitch;
if (stable) {
ReplayHandler.setCameraTilt(orgRoll);
}
}
@Override
protected void orientCamera(float partialTicks) {
Entity entity = mc.getRenderViewEntity();
// Rotate
switch(direction) {
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;
}
if (stable) {
// Stop the minecraft code from doing any rotation
entity.prevRotationPitch = entity.rotationPitch = 0;
entity.prevRotationYaw = entity.rotationYaw = 0;
ReplayHandler.setCameraTilt(0);
}
// Minecraft goes back a little so we have to invert that
GlStateManager.translate(0.0F, 0.0F, 0.1F);
super.orientCamera(partialTicks);
}
}

View File

@@ -0,0 +1,225 @@
package eu.crushedpixel.replaymod.video.entity;
import net.minecraft.client.Minecraft;
import net.minecraft.client.particle.EffectRenderer;
import net.minecraft.client.renderer.*;
import net.minecraft.client.renderer.culling.ClippingHelperImpl;
import net.minecraft.client.renderer.culling.ICamera;
import net.minecraft.client.renderer.texture.TextureMap;
import net.minecraft.entity.Entity;
import net.minecraft.util.AxisAlignedBB;
import net.minecraft.util.EnumWorldBlockLayer;
import net.minecraft.util.MathHelper;
import org.lwjgl.util.glu.Project;
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;
protected int frameCount;
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);
}
}
}
protected void renderWorld(float partialTicks, long finishTimeNano) {
proxied.updateLightmap(partialTicks);
enableDepth();
enableAlpha();
alphaFunc(516, 0.5F);
renderWorldPass(partialTicks, finishTimeNano, 2);
}
protected void renderWorldPass(float partialTicks, long finishTimeNano, int renderPass) {
RenderGlobal renderglobal = mc.renderGlobal;
EffectRenderer effectrenderer = mc.effectRenderer;
Entity entity = mc.getRenderViewEntity();
enableCull();
viewport(0, 0, mc.displayWidth, mc.displayHeight);
proxied.updateFogColor(partialTicks);
clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
setupCameraTransform(partialTicks);
ActiveRenderInfo.updateRenderInfo(mc.thePlayer, mc.gameSettings.thirdPersonView == 2);
ClippingHelperImpl.getInstance();
ICamera camera = new NoCullingCamera();
if (this.mc.gameSettings.renderDistanceChunks >= 4) {
proxied.setupFog(-1, partialTicks);
matrixMode(GL_PROJECTION);
loadIdentity();
gluPerspective(proxied.getFOVModifier(partialTicks, true), (float) mc.displayWidth / mc.displayHeight, 0.05F, proxied.farPlaneDistance * 2.0F);
matrixMode(GL_MODELVIEW);
renderglobal.renderSky(partialTicks, renderPass);
matrixMode(GL_PROJECTION);
loadIdentity();
gluPerspective(proxied.getFOVModifier(partialTicks, true), (float) mc.displayWidth / mc.displayHeight, 0.05F, proxied.farPlaneDistance * MathHelper.SQRT_2);
matrixMode(GL_MODELVIEW);
}
proxied.setupFog(0, partialTicks);
shadeModel(GL_SMOOTH);
if (entity.posY + entity.getEyeHeight() < 128) {
renderCloudsCheck(renderglobal, partialTicks, renderPass);
}
proxied.setupFog(0, partialTicks);
mc.getTextureManager().bindTexture(TextureMap.locationBlocksTexture);
RenderHelper.disableStandardItemLighting();
renderglobal.setupTerrain(entity, partialTicks, camera, frameCount++, mc.thePlayer.isSpectator());
renderglobal.updateChunks(finishTimeNano);
matrixMode(GL_MODELVIEW);
pushMatrix();
disableAlpha();
renderglobal.renderBlockLayer(EnumWorldBlockLayer.SOLID, partialTicks, renderPass, entity);
enableAlpha();
renderglobal.renderBlockLayer(EnumWorldBlockLayer.CUTOUT_MIPPED, partialTicks, renderPass, entity);
mc.getTextureManager().getTexture(TextureMap.locationBlocksTexture).setBlurMipmap(false, false);
renderglobal.renderBlockLayer(EnumWorldBlockLayer.CUTOUT, partialTicks, renderPass, entity);
mc.getTextureManager().getTexture(TextureMap.locationBlocksTexture).restoreLastBlurMipmap();
shadeModel(GL_FLAT);
alphaFunc(516, 0.1F);
matrixMode(GL_MODELVIEW);
popMatrix();
pushMatrix();
RenderHelper.enableStandardItemLighting();
net.minecraftforge.client.ForgeHooksClient.setRenderPass(0);
renderglobal.renderEntities(entity, camera, partialTicks);
net.minecraftforge.client.ForgeHooksClient.setRenderPass(0);
RenderHelper.disableStandardItemLighting();
proxied.disableLightmap();
matrixMode(GL_MODELVIEW);
popMatrix();
enableBlend();
tryBlendFuncSeparate(770, 1, 1, 0);
renderglobal.drawBlockDamageTexture(Tessellator.getInstance(), Tessellator.getInstance().getWorldRenderer(), entity, partialTicks);
disableBlend();
proxied.enableLightmap();
effectrenderer.renderLitParticles(entity, partialTicks);
RenderHelper.disableStandardItemLighting();
proxied.setupFog(0, partialTicks);
effectrenderer.renderParticles(entity, partialTicks);
proxied.disableLightmap();
depthMask(false);
enableCull();
proxied.renderRainSnow(partialTicks);
depthMask(true);
renderglobal.renderWorldBorder(entity, partialTicks);
disableBlend();
enableCull();
tryBlendFuncSeparate(770, 771, 1, 0);
alphaFunc(516, 0.1F);
proxied.setupFog(0, partialTicks);
enableBlend();
depthMask(false);
mc.getTextureManager().bindTexture(TextureMap.locationBlocksTexture);
shadeModel(GL_SMOOTH);
if (mc.gameSettings.fancyGraphics) {
enableBlend();
tryBlendFuncSeparate(770, 771, 1, 0);
renderglobal.renderBlockLayer(EnumWorldBlockLayer.TRANSLUCENT, partialTicks, renderPass, entity);
disableBlend();
} else {
renderglobal.renderBlockLayer(EnumWorldBlockLayer.TRANSLUCENT, partialTicks, renderPass, entity);
}
RenderHelper.enableStandardItemLighting();
net.minecraftforge.client.ForgeHooksClient.setRenderPass(1);
renderglobal.renderEntities(entity, camera, partialTicks);
net.minecraftforge.client.ForgeHooksClient.setRenderPass(-1);
RenderHelper.disableStandardItemLighting();
shadeModel(GL_FLAT);
depthMask(true);
enableCull();
disableBlend();
disableFog();
if (entity.posY + entity.getEyeHeight() >= 128) {
renderCloudsCheck(renderglobal, partialTicks, renderPass);
}
net.minecraftforge.client.ForgeHooksClient.dispatchRenderLast(renderglobal, partialTicks);
}
protected void setupCameraTransform(float partialTicks) {
proxied.farPlaneDistance = (float)(this.mc.gameSettings.renderDistanceChunks * 16);
matrixMode(GL_PROJECTION);
loadIdentity();
gluPerspective(proxied.getFOVModifier(partialTicks, true), (float) this.mc.displayWidth / (float) this.mc.displayHeight, 0.05F, proxied.farPlaneDistance * MathHelper.SQRT_2);
matrixMode(GL_MODELVIEW);
loadIdentity();
orientCamera(partialTicks);
}
protected void orientCamera(float partialTicks) {
proxied.orientCamera(partialTicks);
}
protected void renderCloudsCheck(RenderGlobal renderglobal, float partialTicks, int renderPass) {
if (this.mc.gameSettings.shouldRenderClouds()) {
matrixMode(GL_PROJECTION);
loadIdentity();
gluPerspective(proxied.getFOVModifier(partialTicks, true), (float) this.mc.displayWidth / (float) this.mc.displayHeight, 0.05F, proxied.farPlaneDistance * 4.0F);
matrixMode(GL_MODELVIEW);
pushMatrix();
proxied.setupFog(0, partialTicks);
renderglobal.renderClouds(partialTicks, renderPass);
disableFog();
popMatrix();
matrixMode(GL_PROJECTION);
loadIdentity();
gluPerspective(proxied.getFOVModifier(partialTicks, true), (float)this.mc.displayWidth / (float)this.mc.displayHeight, 0.05F, proxied.farPlaneDistance * MathHelper.SQRT_2);
matrixMode(GL_MODELVIEW);
}
}
public static final class NoCullingCamera implements ICamera {
@Override
public boolean isBoundingBoxInFrustum(AxisAlignedBB p_78546_1_) {
return true;
}
@Override
public void setPosition(double p_78547_1_, double p_78547_3_, double p_78547_5_) {
}
}
}

View File

@@ -0,0 +1,39 @@
package eu.crushedpixel.replaymod.video.entity;
import net.minecraft.client.renderer.GlStateManager;
import net.minecraft.util.MathHelper;
import static net.minecraft.client.renderer.GlStateManager.loadIdentity;
import static net.minecraft.client.renderer.GlStateManager.matrixMode;
import static org.lwjgl.opengl.GL11.GL_MODELVIEW;
import static org.lwjgl.opengl.GL11.GL_PROJECTION;
public class StereoscopicEntityRenderer extends CustomEntityRenderer {
private boolean leftEye;
public void setEye(boolean leftEye) {
this.leftEye = leftEye;
}
protected void translateStereoscopic() {
GlStateManager.translate(leftEye ? 0.07 : -0.07, 0, 0);
}
@Override
protected void setupCameraTransform(float partialTicks) {
proxied.farPlaneDistance = (float)(this.mc.gameSettings.renderDistanceChunks * 16);
matrixMode(GL_PROJECTION);
loadIdentity();
translateStereoscopic();
gluPerspective(proxied.getFOVModifier(partialTicks, true), (float) this.mc.displayWidth / (float) this.mc.displayHeight, 0.05F, proxied.farPlaneDistance * MathHelper.SQRT_2);
matrixMode(GL_MODELVIEW);
loadIdentity();
translateStereoscopic();
orientCamera(partialTicks);
}
}

View File

@@ -0,0 +1,41 @@
package eu.crushedpixel.replaymod.video.entity;
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(TilingFrameRenderer renderer) {
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,79 @@
package eu.crushedpixel.replaymod.video.frame;
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;
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 CubicEntityRenderer entityRenderer;
protected final int displaySize;
protected CubicFrameRenderer(int videoWidth, int videoHeight, boolean stable) {
super(videoWidth, videoHeight, getDisplaySize() * getDisplaySize() * 3);
this.displaySize = getDisplaySize();
this.entityRenderer = new CubicEntityRenderer(stable);
setCustomEntityRenderer(entityRenderer);
}
public CubicFrameRenderer(boolean stable) {
this(getDisplaySize() * 4, getDisplaySize() * 3, stable);
}
@Override
public BufferedImage captureFrame(Timer timer) {
BufferedImage image = new BufferedImage(getVideoWidth(), getVideoHeight(), BufferedImage.TYPE_INT_RGB);
for (CubicEntityRenderer.Direction direction : CubicEntityRenderer.Direction.values()) {
try {
captureFrame(timer, image, direction);
} catch (Throwable t) {
CrashReport crash = CrashReport.makeCrashReport(t, "Rendering frame " + direction + ".");
throw new ReportedException(crash);
}
}
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

@@ -0,0 +1,27 @@
package eu.crushedpixel.replaymod.video.frame;
import net.minecraft.client.Minecraft;
import net.minecraft.util.Timer;
import java.awt.image.BufferedImage;
public class DefaultFrameRenderer extends FrameRenderer {
public DefaultFrameRenderer() {
super(Minecraft.getMinecraft().displayWidth, Minecraft.getMinecraft().displayHeight);
}
@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);
return image;
}
}

View File

@@ -0,0 +1,101 @@
package eu.crushedpixel.replaymod.video.frame;
import eu.crushedpixel.replaymod.video.entity.CubicEntityRenderer;
import net.minecraft.crash.CrashReport;
import net.minecraft.util.ReportedException;
import net.minecraft.util.Timer;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import static java.lang.Math.PI;
public class EquirectangularFrameRenderer extends CubicFrameRenderer {
public EquirectangularFrameRenderer(boolean stable) {
super(getDisplaySize() * 4, getDisplaySize() * 2, stable);
}
@Override
public BufferedImage captureFrame(Timer timer) {
BufferedImage top = captureFrame(timer, CubicEntityRenderer.Direction.TOP);
BufferedImage bottom = captureFrame(timer, CubicEntityRenderer.Direction.BOTTOM);
BufferedImage front = captureFrame(timer, CubicEntityRenderer.Direction.FRONT);
BufferedImage back = captureFrame(timer, CubicEntityRenderer.Direction.BACK);
BufferedImage left = captureFrame(timer, CubicEntityRenderer.Direction.LEFT);
BufferedImage right = captureFrame(timer, CubicEntityRenderer.Direction.RIGHT);
try {
BufferedImage result = cubicToEquirectangular(top, bottom, front, back, left, right);
updateDefaultPreview(result);
return result;
} catch (Throwable t) {
CrashReport crash = CrashReport.makeCrashReport(t, "Transforming cubic to equirectangular image.");
throw new ReportedException(crash);
}
}
protected BufferedImage cubicToEquirectangular(BufferedImage top, BufferedImage bottom, BufferedImage front, BufferedImage back, BufferedImage left, BufferedImage right) {
int rWidth = getVideoWidth();
int rHeight = getVideoHeight();
BufferedImage result = new BufferedImage(rWidth, rHeight, BufferedImage.TYPE_INT_RGB);
int[] resultPixels = ((DataBufferInt) result.getRaster().getDataBuffer()).getData();
for (int i = 0; i < rWidth; i++) {
double yaw = PI * 2 * i / rWidth;
int piQuarter = 8 * i / rWidth - 4;
BufferedImage target;
if (piQuarter < -3) {
target = back;
} else if (piQuarter < -1) {
target = left;
} else if (piQuarter < 1) {
target = front;
} else if (piQuarter < 3) {
target = right;
} else {
target = back;
}
double fYaw = (yaw + PI/4) % (PI / 2) - PI/4;
double d = 1 / Math.cos(fYaw);
double gcXN = (Math.tan(fYaw) + 1) / 2;
for (int j = 0; j < rHeight; j++) {
double cXN = gcXN;
BufferedImage pt = target;
double pitch = PI * j / rHeight - PI / 2;
double cYN = (Math.tan(pitch) * d + 1) / 2;
if (cYN >= 1) {
double pd = Math.tan(PI/2 - pitch);
cXN = (-Math.sin(yaw) * pd + 1) / 2;
cYN = (Math.cos(yaw) * pd + 1) / 2;
pt = 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 = top;
}
int cX = Math.min(displaySize - 1, (int) (cXN * displaySize));
int cY = Math.min(displaySize - 1, (int) (cYN * displaySize));
resultPixels[i + j * rWidth] = 0xff000000 | pt.getRGB(cX, cY); // Make sure we got alpha value as well
}
}
return result;
}
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);
return image;
} catch (Throwable t) {
CrashReport crash = CrashReport.makeCrashReport(t, "Rendering frame " + direction);
throw new ReportedException(crash);
}
}
}

View File

@@ -0,0 +1,225 @@
package eu.crushedpixel.replaymod.video.frame;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.util.concurrent.Runnables;
import eu.crushedpixel.replaymod.gui.GuiVideoRenderer;
import eu.crushedpixel.replaymod.video.entity.CustomEntityRenderer;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.Gui;
import net.minecraft.client.gui.GuiScreen;
import net.minecraft.client.renderer.texture.DynamicTexture;
import net.minecraft.client.renderer.texture.TextureUtil;
import net.minecraft.util.ResourceLocation;
import net.minecraft.util.Timer;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import java.nio.ByteBuffer;
import java.util.Arrays;
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 FrameRenderer {
private static final ResourceLocation noPreviewTexture = new ResourceLocation("replaymod", "logo.jpg");
protected final Minecraft mc = Minecraft.getMinecraft();
private final int videoWidth;
private final int videoHeight;
private Optional<CustomEntityRenderer> customEntityRenderer;
private DynamicTexture previewTexture;
private Runnable renderPreviewCallback;
private boolean previewActive = false;
protected final ByteBuffer buffer;
public FrameRenderer(int videoWidth, int videoHeight) {
this.videoWidth = videoWidth;
this.videoHeight = videoHeight;
this.buffer = BufferUtils.createByteBuffer(mc.displayWidth * mc.displayHeight * 3);
this.customEntityRenderer = Optional.absent();
}
public FrameRenderer(int videoWidth, int videoHeight, int bufferSize) {
this.videoWidth = videoWidth;
this.videoHeight = videoHeight;
this.buffer = BufferUtils.createByteBuffer(bufferSize);
this.customEntityRenderer = Optional.absent();
}
public final int getVideoWidth() {
return videoWidth;
}
public final int getVideoHeight() {
return videoHeight;
}
public void setPreviewActive(boolean previewActive) {
this.previewActive = previewActive;
if (previewActive) {
Arrays.fill(previewTexture.getTextureData(), 0xff000000);
previewTexture.updateDynamicTexture();
}
}
public boolean isPreviewActive() {
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 = Optional.fromNullable(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();
updateDefaultPreview(pixels);
}
}
protected void updateDefaultPreview(int[] pixels) {
if (isPreviewActive()) {
System.arraycopy(pixels, 0, getPreviewTexture().getTextureData(), 0, pixels.length);
getPreviewTexture().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();
if (customEntityRenderer.isPresent()) {
customEntityRenderer.get().updateCameraAndRender(timer.renderPartialTicks);
} else {
mc.entityRenderer.updateCameraAndRender(timer.renderPartialTicks);
}
mc.getFramebuffer().unbindFramebuffer();
popMatrix();
pushMatrix();
mc.getFramebuffer().framebufferRender(mc.displayWidth, mc.displayHeight);
popMatrix();
}
public void setup() {
this.previewTexture = new DynamicTexture(videoWidth, videoHeight) {
@Override
public void updateDynamicTexture() {
bindTexture(getGlTextureId());
TextureUtil.uploadTextureSub(0, getTextureData(), getVideoWidth(), getVideoHeight(), 0, 0, true, false, false);
}
};
}
public void tearDown() {
this.previewTexture.deleteGlTexture();
}
public DynamicTexture getPreviewTexture() {
return previewTexture;
}
/**
* Return the height including any borders of the preview.
* No rendering should be performed by {@link #renderPreview(GuiVideoRenderer)} below this height.
* @param guiScreen The gui screen
* @return Bottom of the preview
*/
public int getPreviewHeight(GuiScreen guiScreen) {
int width = guiScreen.width / 2;
int height = guiScreen.height / 3;
if (width / height < getVideoWidth() / getVideoHeight()) {
height = width * getVideoHeight() / getVideoWidth();
}
return guiScreen.height / 2 + height + 10;
}
/**
* Draw a preview of the current scene on the lower half of the screen.
* @param guiScreen The gui screen
*/
public void renderPreview(GuiVideoRenderer guiScreen) {
int width = guiScreen.width / 2;
int height = guiScreen.height / 3;
if (width / height > getVideoWidth() / getVideoHeight()) {
width = height * getVideoWidth() / getVideoHeight();
} else {
height = width * getVideoHeight() / getVideoWidth();
}
int x = guiScreen.width / 2 - width / 2;
int y = guiScreen.height / 2 + 5;
bindTexture(previewTexture.getGlTextureId());
color(1, 1, 1, 1);
Gui.drawScaledCustomSizeModalRect(x, y, 0, 0, videoWidth, videoHeight, width, height, videoWidth, videoHeight);
}
public static void renderNoPreview(int guiWidth, int guiHeight, int height) {
int width = height * 1280 / 720;
int x = guiWidth / 2 - width / 2;
int y = guiHeight / 2 + 5;
Minecraft.getMinecraft().getTextureManager().bindTexture(noPreviewTexture);
color(1, 1, 1, 1);
Gui.drawScaledCustomSizeModalRect(x, y, 0, 0, 1280, 720, width, height, 1280, 720);
}
}

View File

@@ -0,0 +1,51 @@
package eu.crushedpixel.replaymod.video.frame;
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;
import java.awt.image.BufferedImage;
public class StereoscopicFrameRenderer extends FrameRenderer {
private final StereoscopicEntityRenderer entityRenderer = new StereoscopicEntityRenderer();
public StereoscopicFrameRenderer() {
super(Minecraft.getMinecraft().displayWidth * 2, Minecraft.getMinecraft().displayHeight);
setCustomEntityRenderer(entityRenderer);
}
@Override
public BufferedImage captureFrame(Timer timer) {
BufferedImage image = new BufferedImage(getVideoWidth(), getVideoHeight(), BufferedImage.TYPE_INT_RGB);
try {
captureFrame(timer, image, true);
} catch (Throwable t) {
CrashReport crash = CrashReport.makeCrashReport(t, "Rendering left eye.");
throw new ReportedException(crash);
}
try {
captureFrame(timer, image, false);
} catch (Throwable t) {
CrashReport crash = CrashReport.makeCrashReport(t, "Rendering right eye.");
throw new ReportedException(crash);
}
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

@@ -0,0 +1,130 @@
package eu.crushedpixel.replaymod.video.frame;
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 = new TilingEntityRenderer(this);
private final int tileWidth;
private final int tileHeight;
public TilingFrameRenderer(int videoWidth, int videoHeight) {
super(videoWidth, videoHeight);
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;
}
}