package org.sunflow; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.StringReader; import java.util.Locale; import java.util.logging.Level; import java.util.logging.Logger; import org.codehaus.commons.compiler.CompileException; import org.codehaus.janino.ClassBodyEvaluator; import org.codehaus.janino.Scanner; import org.sunflow.core.Camera; import org.sunflow.core.CameraLens; import org.sunflow.core.Display; import org.sunflow.core.Geometry; import org.sunflow.core.ImageSampler; import org.sunflow.core.Instance; import org.sunflow.core.LightSource; import org.sunflow.core.Modifier; import org.sunflow.core.Options; import org.sunflow.core.ParameterList; import org.sunflow.core.PrimitiveList; import org.sunflow.core.Scene; import org.sunflow.core.SceneParser; import org.sunflow.core.Shader; import org.sunflow.core.Tesselatable; import org.sunflow.core.ParameterList.InterpolationType; import org.sunflow.image.ColorFactory; import org.sunflow.image.ColorFactory.ColorSpecificationException; import org.sunflow.math.BoundingBox; import org.sunflow.math.Matrix4; import org.sunflow.math.Point2; import org.sunflow.math.Point3; import org.sunflow.math.Vector3; import org.sunflow.system.FileUtils; import org.sunflow.system.SearchPath; import org.sunflow.system.Timer; import org.sunflow.system.UI; import org.sunflow.system.UI.Module; /** * This API gives a simple interface for creating scenes procedurally. This is * the main entry point to Sunflow. To use this class, extend from it and * implement the build method which may execute arbitrary code to create a * scene. */ public class SunflowAPI implements SunflowAPIInterface { public static final String VERSION = "1.0.0"; public static final String DEFAULT_OPTIONS = "::options"; private Scene scene; private SearchPath includeSearchPath; private SearchPath textureSearchPath; private ParameterList parameterList; private RenderObjectMap renderObjects; private int currentFrame; static String COULDNT_MESSAGE_FORMAT = "Could not compile: \"%s\""; /** * This is a quick system test which verifies that the user has launched * Java properly. */ public static void runSystemCheck() { final long RECOMMENDED_MAX_SIZE = 800; long maxMb = Runtime.getRuntime().maxMemory() / 1048576; if (maxMb < RECOMMENDED_MAX_SIZE) { UI.printError(Module.API, "JVM available memory is below %d MB (found %d MB only).\nPlease make sure you launched the program with the -Xmx command line options.", RECOMMENDED_MAX_SIZE, maxMb); } String compiler = System.getProperty("java.vm.name"); if (compiler == null || !(compiler.contains("HotSpot") && compiler.contains("Server"))) { UI.printError(Module.API, "You do not appear to be running Sun's server JVM\nPerformance may suffer"); } UI.printDetailed(Module.API, "Java environment settings:"); UI.printDetailed(Module.API, " * Max memory available : %d MB", maxMb); UI.printDetailed(Module.API, " * Virtual machine name : %s", compiler == null ? "true if the update was succesfull, or * false if the update failed */ private boolean update(String name) { boolean success = renderObjects.update(name, parameterList, this); parameterList.clear(success); return success; } @Override public final void searchpath(String type, String path) { switch (type) { case "include": includeSearchPath.addSearchPath(path); break; case "texture": textureSearchPath.addSearchPath(path); break; default: UI.printWarning(Module.API, "Invalid searchpath type: \"%s\"", type); break; } } /** * Attempts to resolve the specified filename by checking it against the * texture search path. * * @param filename filename * @return a path which matches the filename, or filename if no matches are * found */ public final String resolveTextureFilename(String filename) { return textureSearchPath.resolvePath(filename); } /** * Attempts to resolve the specified filename by checking it against the * include search path. * * @param filename filename * @return a path which matches the filename, or filename if no matches are * found */ public final String resolveIncludeFilename(String filename) { return includeSearchPath.resolvePath(filename); } @Override public final void shader(String name, String shaderType) { if (!isIncremental(shaderType)) { // we are declaring a shader for the first time if (renderObjects.has(name)) { UI.printError(Module.API, "Unable to declare shader \"%s\", name is already in use", name); parameterList.clear(true); return; } Shader shader = PluginRegistry.SHADER_PLUGINS.createObject(shaderType); if (shader == null) { UI.printError(Module.API, "Unable to create shader of type \"%s\"", shaderType); return; } renderObjects.put(name, shader); } // update existing shader (only if it is valid) if (lookupShader(name) != null) { update(name); } else { UI.printError(Module.API, "Unable to update shader \"%s\" - shader object was not found", name); parameterList.clear(true); } } @Override public final void modifier(String name, String modifierType) { if (!isIncremental(modifierType)) { // we are declaring a shader for the first time if (renderObjects.has(name)) { UI.printError(Module.API, "Unable to declare modifier \"%s\", name is already in use", name); parameterList.clear(true); return; } Modifier modifier = PluginRegistry.MODIFIER_PLUGINS.createObject(modifierType); if (modifier == null) { UI.printError(Module.API, "Unable to create modifier of type \"%s\"", modifierType); return; } renderObjects.put(name, modifier); } // update existing shader (only if it is valid) if (lookupModifier(name) != null) { update(name); } else { UI.printError(Module.API, "Unable to update modifier \"%s\" - modifier object was not found", name); parameterList.clear(true); } } @Override public final void geometry(String name, String typeName) { if (!isIncremental(typeName)) { // we are declaring a geometry for the first time if (renderObjects.has(name)) { UI.printError(Module.API, "Unable to declare geometry \"%s\", name is already in use", name); parameterList.clear(true); return; } // check tesselatable first if (PluginRegistry.TESSELATABLE_PLUGINS.hasType(typeName)) { Tesselatable tesselatable = PluginRegistry.TESSELATABLE_PLUGINS.createObject(typeName); if (tesselatable == null) { UI.printError(Module.API, "Unable to create tesselatable object of type \"%s\"", typeName); return; } renderObjects.put(name, tesselatable); } else { PrimitiveList primitives = PluginRegistry.PRIMITIVE_PLUGINS.createObject(typeName); if (primitives == null) { UI.printError(Module.API, "Unable to create primitive of type \"%s\"", typeName); return; } renderObjects.put(name, primitives); } } if (lookupGeometry(name) != null) { update(name); } else { UI.printError(Module.API, "Unable to update geometry \"%s\" - geometry object was not found", name); parameterList.clear(true); } } @Override public final void instance(String name, String geoname) { if (!isIncremental(geoname)) { // we are declaring this instance for the first time if (renderObjects.has(name)) { UI.printError(Module.API, "Unable to declare instance \"%s\", name is already in use", name); parameterList.clear(true); return; } parameter("geometry", geoname); renderObjects.put(name, new Instance()); } if (lookupInstance(name) != null) { update(name); } else { UI.printError(Module.API, "Unable to update instance \"%s\" - instance object was not found", name); parameterList.clear(true); } } @Override public final void light(String name, String lightType) { if (!isIncremental(lightType)) { // we are declaring this light for the first time if (renderObjects.has(name)) { UI.printError(Module.API, "Unable to declare light \"%s\", name is already in use", name); parameterList.clear(true); return; } LightSource light = PluginRegistry.LIGHT_SOURCE_PLUGINS.createObject(lightType); if (light == null) { UI.printError(Module.API, "Unable to create light source of type \"%s\"", lightType); return; } renderObjects.put(name, light); } if (lookupLight(name) != null) { update(name); } else { UI.printError(Module.API, "Unable to update instance \"%s\" - instance object was not found", name); parameterList.clear(true); } } @Override public final void camera(String name, String lensType) { if (!isIncremental(lensType)) { // we are declaring this camera for the first time if (renderObjects.has(name)) { UI.printError(Module.API, "Unable to declare camera \"%s\", name is already in use", name); parameterList.clear(true); return; } CameraLens lens = PluginRegistry.CAMERA_LENS_PLUGINS.createObject(lensType); if (lens == null) { UI.printError(Module.API, "Unable to create a camera lens of type \"%s\"", lensType); return; } renderObjects.put(name, new Camera(lens)); } // update existing shader (only if it is valid) if (lookupCamera(name) != null) { update(name); } else { UI.printError(Module.API, "Unable to update camera \"%s\" - camera object was not found", name); parameterList.clear(true); } } @Override public final void options(String name) { if (lookupOptions(name) == null) { if (renderObjects.has(name)) { UI.printError(Module.API, "Unable to declare options \"%s\", name is already in use", name); parameterList.clear(true); return; } renderObjects.put(name, new Options()); } assert lookupOptions(name) != null; update(name); } private boolean isIncremental(String typeName) { return typeName == null || typeName.equals("incremental"); } /** * Retrieve a geometry object by its name, or * null if no geometry was found, or if the specified object is * not a geometry. * * @param name geometry name * @return the geometry object associated with that name */ public final Geometry lookupGeometry(String name) { return renderObjects.lookupGeometry(name); } /** * Retrieve an instance object by its name, or * null if no instance was found, or if the specified object is * not an instance. * * @param name instance name * @return the instance object associated with that name */ private Instance lookupInstance(String name) { return renderObjects.lookupInstance(name); } /** * Retrieve a shader object by its name, or * null if no shader was found, or if the specified object is * not a shader. * * @param name camera name * @return the camera object associate with that name */ private Camera lookupCamera(String name) { return renderObjects.lookupCamera(name); } private Options lookupOptions(String name) { return renderObjects.lookupOptions(name); } /** * Retrieve a shader object by its name, or * null if no shader was found, or if the specified object is * not a shader. * * @param name shader name * @return the shader object associated with that name */ public final Shader lookupShader(String name) { return renderObjects.lookupShader(name); } /** * Retrieve a modifier object by its name, or * null if no modifier was found, or if the specified object is * not a modifier. * * @param name modifier name * @return the modifier object associated with that name */ public final Modifier lookupModifier(String name) { return renderObjects.lookupModifier(name); } /** * Retrieve a light object by its name, or * null if no shader was found, or if the specified object is * not a light. * * @param name light name * @return the light object associated with that name */ private LightSource lookupLight(String name) { return renderObjects.lookupLight(name); } @Override public final void render(String optionsName, Display display) { renderObjects.updateScene(scene); Options opt = lookupOptions(optionsName); if (opt == null) { opt = new Options(); } scene.setCamera(lookupCamera(opt.getString("camera", null))); // shader override String shaderOverrideName = opt.getString("override.shader", "none"); boolean overridePhotons = opt.getBoolean("override.photons", false); if (shaderOverrideName.equals("none")) { scene.setShaderOverride(null, false); } else { Shader shader = lookupShader(shaderOverrideName); if (shader == null) { UI.printWarning(Module.API, "Unable to find shader \"%s\" for override, disabling", shaderOverrideName); } scene.setShaderOverride(shader, overridePhotons); } // baking String bakingInstanceName = opt.getString("baking.instance", null); if (bakingInstanceName != null) { Instance bakingInstance = lookupInstance(bakingInstanceName); if (bakingInstance == null) { UI.printError(Module.API, "Unable to bake instance \"%s\" - not found", bakingInstanceName); return; } scene.setBakingInstance(bakingInstance); } else { scene.setBakingInstance(null); } ImageSampler sampler = PluginRegistry.IMAGE_SAMPLE_PLUGINS.createObject(opt.getString("sampler", "bucket")); scene.render(opt, sampler, display); } @Override public final boolean include(String filename) { if (filename == null) { return false; } filename = includeSearchPath.resolvePath(filename); String extension = FileUtils.getExtension(filename); SceneParser parser = PluginRegistry.PARSER_PLUGINS.createObject(extension); if (parser == null) { UI.printError(Module.API, "Unable to find a suitable parser for: \"%s\" (extension: %s)", filename, extension); return false; } String currentFolder = new File(filename).getAbsoluteFile().getParentFile().getAbsolutePath(); includeSearchPath.addSearchPath(currentFolder); textureSearchPath.addSearchPath(currentFolder); return parser.parse(filename, this); } /** * Retrieve the bounding box of the scene. This method will be valid only * after a first call to {@link #render(String, Display)} has been made. * @return */ public final BoundingBox getBounds() { return scene.getBounds(); } /** * This method does nothing, but may be overriden to create scenes * procedurally. */ public void build() { } /** * Create an API object from the specified file. Java files are read by * Janino and are expected to implement a build method (they implement a * derived class of SunflowAPI. The build method is called if the code * compiles succesfully. Other files types are handled by the parse method. * * @param filename filename to load * @param frameNumber * @return a valid SunflowAPI object or null on failure */ public static SunflowAPI create(String filename, int frameNumber) { if (filename == null) { return new SunflowAPI(); } SunflowAPI api; if (filename.endsWith(".java")) { Timer t = new Timer(); UI.printInfo(Module.API, "Compiling \"" + filename + "\" ..."); t.start(); try { try (FileInputStream stream = new FileInputStream(filename)) { api = (SunflowAPI) ClassBodyEvaluator.createFastClassBodyEvaluator(new Scanner(filename, stream), SunflowAPI.class, ClassLoader.getSystemClassLoader()); } } catch (CompileException e) { UI.printError(Module.API, COULDNT_MESSAGE_FORMAT, filename); UI.printError(Module.API, "%s", e.getMessage()); return null; } catch (IOException e) { UI.printError(Module.API, COULDNT_MESSAGE_FORMAT, filename); UI.printError(Module.API, "%s", e.getMessage()); return null; } t.end(); UI.printInfo(Module.API, "Compile time: " + t.toString()); // allow relative paths String currentFolder = new File(filename).getAbsoluteFile().getParentFile().getAbsolutePath(); api.includeSearchPath.addSearchPath(currentFolder); api.textureSearchPath.addSearchPath(currentFolder); UI.printInfo(Module.API, "Build script running ..."); t.start(); api.currentFrame(frameNumber); api.build(); t.end(); UI.printInfo(Module.API, "Build script time: %s", t.toString()); } else { api = new SunflowAPI(); api = api.include(filename) ? api : null; } return api; } /** * Translate specfied file into the native sunflow scene file format. * * @param filename input filename * @param outputFilename output filename * @return true upon success, false otherwise */ public static boolean translate(String filename, String outputFilename) { FileSunflowAPI api; try { if (outputFilename.endsWith(".sca")) { api = new AsciiFileSunflowAPI(outputFilename); } else if (outputFilename.endsWith(".scb")) { api = new BinaryFileSunflowAPI(outputFilename); } else { UI.printError(Module.API, "Unable to determine output filetype: \"%s\"", outputFilename); return false; } } catch (IOException e) { UI.printError(Module.API, "Unable to create output file - %s", e.getMessage()); return false; } String extension = filename.substring(filename.lastIndexOf('.') + 1); SceneParser parser = PluginRegistry.PARSER_PLUGINS.createObject(extension); if (parser == null) { UI.printError(Module.API, "Unable to find a suitable parser for: \"%s\"", filename); return false; } try { return parser.parse(filename, api); } catch (RuntimeException e) { Logger.getLogger(SunflowAPI.class.getName()).log(Level.SEVERE, null, e); UI.printError(Module.API, "Error occured during translation: %s", e.getMessage()); return false; } finally { api.close(); } } /** * Compile the specified code string via Janino. The code must implement a * build method as described above. The build method is not called on the * output, it is up the caller to do so. * * @param code java code string * @return a valid SunflowAPI object upon succes, null * otherwise. */ public static SunflowAPI compile(String code) { SunflowAPI api = null; try { Timer t = new Timer(); t.start(); try { api = (SunflowAPI) ClassBodyEvaluator.createFastClassBodyEvaluator(new Scanner(null, new StringReader(code)), SunflowAPI.class, (ClassLoader) null); } catch (IOException ex) { Logger.getLogger(SunflowAPI.class.getName()).log(Level.SEVERE, null, ex); } t.end(); UI.printInfo(Module.API, "Compile time: %s", t.toString()); return api; } catch (CompileException e) { UI.printError(Module.API, "%s", e.getMessage()); return null; } } /** * Read the value of the current frame. This value is intended only for * procedural animation creation. It is not used by the Sunflow core in * anyway. The default value is 1. * * @return current frame number */ public int currentFrame() { return currentFrame; } @Override public void currentFrame(int currentFrame) { this.currentFrame = currentFrame; } }