@@ -37,6 +37,7 @@ class ViewerViser(ViewerBase):
3737 """
3838
3939 _viser_module = None
40+ _SH_C0 = 0.28209479177387814
4041
4142 @classmethod
4243 def _get_viser (cls ):
@@ -150,6 +151,7 @@ def __init__(
150151 self ._meshes = {}
151152 self ._instances = {}
152153 self ._scene_handles = {} # Track viser scene node handles
154+ self ._gaussian_splats = {} # Track cached Gaussian splat upload keys, by name
153155 self ._line_segment_counts = {}
154156 self ._line_versions = {}
155157
@@ -194,6 +196,16 @@ def clear_model(self):
194196 self ._remove_plane_handles (plane_name )
195197 self ._plane_meshes = {name : value for name , value in self ._plane_meshes .items () if not owns (name )}
196198
199+ for gaussian_name in list (getattr (self , "_gaussian_splats" , {}).keys ()):
200+ if owns (gaussian_name ):
201+ handle = self ._scene_handles .pop (gaussian_name , None )
202+ if handle is not None :
203+ try :
204+ handle .remove ()
205+ except Exception :
206+ pass
207+ self ._gaussian_splats .pop (gaussian_name , None )
208+
197209 for name , handle in list (getattr (self , "_scene_handles" , {}).items ()):
198210 if not owns (name ):
199211 continue
@@ -204,6 +216,7 @@ def clear_model(self):
204216 self ._scene_handles .pop (name , None )
205217 self ._instances .pop (name , None )
206218 self ._meshes .pop (name , None )
219+ self ._gaussian_splats .pop (name , None )
207220 self ._line_segment_counts .pop (name , None )
208221 self ._line_versions .pop (name , None )
209222
@@ -675,6 +688,7 @@ def log_mesh(
675688 self ._scene_handles [name ].remove ()
676689 except Exception :
677690 pass
691+ del self ._scene_handles [name ]
678692
679693 if hidden :
680694 return
@@ -715,6 +729,25 @@ def _quats_xyzw_to_wxyz(quats_xyzw: np.ndarray) -> np.ndarray:
715729 quats_wxyz [:, 3 ] = quats_xyzw [:, 2 ]
716730 return quats_wxyz [0 ] if was_1d else quats_wxyz
717731
732+ @staticmethod
733+ def _quats_xyzw_to_rotmats (quats_xyzw : np .ndarray ) -> np .ndarray :
734+ """Convert a batch of XYZW quaternions to (N, 3, 3) rotation matrices."""
735+ quats_xyzw = np .asarray (quats_xyzw , dtype = np .float32 )
736+ quats_xyzw = quats_xyzw / np .maximum (np .linalg .norm (quats_xyzw , axis = 1 , keepdims = True ), 1e-12 )
737+ x , y , z , w = quats_xyzw [:, 0 ], quats_xyzw [:, 1 ], quats_xyzw [:, 2 ], quats_xyzw [:, 3 ]
738+ n = quats_xyzw .shape [0 ]
739+ rot = np .empty ((n , 3 , 3 ), dtype = np .float32 )
740+ rot [:, 0 , 0 ] = 1.0 - 2.0 * (y * y + z * z )
741+ rot [:, 0 , 1 ] = 2.0 * (x * y - w * z )
742+ rot [:, 0 , 2 ] = 2.0 * (x * z + w * y )
743+ rot [:, 1 , 0 ] = 2.0 * (x * y + w * z )
744+ rot [:, 1 , 1 ] = 1.0 - 2.0 * (x * x + z * z )
745+ rot [:, 1 , 2 ] = 2.0 * (y * z - w * x )
746+ rot [:, 2 , 0 ] = 2.0 * (x * z - w * y )
747+ rot [:, 2 , 1 ] = 2.0 * (y * z + w * x )
748+ rot [:, 2 , 2 ] = 1.0 - 2.0 * (x * x + y * y )
749+ return rot
750+
718751 def _remove_plane_handles (self , name : str ):
719752 """Remove any plane-grid handles associated with an instance batch."""
720753 handle = self ._plane_handles .pop (name , None )
@@ -1265,6 +1298,7 @@ def log_points(
12651298 self ._scene_handles [name ].remove ()
12661299 except Exception :
12671300 pass
1301+ del self ._scene_handles [name ]
12681302
12691303 if hidden :
12701304 return
@@ -1313,6 +1347,93 @@ def log_points(
13131347 )
13141348 self ._scene_handles [name ] = handle
13151349
1350+ @override
1351+ def log_gaussian (
1352+ self ,
1353+ name : str ,
1354+ gaussian : newton .Gaussian | None ,
1355+ xform : wp .transformf | None = None ,
1356+ hidden : bool = False ,
1357+ ):
1358+ """
1359+ Log a :class:`newton.Gaussian` splat asset using viser's native Gaussian renderer.
1360+
1361+ Note: viser's ``add_gaussian_splats`` is marked experimental upstream and its
1362+ API may change or be removed in a future viser release.
1363+
1364+ Args:
1365+ name: Unique path/name for the Gaussian splat asset.
1366+ gaussian: The :class:`newton.Gaussian` asset to visualize, with centers and
1367+ per-axis scales in meters [m]. ``None`` removes any existing asset at ``name``.
1368+ xform: Optional world-space transform applied to the splat asset; its
1369+ translation component is in meters [m].
1370+ hidden: Whether the splat asset should be hidden.
1371+ """
1372+ name = self ._qualify (name )
1373+
1374+ if gaussian is None or gaussian .count == 0 :
1375+ if name in self ._scene_handles :
1376+ try :
1377+ self ._scene_handles [name ].remove ()
1378+ except Exception :
1379+ pass
1380+ self ._scene_handles .pop (name , None )
1381+ self ._gaussian_splats .pop (name , None )
1382+ return
1383+
1384+ if hidden :
1385+ if name in self ._scene_handles :
1386+ self ._scene_handles [name ].visible = False
1387+ return
1388+
1389+ cached = self ._gaussian_splats .get (name )
1390+
1391+ if (
1392+ cached is None
1393+ or cached ["gaussian" ] is not gaussian
1394+ or cached ["count" ] != gaussian .count
1395+ or self ._scene_handles .get (name ) is not cached ["handle" ]
1396+ ):
1397+ centers = self ._to_numpy (gaussian .positions ).astype (np .float32 )
1398+ rotations_xyzw = self ._to_numpy (gaussian .rotations ).astype (np .float32 )
1399+ scales = self ._to_numpy (gaussian .scales ).astype (np .float32 )
1400+ opacities = self ._to_numpy (gaussian .opacities ).astype (np .float32 ).reshape (- 1 , 1 )
1401+
1402+ # Local-space covariance per Gaussian: Sigma = R * diag(scale^2) * R^T
1403+ rot_mats = self ._quats_xyzw_to_rotmats (rotations_xyzw )
1404+ scale_sq = scales * scales
1405+ covariances = np .einsum ("nij,nj,nkj->nik" , rot_mats , scale_sq , rot_mats ).astype (np .float32 )
1406+
1407+ sh_coeffs = self ._to_numpy (gaussian .sh_coeffs )
1408+ if sh_coeffs is not None and sh_coeffs .shape [1 ] >= 3 :
1409+ rgbs = np .clip (self ._SH_C0 * sh_coeffs [:, :3 ] + 0.5 , 0.0 , 1.0 ).astype (np .float32 )
1410+ else :
1411+ rgbs = np .ones ((gaussian .count , 3 ), dtype = np .float32 )
1412+
1413+ if cached is not None and name in self ._scene_handles :
1414+ try :
1415+ self ._scene_handles [name ].remove ()
1416+ except Exception :
1417+ pass
1418+
1419+ handle = self ._call_scene_method (
1420+ self ._server .scene .add_gaussian_splats ,
1421+ name = name ,
1422+ centers = centers ,
1423+ covariances = covariances ,
1424+ rgbs = rgbs ,
1425+ opacities = opacities ,
1426+ )
1427+ self ._scene_handles [name ] = handle
1428+ self ._gaussian_splats [name ] = {"gaussian" : gaussian , "count" : gaussian .count , "handle" : handle }
1429+
1430+ handle = self ._scene_handles [name ]
1431+ handle .visible = True
1432+ if xform is not None :
1433+ xform_np = np .asarray (xform , dtype = np .float32 )
1434+ handle .position = xform_np [:3 ]
1435+ handle .wxyz = self ._quats_xyzw_to_wxyz (xform_np [3 :7 ])
1436+
13161437 @override
13171438 def log_array (self , name : str , array : wp .array [Any ] | np .ndarray ):
13181439 """Viser viewer does not visualize generic arrays.
0 commit comments