Source code for capytaine.ui.vtk.animation

#!/usr/bin/env python
# coding: utf-8
"""VTK animation for the free surface elevation."""
# Copyright (C) 2017-2019 Matthieu Ancellin
# See LICENSE file at <https://github.com/mancellin/capytaine>

import logging

import numpy as np
from numpy import pi

from capytaine.ui.vtk.helpers import compute_node_data, compute_vtk_polydata
from capytaine.tools.optional_imports import import_optional_dependency

vtk = import_optional_dependency("vtk")

LOG = logging.getLogger(__name__)


[docs]class Animation: """Class to generate an animation of a result of Capytaine, including the elevation of the free surface. The animation is made of a short loop of a single period of the solution in frequency domain. Parameters ---------- loop_duration: float Duration in the loop. For real time animation, the period of the motion. fps: int, optional Number of frames per second in the animation (default: 24). Attributes ---------- frames_per_loop: int Number of frames in one loop actors: list of vtk actor objects The objects in the scene. """ def __init__(self, loop_duration, fps=24): self.fps = fps self.frames_per_loop = int(fps * loop_duration) self.actors = [] self._precomputed_polydatas = {} self._current_frame = 0 # @classmethod # def from_result(self, result): # from capytaine.bodies import TRANSLATION_DOFS_DIRECTIONS, ROTATION_DOFS_AXIS # # if display_dof.lower() in TRANSLATION_DOFS_DIRECTIONS: # direction = np.asarray(TRANSLATION_DOFS_DIRECTIONS[display_dof.lower()]) # def translation_motion(self, frame): # nonlocal direction # pos = np.asarray(self.body_actor.GetPosition()) # pos = (1 - direction) * pos + \ # direction * np.cos(2*np.pi*(frame % self.frames_per_loop)/self.frames_per_loop) # self.body_actor.SetPosition(*pos) # self.update_body_position = translation_motion # # elif display_dof.lower() in ROTATION_DOFS_AXIS: # direction = np.asarray(ROTATION_DOFS_AXIS[display_dof.lower()]) # def rotation_motion(self, frame): # nonlocal direction # pos = np.asarray(self.body_actor.GetOrientation()) # pos = (1 - direction) * pos + \ # direction * np.cos(2*np.pi*(frame % self.frames_per_loop)/self.frames_per_loop) # self.body_actor.SetOrientation(*pos) # self.update_body_position = rotation_motion def _add_actor(self, mesh, faces_motion=None, faces_colors=None, edges=False): """Add an animated object to the scene.""" if faces_motion is not None: nodes_motion = compute_node_data(mesh.merged(), faces_motion) else: nodes_motion = None base_polydata = compute_vtk_polydata(mesh.merged()) mapper = vtk.vtkPolyDataMapper() mapper.SetInputData(base_polydata) actor = vtk.vtkActor() if edges: actor.GetProperty().EdgeVisibilityOn() actor.GetProperty().SetInterpolationToGouraud() actor.SetMapper(mapper) if nodes_motion is not None or faces_colors is not None: LOG.info(f"Precompute motions of {mesh.name} before animation.") self._precomputed_polydatas[actor] = [] for i_frame in range(self.frames_per_loop): new_polydata = vtk.vtkPolyData() new_polydata.DeepCopy(base_polydata) if nodes_motion is not None: # Change points positions at frame i current_deformation = ( np.abs(nodes_motion)*np.cos(np.angle(nodes_motion)-2*pi*i_frame/self.frames_per_loop) ) points = new_polydata.GetPoints() for j in range(mesh.nb_vertices): point = points.GetPoint(j) point = np.asarray(point) + current_deformation[j] points.SetPoint(j, tuple(point)) if faces_colors is not None: # Evaluate scalar field at frame i current_colors = ( np.abs(faces_colors)*np.cos(np.angle(faces_colors)-2*pi*i_frame/self.frames_per_loop) ) max_val = max(abs(faces_colors)) vtk_faces_colors = vtk.vtkFloatArray() for i, color in enumerate(current_colors): vtk_faces_colors.InsertValue(i, (color+max_val)/(2*max_val)) new_polydata.GetCellData().SetScalars(vtk_faces_colors) self._precomputed_polydatas[actor].append(new_polydata) else: self._precomputed_polydatas[actor] = None self.actors.append(actor) return actor
[docs] def add_body(self, body, faces_motion=None, faces_colors=None, edges=False): """Add an floating body to the scene. Parameters ---------- body: FloatingBody The object to include in the scene. faces_motion: dof, optional The motion of the body defined at the center of the faces. faces_colors: iterable of complex numbers, optional Scalar field over the surface of the body that should be displayed with colors. edges: bool, optional Draw the edges of the mesh in the scene. Returns ------- vtk actor object """ actor = self._add_actor(body.mesh.merged(), faces_motion=faces_motion, faces_colors=faces_colors, edges=edges) if faces_colors is None: actor.GetProperty().SetColor((1, 1, 0)) else: lut = vtk.vtkLookupTable() lut.SetNumberOfColors(50) lut.SetHueRange(0, 0.6) lut.SetSaturationRange(0.5, 0.5) lut.SetValueRange(0.8, 0.8) lut.Build() actor.GetMapper().SetLookupTable(lut) return actor
[docs] def add_free_surface(self, free_surface, faces_elevation): """Add the free surface to the scene. Parameters ---------- free_surface: FreeSurface The free surface object faces_elevation: array of complex numbers The elevation of each face of the meshed free surface given as a complex number. Returns ------- vtk actor object """ faces_motion = np.array([(0, 0, elevation) for elevation in faces_elevation]) actor = self._add_actor(free_surface.mesh, faces_motion=faces_motion, faces_colors=faces_motion[:, 2]) lut = vtk.vtkLookupTable() lut.SetNumberOfColors(50) lut.SetHueRange(0.58, 0.58) lut.SetSaturationRange(0.5, 0.5) lut.SetValueRange(0.4, 0.6) lut.Build() actor.GetMapper().SetLookupTable(lut) return actor
def _callback(self, renderer, event): for actor in self.actors: if self._precomputed_polydatas[actor] is not None: actor.GetMapper().SetInputData(self._precomputed_polydatas[actor][self._current_frame % self.frames_per_loop]) renderer.GetRenderWindow().Render() self._current_frame += 1
[docs] def run(self, camera_position=(-10.0, -10.0, 10.0), resolution=(1280, 720)): """Run the animation. Parameters ---------- camera_position: 3-ple of floats, optional The starting position of the camera in the scene. resolution: 2-ple of ints, optional Resolution of the video in pixels. """ # Setup a renderer, render window, and interactor renderer = vtk.vtkRenderer() renderer.SetBackground(1, 1, 1) # Background color white for actor in self.actors: renderer.AddActor(actor) renderer.Modified() camera = vtk.vtkCamera() camera.SetPosition(*camera_position) camera.SetFocalPoint(0, 0, 0) camera.SetViewUp(0, 0, 1) renderer.SetActiveCamera(camera) render_window = vtk.vtkRenderWindow() render_window.SetSize(*resolution) render_window.SetWindowName("Capytaine animation") render_window.AddRenderer(renderer) render_window_interactor = vtk.vtkRenderWindowInteractor() render_window_interactor.SetRenderWindow(render_window) render_window_interactor.GetInteractorStyle().SetCurrentStyleToTrackballCamera() render_window.Render() render_window_interactor.Initialize() # Initialize must be called prior to creating timer events. render_window_interactor.AddObserver('TimerEvent', self._callback) render_window_interactor.CreateRepeatingTimer(int(1000 / self.fps)) render_window_interactor.Start() # Run until stopped by user. del render_window_interactor del render_window
[docs] def save(self, filepath, nb_loops=1, camera_position=(-10.0, -10.0, 10.0), resolution=(1280, 720)): """Save the animation in a video file. Parameters ---------- filepath: string Path of the output file. nb_loop: int, optional Number of periods to save in the file. camera_position: 3-ple of floats, optional The starting position of the camera in the scene. resolution: 2-ple of ints, optional Resolution of the video in pixels. """ renderer = vtk.vtkRenderer() renderer.SetBackground(1, 1, 1) # Background color white for actor in self.actors: renderer.AddActor(actor) renderer.Modified() camera = vtk.vtkCamera() camera.SetPosition(*camera_position) camera.SetFocalPoint(0, 0, 0) camera.SetViewUp(0, 0, 1) renderer.SetActiveCamera(camera) render_window = vtk.vtkRenderWindow() render_window.SetSize(*resolution) render_window.OffScreenRenderingOn() render_window.AddRenderer(renderer) image_filter = vtk.vtkWindowToImageFilter() image_filter.SetInput(render_window) image_filter.SetInputBufferTypeToRGB() image_filter.ReadFrontBufferOff() writer = vtk.vtkOggTheoraWriter() writer.SetInputConnection(image_filter.GetOutputPort()) writer.SetFileName(filepath) writer.SetRate(self.fps) writer.Start() for i_frame in range(nb_loops*self.frames_per_loop): self._callback(renderer, None) image_filter.Modified() writer.Write() writer.End() render_window.Finalize() del image_filter del writer del render_window
[docs] def embed_in_notebook(self, resolution=(640, 360), **kwargs): from tempfile import mkstemp from IPython.core.display import Video class CustomIPythonVideo(Video): def __init__(self, *args, html_attributes="controls", **kwargs): self.html_attributes = html_attributes super(CustomIPythonVideo, self).__init__(*args, **kwargs) def _repr_html_(self): import mimetypes from binascii import b2a_hex, b2a_base64, hexlify width = height = '' if self.width: width = ' width="%d"' % self.width if self.height: height = ' height="%d"' % self.height # External URLs and potentially local files are not embedded into the # notebook output. if not self.embed: url = self.url if self.url is not None else self.filename output = """<video src="{0}" {1} {2} {3}> Your browser does not support the <code>video</code> element. </video>""".format(url, self.html_attributes, width, height) return output # Embedded videos are base64-encoded. mimetype = self.mimetype if self.filename is not None: if not mimetype: mimetype, _ = mimetypes.guess_type(self.filename) with open(self.filename, 'rb') as f: video = f.read() else: video = self.data if isinstance(video, str): # unicode input is already b64-encoded b64_video = video else: b64_video = b2a_base64(video).decode('ascii').rstrip() output = """<video {0} {1} {2}> <source src="data:{3};base64,{4}" type="{3}"> Your browser does not support the video tag. </video>""".format(self.html_attributes, width, height, mimetype, b64_video) return output filepath = mkstemp(suffix=".ogv")[1] self.save(filepath, nb_loops=1, resolution=resolution, **kwargs) return CustomIPythonVideo(filepath, embed=True, width=resolution[0], html_attributes="controls loop autoplay")