Add FOV slider for spherical render methods
Before uploading to YouTube, convert all spherical non-mp4 files to mp4 to be able to inject metadata Properly re-throw errors during the rendering pipeline
This commit is contained in:
@@ -21,6 +21,7 @@ import com.google.api.services.youtube.model.VideoSnippet;
|
|||||||
import com.google.api.services.youtube.model.VideoStatus;
|
import com.google.api.services.youtube.model.VideoStatus;
|
||||||
import com.google.common.base.Supplier;
|
import com.google.common.base.Supplier;
|
||||||
import com.google.common.base.Suppliers;
|
import com.google.common.base.Suppliers;
|
||||||
|
import com.google.common.io.Files;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
import com.google.common.util.concurrent.SettableFuture;
|
import com.google.common.util.concurrent.SettableFuture;
|
||||||
import com.replaymod.render.RenderSettings;
|
import com.replaymod.render.RenderSettings;
|
||||||
@@ -41,8 +42,7 @@ import static com.replaymod.extras.ReplayModExtras.LOGGER;
|
|||||||
public class YoutubeUploader {
|
public class YoutubeUploader {
|
||||||
private static final String CLIENT_ID = "743126594724-mfe7pj1k7e47uu5pk4503c8st9vj9ibu.apps.googleusercontent.com";
|
private static final String CLIENT_ID = "743126594724-mfe7pj1k7e47uu5pk4503c8st9vj9ibu.apps.googleusercontent.com";
|
||||||
private static final String CLIENT_SECRET = "gMwcy3mRYCRamCIjJIYP7rqc";
|
private static final String CLIENT_SECRET = "gMwcy3mRYCRamCIjJIYP7rqc";
|
||||||
private static final String FFMPEG_ODS =
|
private static final String FFMPEG_MP4 = "-i %s -c:v libx264 -preset slow -crf 16 %s";
|
||||||
"-i %s -vf scale=iw:iw*9/16,setdar=16:9 -c:v libx264 -preset slow -crf 16 %s";
|
|
||||||
private static final JsonFactory JSON_FACTORY = new GsonFactory();
|
private static final JsonFactory JSON_FACTORY = new GsonFactory();
|
||||||
private final NetHttpTransport httpTransport;
|
private final NetHttpTransport httpTransport;
|
||||||
private final DataStoreFactory dataStoreFactory;
|
private final DataStoreFactory dataStoreFactory;
|
||||||
@@ -131,68 +131,73 @@ public class YoutubeUploader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private File preUpload() throws InterruptedException, IOException {
|
private File preUpload() throws InterruptedException, IOException {
|
||||||
if (settings.getRenderMethod() == RenderSettings.RenderMethod.ODS) {
|
File outputFile = videoFile;
|
||||||
File tmpFile = new File(videoFile.getParentFile(), System.currentTimeMillis() + ".mp4");
|
|
||||||
tmpFile.deleteOnExit();
|
|
||||||
|
|
||||||
String args = String.format(FFMPEG_ODS, videoFile.getName(), tmpFile.getName());
|
if (settings.getRenderMethod().isSpherical()) {
|
||||||
|
// inject spherical metadata for YouTube unless already done
|
||||||
|
|
||||||
CommandLine commandLine = new CommandLine(settings.getExportCommand());
|
boolean isMp4 = Files.getFileExtension(outputFile.getName()).equalsIgnoreCase("mp4");
|
||||||
commandLine.addArguments(args);
|
|
||||||
LOGGER.info("Re-encoding for ODS with {} {}", settings.getExportCommand(), args);
|
|
||||||
Process process = new ProcessBuilder(commandLine.toStrings()).directory(videoFile.getParentFile()).start();
|
|
||||||
|
|
||||||
final AtomicBoolean active = new AtomicBoolean(true);
|
if (!isMp4) {
|
||||||
final InputStream in = process.getErrorStream();
|
// convert non-mp4 videos into mp4 format to be able to inject metadata
|
||||||
new Thread(() -> {
|
outputFile = new File(outputFile.getParentFile(), System.currentTimeMillis() + ".mp4");
|
||||||
try {
|
outputFile.deleteOnExit();
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
while (active.get()) {
|
String args = String.format(FFMPEG_MP4, videoFile.getName(), outputFile.getName());
|
||||||
char c = (char) in.read();
|
|
||||||
if (c == '\r') {
|
CommandLine commandLine = new CommandLine(settings.getExportCommandOrDefault());
|
||||||
String str = sb.toString();
|
commandLine.addArguments(args);
|
||||||
LOGGER.debug("[FFmpeg] {}", str);
|
LOGGER.info("Re-encoding for metadata injection with {} {}", settings.getExportCommand(), args);
|
||||||
if (str.startsWith("frame=")) {
|
Process process = new ProcessBuilder(commandLine.toStrings()).directory(outputFile.getParentFile()).start();
|
||||||
str = str.substring(6).trim();
|
|
||||||
str = str.substring(0, str.indexOf(' '));
|
final AtomicBoolean active = new AtomicBoolean(true);
|
||||||
double frame = Integer.parseInt(str);
|
final InputStream in = process.getErrorStream();
|
||||||
progress = Suppliers.ofInstance(frame / videoFrames);
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
while (active.get()) {
|
||||||
|
char c = (char) in.read();
|
||||||
|
if (c == '\r') {
|
||||||
|
String str = sb.toString();
|
||||||
|
LOGGER.debug("[FFmpeg] {}", str);
|
||||||
|
if (str.startsWith("frame=")) {
|
||||||
|
str = str.substring(6).trim();
|
||||||
|
str = str.substring(0, str.indexOf(' '));
|
||||||
|
double frame = Integer.parseInt(str);
|
||||||
|
progress = Suppliers.ofInstance(frame / videoFrames);
|
||||||
|
}
|
||||||
|
sb = new StringBuilder();
|
||||||
|
} else {
|
||||||
|
sb.append(c);
|
||||||
}
|
}
|
||||||
sb = new StringBuilder();
|
|
||||||
} else {
|
|
||||||
sb.append(c);
|
|
||||||
}
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
}).start();
|
||||||
e.printStackTrace();
|
|
||||||
|
int result;
|
||||||
|
try {
|
||||||
|
result = process.waitFor();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
process.destroy();
|
||||||
|
throw e;
|
||||||
|
} finally {
|
||||||
|
active.set(false);
|
||||||
|
}
|
||||||
|
if (result != 0) {
|
||||||
|
throw new IOException("FFmpeg returned: " + result);
|
||||||
}
|
}
|
||||||
}).start();
|
|
||||||
|
|
||||||
int result;
|
|
||||||
try {
|
|
||||||
result = process.waitFor();
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
process.destroy();
|
|
||||||
throw e;
|
|
||||||
} finally {
|
|
||||||
active.set(false);
|
|
||||||
}
|
|
||||||
if (result != 0) {
|
|
||||||
throw new IOException("FFmpeg returned: " + result);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MetadataInjector.injectODSMetadata(tmpFile);
|
if (!settings.isInjectSphericalMetadata() || !isMp4) {
|
||||||
|
MetadataInjector.injectMetadata(settings.getRenderMethod(), outputFile,
|
||||||
return tmpFile;
|
settings.getTargetVideoWidth(), settings.getTargetVideoHeight(),
|
||||||
|
settings.getSphericalFovX(), settings.getSphericalFovY());
|
||||||
} else if (settings.getRenderMethod() == RenderSettings.RenderMethod.EQUIRECTANGULAR) {
|
|
||||||
// if metadata hasn't been injected before, inject it before the upload
|
|
||||||
if (!settings.isInject360Metadata()) {
|
|
||||||
MetadataInjector.inject360Metadata(videoFile);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return videoFile;
|
return outputFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Video doUpload(YouTube youTube, File processedFile) throws IOException {
|
private Video doUpload(YouTube youTube, File processedFile) throws IOException {
|
||||||
|
|||||||
@@ -3,10 +3,23 @@ package com.replaymod.render;
|
|||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
import net.minecraft.client.Minecraft;
|
||||||
import net.minecraft.client.resources.I18n;
|
import net.minecraft.client.resources.I18n;
|
||||||
|
import net.minecraft.util.Util;
|
||||||
import org.lwjgl.util.ReadableColor;
|
import org.lwjgl.util.ReadableColor;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static com.replaymod.render.ReplayModRender.LOGGER;
|
||||||
|
|
||||||
|
//#if MC>=10800
|
||||||
|
import net.minecraftforge.fml.common.versioning.ComparableVersion;
|
||||||
|
//#else
|
||||||
|
//$$ import cpw.mods.fml.common.versioning.ComparableVersion;
|
||||||
|
//#endif
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public class RenderSettings {
|
public class RenderSettings {
|
||||||
@@ -21,6 +34,14 @@ public class RenderSettings {
|
|||||||
public String getDescription() {
|
public String getDescription() {
|
||||||
return I18n.format("replaymod.gui.rendersettings.renderer." + name().toLowerCase() + ".description");
|
return I18n.format("replaymod.gui.rendersettings.renderer." + name().toLowerCase() + ".description");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isSpherical() {
|
||||||
|
return this == EQUIRECTANGULAR || this == ODS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasFixedAspectRatio() {
|
||||||
|
return this == EQUIRECTANGULAR || this == ODS || this == CUBIC;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum EncodingPreset {
|
public enum EncodingPreset {
|
||||||
@@ -92,7 +113,9 @@ public class RenderSettings {
|
|||||||
private final boolean stabilizePitch;
|
private final boolean stabilizePitch;
|
||||||
private final boolean stabilizeRoll;
|
private final boolean stabilizeRoll;
|
||||||
private final ReadableColor chromaKeyingColor;
|
private final ReadableColor chromaKeyingColor;
|
||||||
private final boolean inject360Metadata;
|
private final int sphericalFovX;
|
||||||
|
private final int sphericalFovY;
|
||||||
|
private final boolean injectSphericalMetadata;
|
||||||
private final AntiAliasing antiAliasing;
|
private final AntiAliasing antiAliasing;
|
||||||
|
|
||||||
private final String exportCommand;
|
private final String exportCommand;
|
||||||
@@ -129,12 +152,66 @@ public class RenderSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public String getVideoFilters() {
|
public String getVideoFilters() {
|
||||||
if (antiAliasing == AntiAliasing.NONE) {
|
StringBuilder filters = new StringBuilder();
|
||||||
return "";
|
|
||||||
} else {
|
if (antiAliasing != AntiAliasing.NONE) {
|
||||||
double factor = 1.0 / antiAliasing.getFactor();
|
double factor = 1.0 / antiAliasing.getFactor();
|
||||||
return String.format("-vf scale=iw*%1$s:ih*%1$s ", factor);
|
filters.append(String.format("-filter:v scale=iw*%1$s:ih*%1$s ", factor));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return filters.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getExportCommandOrDefault() {
|
||||||
|
return exportCommand.isEmpty() ? findFFmpeg() : exportCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String findFFmpeg() {
|
||||||
|
switch (Util.getOSType()) {
|
||||||
|
case WINDOWS:
|
||||||
|
// Allow windows users to unpack the ffmpeg archive into a sub-folder of their .minecraft folder
|
||||||
|
File inDotMinecraft = new File(Minecraft.getMinecraft().mcDataDir, "ffmpeg/bin/ffmpeg.exe");
|
||||||
|
if (inDotMinecraft.exists()) {
|
||||||
|
LOGGER.debug("FFmpeg found in .minecraft/ffmpeg");
|
||||||
|
return inDotMinecraft.getAbsolutePath();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case OSX:
|
||||||
|
// The PATH doesn't seem to be set as expected on OSX, therefore we check some common locations ourselves
|
||||||
|
for (String path : new String[]{"/usr/local/bin/ffmpeg", "/usr/bin/ffmpeg"}) {
|
||||||
|
File file = new File(path);
|
||||||
|
if (file.exists()) {
|
||||||
|
LOGGER.debug("Found FFmpeg at {}", path);
|
||||||
|
return path;
|
||||||
|
} else {
|
||||||
|
LOGGER.debug("FFmpeg not located at {}", path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Homebrew doesn't seem to reliably symlink its installed binaries either
|
||||||
|
File homebrewFolder = new File("/usr/local/Cellar/ffmpeg");
|
||||||
|
String[] homebrewVersions = homebrewFolder.list();
|
||||||
|
if (homebrewVersions != null) {
|
||||||
|
Optional<File> latestOpt = Arrays.stream(homebrewVersions)
|
||||||
|
.map(ComparableVersion::new) // Convert file name to comparable version
|
||||||
|
.sorted(Comparator.reverseOrder()) // Sort for latest version
|
||||||
|
.map(ComparableVersion::toString) // Convert back to file name
|
||||||
|
.map(v -> new File(new File(homebrewFolder, v), "bin/ffmpeg")) // Convert to binary files
|
||||||
|
.filter(File::exists) // Filter invalid installations (missing executable)
|
||||||
|
.findFirst(); // Take first one
|
||||||
|
if (latestOpt.isPresent()) {
|
||||||
|
File latest = latestOpt.get();
|
||||||
|
LOGGER.debug("Found {} versions of FFmpeg installed with homebrew, chose {}",
|
||||||
|
homebrewVersions.length, latest);
|
||||||
|
return latest.getAbsolutePath();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case LINUX: // Linux users are entrusted to have their PATH configured correctly (most package manager do this)
|
||||||
|
case SOLARIS: // Never heard of anyone running this mod on Solaris having any problems
|
||||||
|
case UNKNOWN: // Unknown OS, just try to use "ffmpeg"
|
||||||
|
}
|
||||||
|
LOGGER.debug("Using default FFmpeg executable");
|
||||||
|
return "ffmpeg";
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import com.replaymod.render.utils.StreamPipe;
|
|||||||
import net.minecraft.client.Minecraft;
|
import net.minecraft.client.Minecraft;
|
||||||
import net.minecraft.crash.CrashReport;
|
import net.minecraft.crash.CrashReport;
|
||||||
import net.minecraft.crash.CrashReportCategory;
|
import net.minecraft.crash.CrashReportCategory;
|
||||||
import net.minecraft.util.Util;
|
|
||||||
import org.apache.commons.exec.CommandLine;
|
import org.apache.commons.exec.CommandLine;
|
||||||
import org.apache.commons.io.FileUtils;
|
import org.apache.commons.io.FileUtils;
|
||||||
import org.apache.commons.io.IOUtils;
|
import org.apache.commons.io.IOUtils;
|
||||||
@@ -22,17 +21,8 @@ import java.io.IOException;
|
|||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.nio.channels.Channels;
|
import java.nio.channels.Channels;
|
||||||
import java.nio.channels.WritableByteChannel;
|
import java.nio.channels.WritableByteChannel;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
//#if MC>=10800
|
|
||||||
import net.minecraftforge.fml.common.versioning.ComparableVersion;
|
|
||||||
//#else
|
|
||||||
//$$ import cpw.mods.fml.common.versioning.ComparableVersion;
|
|
||||||
//#endif
|
|
||||||
|
|
||||||
import static com.replaymod.render.ReplayModRender.LOGGER;
|
import static com.replaymod.render.ReplayModRender.LOGGER;
|
||||||
import static org.apache.commons.lang3.Validate.isTrue;
|
import static org.apache.commons.lang3.Validate.isTrue;
|
||||||
|
|
||||||
@@ -64,7 +54,7 @@ public class VideoWriter implements FrameConsumer<RGBFrame> {
|
|||||||
.replace("%BITRATE%", String.valueOf(settings.getBitRate()))
|
.replace("%BITRATE%", String.valueOf(settings.getBitRate()))
|
||||||
.replace("%FILTERS%", settings.getVideoFilters());
|
.replace("%FILTERS%", settings.getVideoFilters());
|
||||||
|
|
||||||
String executable = settings.getExportCommand().isEmpty() ? findFFmpeg() : settings.getExportCommand();
|
String executable = settings.getExportCommandOrDefault();
|
||||||
LOGGER.info("Starting {} with args: {}", executable, commandArgs);
|
LOGGER.info("Starting {} with args: {}", executable, commandArgs);
|
||||||
String[] cmdline;
|
String[] cmdline;
|
||||||
try {
|
try {
|
||||||
@@ -86,54 +76,6 @@ public class VideoWriter implements FrameConsumer<RGBFrame> {
|
|||||||
channel = Channels.newChannel(outputStream);
|
channel = Channels.newChannel(outputStream);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String findFFmpeg() {
|
|
||||||
switch (Util.getOSType()) {
|
|
||||||
case WINDOWS:
|
|
||||||
// Allow windows users to unpack the ffmpeg archive into a sub-folder of their .minecraft folder
|
|
||||||
File inDotMinecraft = new File(Minecraft.getMinecraft().mcDataDir, "ffmpeg/bin/ffmpeg.exe");
|
|
||||||
if (inDotMinecraft.exists()) {
|
|
||||||
LOGGER.debug("FFmpeg found in .minecraft/ffmpeg");
|
|
||||||
return inDotMinecraft.getAbsolutePath();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case OSX:
|
|
||||||
// The PATH doesn't seem to be set as expected on OSX, therefore we check some common locations ourselves
|
|
||||||
for (String path : new String[]{"/usr/local/bin/ffmpeg", "/usr/bin/ffmpeg"}) {
|
|
||||||
File file = new File(path);
|
|
||||||
if (file.exists()) {
|
|
||||||
LOGGER.debug("Found FFmpeg at {}", path);
|
|
||||||
return path;
|
|
||||||
} else {
|
|
||||||
LOGGER.debug("FFmpeg not located at {}", path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Homebrew doesn't seem to reliably symlink its installed binaries either
|
|
||||||
File homebrewFolder = new File("/usr/local/Cellar/ffmpeg");
|
|
||||||
String[] homebrewVersions = homebrewFolder.list();
|
|
||||||
if (homebrewVersions != null) {
|
|
||||||
Optional<File> latestOpt = Arrays.stream(homebrewVersions)
|
|
||||||
.map(ComparableVersion::new) // Convert file name to comparable version
|
|
||||||
.sorted(Comparator.reverseOrder()) // Sort for latest version
|
|
||||||
.map(ComparableVersion::toString) // Convert back to file name
|
|
||||||
.map(v -> new File(new File(homebrewFolder, v), "bin/ffmpeg")) // Convert to binary files
|
|
||||||
.filter(File::exists) // Filter invalid installations (missing executable)
|
|
||||||
.findFirst(); // Take first one
|
|
||||||
if (latestOpt.isPresent()) {
|
|
||||||
File latest = latestOpt.get();
|
|
||||||
LOGGER.debug("Found {} versions of FFmpeg installed with homebrew, chose {}",
|
|
||||||
homebrewVersions.length, latest);
|
|
||||||
return latest.getAbsolutePath();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case LINUX: // Linux users are entrusted to have their PATH configured correctly (most package manager do this)
|
|
||||||
case SOLARIS: // Never heard of anyone running this mod on Solaris having any problems
|
|
||||||
case UNKNOWN: // Unknown OS, just try to use "ffmpeg"
|
|
||||||
}
|
|
||||||
LOGGER.debug("Using default FFmpeg executable");
|
|
||||||
return "ffmpeg";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() throws IOException {
|
public void close() throws IOException {
|
||||||
IOUtils.closeQuietly(outputStream);
|
IOUtils.closeQuietly(outputStream);
|
||||||
|
|||||||
@@ -93,7 +93,9 @@ public class GuiExportFailed extends GuiScreen {
|
|||||||
oldSettings.isStabilizePitch(),
|
oldSettings.isStabilizePitch(),
|
||||||
oldSettings.isStabilizeRoll(),
|
oldSettings.isStabilizeRoll(),
|
||||||
oldSettings.getChromaKeyingColor(),
|
oldSettings.getChromaKeyingColor(),
|
||||||
oldSettings.isInject360Metadata(),
|
oldSettings.getSphericalFovX(),
|
||||||
|
oldSettings.getSphericalFovY(),
|
||||||
|
oldSettings.isInjectSphericalMetadata(),
|
||||||
oldSettings.getAntiAliasing(),
|
oldSettings.getAntiAliasing(),
|
||||||
oldSettings.getExportCommand(),
|
oldSettings.getExportCommand(),
|
||||||
oldSettings.getEncodingPreset().getValue(),
|
oldSettings.getEncodingPreset().getValue(),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.replaymod.render.gui;
|
package com.replaymod.render.gui;
|
||||||
|
|
||||||
|
import com.google.common.base.Preconditions;
|
||||||
import com.google.common.util.concurrent.FutureCallback;
|
import com.google.common.util.concurrent.FutureCallback;
|
||||||
import com.google.common.util.concurrent.Futures;
|
import com.google.common.util.concurrent.Futures;
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
@@ -149,6 +150,20 @@ public class GuiRenderSettings extends GuiScreen implements Closeable {
|
|||||||
.setI18nLabel("replaymod.gui.rendersettings.chromakey");
|
.setI18nLabel("replaymod.gui.rendersettings.chromakey");
|
||||||
public final GuiColorPicker chromaKeyingColor = new GuiColorPicker().setSize(30, 15);
|
public final GuiColorPicker chromaKeyingColor = new GuiColorPicker().setSize(30, 15);
|
||||||
|
|
||||||
|
public final int minSphericalFov = 120;
|
||||||
|
public final int maxSphericalFov = 360;
|
||||||
|
public final int sphericalFovSteps = 5;
|
||||||
|
public final GuiSlider sphericalFovSlider = new GuiSlider()
|
||||||
|
.onValueChanged(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
sphericalFovSlider.setText(I18n.format("replaymod.gui.rendersettings.sphericalFov")
|
||||||
|
+ ": " + (minSphericalFov + sphericalFovSlider.getValue() * sphericalFovSteps) + "°");
|
||||||
|
|
||||||
|
updateInputs();
|
||||||
|
}
|
||||||
|
}).setSize(200, 20).setSteps((maxSphericalFov - minSphericalFov) / sphericalFovSteps);
|
||||||
|
|
||||||
public final GuiCheckbox inject360Metadata = new GuiCheckbox()
|
public final GuiCheckbox inject360Metadata = new GuiCheckbox()
|
||||||
.setI18nLabel("replaymod.gui.rendersettings.360metadata");
|
.setI18nLabel("replaymod.gui.rendersettings.360metadata");
|
||||||
|
|
||||||
@@ -161,8 +176,7 @@ public class GuiRenderSettings extends GuiScreen implements Closeable {
|
|||||||
.addElements(new GridLayout.Data(0, 0.5),
|
.addElements(new GridLayout.Data(0, 0.5),
|
||||||
new GuiLabel().setI18nText("replaymod.gui.rendersettings.stabilizecamera"), stabilizePanel,
|
new GuiLabel().setI18nText("replaymod.gui.rendersettings.stabilizecamera"), stabilizePanel,
|
||||||
chromaKeyingCheckbox, chromaKeyingColor,
|
chromaKeyingCheckbox, chromaKeyingColor,
|
||||||
inject360Metadata,
|
inject360Metadata, sphericalFovSlider,
|
||||||
new GuiLabel(), // to show the anti-aliasing options in a new line
|
|
||||||
new GuiLabel().setI18nText("replaymod.gui.rendersettings.antialiasing"), antiAliasingDropdown));
|
new GuiLabel().setI18nText("replaymod.gui.rendersettings.antialiasing"), antiAliasingDropdown));
|
||||||
|
|
||||||
public final GuiTextField exportCommand = new GuiTextField().setI18nHint("replaymod.gui.rendersettings.command")
|
public final GuiTextField exportCommand = new GuiTextField().setI18nHint("replaymod.gui.rendersettings.command")
|
||||||
@@ -289,6 +303,11 @@ public class GuiRenderSettings extends GuiScreen implements Closeable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected void updateInputs() {
|
protected void updateInputs() {
|
||||||
|
RenderSettings.RenderMethod renderMethod = renderMethodDropdown.getSelectedValue();
|
||||||
|
|
||||||
|
// Enable/Disable video with input
|
||||||
|
videoWidth.setEnabled(!renderMethod.hasFixedAspectRatio());
|
||||||
|
|
||||||
// Validate video width and height
|
// Validate video width and height
|
||||||
String error = isResolutionValid();
|
String error = isResolutionValid();
|
||||||
if (error == null) {
|
if (error == null) {
|
||||||
@@ -311,7 +330,7 @@ public class GuiRenderSettings extends GuiScreen implements Closeable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Enable/Disable camera stabilization checkboxes
|
// Enable/Disable camera stabilization checkboxes
|
||||||
switch (renderMethodDropdown.getSelectedValue()) {
|
switch (renderMethod) {
|
||||||
case CUBIC:
|
case CUBIC:
|
||||||
case EQUIRECTANGULAR:
|
case EQUIRECTANGULAR:
|
||||||
case ODS:
|
case ODS:
|
||||||
@@ -321,10 +340,12 @@ public class GuiRenderSettings extends GuiScreen implements Closeable {
|
|||||||
stabilizePanel.forEach(IGuiCheckbox.class).setDisabled();
|
stabilizePanel.forEach(IGuiCheckbox.class).setDisabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enable/Disable Spherical FOV slider
|
||||||
|
sphericalFovSlider.setEnabled(renderMethod.isSpherical());
|
||||||
|
|
||||||
// Enable/Disable inject metadata checkbox
|
// Enable/Disable inject metadata checkbox
|
||||||
if (encodingPresetDropdown.getSelectedValue().getFileExtension().equals("mp4")
|
if (encodingPresetDropdown.getSelectedValue().getFileExtension().equals("mp4")
|
||||||
&& (renderMethodDropdown.getSelectedValue() == RenderSettings.RenderMethod.EQUIRECTANGULAR
|
&& renderMethod.isSpherical()) {
|
||||||
|| renderMethodDropdown.getSelectedValue() == RenderSettings.RenderMethod.ODS)) {
|
|
||||||
inject360Metadata.setEnabled().setTooltip(null);
|
inject360Metadata.setEnabled().setTooltip(null);
|
||||||
} else {
|
} else {
|
||||||
inject360Metadata.setDisabled().setTooltip(new GuiTooltip().setColor(Colors.RED)
|
inject360Metadata.setDisabled().setTooltip(new GuiTooltip().setColor(Colors.RED)
|
||||||
@@ -335,33 +356,72 @@ public class GuiRenderSettings extends GuiScreen implements Closeable {
|
|||||||
protected String isResolutionValid() {
|
protected String isResolutionValid() {
|
||||||
RenderSettings.EncodingPreset preset = encodingPresetDropdown.getSelectedValue();
|
RenderSettings.EncodingPreset preset = encodingPresetDropdown.getSelectedValue();
|
||||||
RenderSettings.RenderMethod method = renderMethodDropdown.getSelectedValue();
|
RenderSettings.RenderMethod method = renderMethodDropdown.getSelectedValue();
|
||||||
int videoWidth = this.videoWidth.getInteger();
|
|
||||||
int videoHeight = this.videoHeight.getInteger();
|
int videoHeight = this.videoHeight.getInteger();
|
||||||
|
int videoWidth;
|
||||||
|
if (method.hasFixedAspectRatio()) {
|
||||||
|
// cubic rendering requires an aspect ratio of 4:3,
|
||||||
|
// therefore the height must be divisible by 3
|
||||||
|
if (method == RenderSettings.RenderMethod.CUBIC && videoHeight % 3 != 0) {
|
||||||
|
return "replaymod.gui.rendersettings.customresolution.warning.cubic.height";
|
||||||
|
}
|
||||||
|
|
||||||
|
int sphericalFov = minSphericalFov + sphericalFovSlider.getValue() * sphericalFovSteps;
|
||||||
|
videoWidth = videoWidthForHeight(method, videoHeight, sphericalFov, sphericalFov);
|
||||||
|
|
||||||
|
this.videoWidth.setValue(videoWidth); // TODO: isResolutionValid should not have side-effects
|
||||||
|
} else {
|
||||||
|
videoWidth = this.videoWidth.getInteger();
|
||||||
|
}
|
||||||
|
|
||||||
// Make sure the export arguments haven't been changed manually
|
// Make sure the export arguments haven't been changed manually
|
||||||
if (exportArguments.getText().equals(preset.getValue())) {
|
if (exportArguments.getText().equals(preset.getValue())) {
|
||||||
// Yuv420 requires both dimensions to be even
|
// Yuv420 requires both dimensions to be even
|
||||||
if (preset.isYuv420()
|
if (preset.isYuv420()
|
||||||
&& (videoWidth % 2 != 0 || videoHeight % 2 != 0)) {
|
&& (videoWidth % 2 != 0 || videoHeight % 2 != 0)) {
|
||||||
|
|
||||||
|
if (method == RenderSettings.RenderMethod.CUBIC) {
|
||||||
|
// cubic yuv rendering has the special case that the height must be
|
||||||
|
// divisible by both 3 and 2 - tell the user about it with a special message
|
||||||
|
return "replaymod.gui.rendersettings.customresolution.warning.yuv420.cubic";
|
||||||
|
}
|
||||||
|
|
||||||
return "replaymod.gui.rendersettings.customresolution.warning.yuv420";
|
return "replaymod.gui.rendersettings.customresolution.warning.yuv420";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (method == RenderSettings.RenderMethod.CUBIC
|
|
||||||
&& (videoWidth * 3 / 4 != videoHeight || videoWidth * 3 % 4 != 0)) {
|
|
||||||
return "replaymod.gui.rendersettings.customresolution.warning.cubic";
|
|
||||||
}
|
|
||||||
if (method == RenderSettings.RenderMethod.EQUIRECTANGULAR
|
|
||||||
&& (videoWidth / 2 != videoHeight || videoWidth % 2 != 0)) {
|
|
||||||
return "replaymod.gui.rendersettings.customresolution.warning.equirectangular";
|
|
||||||
}
|
|
||||||
if (method == RenderSettings.RenderMethod.ODS
|
|
||||||
&& videoWidth != videoHeight) {
|
|
||||||
return "replaymod.gui.rendersettings.customresolution.warning.ods";
|
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected int videoWidthForHeight(RenderSettings.RenderMethod method, int height,
|
||||||
|
int sphericalFovX, int sphericalFovY) {
|
||||||
|
if (method.isSpherical()) {
|
||||||
|
if (sphericalFovY < 180) {
|
||||||
|
// calculate the non-cropped height of the video
|
||||||
|
height = Math.round(height * 180 / (float) sphericalFovY);
|
||||||
|
}
|
||||||
|
|
||||||
|
int width = height * 2;
|
||||||
|
|
||||||
|
if (sphericalFovX < 360) {
|
||||||
|
// crop the resulting width
|
||||||
|
width = Math.round(width * (float) sphericalFovX / 360);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method == RenderSettings.RenderMethod.ODS) {
|
||||||
|
width = Math.round(width / 2f);
|
||||||
|
}
|
||||||
|
|
||||||
|
return width;
|
||||||
|
|
||||||
|
} else if (method == RenderSettings.RenderMethod.CUBIC) {
|
||||||
|
Preconditions.checkArgument(height % 3 == 0);
|
||||||
|
return height / 3 * 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IllegalArgumentException();
|
||||||
|
}
|
||||||
|
|
||||||
public void load(RenderSettings settings) {
|
public void load(RenderSettings settings) {
|
||||||
renderMethodDropdown.setSelected(settings.getRenderMethod());
|
renderMethodDropdown.setSelected(settings.getRenderMethod());
|
||||||
encodingPresetDropdown.setSelected(settings.getEncodingPreset());
|
encodingPresetDropdown.setSelected(settings.getEncodingPreset());
|
||||||
@@ -397,7 +457,8 @@ public class GuiRenderSettings extends GuiScreen implements Closeable {
|
|||||||
chromaKeyingCheckbox.setChecked(true);
|
chromaKeyingCheckbox.setChecked(true);
|
||||||
chromaKeyingColor.setColor(settings.getChromaKeyingColor());
|
chromaKeyingColor.setColor(settings.getChromaKeyingColor());
|
||||||
}
|
}
|
||||||
inject360Metadata.setChecked(settings.isInject360Metadata());
|
sphericalFovSlider.setValue((settings.getSphericalFovX() - minSphericalFov) / sphericalFovSteps);
|
||||||
|
inject360Metadata.setChecked(settings.isInjectSphericalMetadata());
|
||||||
antiAliasingDropdown.setSelected(settings.getAntiAliasing());
|
antiAliasingDropdown.setSelected(settings.getAntiAliasing());
|
||||||
exportCommand.setText(settings.getExportCommand());
|
exportCommand.setText(settings.getExportCommand());
|
||||||
exportArguments.setText(settings.getExportArguments());
|
exportArguments.setText(settings.getExportArguments());
|
||||||
@@ -406,6 +467,8 @@ public class GuiRenderSettings extends GuiScreen implements Closeable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public RenderSettings save(boolean serialize) {
|
public RenderSettings save(boolean serialize) {
|
||||||
|
int sphericalFov = minSphericalFov + sphericalFovSlider.getValue() * sphericalFovSteps;
|
||||||
|
|
||||||
return new RenderSettings(
|
return new RenderSettings(
|
||||||
renderMethodDropdown.getSelectedValue(),
|
renderMethodDropdown.getSelectedValue(),
|
||||||
encodingPresetDropdown.getSelectedValue(),
|
encodingPresetDropdown.getSelectedValue(),
|
||||||
@@ -419,6 +482,7 @@ public class GuiRenderSettings extends GuiScreen implements Closeable {
|
|||||||
stabilizePitch.isChecked() && (serialize || stabilizePitch.isEnabled()),
|
stabilizePitch.isChecked() && (serialize || stabilizePitch.isEnabled()),
|
||||||
stabilizeRoll.isChecked() && (serialize || stabilizeRoll.isEnabled()),
|
stabilizeRoll.isChecked() && (serialize || stabilizeRoll.isEnabled()),
|
||||||
chromaKeyingCheckbox.isChecked() ? chromaKeyingColor.getColor() : null,
|
chromaKeyingCheckbox.isChecked() ? chromaKeyingColor.getColor() : null,
|
||||||
|
sphericalFov, sphericalFov,
|
||||||
inject360Metadata.isChecked() && (serialize || inject360Metadata.isEnabled()),
|
inject360Metadata.isChecked() && (serialize || inject360Metadata.isEnabled()),
|
||||||
antiAliasingDropdown.getSelectedValue(),
|
antiAliasingDropdown.getSelectedValue(),
|
||||||
exportCommand.getText(),
|
exportCommand.getText(),
|
||||||
@@ -435,7 +499,7 @@ public class GuiRenderSettings extends GuiScreen implements Closeable {
|
|||||||
|
|
||||||
private RenderSettings getDefaultRenderSettings() {
|
private RenderSettings getDefaultRenderSettings() {
|
||||||
return new RenderSettings(RenderSettings.RenderMethod.DEFAULT, RenderSettings.EncodingPreset.MP4_DEFAULT, 1920, 1080, 60, 10 << 20, null,
|
return new RenderSettings(RenderSettings.RenderMethod.DEFAULT, RenderSettings.EncodingPreset.MP4_DEFAULT, 1920, 1080, 60, 10 << 20, null,
|
||||||
true, false, false, false, null, false, RenderSettings.AntiAliasing.NONE, "", RenderSettings.EncodingPreset.MP4_DEFAULT.getValue(), false);
|
true, false, false, false, null, 360, 180, false, RenderSettings.AntiAliasing.NONE, "", RenderSettings.EncodingPreset.MP4_DEFAULT.getValue(), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ import com.coremedia.iso.IsoFile;
|
|||||||
import com.coremedia.iso.boxes.*;
|
import com.coremedia.iso.boxes.*;
|
||||||
import com.google.common.primitives.Bytes;
|
import com.google.common.primitives.Bytes;
|
||||||
import com.googlecode.mp4parser.BasicContainer;
|
import com.googlecode.mp4parser.BasicContainer;
|
||||||
|
import com.replaymod.render.RenderSettings;
|
||||||
import org.apache.commons.io.FileUtils;
|
import org.apache.commons.io.FileUtils;
|
||||||
import org.apache.commons.io.IOUtils;
|
import org.apache.commons.io.IOUtils;
|
||||||
|
import org.lwjgl.util.Dimension;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
@@ -30,12 +32,20 @@ public class MetadataInjector {
|
|||||||
"<GSpherical:Stitched>true</GSpherical:Stitched> " +
|
"<GSpherical:Stitched>true</GSpherical:Stitched> " +
|
||||||
"<GSpherical:StitchingSoftware>"+STITCHING_SOFTWARE+"</GSpherical:StitchingSoftware> " +
|
"<GSpherical:StitchingSoftware>"+STITCHING_SOFTWARE+"</GSpherical:StitchingSoftware> " +
|
||||||
"<GSpherical:ProjectionType>equirectangular</GSpherical:ProjectionType> ";
|
"<GSpherical:ProjectionType>equirectangular</GSpherical:ProjectionType> ";
|
||||||
|
private static final String SPHERICAL_CROP_XML =
|
||||||
|
"<GSpherical:FullPanoWidthPixels>%d</GSpherical:FullPanoWidthPixels> " +
|
||||||
|
"<GSpherical:FullPanoHeightPixels>%d</GSpherical:FullPanoHeightPixels> " +
|
||||||
|
"<GSpherical:CroppedAreaImageWidthPixels>%d</GSpherical:CroppedAreaImageWidthPixels> " +
|
||||||
|
"<GSpherical:CroppedAreaImageHeightPixels>%d</GSpherical:CroppedAreaImageHeightPixels> " +
|
||||||
|
"<GSpherical:CroppedAreaLeftPixels>%d</GSpherical:CroppedAreaLeftPixels> " +
|
||||||
|
"<GSpherical:CroppedAreaTopPixels>%d</GSpherical:CroppedAreaTopPixels> ";
|
||||||
private static final String STEREO_XML_CONTENTS = "<GSpherical:StereoMode>top-bottom</GSpherical:StereoMode>";
|
private static final String STEREO_XML_CONTENTS = "<GSpherical:StereoMode>top-bottom</GSpherical:StereoMode>";
|
||||||
private static final String SPHERICAL_XML_FOOTER = "</rdf:SphericalVideo>";
|
private static final String SPHERICAL_XML_FOOTER = "</rdf:SphericalVideo>";
|
||||||
|
|
||||||
private static final String XML_360_METADATA = SPHERICAL_XML_HEADER + SPHERICAL_XML_CONTENTS + SPHERICAL_XML_FOOTER;
|
private static final String XML_MONO_METADATA = SPHERICAL_XML_HEADER + SPHERICAL_XML_CONTENTS
|
||||||
private static final String XML_ODS_METADATA = SPHERICAL_XML_HEADER + SPHERICAL_XML_CONTENTS
|
+ SPHERICAL_CROP_XML + SPHERICAL_XML_FOOTER;
|
||||||
+ STEREO_XML_CONTENTS + SPHERICAL_XML_FOOTER;
|
private static final String XML_STEREO_METADATA = SPHERICAL_XML_HEADER + SPHERICAL_XML_CONTENTS
|
||||||
|
+ SPHERICAL_CROP_XML + STEREO_XML_CONTENTS + SPHERICAL_XML_FOOTER;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* These bytes are taken from the variable 'spherical_uuid_id'
|
* These bytes are taken from the variable 'spherical_uuid_id'
|
||||||
@@ -48,18 +58,43 @@ public class MetadataInjector {
|
|||||||
(byte)0x02, (byte)0x52, (byte)0x1f, (byte)0xdd
|
(byte)0x02, (byte)0x52, (byte)0x1f, (byte)0xdd
|
||||||
};
|
};
|
||||||
|
|
||||||
private static final byte[] BYTES_360_METADATA = Bytes.concat(UUID_BYTES, XML_360_METADATA.getBytes());
|
public static void injectMetadata(RenderSettings.RenderMethod renderMethod, File videoFile,
|
||||||
private static final byte[] BYTES_ODS_METADATA = Bytes.concat(UUID_BYTES, XML_ODS_METADATA.getBytes());
|
int videoWidth, int videoHeight,
|
||||||
|
int sphericalFovX, int sphericalFovY) {
|
||||||
|
String xmlString;
|
||||||
|
switch (renderMethod) {
|
||||||
|
case EQUIRECTANGULAR:
|
||||||
|
xmlString = XML_MONO_METADATA;
|
||||||
|
break;
|
||||||
|
case ODS:
|
||||||
|
xmlString = XML_STEREO_METADATA;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Invalid render method");
|
||||||
|
}
|
||||||
|
|
||||||
public static void inject360Metadata(File videoFile) {
|
Dimension original = getOriginalDimensions(videoWidth, videoHeight, sphericalFovX, sphericalFovY);
|
||||||
writeMetadata(videoFile, BYTES_360_METADATA);
|
writeMetadata(videoFile, String.format(xmlString,
|
||||||
|
original.getWidth(), original.getHeight(), videoWidth, videoHeight,
|
||||||
|
(original.getWidth() - videoWidth) / 2, (original.getHeight() - videoHeight) / 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void injectODSMetadata(File videoFile) {
|
private static Dimension getOriginalDimensions(int videoWidth, int videoHeight,
|
||||||
writeMetadata(videoFile, BYTES_ODS_METADATA);
|
int sphericalFovX, int sphericalFovY) {
|
||||||
|
if (sphericalFovX < 360) {
|
||||||
|
videoWidth = Math.round(videoWidth * 360 / (float) sphericalFovX);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sphericalFovY < 180) {
|
||||||
|
videoHeight = Math.round(videoHeight * 180 / (float) sphericalFovY);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Dimension(videoWidth, videoHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void writeMetadata(File videoFile, byte[] metadata) {
|
private static void writeMetadata(File videoFile, String metadata) {
|
||||||
|
byte[] bytes = Bytes.concat(UUID_BYTES, metadata.getBytes());
|
||||||
|
|
||||||
File tempFile = null;
|
File tempFile = null;
|
||||||
FileOutputStream videoFileOutputStream = null;
|
FileOutputStream videoFileOutputStream = null;
|
||||||
IsoFile tempIsoFile = null;
|
IsoFile tempIsoFile = null;
|
||||||
@@ -80,7 +115,7 @@ public class MetadataInjector {
|
|||||||
|
|
||||||
//create a new UserBox, which actually contains the Metadata bytes
|
//create a new UserBox, which actually contains the Metadata bytes
|
||||||
UserBox metadataBox = new UserBox(new byte[0]);
|
UserBox metadataBox = new UserBox(new byte[0]);
|
||||||
metadataBox.setData(metadata);
|
metadataBox.setData(bytes);
|
||||||
|
|
||||||
//add the Metadata UserBox to the Movie Track
|
//add the Metadata UserBox to the Movie Track
|
||||||
trackBox.addBox(metadataBox);
|
trackBox.addBox(metadataBox);
|
||||||
@@ -97,7 +132,7 @@ public class MetadataInjector {
|
|||||||
videoFileOutputStream = new FileOutputStream(videoFile);
|
videoFileOutputStream = new FileOutputStream(videoFile);
|
||||||
tempIsoFile.getBox(videoFileOutputStream.getChannel());
|
tempIsoFile.getBox(videoFileOutputStream.getChannel());
|
||||||
} catch(Exception e) {
|
} catch(Exception e) {
|
||||||
LOGGER.error("360 Degree Metadata couldn't be injected", e);
|
LOGGER.error("Spherical Metadata couldn't be injected", e);
|
||||||
} finally {
|
} finally {
|
||||||
IOUtils.closeQuietly(tempIsoFile);
|
IOUtils.closeQuietly(tempIsoFile);
|
||||||
IOUtils.closeQuietly(videoFileOutputStream);
|
IOUtils.closeQuietly(videoFileOutputStream);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.replaymod.render.processor;
|
|||||||
import com.replaymod.render.frame.CubicOpenGlFrame;
|
import com.replaymod.render.frame.CubicOpenGlFrame;
|
||||||
import com.replaymod.render.frame.RGBFrame;
|
import com.replaymod.render.frame.RGBFrame;
|
||||||
import com.replaymod.render.utils.ByteBufferPool;
|
import com.replaymod.render.utils.ByteBufferPool;
|
||||||
|
import lombok.Getter;
|
||||||
import org.apache.commons.lang3.Validate;
|
import org.apache.commons.lang3.Validate;
|
||||||
import org.lwjgl.util.Dimension;
|
import org.lwjgl.util.Dimension;
|
||||||
|
|
||||||
@@ -18,6 +19,7 @@ public class EquirectangularToRGBProcessor extends AbstractFrameProcessor<CubicO
|
|||||||
private static final byte IMAGE_TOP = 4;
|
private static final byte IMAGE_TOP = 4;
|
||||||
private static final byte IMAGE_BOTTOM = 5;
|
private static final byte IMAGE_BOTTOM = 5;
|
||||||
|
|
||||||
|
@Getter
|
||||||
private final int frameSize;
|
private final int frameSize;
|
||||||
private final int width;
|
private final int width;
|
||||||
private final int height;
|
private final int height;
|
||||||
@@ -26,18 +28,36 @@ public class EquirectangularToRGBProcessor extends AbstractFrameProcessor<CubicO
|
|||||||
private final int[][] imageX;
|
private final int[][] imageX;
|
||||||
private final int[][] imageY;
|
private final int[][] imageY;
|
||||||
|
|
||||||
|
public EquirectangularToRGBProcessor(int outputWidth, int outputHeight, int sphericalFovX) {
|
||||||
|
// calculate the dimensions of the original equirectangular projection
|
||||||
|
// (before cropping according to FOV)
|
||||||
|
width = outputWidth;
|
||||||
|
height = outputHeight;
|
||||||
|
|
||||||
public EquirectangularToRGBProcessor(int frameSize) {
|
int fullWidth;
|
||||||
this.frameSize = frameSize;
|
if (sphericalFovX < 360) {
|
||||||
|
fullWidth = Math.round(width * 360 / (float) sphericalFovX);
|
||||||
|
} else {
|
||||||
|
fullWidth = width;
|
||||||
|
}
|
||||||
|
|
||||||
|
int fullHeight = fullWidth / 2;
|
||||||
|
|
||||||
|
frameSize = fullWidth / 4;
|
||||||
|
|
||||||
width = frameSize * 4;
|
|
||||||
height = frameSize * 2;
|
|
||||||
image = new byte[height][width];
|
image = new byte[height][width];
|
||||||
imageX = new int[height][width];
|
imageX = new int[height][width];
|
||||||
imageY = new int[height][width];
|
imageY = new int[height][width];
|
||||||
for (int i = 0; i < width; i++) {
|
|
||||||
double yaw = PI * 2 * i / width;
|
int xOffset = (fullWidth - width) / 2;
|
||||||
int piQuarter = 8 * i / width - 4;
|
int yOffset = (fullHeight - height) / 2;
|
||||||
|
|
||||||
|
for (int x = 0; x < width; x++) {
|
||||||
|
// get x position relative to the full projection
|
||||||
|
int i = xOffset + x;
|
||||||
|
|
||||||
|
double yaw = PI * 2 * i / fullWidth;
|
||||||
|
int piQuarter = 8 * i / fullWidth - 4;
|
||||||
byte target;
|
byte target;
|
||||||
if (piQuarter < -3) {
|
if (piQuarter < -3) {
|
||||||
target = IMAGE_BACK;
|
target = IMAGE_BACK;
|
||||||
@@ -50,13 +70,16 @@ public class EquirectangularToRGBProcessor extends AbstractFrameProcessor<CubicO
|
|||||||
} else {
|
} else {
|
||||||
target = IMAGE_BACK;
|
target = IMAGE_BACK;
|
||||||
}
|
}
|
||||||
double fYaw = (yaw + PI/4) % (PI / 2) - PI/4;
|
double fYaw = (yaw + PI / 4) % (PI / 2) - PI / 4;
|
||||||
double d = 1 / Math.cos(fYaw);
|
double d = 1 / Math.cos(fYaw);
|
||||||
double gcXN = (Math.tan(fYaw) + 1) / 2;
|
double gcXN = (Math.tan(fYaw) + 1) / 2;
|
||||||
for (int j = 0; j < height; j++) {
|
for (int y = 0; y < height; y++) {
|
||||||
|
// get y position relative to the full projection
|
||||||
|
int j = yOffset + y;
|
||||||
|
|
||||||
double cXN = gcXN;
|
double cXN = gcXN;
|
||||||
byte pt = target;
|
byte pt = target;
|
||||||
double pitch = PI * j / height - PI / 2;
|
double pitch = PI * j / fullHeight - PI / 2;
|
||||||
double cYN = (Math.tan(pitch) * d + 1) / 2;
|
double cYN = (Math.tan(pitch) * d + 1) / 2;
|
||||||
|
|
||||||
if (cYN >= 1) {
|
if (cYN >= 1) {
|
||||||
@@ -74,9 +97,9 @@ public class EquirectangularToRGBProcessor extends AbstractFrameProcessor<CubicO
|
|||||||
|
|
||||||
int imgX = (int) Math.min(frameSize - 1, (cXN * frameSize));
|
int imgX = (int) Math.min(frameSize - 1, (cXN * frameSize));
|
||||||
int imgY = (int) Math.min(frameSize - 1, (cYN * frameSize));
|
int imgY = (int) Math.min(frameSize - 1, (cYN * frameSize));
|
||||||
image[j][i] = pt;
|
image[y][x] = pt;
|
||||||
imageX[j][i] = imgX;
|
imageX[y][x] = imgX;
|
||||||
imageY[j][i] = frameSize - imgY - 1; // The OpenGl buffer contains data flipped vertically
|
imageY[y][x] = frameSize - imgY - 1; // The OpenGl buffer contains data flipped vertically
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import java.nio.ByteBuffer;
|
|||||||
public class ODSToRGBProcessor extends AbstractFrameProcessor<ODSOpenGlFrame, RGBFrame> {
|
public class ODSToRGBProcessor extends AbstractFrameProcessor<ODSOpenGlFrame, RGBFrame> {
|
||||||
private final EquirectangularToRGBProcessor processor;
|
private final EquirectangularToRGBProcessor processor;
|
||||||
|
|
||||||
public ODSToRGBProcessor(int frameSize) {
|
public ODSToRGBProcessor(int outputWidth, int outputHeight, int sphericalFovX) {
|
||||||
processor = new EquirectangularToRGBProcessor(frameSize);
|
processor = new EquirectangularToRGBProcessor(outputWidth, outputHeight / 2, sphericalFovX);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -34,4 +34,8 @@ public class ODSToRGBProcessor extends AbstractFrameProcessor<ODSOpenGlFrame, RG
|
|||||||
public void close() throws IOException {
|
public void close() throws IOException {
|
||||||
processor.close();
|
processor.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getFrameSize() {
|
||||||
|
return processor.getFrameSize();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,19 +61,27 @@ public class Pipelines {
|
|||||||
|
|
||||||
public static Pipeline<CubicOpenGlFrame, RGBFrame> newEquirectangularPipeline(RenderInfo renderInfo, FrameConsumer<RGBFrame> consumer) {
|
public static Pipeline<CubicOpenGlFrame, RGBFrame> newEquirectangularPipeline(RenderInfo renderInfo, FrameConsumer<RGBFrame> consumer) {
|
||||||
RenderSettings settings = renderInfo.getRenderSettings();
|
RenderSettings settings = renderInfo.getRenderSettings();
|
||||||
|
|
||||||
|
EquirectangularToRGBProcessor processor = new EquirectangularToRGBProcessor(settings.getVideoWidth(),
|
||||||
|
settings.getVideoHeight(), settings.getSphericalFovX());
|
||||||
|
|
||||||
FrameCapturer<CubicOpenGlFrame> capturer;
|
FrameCapturer<CubicOpenGlFrame> capturer;
|
||||||
if (PixelBufferObject.SUPPORTED) {
|
if (PixelBufferObject.SUPPORTED) {
|
||||||
capturer = new CubicPboOpenGlFrameCapturer(new EntityRendererHandler(settings, renderInfo), renderInfo, settings.getVideoWidth() / 4);
|
capturer = new CubicPboOpenGlFrameCapturer(new EntityRendererHandler(settings, renderInfo), renderInfo, processor.getFrameSize());
|
||||||
} else {
|
} else {
|
||||||
capturer = new CubicOpenGlFrameCapturer(new EntityRendererHandler(settings, renderInfo), renderInfo, settings.getVideoWidth() / 4);
|
capturer = new CubicOpenGlFrameCapturer(new EntityRendererHandler(settings, renderInfo), renderInfo, processor.getFrameSize());
|
||||||
}
|
}
|
||||||
return new Pipeline<>(capturer, new EquirectangularToRGBProcessor(settings.getVideoWidth() / 4), consumer);
|
return new Pipeline<>(capturer, processor, consumer);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Pipeline<ODSOpenGlFrame, RGBFrame> newODSPipeline(RenderInfo renderInfo, FrameConsumer<RGBFrame> consumer) {
|
public static Pipeline<ODSOpenGlFrame, RGBFrame> newODSPipeline(RenderInfo renderInfo, FrameConsumer<RGBFrame> consumer) {
|
||||||
RenderSettings settings = renderInfo.getRenderSettings();
|
RenderSettings settings = renderInfo.getRenderSettings();
|
||||||
|
|
||||||
|
ODSToRGBProcessor processor = new ODSToRGBProcessor(settings.getVideoWidth(),
|
||||||
|
settings.getVideoHeight(), settings.getSphericalFovX());
|
||||||
|
|
||||||
FrameCapturer<ODSOpenGlFrame> capturer =
|
FrameCapturer<ODSOpenGlFrame> capturer =
|
||||||
new ODSFrameCapturer(new EntityRendererHandler(settings, renderInfo), renderInfo, settings.getVideoWidth() / 4);
|
new ODSFrameCapturer(new EntityRendererHandler(settings, renderInfo), renderInfo, processor.getFrameSize());
|
||||||
return new Pipeline<>(capturer, new ODSToRGBProcessor(settings.getVideoWidth() / 4), consumer);
|
return new Pipeline<>(capturer, processor, consumer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import net.minecraft.client.Minecraft;
|
|||||||
import net.minecraft.client.gui.ScaledResolution;
|
import net.minecraft.client.gui.ScaledResolution;
|
||||||
import net.minecraft.client.renderer.OpenGlHelper;
|
import net.minecraft.client.renderer.OpenGlHelper;
|
||||||
import net.minecraft.client.shader.Framebuffer;
|
import net.minecraft.client.shader.Framebuffer;
|
||||||
|
import net.minecraft.util.ReportedException;
|
||||||
import net.minecraft.util.Timer;
|
import net.minecraft.util.Timer;
|
||||||
import org.lwjgl.input.Mouse;
|
import org.lwjgl.input.Mouse;
|
||||||
import org.lwjgl.opengl.Display;
|
import org.lwjgl.opengl.Display;
|
||||||
@@ -150,12 +151,15 @@ public class VideoRenderer implements RenderInfo {
|
|||||||
|
|
||||||
renderingPipeline.run();
|
renderingPipeline.run();
|
||||||
|
|
||||||
if (settings.isInject360Metadata()) {
|
if (mc.hasCrashed) {
|
||||||
if (settings.getRenderMethod() == RenderSettings.RenderMethod.ODS) {
|
setFailure(mc.crashReporter.getCrashCause());
|
||||||
MetadataInjector.injectODSMetadata(settings.getOutputFile());
|
throw new ReportedException(mc.crashReporter);
|
||||||
} else {
|
}
|
||||||
MetadataInjector.inject360Metadata(settings.getOutputFile());
|
|
||||||
}
|
if (settings.isInjectSphericalMetadata()) {
|
||||||
|
MetadataInjector.injectMetadata(settings.getRenderMethod(), settings.getOutputFile(),
|
||||||
|
settings.getTargetVideoWidth(), settings.getTargetVideoHeight(),
|
||||||
|
settings.getSphericalFovX(), settings.getSphericalFovY());
|
||||||
}
|
}
|
||||||
|
|
||||||
finish();
|
finish();
|
||||||
|
|||||||
Reference in New Issue
Block a user