Source code for plato.draw.povray.Scene

import multiprocessing
import os
import subprocess
import tempfile

import numpy as np

from ... import draw
from ... import math
from ... import __version__

[docs]class Scene(draw.Scene): __doc__ = (draw.Scene.__doc__ or '') + """ This Scene supports the following features: * *antialiasing*: Enable antialiasing using the given value (default 0.3). * *ambient_light*: Enable trivial ambient lighting. The given value indicates the magnitude of the light (in [0, 1]). * *directional_light*: Add directional lights. The given value indicates the magnitude*direction normal vector. * *multithreading*: Enable multithreaded rendering. The given value indicates the number of threads to use. * *transparent_background*: Render with a transparent background when calling save() or show() """
[docs] def render(self): """Render all the shapes in this scene. :returns: povray string representing the entire scene """ lines = [] size_pixels = np.round(self.size_pixels).astype(np.int32) lines.append('// povray +W{} +H{}'.format(*size_pixels)) lines.append('// generated by plato v{}'.format(__version__)) background = (1, 1, 1) lines.append('background {{color rgb <{},{},{}>}}'.format(*background)) lines.extend(self.render_camera()) lines.extend(self.render_lights()) shapeIndex = 0 for i, prim in enumerate(self._primitives): lines.extend(prim.render( translation=self.translation, rotation=self.rotation, name_suffix=i)) return '\n'.join(lines)
def render_camera(self): (width, height) = self.size/self.zoom dz = np.linalg.norm(self.size/self.zoom) camera = ('camera {{ orthographic location <0, 0, {dz}> up <0, {height}, 0> ' 'right <{width}, 0, 0> look_at <0, 0, 0> }}').format( dz=dz, height=height, width=-width) return [camera] def render_lights(self): result = [] result.append('global_settings {ambient_light rgb <0, 0, 0>}') # adjust povray lights to be of the same intensity as other lights light_scale = 5./3 if 'ambient_light' in self.enabled_features: config = self.get_feature_config('ambient_light') magnitude = config.get('value', 0.25)*light_scale (width, height) = self.size/self.zoom dz = np.sqrt(np.sum(width**2 + height**2)) pos = (0, 0, 2*dz) basis0 = (4*dz, 0, 0) basis1 = (0, 4*dz, 0) light = ('light_source {{ <{pos[0]}, {pos[1]}, {pos[2]}> color ' 'rgb<{mag}, {mag}, {mag}> ' 'area_light <{basis0[0]}, {basis0[1]}, {basis0[2]}>, ' '<{basis1[0]}, {basis1[1]}, {basis1[2]}>, 5, 5 ' 'adaptive 1 jitter shadowless }}').format( pos=pos, mag=magnitude, basis0=basis0, basis1=basis1) result.append(light) if 'directional_light' in self.enabled_features: config = self.get_feature_config('directional_light') lights = config.get('value', (.25, .5, -1)) lights = np.atleast_2d(lights).astype(np.float32) dz = np.sqrt(np.sum((self.size/self.zoom)**2)) for direction in lights: magnitude = np.linalg.norm(direction) if magnitude < 1e-6: continue norm = direction/magnitude position = -norm*dz*2 magnitude *= light_scale # we want to rotate the basis vectors, constructed to # be at (0, 0, dz), to be perpendicular to the given # direction halftheta = np.arccos(norm[2])/2 cross = np.cross([0, 0, 1], norm) cross /= np.linalg.norm(cross) if np.any(np.logical_not(np.isfinite(cross))): quat = [1., 0, 0, 0] else: quat = np.array([np.cos(halftheta)] + (np.sin(halftheta)*cross).tolist()) light_length = dz*2 basis0 = np.array([light_length, 0, 0]) basis0 = math.quatrot(quat, basis0) basis1 = np.array([0, light_length, 0]) basis1 = math.quatrot(quat, basis1) light = ('light_source {{ <{pos[0]}, {pos[1]}, {pos[2]}> color ' 'rgb<{mag}, {mag}, {mag}> ' 'area_light <{basis0[0]}, {basis0[1]}, {basis0[2]}>, ' '<{basis1[0]}, {basis1[1]}, {basis1[2]}>, 5, 5 ' 'adaptive 1 jitter }}').format( pos=position, mag=magnitude, basis0=basis0, basis1=basis1) result.append(light) return result
[docs] def show(self): """Render the scene to an image and display using ipython.""" import IPython.display with tempfile.NamedTemporaryFile(suffix='.png') as temp: self.save(temp.name) img = IPython.display.Image(filename=temp.name) return IPython.display.display(img, display_id=str(id(self)))
[docs] def save(self, filename): """Save the scene, either as povray source or a rendered image. :param filename: target filename to save the result into. If filename ends in .pov, save the povray source, otherwise call povray to render the image """ (width, height) = self.size_pixels povstring = self.render() if 'antialiasing' in self.enabled_features: antialiasing = self.get_feature_config('antialiasing').get('value', .3) else: antialiasing=None if 'multithreading' in self.enabled_features: threads = self.get_feature_config('multithreading').get( 'value', multiprocessing.cpu_count()) else: threads = None if 'transparent_background' in self.enabled_features: transparent_background = self.get_feature_config('transparent_background').get( 'value', True) else: transparent_background = False if filename.endswith('.pov'): with open(filename, 'w') as f: f.write(povstring) return 0 else: return self.call_povray( povstring, filename, width, height, antialiasing, threads, transparent_background)
@staticmethod def call_povray(contents, filename, width, height, antialiasing=None, threads=None, transparent_background=False): povfile = filename + '.pov' with open(povfile, 'w') as f: f.write(contents) command = ['povray', '+I{}'.format(povfile), '+O{}'.format(filename), '+W{}'.format(width), '+H{}'.format(height)] if antialiasing: command.append('+A{}'.format(antialiasing)) if threads: command.append('+WT{}'.format(threads)) if transparent_background: command.append('+UA') try: return subprocess.check_call(command) finally: os.remove(povfile) def _repr_png_(self): with tempfile.NamedTemporaryFile(suffix='.png') as temp: self.save(temp.name) with open(temp.name, 'rb') as f: return f.read()