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:
Jonas Herzig
2021-02-28 01:36:30 +01:00
parent 8ff27437af
commit dda6bd41b6
6 changed files with 84 additions and 8 deletions

View File

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

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

View File

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

View File

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

View File

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