import os from trame.app import get_server from trame.ui.vuetify import SinglePageLayout from trame.widgets import vuetify from trame.widgets import vtk as vtk_widget import vtk import xarray as xr import pyvista as pv import numpy as np import matplotlib.pyplot as plt from vtkmodules.vtkCommonCore import vtkLookupTable from vtkmodules.vtkCommonColor import vtkNamedColors from vtkmodules.vtkCommonDataModel import vtkPlane from vtkmodules.vtkFiltersCore import vtkCutter from vtkmodules.vtkFiltersModeling import vtkOutlineFilter from vtkmodules.vtkRenderingCore import ( vtkActor, vtkPolyDataMapper, vtkRenderer, vtkRenderWindow, vtkRenderWindowInteractor, ) def main(server=None, **kwargs): CURRENT_DIRECTORY = os.path.abspath(os.path.dirname(__file__)) # ----------------------------------------------------------------------------- # NetCDF to VTK # ----------------------------------------------------------------------------- ds = xr.open_dataset(os.path.join(CURRENT_DIRECTORY, "../../../../../Data/Field_20251119_140509_BuildingGroupFloor.nc")) # ----- Load fluid field ----- # Vtk uses right-handed coordinate system, while unity uses left-handed coordinate system. So the # z asix needs to be reversed here. velFieldRaw = ds["velField"][:, ::-1, :, :] nTime, nz, ny, nx, _ = velFieldRaw.shape print(f"Fluid field resolution: {nx=}, {ny=}, {nz=}") # ----- Load dx ----- velXCoor = ds["velX"] dx = float(velXCoor.values[1] - velXCoor.values[0]) print(f"{dx=}") # ----- Load dt ----- timeList = ds["time"] dt = float(timeList.values[1] - timeList.values[0]) dt = round(dt, 2) print(f"{dt=}") # ----- Fluid field at the first stored time step ----- velField0 = velFieldRaw[0, :, :, :, :3] velFieldMag0 = np.linalg.norm(velField0.values, axis=-1).ravel() # ----- Build vtk data ----- grid = pv.ImageData() grid.dimensions = (nx, ny, nz) grid.origin = (-(nx-1) * dx / 2, -(ny-1) * dx / 2 + 10, -(nz-1) * dx / 2) grid.spacing = (dx, dx, dx) grid.point_data["vel_mag"] = velFieldMag0 vtk_producer = vtk.vtkTrivialProducer() vtk_producer.SetOutput(grid) # ----------------------------------------------------------------------------- # PLY Reader (for building model) # ----------------------------------------------------------------------------- modelReader = vtk.vtkPLYReader() # modelReader.SetFileName("Data/SmallBuilding3.ply") modelReader.SetFileName(os.path.join(CURRENT_DIRECTORY, "../../../../Data/Buildings-GroupAll.ply")) modelReader.Update() modelMapper = vtk.vtkPolyDataMapper() modelMapper.SetInputConnection(modelReader.GetOutputPort()) modelActor = vtk.vtkActor() modelActor.SetMapper(modelMapper) transform = vtk.vtkTransform() transform.PostMultiply() # Small building 3 # transform.RotateX(-90.0) # transform.RotateY(50.0) # Building Group transform.Translate(25, 0.2, -12) transform.RotateX(180.0) transform.RotateY(180.0) modelActor.SetUserTransform(transform) # ----------------------------------------------------------------------------- # Render VTK fluid field # ----------------------------------------------------------------------------- renderer = vtkRenderer() renderWindow = vtkRenderWindow() renderWindow.AddRenderer(renderer) renderWindowInteractor = vtkRenderWindowInteractor() renderWindowInteractor.SetRenderWindow(renderWindow) renderWindowInteractor.GetInteractorStyle().SetCurrentStyleToTrackballCamera() # ----- Render outline ----- colors = vtkNamedColors() outline = vtkOutlineFilter() outline.SetInputConnection(vtk_producer.GetOutputPort()) outlineMapper = vtkPolyDataMapper() outlineMapper.SetInputConnection(outline.GetOutputPort()) outlineActor = vtkActor() outlineActor.SetMapper(outlineMapper) outlineActor.GetProperty().SetColor(colors.GetColor3d("White")) # ----- Colormap ----- def create_vtk_lut_from_matplotlib(cmap_name, num_colors=256): mpl_cmap = plt.get_cmap(cmap_name) lut = vtkLookupTable() lut.SetNumberOfTableValues(num_colors) for i in range(num_colors): rgba = mpl_cmap(i / (num_colors - 1)) lut.SetTableValue(i, rgba[0], rgba[1], rgba[2], 1.0) lut.SetRange(0, 15) lut.Build() return lut turbo_lut = create_vtk_lut_from_matplotlib("turbo") # ----- Render a slice ----- center_x = grid.center[0] center_y = grid.center[1] center_z = grid.center[2] plane = vtkPlane() plane.SetOrigin(center_x, center_y, center_z) plane.SetNormal(0, 1, 0) print(f"{grid.center[1]=}") cutter = vtkCutter() cutter.SetInputConnection(vtk_producer.GetOutputPort()) cutter.SetCutFunction(plane) cutter.Update() slice_mapper = vtkPolyDataMapper() slice_mapper.SetInputConnection(cutter.GetOutputPort()) slice_mapper.SetScalarModeToUsePointFieldData() slice_mapper.SelectColorArray("vel_mag") slice_mapper.SetLookupTable(turbo_lut) slice_mapper.SetScalarRange(0, 15) slice_actor = vtkActor() slice_actor.SetMapper(slice_mapper) # Add the actors to the renderer renderer.AddActor(outlineActor) renderer.AddActor(slice_actor) renderer.AddActor(modelActor) renderer.ResetCamera() renderWindow.Render() # ----------------------------------------------------------------------------- # GUI # ----------------------------------------------------------------------------- server = get_server(client_type = "vue2") ctrl = server.controller state = server.state @state.change("time") def update_time(time, **kwargs): time_index = round(time / dt) - 1 newVelField = velFieldRaw[time_index, :, :, :, :3] newVelMagFlat = np.linalg.norm(newVelField.values, axis=-1).ravel() grid.point_data["vel_mag"] = newVelMagFlat grid.Modified() ctrl.view_update() @state.change("ypos") def update_ypos(ypos, **kwargs): plane.SetOrigin(center_x, ypos, center_z) ctrl.view_update() def reset_ypos(): state.ypos = center_y with SinglePageLayout(server) as layout: layout.title.set_text("Fluid Field Visualizer") with layout.toolbar: vuetify.VSpacer() # Time slider vuetify.VSlider( label="Time (s)", v_model=("time", 0), min=dt, max=nTime * dt, step=dt, thumb_label=True, hide_details=True, dense=True, style="max-width: 300px; margin-top: 40px; margin-bottom: 10px;", ) vuetify.VDivider(vertical=True, classes="mx-2") # Slice position silder vuetify.VSlider( label="Y Position (m)", v_model=("ypos", center_y), min=grid.bounds[2] + dx, max=grid.bounds[3] - dx, step=dx, thumb_label=True, hide_details=True, dense=True, style="max-width: 300px; margin-top: 40px; margin-bottom: 10px;", ) with vuetify.VBtn(icon=True, click=reset_ypos): vuetify.VIcon("mdi-restore") vuetify.VDivider(vertical=True, classes="mx-2") with layout.content: with vuetify.VContainer( fluid=True, classes="pa-0 fill-height", ): view = vtk_widget.VtkLocalView(renderWindow) ctrl.view_update = view.update server.start() if __name__ == "__main__": main()