Source code for diffsim.visualizer

"""
Visualization utilities using Polyscope

Interactive 3D visualization for simulations via Polyscope. Supports:
- Real-time visualization of tetrahedral meshes
- Playback controls (play/pause, step, reset)
- Display of energy values and material properties
- Offline frame capture
- Volume mesh rendering
"""

import polyscope as ps
import numpy as np


[docs] class PolyscopeVisualizer: """ Interactive 3D visualizer for physics simulations using Polyscope This class provides real-time visualization of tetrahedral mesh deformation during physics simulation. It displays the mesh as a volume with proper shading, shows simulation statistics, and provides interactive controls. Features: - **Interactive controls**: Play/pause, step-by-step execution, reset - **Real-time statistics**: Time, step count, energy values - **Material display**: Shows Young's modulus and Poisson's ratio - **Solver parameters**: Displays time step, gravity, etc. - Volume rendering via Polyscope Parameters: simulator (Simulator): The simulator instance to visualize window_name (str): Window title (default: "DiffSim Physics Simulator") Attributes: simulator (Simulator): Reference to the simulator mesh (ps.VolumeMesh): Polyscope volume mesh object is_playing (bool): Whether simulation is currently running show_wireframe (bool): Whether to show mesh edges Example: >>> sim = Simulator(mesh, material, solver) >>> viz = PolyscopeVisualizer(sim) >>> viz.run(max_steps=1000, steps_per_frame=5) """
[docs] def __init__(self, simulator, window_name="DiffSim Physics Simulator"): """ Initialize visualizer Args: simulator: Simulator object window_name: window title """ self.simulator = simulator self.window_name = window_name # Initialize polyscope ps.init() ps.set_ground_plane_mode("tile") # Show ground plane as a tile grid ps.set_ground_plane_height(0.0) # Align with y=0 ps.set_up_dir("y_up") # Set Y as up direction # Register mesh self._register_mesh() # Animation control self.is_playing = False self.show_wireframe = True
def _register_mesh(self): """Register the mesh with polyscope (as volume mesh)""" # Register as tetrahedral volume mesh for proper visualization vertices = self.simulator.positions.cpu().numpy() tetrahedra = self.simulator.mesh.tetrahedra.cpu().numpy() self.mesh = ps.register_volume_mesh( "simulation_mesh", vertices, tetrahedra, enabled=True ) # Add some nice visualization options self.mesh.set_color((0.3, 0.6, 0.9)) self.mesh.set_edge_width(1.0) self.mesh.set_material("wax")
[docs] def update(self): """Update visualization with current simulation state""" vertices = self.simulator.positions.cpu().numpy() self.mesh.update_vertex_positions(vertices)
[docs] def run(self, max_steps=None, steps_per_frame=1): """ Run interactive visualization loop Args: max_steps: maximum number of simulation steps (None for infinite) steps_per_frame: number of simulation steps per frame """ step_count = 0 def callback(): nonlocal step_count # UI controls if ps.imgui.Button("Play/Pause"): self.is_playing = not self.is_playing ps.imgui.SameLine() if ps.imgui.Button("Step"): self.simulator.step() self.update() step_count += 1 ps.imgui.SameLine() if ps.imgui.Button("Reset"): self.simulator.reset() self.update() step_count = 0 # Display info ps.imgui.Text(f"Time: {self.simulator.time:.3f} s") ps.imgui.Text(f"Steps: {step_count}") ps.imgui.Text(f"Vertices: {self.simulator.mesh.num_vertices}") ps.imgui.Text(f"Elements: {self.simulator.mesh.num_elements}") # Energy display ke, ee, ge = self.simulator.compute_energy() ps.imgui.Text(f"Kinetic Energy: {ke:.2f} J") ps.imgui.Text(f"Elastic Energy: {ee:.2f} J") ps.imgui.Text(f"Total Energy: {ke + ee + ge:.2f} J") # Material parameters ps.imgui.Separator() ps.imgui.Text("Material Properties:") ps.imgui.Text(f"Young's Modulus: {self.simulator.material.E:.2e} Pa") ps.imgui.Text(f"Poisson's Ratio: {self.simulator.material.nu:.3f}") # Simulation parameters ps.imgui.Separator() ps.imgui.Text("Solver Parameters:") ps.imgui.Text(f"Time Step: {self.simulator.solver.dt:.4f} s") g_val = ( self.simulator.solver.gravity_value if self.simulator.solver.gravity is None else self.simulator.solver.gravity[1].item() ) ps.imgui.Text(f"Gravity: {g_val:.2f} m/s²") # Run simulation if playing if self.is_playing: for _ in range(steps_per_frame): if max_steps is None or step_count < max_steps: self.simulator.step() step_count += 1 self.update() # Set callback and show ps.set_user_callback(callback) ps.show()
[docs] def animate_offline(self, num_steps, output_frames=None): """ Run simulation and optionally save frames Args: num_steps: number of simulation steps output_frames: if not None, save frames to this directory """ for i in range(num_steps): self.simulator.step() self.update() if output_frames is not None and i % 10 == 0: ps.screenshot(f"{output_frames}/frame_{i:05d}.png") if i % 100 == 0: print(f"Step {i}/{num_steps}")