#!/usr/bin/env python3 ######################################################################### # Copyright (C) 2011-2012 Tavian Barnes # # # # This file is part of Dimension. # # # # Dimension 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. # # # # Dimension 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 this program. If not, see . # ######################################################################### import argparse import re import os import sys import threading from contextlib import contextmanager from dimension import * def main(): """Invoke the client from the command line.""" # Parse the command line parser = DimensionArgumentParser( epilog = "@PACKAGE_STRING@\n" "@PACKAGE_URL@\n" "Copyright (C) 2009-2011 Tavian Barnes <@PACKAGE_BUGREPORT@>\n" "Licensed under the GNU General Public License", formatter_class = argparse.RawDescriptionHelpFormatter, conflict_handler = "resolve", # For -h as height instead of help ) parser.add_argument("-V", "--version", action = "version", version = "@PACKAGE_STRING@") parser.add_argument("-w", "--width", action = "store", type = int, default = 768, help = "image width (default: %(default)s)") parser.add_argument("-h", "--height", action = "store", type = int, default = 480, help = "image height (default: %(default)s)") parser.add_argument("--region", action = "store", type = str, help = "subregion to render, as \"(x1, y1)->(x2, y2)\"") parser.add_argument("-v", "--verbose", action = "store_true", help = "print more information") parser.add_argument("-q", "--quiet", action = "store_true", help = "print less information") parser.add_argument("--threads", action = "store", type = int, help = "the number of threads to render with") parser.add_argument("--quality", action = "store", type = str, help = "the scene quality") parser.add_argument("--adc-bailout", action = "store", type = str, help = "the ADC bailout (default: 1/255)") parser.add_argument("-o", "--output", action = "store", type = str, help = "the output image file") parser.add_argument("input", action = "store", type = str, help = "the input scene description file") parser.add_argument("-p", "--preview", action = "store_true", help = "display a preview while the image renders") # Debugging/testing options parser.add_argument("--strict", action = "store_true", help = argparse.SUPPRESS) args = parser.parse_args() # Calculate subregion calculate_subregion(args) # Default output is basename(input).png if args.output is None: noext = os.path.splitext(os.path.basename(args.input))[0] args.output = noext + ".png" # Handle the --strict option die_on_warnings(args.strict) # Sandbox dictionary for the scene sandbox = { } sandbox.update(__import__("dimension").__dict__) sandbox.update(__import__("math").__dict__) # Defaults/available variables sandbox.update({ "image_width" : args.width, "image_height" : args.height, "objects" : [], "lights" : [], "camera" : PerspectiveCamera(), "default_texture" : Texture(finish = Ambient(sRGB(0.1)) + Diffuse(sRGB(0.7))), "default_interior" : Interior(), "background" : Black, "recursion_limit" : None, }) # Execute the input script if not args.quiet: print("Parsing scene ...") # Run with the script's dirname as the working directory workdir = os.path.dirname(os.path.abspath(args.input)) parse_timer = Timer() with open(args.input) as fh, working_directory(workdir): exec(compile(fh.read(), args.input, "exec"), sandbox) parse_timer.stop() # Make the canvas canvas = Canvas(width = args.region_width, height = args.region_height) canvas.optimize_PNG() if args.preview: canvas.optimize_GL() # Make the scene object scene = Scene(canvas = canvas, objects = sandbox["objects"], lights = sandbox["lights"], camera = sandbox["camera"]) scene.region_x = args.region_x scene.region_y = args.region_y scene.outer_width = args.width scene.outer_height = args.height scene.default_texture = sandbox["default_texture"] scene.default_interior = sandbox["default_interior"] scene.background = sandbox["background"] if sandbox["recursion_limit"] is not None: scene.recursion_limit = sandbox["recursion_limit"] if args.threads is not None: scene.nthreads = args.threads if args.quality is not None: scene.quality = args.quality if args.adc_bailout is not None: pattern = r"^(.*)/(.*)$" match = re.match(pattern, args.adc_bailout) if match is not None: args.adc_bailout = float(match.group(1))/float(match.group(2)) scene.adc_bailout = float(args.adc_bailout) # Ray-trace the scene with scene.render_async() as future: bar = None if not args.quiet: if scene.nthreads == 1: render_message = "Rendering scene" else: render_message = "Rendering scene (using %d threads)" % scene.nthreads bar = progress_bar_async(render_message, future) if args.preview: from dimension import preview preview.show_preview(canvas, future) if bar is not None: join_progress_bar(bar) # Write the output file export_timer = Timer() with canvas.write_PNG_async(args.output) as future: if not args.quiet: progress_bar("Writing %s" % args.output, future) export_timer.stop() # Print execution times if args.verbose: print() print("Parsing time: ", parse_timer) print("Bounding time: ", scene.bounding_timer) print("Rendering time: ", scene.render_timer) print("Exporting time: ", export_timer) class DimensionArgumentParser(argparse.ArgumentParser): """ Specialized parser to print --version output to stdout rather than stderr. """ def exit(self, status = 0, message = None): if message: file = sys.stdout if status == 0 else sys.stderr file.write(message) sys.exit(status) def calculate_subregion(args): if args.region is None: args.region_x = 0 args.region_y = 0 args.region_width = args.width args.region_height = args.height else: pattern = r"^\s*\(\s*(\d*)\s*,\s*(\d*)\s*\)\s*->\s*\(\s*(\d*)\s*,\s*(\d*)\s*\)\s*$" match = re.match(pattern, args.region) if match is None: raise RuntimeError("range specified in invalid format.") args.region_x = int(match.group(1)) args.region_y = int(match.group(2)) region_xmax = int(match.group(3)) region_ymax = int(match.group(4)) args.region_width = region_xmax - args.region_x args.region_height = region_ymax - args.region_y if args.region_width <= 0 or args.region_height <= 0: raise RuntimeError("region is degenerate.") if region_xmax >= args.width or region_ymax > args.height: raise RuntimeError("region exceeds bounds of image.") @contextmanager def working_directory(newwd): """Change the working directory within a with statement.""" oldwd = os.getcwd() try: os.chdir(newwd) yield finally: os.chdir(oldwd) def progress_bar(str, future): """Display a progress bar while a Future completes.""" print(str, end = " ") sys.stdout.flush() term_width = terminal_width() width = term_width - (len(str) + 1)%term_width for i in range(width): future.wait((i + 1)/width) print(".", end = "") sys.stdout.flush() print() sys.stdout.flush() def progress_bar_async(str, future): thread = threading.Thread(target = progress_bar, args = (str, future)) thread.start() return (thread, future) def join_progress_bar(bar): """Handle joining a progress bar thread in a way responsive to ^C.""" thread = bar[0] future = bar[1] try: thread.join() except: future.cancel() thread.join() raise