Module generativepy.povray
povray module uses the Povray application to render 3D images.
It uses the Vapory Python library to allow scenes to be described in Python.
This module also provides 3d axes, function plotting, and default camera and light configurations.
Functions
def example_povray_draw_function(pixel_width, pixel_height, frame_no, frame_count)
-
This is an example draw function for use with
make_povray_image()
and similar functions. It is a dummy function used to document the required parameters.Args
pixel_width
- int - The width of the image in pixels.
pixel_height
- int - The height of the image in pixels
frame_no
- int - the number of the current frame. For single images this will always be 0. For animations this
paint function will be called
frame_count
times (once for each frame) withframe_no
incrementing by 1 each time (ie it counts from 0 toframe_count
- 1. frame_count
- int - The total number of frames being created.For single images this will always be 0. For animations this will be set to the total number of frames in the animation.
Returns
The completed povray scene object
def get_color(color)
-
Convert a generativepy
Color
object into a Povray color.A Povray is a list of 4 values, however the alpha component works differently. In generativepy alpha 1 is fully opaque and 0 is fully transparent. In Povray alpha 0 is fully opaque and 1 is fully transparent.
Args
color
Color
object - the color.
Returns
A Povray color as a 4-tuple
def make_povray_frame(draw, width, height)
-
Used to create a single povray image as a frame. A frame is a NumPy array with shape (pixel_height, pixel_width, 3). Povray images are always RGB images.
make_povray_frame()
calls the user supplieddraw
function to create a povray scene. It then renders the image to a NumPy array (a "frame").The draw function must have the signature described for
example_draw_function
.Args
draw
- function - A drawing function object, see below.
width
- int - The width of the image that will be created, in pixels.
height
- int - The height of the image that will be created, in pixels.
Returns
A frame.
def make_povray_frames(draw, width, height, count)
-
Used to sequence of povray images as a frame. A frame is a NumPy array with shape (pixel_height, pixel_width, 3). Povray images are always RGB images.
make_povray_frames()
repetedly calls the user supplieddraw
function to create a series of povray scenes. On each call todraw
, theframe_no
parameter. It runs from 0 rocount
-1.Each image is rendered to a NumPy array (a "frame").
The draw function must have the signature described for
example_draw_function
.Args
draw
- function - A drawing function object, see below.
width
- int - The width of the image that will be created, in pixels.
height
- int - The height of the image that will be created, in pixels.
count
- int - The number of frames to create.
Yield
A lazy iterator returning a sequncve of frames. The number of frames is determined by the
count
parameter. def make_povray_image(outfile, draw, width, height)
-
Used to create a single PNG image of a 3D povray scene.
make_povray_image()
calls the user supplieddraw
function to create a povray scene. It then renders the image to a PNG file.The draw function must have the signature described for
example_draw_function
.Args
outfile
- str - The path and filename for the output PNG file. The '.png' extension is optional,
- it will be added if it isn't present.
draw
- function - A drawing function object, see below.
width
- int - The width of the image that will be created, in pixels.
height
- int - The height of the image that will be created, in pixels.
Classes
class Axes3d
-
Represents a set of 3D axes, including labels.
Expand source code
class Axes3d: """ Represents a set of 3D axes, including labels. """ def __init__(self): self.position = (-2,)*3 self.size = (4,)*3 self._start = (-2,) * 3 self.end = None self._extent = (4,) * 3 self.divisions = (0.5,)*3 self.div_positions = None self.axis_thickness = 0.02 self.color = Color("blue").light1 self.texture = None self.division_formatters = (None,)*3 @property def start(self): """Start of x, y, z range""" return self._start @property def extent(self): """Extent of x, y, z range""" return self._extent def of_start(self, start): """ The start coordinates. Args: start: tuple(number, number, number) - start value of x, y, z axes Returns: self """ self._start = tuple(start) return self def of_extent(self, extent): """ The coordinate extents. Args: extent: tuple(number, number, number) - length of x, y, z axes Returns: self """ self._extent = tuple(extent) return self def with_divisions(self, divisions): """ The division spacing for each axis. Args: divisions: tuple(number, number, number) - division spacing of x, y, z axes Returns: self """ self.divisions = tuple(divisions) return self def with_division_formatters(self, formatters): """ The division label formatters for each axis. A formatter is a function that accepts 2 values: * `value`: number - the division value * `div`: number - the division spacing Args: formatters: tuple(function, function, function) - formatter functions for x, y, z axes. Returns: self """ self.division_formatters = tuple(formatters) return self def transform_from_graph(self, point): """ Transform a point from graph space to Povray space. Args: point: 3-tuple, the point in graph coordinates. Returns: Transformed point as a 3-tuple """ x = ((point[0] - self._start[0]) * self.size[0] / (self.end[0] - self._start[0])) + self.position[0] y = ((point[1] - self._start[1]) * self.size[1] / (self.end[1] - self._start[1])) + self.position[1] z = ((point[2] - self._start[2]) * self.size[2] / (self.end[2] - self._start[2])) + self.position[2] return x, y, z def division_linestyle(self, pattern=Color(0), line_width=None): """ Sets the linestyle for axis division lines Args: pattern: `Color` object - line colour. line_width: number - width of line in Povray units. Returns: self """ self.color = pattern if line_width is not None: self.axis_thickness = line_width return self def _make_xy_planes(self): items = [] xstart = [p for p in self.div_positions[0]] xend = xstart ystart = [self._start[1] for p in self.div_positions[0]] yend = [self.end[1] for p in self.div_positions[0]] zstart = [self._start[2] for p in self.div_positions[0]] zend = zstart for i, _ in enumerate(self.div_positions[0]): start = self.transform_from_graph((xstart[i], ystart[i], zstart[i])) end = self.transform_from_graph((xend[i], yend[i], zend[i])) items.append(Cylinder(start, end, self.axis_thickness, self.texture)) xstart = [self._start[0] for p in self.div_positions[1]] xend = [self.end[0] for p in self.div_positions[1]] ystart = [p for p in self.div_positions[1]] yend = ystart zstart = [self._start[2] for p in self.div_positions[1]] zend = zstart for i, _ in enumerate(self.div_positions[1]): start = self.transform_from_graph((xstart[i], ystart[i], zstart[i])) end = self.transform_from_graph((xend[i], yend[i], zend[i])) items.append(Cylinder(start, end, self.axis_thickness, self.texture)) return items def _make_xz_planes(self): items = [] xstart = [p for p in self.div_positions[0]] xend = xstart ystart = [self._start[1] for p in self.div_positions[0]] yend = ystart zstart = [self._start[2] for p in self.div_positions[0]] zend = [self.end[2] for p in self.div_positions[0]] for i, _ in enumerate(self.div_positions[0]): start = self.transform_from_graph((xstart[i], ystart[i], zstart[i])) end = self.transform_from_graph((xend[i], yend[i], zend[i])) items.append(Cylinder(start, end, self.axis_thickness, self.texture)) xstart = [self._start[0] for p in self.div_positions[2]] xend = [self.end[0] for p in self.div_positions[2]] ystart = [self._start[1] for p in self.div_positions[2]] yend = ystart zstart = [p for p in self.div_positions[2]] zend = zstart for i, _ in enumerate(self.div_positions[2]): start = self.transform_from_graph((xstart[i], ystart[i], zstart[i])) end = self.transform_from_graph((xend[i], yend[i], zend[i])) items.append(Cylinder(start, end, self.axis_thickness, self.texture)) return items def _make_yz_planes(self): items = [] xstart = [self._start[0] for p in self.div_positions[2]] xend = xstart ystart = [self._start[1] for p in self.div_positions[2]] yend = [self.end[1] for p in self.div_positions[2]] zstart = [p for p in self.div_positions[2]] zend = zstart for i, _ in enumerate(self.div_positions[2]): start = self.transform_from_graph((xstart[i], ystart[i], zstart[i])) end = self.transform_from_graph((xend[i], yend[i], zend[i])) items.append(Cylinder(start, end, self.axis_thickness, self.texture)) xstart = [self._start[0] for p in self.div_positions[1]] xend = xstart ystart = [p for p in self.div_positions[1]] yend = ystart zstart = [self._start[2] for p in self.div_positions[1]] zend = [self.end[2] for p in self.div_positions[1]] for i, _ in enumerate(self.div_positions[1]): start = self.transform_from_graph((xstart[i], ystart[i], zstart[i])) end = self.transform_from_graph((xend[i], yend[i], zend[i])) items.append(Cylinder(start, end, self.axis_thickness, self.texture)) return items def _make_axis_box(self): sx, sy, sz = self.position ex, ey, ez = [e + s for e, s in zip(self.size, self.position)] items = [ Cylinder((sx, sy, sz), (ex, sy, sz), self.axis_thickness, self.texture), Cylinder((sx, sy, sz), (sx, ey, sz), self.axis_thickness, self.texture), Cylinder((sx, sy, sz), (sx, sy, ez), self.axis_thickness, self.texture), Cylinder((ex, sy, sz), (ex, ey, sz), self.axis_thickness, self.texture), Cylinder((ex, sy, sz), (ex, sy, ez), self.axis_thickness, self.texture), Cylinder((sx, ey, sz), (ex, ey, sz), self.axis_thickness, self.texture), Cylinder((sx, ey, sz), (sx, ey, ez), self.axis_thickness, self.texture), Cylinder((sx, sy, ez), (ex, sy, ez), self.axis_thickness, self.texture), Cylinder((sx, sy, ez), (sx, ey, ez), self.axis_thickness, self.texture), ] return items def _format_div(self, value, div, formatter=None): """ Formats a division value into a string. If a formatter is supplied it will be used to convert the value top a string. If the division spacing is an integer, the string will be an integer (no dp). If the division spacing is float, the string will be rounded to 3 decimal places Args: value: number - value to be formatted div: number - division spacing formatter: formatting function - accepts vale and div, returns a formatted value string Returns: String representation of the value """ if formatter: return formatter(value, div) if isinstance(value, int): return str(value) return str(round(value*1000)/1000) def _make_text_item(self, text, pos, offset, rotation=(90, 0, 0)): text = Text( "ttf", '"/usr/share/fonts/truetype/msttcorefonts/ariali.ttf"', f'"{text}"', # Povray requires "" around text 0.1, 0, self.texture, "rotate", rotation, "translate", offset, "scale", 0.2, ) return Union(text, "translate", pos,) def _make_labels(self): items = [] for p in self.div_positions[0]: s = self._format_div(p, self.divisions[0], self.division_formatters[0]) p = self.transform_from_graph((p, 0, 0))[0] if -1.8 < p < 1.8: items.append(self._make_text_item(s, (p, 2.3, -2), (-0.5, 0, -1), (90, 0, -90))) for p in self.div_positions[1]: s = self._format_div(p, self.divisions[1], self.division_formatters[1]) p = self.transform_from_graph((0, p, 0))[1] if -1.8 < p < 1.8: items.append(self._make_text_item(s, (2, p, -2), (0, 0, -1))) for p in self.div_positions[2]: s = self._format_div(p, self.divisions[2], self.division_formatters[1]) p = self.transform_from_graph((0, 0, p))[2] if -1.8 < p < 1.8: items.append(self._make_text_item(s, (2, -2, p), (0.5, 0, 0))) return items def _make_axes(self): return Union( *self._make_xy_planes(), *self._make_xz_planes(), *self._make_yz_planes(), *self._make_axis_box(), *self._make_labels(), "rotate", [-90, 0, 0], "translate", [0, 0.5, 0], "no_shadow" ) def _get_divs(self, start, end, div): divs = [] n = math.ceil(start/div)*div while n <= end: divs.append(n) n += div return divs def get(self): """ Gets the axes object Returns: A Vapory Union object thtat draws the axes """ self.texture = Texture(Pigment("color", get_color(self.color)), Finish("ambient", 1, "diffuse", 0)) self.end = [ex + s for ex, s in zip(self._extent, self._start)] self.div_positions = [self._get_divs(self._start[i], self.end[i], self.divisions[i]) for i in range(3)] return self._make_axes()
Instance variables
prop extent
-
Extent of x, y, z range
Expand source code
@property def extent(self): """Extent of x, y, z range""" return self._extent
prop start
-
Start of x, y, z range
Expand source code
@property def start(self): """Start of x, y, z range""" return self._start
Methods
def division_linestyle(self, pattern=<generativepy.color.Color object>, line_width=None)
-
Sets the linestyle for axis division lines
Args
pattern
Color
object - line colour.line_width
- number - width of line in Povray units.
Returns
self
def get(self)
-
Gets the axes object
Returns
A Vapory Union object thtat draws the axes
def of_extent(self, extent)
-
The coordinate extents.
Args
extent
- tuple(number, number, number) - length of x, y, z axes
Returns
self
def of_start(self, start)
-
The start coordinates.
Args
start
- tuple(number, number, number) - start value of x, y, z axes
Returns
self
def transform_from_graph(self, point)
-
Transform a point from graph space to Povray space.
Args
point
- 3-tuple, the point in graph coordinates.
Returns
Transformed point as a 3-tuple
def with_division_formatters(self, formatters)
-
The division label formatters for each axis.
A formatter is a function that accepts 2 values:
value
: number - the division valuediv
: number - the division spacing
Args
formatters
- tuple(function, function, function) - formatter functions for x, y, z axes.
Returns
self
def with_divisions(self, divisions)
-
The division spacing for each axis.
Args
divisions
- tuple(number, number, number) - division spacing of x, y, z axes
Returns
self
class Camera3d
-
Creates a Povray camera, at a particlular location, looking at the origin
Expand source code
class Camera3d: """ Creates a Povray camera, at a particlular location, looking at the origin """ def __init__(self): self.x = 5 self.y = 0 self.z = 0 self.lookat = (0, 0, 0) def position(self, x, y, z): """ Set the position in x, y, z space Args: x: number - the x position of the camera. y: number - the y position of the camera. z: number - the y position of the camera. Returns: self """ self.x = x self.y = y self.z = z return self def polar_position(self, distance, angle, elevation): """ Set the position in polar coordinates Args: distance: number - the distance of the camera from the origin. angle: number - the horizontal angle of the camera. elevation: number - the elevation angle of the camera. Returns: self """ self.x = distance * math.cos(angle) self.y = distance * math.sin(angle) self.z = distance * math.sin(elevation) return self def standard_plot(self): """ Set the position to view a standard plot Returns: self """ self.x = 5 self.y = 1 self.z = -5 return self def get(self): """ Gets the configured Camera object Returns: A Vapory Camera object """ return Camera( "location", [self.x, self.y, self.z], "look_at", self.lookat )
Methods
def get(self)
-
Gets the configured Camera object
Returns
A Vapory Camera object
def polar_position(self, distance, angle, elevation)
-
Set the position in polar coordinates
Args
distance
- number - the distance of the camera from the origin.
angle
- number - the horizontal angle of the camera.
elevation
- number - the elevation angle of the camera.
Returns
self
def position(self, x, y, z)
-
Set the position in x, y, z space
Args
x
- number - the x position of the camera.
y
- number - the y position of the camera.
z
- number - the y position of the camera.
Returns
self
def standard_plot(self)
-
Set the position to view a standard plot
Returns
self
class Lights3d
-
Creates a set of lights for the scene.
Expand source code
class Lights3d: """ Creates a set of lights for the scene. """ def __init__(self): self.lights = () def standard(self, color=Color(1)): """ Adds two lights at fixed positions, suitable for a typical scene. Args: color: `Color` object - the light colour, default white. Returns: self """ self.lights = ( LightSource([0, 0, 0], "color", get_color(color), "translate", [5, 5, 5]), LightSource([0, 0, 0], "color", get_color(color), "translate", [5, 5, -5]) ) return self def standard_plot(self, color=Color(1)): """ Adds two lights at fixed positions, suitable for a standard 3D plot. Args: color: `Color` object - the light colour, default white. Returns: self """ self.lights = ( LightSource([0, 0, 0], "color", get_color(color), "translate", [5, 5, 5]), LightSource([0, 0, 0], "color", get_color(color), "translate", [5, 5, -5]) ) return self def get(self): """ Gets the configured Light objects Returns: A tuple of Vapory Light objects """ return self.lights
Methods
def get(self)
-
Gets the configured Light objects
Returns
A tuple of Vapory Light objects
def standard(self, color=<generativepy.color.Color object>)
-
Adds two lights at fixed positions, suitable for a typical scene.
Args
color
Color
object - the light colour, default white.
Returns
self
def standard_plot(self, color=<generativepy.color.Color object>)
-
Adds two lights at fixed positions, suitable for a standard 3D plot.
Args
color
Color
object - the light colour, default white.
Returns
self
class Plot3dZofXY (axes)
-
Expand source code
class Plot3dZofXY: def __init__(self, axes): self.axes = axes self.start = (axes.start[0], axes.start[1]) self.end = (axes.extent[0] + axes.start[0], axes.extent[1] + axes.start[1]) self.steps = 40 self.grid_factor = 5 self.func = lambda x, y: math.cos(math.sqrt((x**2 + y**2))*2) self.color = Color("lightgreen") self.line_color = Color("green") self.line_thickness = 0.02 def function(self, f): self.func = f return self def grid_linestyle(self, pattern=Color(0), line_width=None): """ Sets the linestyle for plot mesh lines Args: pattern: `Color` object - line colour. line_width: number 0 width of line in Povray units. Returns: self """ self.line_color = pattern if line_width is not None: self.line_thickness = line_width return self def fill(self, pattern=Color(0)): """ Sets the fill for the plot Args: pattern: - `Color` object, fill colour. Returns: self """ self.color = pattern return self def _convert_points(self, x, y, z): self.axes.end = [ex + s for ex, s in zip(self.axes.extent, self.axes.start)] x = ((x - self.axes._start[0]) * self.axes.size[0] / (self.axes.end[0] - self.axes._start[0])) + self.axes.position[0] y = ((y - self.axes._start[1]) * self.axes.size[1] / (self.axes.end[1] - self.axes._start[1])) + self.axes.position[1] z = ((z - self.axes._start[2]) * self.axes.size[2] / (self.axes.end[2] - self.axes._start[2])) + self.axes.position[2] return x, y, z def get(self): """ Gets the plot object Returns: A Vapory Union object that draws the plot """ x = np.linspace(self.start[0], self.end[0], self.steps) y = np.linspace(self.start[1], self.end[1], self.steps) xx, yy = np.meshgrid(x, y) vf = np.vectorize(self.func) ff = vf(xx, yy) vf = np.vectorize(self._convert_points) xx, yy, ff = vf(xx, yy, ff) mesh = ["mesh {\n"] for i in range(self.steps - 1): for j in range(self.steps - 1): mesh.append("triangle {" f"<{xx[i+1, j]}, {yy[i+1, j]}, {ff[i+1, j]}>,<{xx[i, j]}, {yy[i, j]}, {ff[i, j]}>,<{xx[i, j+1]}, {yy[i, j+1]}, {ff[i, j+1]}>"+ "}\n") mesh.append("triangle {" f"<{xx[i+1, j+1]}, {yy[i+1, j+1]}, {ff[i+1, j+1]}>,<{xx[i+1, j]}, {yy[i+1, j]}, {ff[i+1, j]}>,<{xx[i, j+1]}, {yy[i, j+1]}, {ff[i, j+1]}>"+ "}\n") texture = Texture(Pigment("color", get_color(self.color)), Finish("ambient", 0.5, "diffuse", 0.5)) mesh.append(str(texture)) mesh.append("rotate <-90, 0, 0> translate<0, 0.5, 0>}") squares = " ".join(mesh) grid = ["union {\n"] for i in range(self.steps - 1): for j in range(self.steps - 1): if not j % self.grid_factor: grid.append("cylinder {" f"<{xx[i + 1, j]}, {yy[i + 1, j]}, {ff[i + 1, j]}>,<{xx[i, j]}, {yy[i, j]}, {ff[i, j]}>,{self.line_thickness}"+ "}\n") if not i % self.grid_factor: grid.append("cylinder {" f"<{xx[i, j + 1]}, {yy[i, j + 1]}, {ff[i, j + 1]}>,<{xx[i, j]}, {yy[i, j]}, {ff[i, j]}>,{self.line_thickness}"+ "}\n") texture = Texture(Pigment("color", get_color(self.line_color)), Finish("ambient", 1)) grid.append(str(texture)) grid.append("rotate <-90, 0, 0> translate<0, 0.5, 0>}") lines = " ".join(grid) return " ".join(("union {", squares, lines, "}"))
Methods
def fill(self, pattern=<generativepy.color.Color object>)
-
Sets the fill for the plot
Args
pattern
-
Color
object, fill colour.
Returns
self
def function(self, f)
def get(self)
-
Gets the plot object
Returns
A Vapory Union object that draws the plot
def grid_linestyle(self, pattern=<generativepy.color.Color object>, line_width=None)
-
Sets the linestyle for plot mesh lines
Args
pattern
Color
object - line colour.line_width
- number 0 width of line in Povray units.
Returns
self
class Scene3d
-
Creates a 3D Vapory scene, adding a camera, lights, and a collection of objects.
Expand source code
class Scene3d: """ Creates a 3D Vapory scene, adding a camera, lights, and a collection of objects. """ def __init__(self): self.camera_item = None self.background_color = Color(1) self.content = [] def camera(self, camera): """ Add a camera. Args: camera: Vapory camera object - the camera. Returns: self """ self.camera_item = camera return self def background(self, color): self.background_color = Color(1) return self def add(self, items): """ Add a set of items. Items can include 3D models and lights. Args: items: tuple of Vapory items - the scene items. Returns: self """ self.content.extend(items) return self def get(self): """ Gets the configured Scene object Returns: A Vapory Scene object """ return Scene(self.camera_item, [Background("color", get_color(self.background_color))] + self.content)
Methods
def add(self, items)
-
Add a set of items. Items can include 3D models and lights.
Args
items
- tuple of Vapory items - the scene items.
Returns
self
def background(self, color)
def camera(self, camera)
-
Add a camera.
Args
camera
- Vapory camera object - the camera.
Returns
self
def get(self)
-
Gets the configured Scene object
Returns
A Vapory Scene object