Implement ffmpeg-independent PNGWriter (closes #476)
Creates a dedicated folder for each video and does not depend on ffmpeg, just like the EXRWriter (but supporting all MC versions). Also does 32 bit depth export by chunking the 32-bit float into the 8-bit BGRA component channels.
This commit is contained in:
@@ -358,7 +358,8 @@ There are **7 Encoding Presets** you can choose from:
|
|||||||
While these video files are of perfect quality, most **non-FFmpeg-based video players and video editing software** (e.g. QuickTime Player, Sony Vegas and Adobe Premiere) can't play these videos. Therefore, you should instead use the **MP4 - High Quality** preset in most cases.
|
While these video files are of perfect quality, most **non-FFmpeg-based video players and video editing software** (e.g. QuickTime Player, Sony Vegas and Adobe Premiere) can't play these videos. Therefore, you should instead use the **MP4 - High Quality** preset in most cases.
|
||||||
- **PNG Sequence**
|
- **PNG Sequence**
|
||||||
Exports the sequence as individual frames in the **PNG Format**.
|
Exports the sequence as individual frames in the **PNG Format**.
|
||||||
**Warning:** This can create a huge amount of files, so make sure to save them in a separate folder.
|
The images are of perfect quality.
|
||||||
|
Depth map images are 32-bit float encoded in the 8-bit components in BGRA order and will need some kind of post-processing to be useful in most editors.
|
||||||
- **OpenEXR Sequence** (Minecraft 1.14 and above)
|
- **OpenEXR Sequence** (Minecraft 1.14 and above)
|
||||||
Exports the sequence as individual frames in the **OpenEXR Format**.
|
Exports the sequence as individual frames in the **OpenEXR Format**.
|
||||||
The images are of perfect quality and may contain
|
The images are of perfect quality and may contain
|
||||||
|
|||||||
71
src/main/java/com/replaymod/render/PNGWriter.java
Normal file
71
src/main/java/com/replaymod/render/PNGWriter.java
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
package com.replaymod.render;
|
||||||
|
|
||||||
|
import com.replaymod.core.versions.MCVer;
|
||||||
|
import com.replaymod.render.blend.Util.IOConsumer;
|
||||||
|
import com.replaymod.render.frame.BitmapFrame;
|
||||||
|
import com.replaymod.render.rendering.Channel;
|
||||||
|
import com.replaymod.render.rendering.FrameConsumer;
|
||||||
|
import com.replaymod.render.utils.ByteBufferPool;
|
||||||
|
import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension;
|
||||||
|
import de.johni0702.minecraft.gui.versions.Image;
|
||||||
|
import net.minecraft.util.crash.CrashReport;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class PNGWriter implements FrameConsumer<BitmapFrame> {
|
||||||
|
|
||||||
|
private final Path outputFolder;
|
||||||
|
|
||||||
|
public PNGWriter(Path outputFolder) throws IOException {
|
||||||
|
this.outputFolder = outputFolder;
|
||||||
|
|
||||||
|
Files.createDirectories(outputFolder);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void consume(Map<Channel, BitmapFrame> channels) {
|
||||||
|
BitmapFrame bgraFrame = channels.get(Channel.BRGA);
|
||||||
|
BitmapFrame depthFrame = channels.get(Channel.DEPTH);
|
||||||
|
try {
|
||||||
|
if (bgraFrame != null) {
|
||||||
|
withImage(bgraFrame, image ->
|
||||||
|
image.writePNG(outputFolder.resolve(bgraFrame.getFrameId() + ".png").toFile()));
|
||||||
|
}
|
||||||
|
if (depthFrame != null) {
|
||||||
|
withImage(depthFrame, image ->
|
||||||
|
image.writePNG(outputFolder.resolve(depthFrame.getFrameId() + ".depth.png").toFile()));
|
||||||
|
}
|
||||||
|
} catch (Throwable t) {
|
||||||
|
MCVer.getMinecraft().setCrashReport(CrashReport.create(t, "Exporting EXR frame"));
|
||||||
|
} finally {
|
||||||
|
channels.values().forEach(it -> ByteBufferPool.release(it.getByteBuffer()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void withImage(BitmapFrame frame, IOConsumer<Image> consumer) throws IOException {
|
||||||
|
ByteBuffer buffer = frame.getByteBuffer();
|
||||||
|
ReadableDimension size = frame.getSize();
|
||||||
|
int width = size.getWidth();
|
||||||
|
int height = size.getHeight();
|
||||||
|
try (Image image = new Image(width, height)) {
|
||||||
|
for (int y = 0; y < height; y++) {
|
||||||
|
for (int x = 0; x < width; x++) {
|
||||||
|
byte b = buffer.get();
|
||||||
|
byte g = buffer.get();
|
||||||
|
byte r = buffer.get();
|
||||||
|
byte a = buffer.get();
|
||||||
|
image.setRGBA(x, y, r, g, b, a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
consumer.accept(image);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -72,7 +72,7 @@ public class RenderSettings {
|
|||||||
|
|
||||||
EXR(null, "exr"),
|
EXR(null, "exr"),
|
||||||
|
|
||||||
PNG("\"%FILENAME%-%06d.png\"", "png");
|
PNG(null, "png");
|
||||||
|
|
||||||
private final String preset;
|
private final String preset;
|
||||||
private final String fileExtension;
|
private final String fileExtension;
|
||||||
|
|||||||
@@ -406,8 +406,9 @@ public class GuiRenderSettings extends AbstractGuiPopup<GuiRenderSettings> {
|
|||||||
|
|
||||||
// Enable/Disable various options for blend export
|
// Enable/Disable various options for blend export
|
||||||
boolean isEXR = encodingPresetDropdown.getSelectedValue() == RenderSettings.EncodingPreset.EXR;
|
boolean isEXR = encodingPresetDropdown.getSelectedValue() == RenderSettings.EncodingPreset.EXR;
|
||||||
|
boolean isPNG = encodingPresetDropdown.getSelectedValue() == RenderSettings.EncodingPreset.PNG;
|
||||||
boolean isBlend = renderMethod == RenderSettings.RenderMethod.BLEND;
|
boolean isBlend = renderMethod == RenderSettings.RenderMethod.BLEND;
|
||||||
boolean isFFmpeg = !isBlend && !isEXR;
|
boolean isFFmpeg = !isBlend && !isEXR && !isPNG;
|
||||||
if (isBlend) {
|
if (isBlend) {
|
||||||
videoWidth.setDisabled();
|
videoWidth.setDisabled();
|
||||||
videoHeight.setDisabled();
|
videoHeight.setDisabled();
|
||||||
@@ -417,11 +418,11 @@ public class GuiRenderSettings extends AbstractGuiPopup<GuiRenderSettings> {
|
|||||||
exportArguments.setEnabled(isFFmpeg);
|
exportArguments.setEnabled(isFFmpeg);
|
||||||
antiAliasingDropdown.setEnabled(isFFmpeg);
|
antiAliasingDropdown.setEnabled(isFFmpeg);
|
||||||
|
|
||||||
if (isEXR) {
|
if (isEXR || isPNG) {
|
||||||
depthMap.setEnabled().setTooltip(null);
|
depthMap.setEnabled().setTooltip(null);
|
||||||
} else {
|
} else {
|
||||||
depthMap.setDisabled().setTooltip(new GuiTooltip().setColor(Colors.RED)
|
depthMap.setDisabled().setTooltip(new GuiTooltip().setColor(Colors.RED)
|
||||||
.setI18nText("replaymod.gui.rendersettings.depthmap.onlyexr"));
|
.setI18nText("replaymod.gui.rendersettings.depthmap.only_exr_or_png"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enable/Disable export args reset button
|
// Enable/Disable export args reset button
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import com.replaymod.pathing.player.AbstractTimelinePlayer;
|
|||||||
import com.replaymod.pathing.player.ReplayTimer;
|
import com.replaymod.pathing.player.ReplayTimer;
|
||||||
import com.replaymod.pathing.properties.TimestampProperty;
|
import com.replaymod.pathing.properties.TimestampProperty;
|
||||||
import com.replaymod.render.CameraPathExporter;
|
import com.replaymod.render.CameraPathExporter;
|
||||||
|
import com.replaymod.render.PNGWriter;
|
||||||
import com.replaymod.render.RenderSettings;
|
import com.replaymod.render.RenderSettings;
|
||||||
import com.replaymod.render.ReplayModRender;
|
import com.replaymod.render.ReplayModRender;
|
||||||
import com.replaymod.render.FFmpegWriter;
|
import com.replaymod.render.FFmpegWriter;
|
||||||
@@ -125,15 +126,17 @@ public class VideoRenderer implements RenderInfo {
|
|||||||
} else {
|
} else {
|
||||||
FrameConsumer<BitmapFrame> frameConsumer;
|
FrameConsumer<BitmapFrame> frameConsumer;
|
||||||
if (settings.getEncodingPreset() == RenderSettings.EncodingPreset.EXR) {
|
if (settings.getEncodingPreset() == RenderSettings.EncodingPreset.EXR) {
|
||||||
ffmpegWriter = null;
|
|
||||||
//#if MC>=11400
|
//#if MC>=11400
|
||||||
frameConsumer = new EXRWriter(settings.getOutputFile().toPath());
|
frameConsumer = new EXRWriter(settings.getOutputFile().toPath());
|
||||||
//#else
|
//#else
|
||||||
//$$ throw new UnsupportedOperationException("EXR requires LWJGL3");
|
//$$ throw new UnsupportedOperationException("EXR requires LWJGL3");
|
||||||
//#endif
|
//#endif
|
||||||
|
} else if (settings.getEncodingPreset() == RenderSettings.EncodingPreset.PNG) {
|
||||||
|
frameConsumer = new PNGWriter(settings.getOutputFile().toPath());
|
||||||
} else {
|
} else {
|
||||||
frameConsumer = ffmpegWriter = new FFmpegWriter(this);
|
frameConsumer = new FFmpegWriter(this);
|
||||||
}
|
}
|
||||||
|
ffmpegWriter = frameConsumer instanceof FFmpegWriter ? (FFmpegWriter) frameConsumer : null;
|
||||||
FrameConsumer<BitmapFrame> previewingFrameConsumer = new FrameConsumer<BitmapFrame>() {
|
FrameConsumer<BitmapFrame> previewingFrameConsumer = new FrameConsumer<BitmapFrame>() {
|
||||||
@Override
|
@Override
|
||||||
public void consume(Map<Channel, BitmapFrame> channels) {
|
public void consume(Map<Channel, BitmapFrame> channels) {
|
||||||
|
|||||||
Reference in New Issue
Block a user