Rework rendering
Adds default, stereoscopic, tiling, cubic and equirectangular frame rendering
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
391
src/main/java/eu/crushedpixel/replaymod/video/VideoRenderer.java
Normal file
391
src/main/java/eu/crushedpixel/replaymod/video/VideoRenderer.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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_) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user