/* Copyright (c) 2010-2016 Jesper Öqvist <jesper@llbit.se>
*
* This file is part of Chunky.
*
* Chunky is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Chunky is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with Chunky. If not, see <http://www.gnu.org/licenses/>.
*/
package se.llbit.chunky.main;
import se.llbit.chunky.PersistentSettings;
import se.llbit.chunky.Plugin;
import se.llbit.chunky.renderer.ConsoleProgressListener;
import se.llbit.chunky.renderer.OutputMode;
import se.llbit.chunky.renderer.RayTracerFactory;
import se.llbit.chunky.renderer.RenderContext;
import se.llbit.chunky.renderer.RenderContextFactory;
import se.llbit.chunky.renderer.RenderController;
import se.llbit.chunky.renderer.RenderManager;
import se.llbit.chunky.renderer.Renderer;
import se.llbit.chunky.renderer.RendererFactory;
import se.llbit.chunky.renderer.SceneProvider;
import se.llbit.chunky.renderer.SnapshotControl;
import se.llbit.chunky.renderer.scene.AsynchronousSceneManager;
import se.llbit.chunky.renderer.scene.PathTracer;
import se.llbit.chunky.renderer.scene.PreviewRayTracer;
import se.llbit.chunky.renderer.scene.Scene;
import se.llbit.chunky.renderer.scene.SceneFactory;
import se.llbit.chunky.renderer.scene.SceneLoadingError;
import se.llbit.chunky.renderer.scene.SceneManager;
import se.llbit.chunky.renderer.scene.SynchronousSceneManager;
import se.llbit.chunky.resources.TexturePackLoader;
import se.llbit.chunky.ui.ChunkyFx;
import se.llbit.chunky.ui.render.RenderControlsTabTransformer;
import se.llbit.json.JsonValue;
import se.llbit.log.Level;
import se.llbit.log.Log;
import se.llbit.log.Receiver;
import se.llbit.util.TaskTracker;
import se.llbit.util.Util;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
/**
* Chunky is a Minecraft mapping and rendering tool created by
* Jesper Öqvist (jesper@llbit.se).
*
* <p>Read more about Chunky at http://chunky.llbit.se .
*/
public class Chunky {
/** A log receiver suitable for headless rendering. */
private static final Receiver HEADLESS_LOG_RECEIVER = new Receiver() {
@Override public void logEvent(Level level, String message) {
if (level == Level.ERROR) {
System.err.println(); // Clear the current progress line.
System.err.println(message);
} else {
System.out.println(); // Clear the current progress line.
System.out.println(message);
}
}
};
public final ChunkyOptions options;
private RenderController renderController;
private SceneFactory sceneFactory = SceneFactory.DEFAULT;
private RenderContextFactory renderContextFactory = RenderContext::new;
private RendererFactory rendererFactory = RenderManager::new;
private RayTracerFactory previewRayTracerFactory = PreviewRayTracer::new;
private RayTracerFactory rayTracerFactory = PathTracer::new;
private RenderControlsTabTransformer renderControlsTabTransformer = tabs -> tabs;
/**
* @return The name of this application, including version string.
*/
public static String getAppName() {
return String.format("Chunky %s", Version.getVersion());
}
public Chunky(ChunkyOptions options) {
this.options = options;
}
/**
* Start a headless (no GUI) render.
*
* @return error code
*/
private int doHeadlessRender() {
// TODO: This may not be needed after switching to JavaFX:
System.setProperty("java.awt.headless", "true");
Log.setReceiver(HEADLESS_LOG_RECEIVER, Level.INFO, Level.WARNING, Level.ERROR);
RenderContext context = renderContextFactory.newRenderContext(this);
Renderer renderer = rendererFactory.newRenderer(context, true);
SynchronousSceneManager sceneManager = new SynchronousSceneManager(context, renderer);
renderer.setSceneProvider(sceneManager);
TaskTracker taskTracker = new TaskTracker(new ConsoleProgressListener(),
(tracker, previous, name, size) -> new TaskTracker.Task(tracker, previous, name, size) {
@Override public void close() {
super.close();
long endTime = System.currentTimeMillis();
int seconds = (int) ((endTime - startTime) / 1000);
System.out.format("\r%s took %dm %ds%n", name, seconds / 60, seconds % 60);
}
});
sceneManager.setTaskTracker(taskTracker);
renderer.setSnapshotControl(SnapshotControl.DEFAULT);
renderer.setOnFrameCompleted((scene, spp) -> {
if (SnapshotControl.DEFAULT.saveSnapshot(scene, spp)) {
// Save the current frame.
scene.saveSnapshot(context.getSceneDirectory(), taskTracker);
}
if (SnapshotControl.DEFAULT.saveRenderDump(scene, spp)) {
// Save the scene description and current render dump.
try {
sceneManager.saveScene();
} catch (InterruptedException e) {
throw new Error(e);
}
}
});
renderer.setRenderTask(taskTracker.backgroundTask());
renderer.setOnRenderCompleted((time, sps) -> {
System.out.println("Render job finished.");
int seconds = (int) ((time / 1000) % 60);
int minutes = (int) ((time / 60000) % 60);
int hours = (int) (time / 3600000);
System.out.println(String
.format("Total rendering time: %d hours, %d minutes, %d seconds", hours, minutes, seconds));
System.out.println("Average samples per second (SPS): " + sps);
});
try {
sceneManager.loadScene(options.sceneName);
if (options.target != -1) {
sceneManager.getScene().setTargetSpp(options.target);
}
sceneManager.getScene().startHeadlessRender();
renderer.start();
renderer.join();
return 0;
} catch (FileNotFoundException e) {
System.err.format("Scene \"%s\" not found!%n", options.sceneName);
return 1;
} catch (IOException e) {
System.err.format("IO error while loading scene (%s)%n", e.getMessage());
return 1;
} catch (SceneLoadingError e) {
System.err.format("Scene loading error (%s)%n", e.getMessage());
return 1;
} catch (InterruptedException e) {
System.err.println("Interrupted while loading scene");
return 1;
} finally {
renderer.shutdown();
}
}
/**
* Main entry point for Chunky. Chunky should normally be started via
* the launcher which sets up the classpath with all dependencies.
*/
public static void main(final String[] args) {
CommandLineOptions cmdline = new CommandLineOptions(args);
if (cmdline.configurationError) {
System.exit(1);
}
int exitCode = 0;
if (cmdline.mode != CommandLineOptions.Mode.NOTHING) {
Chunky chunky = new Chunky(cmdline.options);
chunky.loadPlugins();
try {
switch (cmdline.mode) {
case HEADLESS_RENDER:
exitCode = chunky.doHeadlessRender();
break;
case SNAPSHOT:
exitCode = chunky.doSnapshot();
break;
case DEFAULT:
ChunkyFx.startChunkyUI(chunky);
break;
}
} catch (Throwable t) {
Log.error("Unchecked exception caused Chunky to close.", t);
exitCode = 2;
}
if (exitCode != 0) {
System.exit(exitCode);
}
}
}
/**
* This can be used by plugins to load the default Minecraft textures.
*/
public static void loadDefaultTextures() {
TexturePackLoader.loadTexturePacks(new String[0], false);
}
private void loadPlugins() {
JsonValue plugins = PersistentSettings.getPlugins();
for (JsonValue plugin : plugins.array()) {
String jar = plugin.object().get("jar").stringValue("");
String main = plugin.object().get("main").stringValue("");
// The MD5 checksum is only for Jar integrity checking, not security!
// Plugin Jar trust is implicit. Only install plugins that you trust!
String md5 = plugin.object().get("md5").stringValue("");
boolean enabled = plugin.object().get("enabled").boolValue(true);
if (!jar.endsWith(".jar")) {
Log.error("Plugin Jar path does not seem to point to a Jar file: " + jar);
}
if (!enabled) {
// Skip disabled plugin.
continue;
}
if (main.isEmpty()) {
Log.error("Plugin has no main class declared: " + jar);
continue;
}
if (md5.isEmpty()) {
Log.error("Plugin missing MD5 checksum: " + jar);
continue;
}
File pluginJar = new File(jar);
if (pluginJar.isFile()) {
if (!verifyChecksumMd5(pluginJar, md5)) {
Log.error("Plugin is corrupt (MD5 check failed): " + jar);
continue;
}
try {
URLClassLoader classLoader = new URLClassLoader(new URL[] {pluginJar.toURI().toURL()});
Class<?> pluginClass = classLoader.loadClass(main);
Plugin pluginInstance = (Plugin) pluginClass.newInstance();
pluginInstance.attach(this);
Log.info("Plugin loaded: " + jar);
} catch (ClassCastException e) {
Log.error("Failed to load plugin " + pluginJar.getAbsolutePath()
+ ". Main plugin class has wrong type", e);
} catch (Throwable e) {
Log.error("Failed to load plugin " + pluginJar.getAbsolutePath(), e);
}
}
}
}
private static boolean verifyChecksumMd5(File pluginJar, String expected) {
String actual = Util.md5sum(pluginJar);
return actual.equalsIgnoreCase(expected);
}
/**
* Save a snapshot for a scene.
*
* <p>This currently disregards the various factories for the
* render context and scene construction.
*/
private int doSnapshot() {
Log.setReceiver(HEADLESS_LOG_RECEIVER, Level.INFO, Level.WARNING, Level.ERROR);
try {
File file = options.getSceneDescriptionFile();
try (FileInputStream in = new FileInputStream(file)) {
Scene scene = new Scene();
scene.loadDescription(in); // Load description to get current SPP & canvas size.
RenderContext context = new RenderContext(this);
TaskTracker taskTracker = new TaskTracker(new ConsoleProgressListener(),
TaskTracker.Task::new,
(tracker, previous, name, size) -> new TaskTracker.Task(tracker, previous, name, size) {
@Override public void update() {
// Don't report task state to progress listener.
}
});
scene.loadDump(context, taskTracker); // Load the render dump.
OutputMode outputMode = scene.getOutputMode();
if (options.imageOutputFile.isEmpty()) {
String extension = ".png";
if (outputMode == OutputMode.TIFF_32) {
extension = ".tiff";
}
options.imageOutputFile = String.format("%s-%d%s", scene.name(), scene.spp, extension);
}
switch (outputMode) {
case PNG:
System.out.println("Image output mode: PNG");
break;
case TIFF_32:
System.out.println("Image output mode: TIFF32");
break;
}
scene.saveFrame(new File(options.imageOutputFile), taskTracker);
System.out.println("Saved snapshot to " + options.imageOutputFile);
return 0;
}
} catch (IOException e) {
System.err.println("Failed to dump snapshot: " + e.getMessage());
return 1;
}
}
public synchronized SceneManager getSceneManager() {
return getRenderController().getSceneManager();
}
public boolean sceneInitialized() {
return renderController != null;
}
public RenderController getRenderController() {
if (renderController == null) {
RenderContext context = renderContextFactory.newRenderContext(this);
Renderer renderer = rendererFactory.newRenderer(context, false);
AsynchronousSceneManager sceneManager = new AsynchronousSceneManager(context, renderer);
SceneProvider sceneProvider = sceneManager.getSceneProvider();
renderer.setSceneProvider(sceneProvider);
renderer.start();
sceneManager.start();
renderController = new RenderController(context, renderer, sceneManager, sceneProvider);
}
return renderController;
}
public void setRenderContextFactory(RenderContextFactory renderContextFactory) {
this.renderContextFactory = renderContextFactory;
}
public RenderContextFactory getRenderContextFactory() {
return renderContextFactory;
}
public RenderContext getRenderContext() {
return getRenderController().getContext();
}
public void setSceneFactory(SceneFactory sceneFactory) {
this.sceneFactory = sceneFactory;
}
public SceneFactory getSceneFactory() {
return sceneFactory;
}
public void setPreviewRayTracerFactory(RayTracerFactory previewRayTracerFactory) {
this.previewRayTracerFactory = previewRayTracerFactory;
}
public RayTracerFactory getPreviewRayTracerFactory() {
return previewRayTracerFactory;
}
public void setRayTracerFactory(RayTracerFactory rayTracerFactory) {
this.rayTracerFactory = rayTracerFactory;
}
public RayTracerFactory getRayTracerFactory() {
return rayTracerFactory;
}
public void setRenderControlsTabTransformer(
RenderControlsTabTransformer renderControlsTabTransformer) {
this.renderControlsTabTransformer = renderControlsTabTransformer;
}
public RenderControlsTabTransformer getRenderControlsTabTransformer() {
return renderControlsTabTransformer;
}
}