Repository: Genesis-Embodied-AI/Genesis
File: genesis/engine/entities/rigid_entity/rigid_link.py (Python)

Code before:
        self._inertial_pos: "np.typing.ArrayLike | None" = inertial_pos
        if inertial_quat is not None:
            inertial_quat = np.asarray(inertial_quat, dtype=gs.np_float)
        self._inertial_quat: "np.typing.ArrayLike | None" = inertial_quat
        self._inertial_mass: float | None = inertial_mass
        self._inertial_i: "np.typing.ArrayLike | None" = inertial_i
        self._invweight: float | None = invweight

        self._visualize_contact = visualize_contact

        self._geoms: list[RigidGeom] = gs.List()
        self._vgeoms: list[RigidVisGeom] = gs.List()

    def _build(self):
        for geom in self._geoms:
            geom._build()

        for vgeom in self._vgeoms:
            vgeom._build()

        if self._inertial_mass is None or self._inertial_pos is None or self._inertial_i is None:
            # Determine which geom list to use: geoms first, then vgeoms, then fallback
            if len(self._geoms) > 0:
                geom_list = self._geoms
                get_mesh = lambda geom: trimesh.Trimesh(geom.init_verts, geom.init_faces)
                get_pos = lambda geom: geom._init_pos
                get_quat = lambda geom: geom._init_quat
            elif len(self._vgeoms) > 0:
                geom_list = self._vgeoms
                get_mesh = lambda geom: trimesh.Trimesh(geom.init_vverts, geom.init_vfaces)
                get_pos = lambda geom: geom._init_pos
                get_quat = lambda geom: geom._init_quat
            else:
                # Fallback: use default values
                self._inertial_mass = 1e-3
                self._inertial_pos = np.zeros(3, dtype=gs.np_float)
                self._inertial_i = np.zeros((3, 3), dtype=gs.np_float)
                geom_list = []

            # Process each geom individually and compose their properties
            if len(geom_list) > 0:
                total_mass = 0.0
                total_com = np.zeros(3, dtype=gs.np_float)
                total_inertia = np.zeros((3, 3), dtype=gs.np_float)

                for geom in geom_list:
                    inertia_mesh = get_mesh(geom)
                    geom_pos = get_pos(geom)
                    geom_quat = get_quat(geom)

                    if inertia_mesh.is_watertight:

Review comment (suggestion):
It is not clear if collision geometry of visual geometry should be used to compute such property. I have the feeling that collision geometries may be very off. There are often rough approximation using primitives, unless they are the output of convex decomposition. You can easily check in metadata of meshes whether they are the output of convex decomposition and prefer them in such a case (or more generally if they are true meshes instead of primitive geometries), otherwise ressort on visual. Another option would be to add some flag in morph to control whether collision or visual should be preferred in such a case, but I would rather avoid that.

I would recommend adding some info message to explain that spatial inertia informations are being deduced from the geometry.




Code after:
        self._inertial_pos: "np.typing.ArrayLike | None" = inertial_pos
        if inertial_quat is not None:
            inertial_quat = np.asarray(inertial_quat, dtype=gs.np_float)
        self._inertial_quat: "np.typing.ArrayLike | None" = inertial_quat
        self._inertial_mass: float | None = inertial_mass
        self._inertial_i: "np.typing.ArrayLike | None" = inertial_i
        self._invweight: float | None = invweight

        self._visualize_contact = visualize_contact

        self._geoms: list[RigidGeom] = gs.List()
        self._vgeoms: list[RigidVisGeom] = gs.List()

    def _build(self):
        for geom in self._geoms:
            geom._build()

        for vgeom in self._vgeoms:
            vgeom._build()

        if self._inertial_mass is None or self._inertial_pos is None or self._inertial_i is None:
            # Determine which geom list to use: geoms first, then vgeoms, then fallback
            if len(self._geoms) > 0:
                geom_list = self._geoms
                is_visual = False
            elif len(self._vgeoms) > 0:
                gs.logger.info(
                    f"Link mass is not specified and collision geoms can not be found for "
                    f"link '{self.name}'. Using visual geoms to compute inertial properties."
                )
                geom_list = self._vgeoms
                is_visual = True
            else:
                gs.logger.info(
                    f"Link mass is not specified and no geoms found for "
                    f"link '{self.name}'. Inertial and mass are set to zero."
                )
                self._inertial_mass = gs.EPS
                self._inertial_pos = np.zeros(3, dtype=gs.np_float)
                self._inertial_i = np.eye(3, dtype=gs.np_float) * gs.EPS
                geom_list = []

            # Process each geom individually and compose their properties
            if len(geom_list) > 0:
                total_mass = gs.EPS  # to avoid nan in inv_mass in interactive mode
                total_com = np.zeros(3, dtype=gs.np_float)
                total_inertia = np.eye(3, dtype=gs.np_float) * gs.EPS

                for geom in geom_list:
                    # Create mesh based on geom type
                    if is_visual: