def check_bb_intersection(obj1: MeshObject, obj2: MeshObject):
        """
        Checks if there is a bounding box collision, these don't have to be axis-aligned, but if they are not:
        The surrounding/including axis-aligned bounding box is calculated and used to check the intersection.

        :param obj1: object 1  to check for intersection, must be a mesh
        :param obj2: object 2  to check for intersection, must be a mesh
        :return: True if the two bounding boxes intersect with each other
        """
        b1w = obj1.get_bound_box()

        def min_and_max_point(bb):
            """
            Find the minimum and maximum point of the bounding box
            :param bb: bounding box
            :return: min, max
            """
            values = np.array(bb)
            return np.min(values, axis=0), np.max(values, axis=0)

        # get min and max point of the axis-aligned bounding box
        min_b1, max_b1 = min_and_max_point(b1w)
        b2w = obj2.get_bound_box()
        # get min and max point of the axis-aligned bounding box
        min_b2, max_b2 = min_and_max_point(b2w)
        return CollisionUtility.check_bb_intersection_on_values(
            min_b1, max_b1, min_b2, max_b2)
示例#2
0
    def _make_ceiling_emissive(obj: MeshObject, collection_of_mats: Dict[str, Dict[str, Material]],
                               ceiling_emission_strength: float = 1.5):
        """ Makes the given ceiling object emissive, s.t. there is always a little bit ambient light.

        :param obj: The ceiling object.
        :param collection_of_mats: A dictionary that contains materials for lamps, windows and ceilings.
        :param ceiling_emission_strength: The emission strength that should be used for the ceiling. Default: 1.5
        """
        for i, m in enumerate(obj.get_materials()):
            if m is None:
                continue
            mat_name = m.get_name()
            if mat_name in collection_of_mats["ceiling"]:
                # this material was used as a ceiling before use that one
                obj.set_material(i, collection_of_mats["ceiling"][mat_name])
                continue
            # copy the material if more than one users is using it
            if m.get_users() > 1:
                m = m.duplicate()
                obj.set_material(i, m)
            # rename the material
            m.set_name(m.get_name() + "_emission")

            if not m.get_nodes_with_type("Emission") and m.get_nodes_with_type("BsdfPrincipled"):
                m.make_emissive(emission_strength=ceiling_emission_strength, emission_color=[1, 1, 1, 1],
                                keep_using_base_color=False)
                collection_of_mats["ceiling"][mat_name] = m
    def replace(obj_to_remove: MeshObject,
                obj_to_add: MeshObject,
                check_collision_with: Optional[List[MeshObject]] = None,
                scale: bool = True,
                relative_pose_sampler: Callable[[MeshObject], None] = None):
        """ Scale, translate, rotate obj_to_add to match obj_to_remove and check if there is a bounding box collision
        returns a boolean.

        :param obj_to_remove: An object to remove from the scene.
        :param obj_to_add: An object to put in the scene instead of obj_to_remove.
        :param check_collision_with: A list of objects, which are not checked for collisions with.
        :param scale: Scales obj_to_add to match obj_to_remove dimensions.
        :param relative_pose_sampler: A function that randomly perturbs the pose of the object to replace with (after it has been aligned to the object to replace).
        """
        if check_collision_with is None:
            check_collision_with = []
        # New object takes location, rotation and rough scale of original object
        obj_to_add.set_location(obj_to_remove.get_location())
        obj_to_add.set_rotation_euler(obj_to_remove.get_rotation())
        if scale:
            obj_to_add.set_scale(
                ObjectReplacer._bb_ratio(obj_to_remove.get_bound_box(True),
                                         obj_to_add.get_bound_box(True)))
        if relative_pose_sampler is not None:
            relative_pose_sampler(obj_to_add)

        # Check for collision between the new object and other objects in the scene
        return CollisionUtility.check_intersections(obj_to_add, None, [
            obj for obj in check_collision_with
            if obj != obj_to_add and obj != obj_to_remove
        ], [])
    def drop(obj: MeshObject, up_direction: np.ndarray, surface_height: float):
        """ Moves object "down" until its bounding box touches the bounding box of the surface. This uses bounding boxes
            which are not aligned optimally, this will cause objects to be placed slightly to high.

        :param obj: Object to move. Type: blender object.
        :param up_direction: Vector which points into the opposite drop direction.
        :param surface_height: Height of the surface above its origin.
        """
        obj_bounds = obj.get_bound_box()
        obj_height = min([up_direction.dot(corner) for corner in obj_bounds])

        obj.set_location(obj.get_location() - up_direction *
                         (obj_height - surface_height))
示例#5
0
    def load_rocks(path: str, subsec_num: int, objects: list = [], sample_objects: bool = False, amount: int = None) -> List[MeshObject]:
        """ Loads rocks from the given blend file.

        :param path: Path to a .blend file containing desired rock/cliff objects in //Object// section.
        :param subsec_num: Number of a corresponding cell (batch) in `rocks` list in configuration. Used for name generation.
        :param objects: List of rock-/cliff-object names to be loaded. If not specified then `amount` property is used for consequential loading.
        :param sample_objects: Toggles the uniform sampling of objects to load. Takes into account `objects` and `amount` parameters. Requires 'amount' param to be defined.
        :param amount: Amount of rock-/cliff-object to load. If not specified, the amount will be set to the amount of suitable
                       objects in the current section of a blend file. Must be bigger than 0.
        :return: List of loaded objects.
        """
        loaded_objects = []
        obj_types = ["Rock", "Cliff"]
        amount_defined = False

        obj_list = []
        with bpy.data.libraries.load(path) as (data_from, data_to):
            # if list of names is empty
            if not objects:
                # get list of rocks suitable for loading - objects that are rocks or cliffs
                for obj_type in obj_types:
                    obj_list += [obj for obj in data_from.objects if obj_type in obj]
            else:
                # if names are defined - get those that are available in this .blend file
                obj_list = [obj for obj in data_from.objects if obj in objects]

        # get amount of rocks in this batch, set to all suitable if not defined
        if amount is not None:
            amount_defined = True
            if amount == 0:
                raise RuntimeError("Amount param can't be equal to zero!")
        else:
            amount = len(obj_list)

        for i in range(amount):
            # load rock: choose random from the list if sampling is True, go through list if not
            if sample_objects and amount_defined:
                obj = choice(obj_list)
            else:
                obj = obj_list[i % len(obj_list)]
            bpy.ops.wm.append(filepath=os.path.join(path, "/Object", obj), filename=obj,
                              directory=os.path.join(path + "/Object"))
            loaded_obj = MeshObject(bpy.context.scene.objects[obj])
            # set custom name for easier tracking in the scene
            loaded_obj.set_name(obj + "_" + str(subsec_num) + "_" + str(i))
            # append to return list
            loaded_objects.append(loaded_obj)

        return loaded_objects
示例#6
0
    def run(self):
        """ Samples the selected objects poses on a selected surface. """

        # Collect parameters
        up_direction = self.config.get_vector3d(
            "up_direction", mathutils.Vector([0., 0., 1.])).normalized()
        min_distance = self.config.get_float("min_distance", 0.25)
        max_distance = self.config.get_float("max_distance", 0.6)
        max_tries = self.config.get_int("max_iterations", 100)
        objects = convert_to_meshes(self.config.get_list("objects_to_sample"))
        surface = self.config.get_list("surface")
        if len(surface) > 1:
            raise Exception(
                "This module operates with only one `surface` object while more than one was returned by "
                "the Provider. Please, configure the corresponding Provider's `conditions` accordingly such "
                "that it returns only one object! Tip: use getter.Entity's 'index' parameter."
            )
        else:
            surface = MeshObject(surface[0])

        # Define method to sample new object poses
        def sample_pose(obj: MeshObject):
            obj.set_location(self.config.get_vector3d("pos_sampler"))
            obj.set_rotation_euler(self.config.get_vector3d("rot_sampler"))

        # Sample objects on the given surface
        sample_poses_on_surface(objects_to_sample=objects,
                                surface=surface,
                                sample_pose_func=sample_pose,
                                max_tries=max_tries,
                                min_distance=min_distance,
                                max_distance=max_distance,
                                up_direction=up_direction)
    def run(self):
        """ Extracts floors in the following steps:
        1. Searchs for the specified object.
        2. Splits the surfaces which point upwards at a specified level away.
        """

        mesh_objects = []
        for obj in self.config.get_list("selector"):
            if obj.type != "MESH":
                warnings.warn(
                    "The object: {} is not a mesh but was selected in the FloorExtractor!"
                    .format(obj.name))
                continue
            mesh_objects.append(MeshObject(obj))

        floors = extract_floor(
            mesh_objects=mesh_objects,
            compare_angle_degrees=radians(
                self.config.get_float('compare_angle_degrees', 7.5)),
            compare_height=self.config.get_float('compare_height', 0.15),
            new_name_for_object=self.config.get_string("name_for_split_obj",
                                                       "Floor"),
            should_skip_if_object_is_already_there=self.config.get_bool(
                "should_skip_if_object_is_already_there", False))

        add_properties = self.config.get_raw_dict("add_properties", {})
        if add_properties:
            config = Config({"add_properties": add_properties})
            loader_interface = LoaderInterface(config)
            loader_interface._set_properties(floors)
示例#8
0
    def _load_mesh(obj_id: int, model_p: dict, bop_dataset_name: str, has_external_texture: bool, temp_dir: str, allow_duplication: bool, scale: float = 1) -> MeshObject:
        """ Loads BOP mesh and sets category_id.

        :param obj_id: The obj_id of the BOP Object.
        :param model_p: model parameters defined in dataset_params.py in bop_toolkit.
        :param bop_dataset_name: The name of the used bop dataset.
        :param has_external_texture: Set to True, if the object has an external texture.
        :param temp_dir: A temp directory which is used for writing the temporary .ply file.
        :param allow_duplication: If True, the object is duplicated if it already exists.
        :param scale: factor to transform set pose in mm or meters.
        :return: Loaded mesh object.
        """

        model_path = model_p['model_tpath'].format(**{'obj_id': obj_id})

        texture_file_path = ""  # only needed for ycbv objects

        # Gets the objects if it is already loaded         
        cur_obj = BopLoader._get_loaded_obj(model_path)
        # if the object was not previously loaded - load it, if duplication is allowed - duplicate it
        if cur_obj is None:
            if has_external_texture:
                if os.path.exists(model_path):
                    new_file_ply_content = ""
                    with open(model_path, "r") as file:
                        new_file_ply_content = file.read()
                        texture_pos = new_file_ply_content.find("comment TextureFile ") + len("comment TextureFile ")
                        texture_file_name = new_file_ply_content[texture_pos:
                                                                 new_file_ply_content.find("\n", texture_pos)]
                        texture_file_path = os.path.join(os.path.dirname(model_path), texture_file_name)
                        new_file_ply_content = new_file_ply_content.replace("property float texture_u",
                                                                            "property float s")
                        new_file_ply_content = new_file_ply_content.replace("property float texture_v",
                                                                            "property float t")
                    model_name = os.path.basename(model_path)
                    tmp_ply_file = os.path.join(temp_dir, model_name)
                    with open(tmp_ply_file, "w") as file:
                        file.write(new_file_ply_content)
                    bpy.ops.import_mesh.ply(filepath=tmp_ply_file)
                    cur_obj = bpy.context.selected_objects[-1]
            else:
                bpy.ops.import_mesh.ply(filepath=model_path)
                cur_obj = bpy.context.selected_objects[-1]
        elif allow_duplication:
            bpy.ops.object.duplicate({"object": cur_obj, "selected_objects": [cur_obj]})
            cur_obj = bpy.context.selected_objects[-1]

        cur_obj.scale = Vector((scale, scale, scale))
        cur_obj['category_id'] = obj_id
        cur_obj['model_path'] = model_path
        cur_obj["is_bop_object"] = True
        cur_obj["bop_dataset_name"] = bop_dataset_name
        
        if not has_external_texture:
            mat = BopLoader._load_materials(cur_obj, bop_dataset_name)
            mat.map_vertex_color()
        elif texture_file_path != "":
            # ycbv objects contain normal image textures, which should be used instead of the vertex colors
            BopLoader._load_texture(cur_obj, texture_file_path, bop_dataset_name)
        return MeshObject(cur_obj)
    def check_above_surface(obj: MeshObject, surface: MeshObject,
                            up_direction: np.ndarray) -> bool:
        """ Check if all corners of the bounding box are "above" the surface

        :param obj: Object for which the check is carried out. Type: blender object.
        :param surface: The surface object.
        :param up_direction: The direction that indicates "above" direction.
        :return: True if the bounding box is above the surface, False - if not.
        """
        for point in obj.get_bound_box():
            if not surface.position_is_above_object(
                    point + up_direction,
                    -up_direction,
                    check_no_objects_in_between=False):
                return False
        return True
示例#10
0
    def _correct_materials(obj: MeshObject):
        """ If the used material contains an alpha texture, the alpha texture has to be flipped to be correct

        :param obj: object where the material maybe wrong
        """
        for material in obj.get_materials():
            if material is None:
                continue
            texture_nodes = material.get_nodes_with_type("ShaderNodeTexImage")
            if texture_nodes and len(texture_nodes) > 1:
                principled_bsdf = material.get_the_one_node_with_type(
                    "BsdfPrincipled")
                # find the image texture node which is connect to alpha
                node_connected_to_the_alpha = None
                for node_links in principled_bsdf.inputs["Alpha"].links:
                    if "ShaderNodeTexImage" in node_links.from_node.bl_idname:
                        node_connected_to_the_alpha = node_links.from_node
                # if a node was found which is connected to the alpha node, add an invert between the two
                if node_connected_to_the_alpha is not None:
                    invert_node = material.new_node("ShaderNodeInvert")
                    invert_node.inputs["Fac"].default_value = 1.0
                    material.insert_node_instead_existing_link(
                        node_connected_to_the_alpha.outputs["Color"],
                        invert_node.inputs["Color"],
                        invert_node.outputs["Color"],
                        principled_bsdf.inputs["Alpha"])
示例#11
0
    def cut_plane(plane: MeshObject):
        """
        Cuts the floor plane in several pieces randomly. This is used for selecting random edges for the extrusions
        later on. This function assumes the current `plane` object is already selected and no other object is
        selected.

        :param plane: The object, which should be split in edit mode.
        """

        # save the size of the plane to determine a best split value
        x_size = plane.get_scale()[0]
        y_size = plane.get_scale()[1]

        # switch to edit mode and select all faces
        bpy.ops.object.mode_set(mode='EDIT')
        bpy.ops.mesh.select_all(action='SELECT')
        bpy.ops.object.mode_set(mode='OBJECT')

        # convert plane to BMesh object
        bm = plane.mesh_as_bmesh(True)
        bm.faces.ensure_lookup_table()
        # find all selected edges
        edges = [e for e in bm.edges if e.select]

        biggest_face_id = np.argmax([f.calc_area() for f in bm.faces])
        biggest_face = bm.faces[biggest_face_id]
        # find the biggest face
        faces = [f for f in bm.faces if f == biggest_face]
        geom = []
        geom.extend(edges)
        geom.extend(faces)

        # calculate cutting point
        cutting_point = [
            x_size * random.uniform(-1, 1), y_size * random.uniform(-1, 1), 0
        ]
        # select a random axis to specify in which direction to cut
        direction_axis = [1, 0, 0] if random.uniform(0, 1) < 0.5 else [0, 1, 0]

        # cut the plane and update the final mesh
        bmesh.ops.bisect_plane(bm,
                               dist=0.01,
                               geom=geom,
                               plane_co=cutting_point,
                               plane_no=direction_axis)
        plane.update_from_bmesh(bm)
示例#12
0
    def _set_shading(self, entity: bpy.types.Object, value: dict):
        """ Switches shading mode of the selected entity.

        :param entity: An entity to modify. Type: bpy.types.Object
        :param value: Configuration data. Type: dict.
        """
        MeshObject(entity).set_shading_mode(value["shading_mode"],
                                            value["angle_value"])
示例#13
0
    def _add_uv_mapping(self, entity: bpy.types.Object, value: dict):
        """ Adds a uv map to an object if uv map is missing.

        :param entity: An object to modify. Type: bpy.types.Object.
        :param value: Configuration data. Type: dict.
        """
        MeshObject(entity).add_uv_mapping(
            value["projection"], overwrite=value["forced_recalc_of_uv_maps"])
示例#14
0
    def _make_lamp_emissive(obj: MeshObject, light: Tuple[List[str], List[str]],
                            collection_of_mats: Dict[str, Dict[str, Material]],
                            lightbulb_emission_strength: float = 15,
                            lampshade_emission_strength: float = 7):
        """ Adds an emission shader to the object materials which are specified in the light dictionary

        :param obj: The blender object.
        :param light: A tuple of two lists. The first list specifies all materials which should act as a lightbulb, the second one lists all materials corresponding to lampshades.
        :param collection_of_mats: A dictionary that contains materials for lamps, windows and ceilings.
        :param lightbulb_emission_strength: The emission strength that should be used for light bulbs. Default: 15
        :param lampshade_emission_strength: The emission strength that should be used for lamp shades. Default: 7
        """
        for i, m in enumerate(obj.get_materials()):
            if m is None:
                continue
            mat_name = m.get_name()
            if "." in mat_name:
                mat_name = mat_name[:mat_name.find(".")]
            if mat_name in light[0] or mat_name in light[1]:
                old_mat_name = m.get_name()
                if old_mat_name in collection_of_mats["lamp"]:
                    # this material was used as a ceiling before use that one
                    obj.set_material(i, collection_of_mats["lamp"][old_mat_name])
                    continue
                # copy the material if more than one users is using it
                if m.get_users() > 1:
                    m = m.duplicate()
                    obj.set_material(i, m)
                # rename the material
                m.set_name(m.get_name() + "_emission")

                emission = m.get_nodes_with_type("Emission")
                if not emission:
                    if mat_name in light[0]:
                        # If the material corresponds to light bulb
                        emission_strength = lightbulb_emission_strength
                    else:
                        # If the material corresponds to a lampshade
                        emission_strength = lampshade_emission_strength

                    m.make_emissive(emission_strength, keep_using_base_color=False,
                                    emission_color=m.blender_obj.diffuse_color)
                    collection_of_mats["lamp"][old_mat_name] = m
示例#15
0
    def run(self):
        """ Selects objects, gets a list of appropriate values of objects' attributes, custom properties, or some
            processed custom data and optionally performs some operation of this list.

        :return: List of values (only if `get` was specified or a custom function was called)
                 or a singular int, float, or mathutils.Vector value (if some operation was applied).
        """
        objects = self.config.get_list("entities")
        look_for = self.config.get_string("get")

        cp_search = False
        cf_search = False
        if look_for.startswith('cp_'):
            look_for = look_for[3:]
            cp_search = True
        elif look_for.startswith('cf_'):
            look_for = look_for[3:]
            cf_search = True

        raw_result = []
        for obj in objects:
            if hasattr(obj, look_for) and not cp_search:
                raw_result.append(getattr(obj, look_for))
            elif look_for in obj and cp_search:
                raw_result.append(obj[look_for])
            elif look_for == "bounding_box_means" and cf_search:
                bb_mean = np.mean(MeshObject(obj).get_bound_box(), axis=0)
                raw_result.append(mathutils.Vector(bb_mean))
            else:
                raise RuntimeError("Unknown parameter name: " + look_for)

        if self.config.has_param("transform_by"):
            transform_by = self.config.get_string("transform_by")
            if self._check_compatibility(raw_result):
                if transform_by == "sum":
                    ref_result = self._sum(raw_result)
                elif transform_by == "avg":
                    ref_result = self._avg(raw_result)
                else:
                    raise RuntimeError("Unknown transform_by: " + transform_by)
            else:
                raise RuntimeError("Performing " + str(transform_by) + " on " +
                                   str(look_for) + " " +
                                   str(type(raw_result[0])) +
                                   " type is not allowed!")
            result = ref_result
        else:
            result = raw_result

        if self.config.has_param("index"):
            result = result[self.config.get_int("index")]

        return result
示例#16
0
    def _add_displace(self, entity: bpy.types.Object, value: dict):
        """ Adds a displace modifier with texture to an object.

        :param entity: An object to modify. Type: bpy.types.Object.
        :param value: Configuration data. Type: dict.
        """
        MeshObject(entity).add_displace_modifier(
            texture=value["texture"],
            mid_level=value["mid_level"],
            strength=value["strength"],
            min_vertices_for_subdiv=value["min_vertices_for_subdiv"],
            subdiv_level=value["subdiv_level"])
示例#17
0
    def _add_rigidbody(self):
        """ Adds a rigidbody element to all mesh objects and sets their physics attributes depending on their custom properties """

        # Temporary function which returns either the value set in the custom properties (if set) or the fallback value.
        def get_physics_attribute(obj, cp_name, default_value):
            if cp_name in obj:
                return obj[cp_name]
            else:
                return default_value

        # Go over all mesh objects and set their physics attributes based on the custom properties or (if not set) based on the module config
        for obj in get_all_blender_mesh_objects():
            mesh_obj = MeshObject(obj)
            # Skip if the object has already an active rigid body component
            if mesh_obj.get_rigidbody() is None:
                if "physics" not in obj:
                    raise Exception(
                        "The obj: '{}' has no physics attribute, each object needs one."
                        .format(obj.name))

                # Collect physics attributes
                collision_shape = get_physics_attribute(
                    obj, "physics_collision_shape", self.collision_shape)
                collision_margin = get_physics_attribute(
                    obj, "physics_collision_margin", self.collision_margin)
                mass = get_physics_attribute(obj, "physics_mass",
                                             None if self.mass_scaling else 1)
                collision_mesh_source = get_physics_attribute(
                    obj, "physics_collision_mesh_source",
                    self.collision_mesh_source)
                friction = get_physics_attribute(obj, "physics_friction",
                                                 self.friction)
                angular_damping = get_physics_attribute(
                    obj, "physics_angular_damping", self.angular_damping)
                linear_damping = get_physics_attribute(
                    obj, "physics_linear_damping", self.linear_damping)

                # Set physics attributes
                mesh_obj.enable_rigidbody(
                    active=obj["physics"],
                    collision_shape="COMPOUND" if collision_shape
                    == "CONVEX_DECOMPOSITION" else collision_shape,
                    collision_margin=collision_margin,
                    mass=mass,
                    mass_factor=self.mass_factor,
                    collision_mesh_source=collision_mesh_source,
                    friction=friction,
                    angular_damping=angular_damping,
                    linear_damping=linear_damping)

                # Check if object needs decomposition
                if collision_shape == "CONVEX_DECOMPOSITION":
                    mesh_obj.build_convex_decomposition_collision_shape(
                        self.vhacd_path, self._temp_dir,
                        self.convex_decomposition_cache_path)
示例#18
0
def _assign_materials_to_floor_wall_ceiling(floor_obj: MeshObject,
                                            wall_obj: MeshObject,
                                            ceiling_obj: MeshObject,
                                            assign_material_to_ceiling: bool,
                                            materials: List[Material]):
    """
    Assigns materials to the floor, wall and ceiling. These are randomly selected from the CCMaterials. This means
    it is required that the CCMaterialLoader has been executed before, this module is run.
    """

    # first create a uv mapping for each of the three objects
    for obj in [floor_obj, wall_obj, ceiling_obj]:
        if obj is not None:
            obj.edit_mode()
            bpy.ops.mesh.select_all(action='SELECT')
            bpy.ops.uv.cube_project(cube_size=1.0)
            obj.object_mode()

    if materials:
        floor_obj.replace_materials(random.choice(materials))
        wall_obj.replace_materials(random.choice(materials))
        if ceiling_obj is not None and assign_material_to_ceiling:
            ceiling_obj.replace_materials(random.choice(materials))
    else:
        warnings.warn(
            "There were no CCMaterials found, which means the CCMaterialLoader was not executed first!"
            "No materials have been assigned to the walls, floors and possible ceiling."
        )
示例#19
0
def _sample_new_object_poses_on_face(current_obj: MeshObject, face_bb,
                                     bvh_cache_for_intersection: dict,
                                     placed_objects: List[MeshObject],
                                     wall_obj: MeshObject):
    """
    Sample new object poses on the current `floor_obj`.

    :param face_bb:
    :return: True, if there is no collision
    """
    random_placed_value = [
        random.uniform(face_bb[0][i], face_bb[1][i]) for i in range(2)
    ]
    random_placed_value.append(0.0)  # floor z value

    random_placed_rotation = [0, 0, random.uniform(0, np.pi * 2.0)]

    current_obj.set_location(random_placed_value)
    current_obj.set_rotation_euler(random_placed_rotation)

    # Remove bvh cache, as object has changed
    if current_obj.get_name() in bvh_cache_for_intersection:
        del bvh_cache_for_intersection[current_obj.get_name()]

    # perform check if object can be placed there
    no_collision = CollisionUtility.check_intersections(
        current_obj,
        bvh_cache=bvh_cache_for_intersection,
        objects_to_check_against=placed_objects,
        list_of_objects_with_no_inside_check=[wall_obj])
    return no_collision
示例#20
0
    def _make_window_emissive(obj: MeshObject, collection_of_mats: Dict[str, Dict[str, Material]]):
        """ Makes the given window object emissive.

        For each material with alpha < 1.
        Uses a light path node to make it emit light, but at the same time look like a principle material.
        Otherwise windows would be completely white.

        :param obj: A window object.
        :param collection_of_mats: A dictionary that contains materials for lamps, windows and ceilings.
        """
        for i, m in enumerate(obj.get_materials()):
            if m is None:
                continue

            # All parameters imported from the .mtl file are stored inside the principled bsdf node
            principled_node = m.get_the_one_node_with_type("BsdfPrincipled")
            alpha = principled_node.inputs['Alpha'].default_value

            if alpha < 1:
                mat_name = m.get_name()
                if mat_name in collection_of_mats["window"]:
                    # this material was used as a ceiling before use that one
                    obj.set_material(i, collection_of_mats["window"][mat_name])
                    continue
                # copy the material if more than one users is using it
                if m.get_users() > 1:
                    m = m.duplicate()
                    obj.set_material(i, m)
                # rename the material
                m.set_name(m.get_name() + "_emission")
                if not m.get_nodes_with_type('Emission'):
                    transparent_node = m.new_node('ShaderNodeBsdfDiffuse')
                    transparent_node.inputs['Color'].default_value[:3] = (0.285, 0.5, 0.48)

                    m.make_emissive(emission_strength=10, keep_using_base_color=False, emission_color=[1, 1, 1, 1],
                                    non_emissive_color_socket=transparent_node.outputs['BSDF'])

                collection_of_mats["window"][mat_name] = m
    def __init__(self, replica_mesh: MeshObject, replica_floor: MeshObject, height_list_file_path: str):
        """ Collect object containing all floors of all rooms and read in text file containing possible height values.

        :param replica_mesh: The replica mesh object.
        :param replica_floor: The replica floor object.
        :param height_list_file_path: The path to the file containing possible height values.
        """
        # Determine bounding box of the scene
        bounding_box = replica_mesh.get_bound_box()
        self.bounding_box = {"min": bounding_box[0], "max": bounding_box[-2]}
        self.floor_object = replica_floor

        with open(height_list_file_path) as file:
            self.floor_height_values = [float(val) for val in ast.literal_eval(file.read())]
    def run(self):
        # Load the height levels of this scene
        if not self.config.get_bool('is_replica_object', False):
            file_path = self.config.get_string('height_list_path')
        else:
            folder_path = os.path.join('replica', 'height_levels',
                                       self.config.get_string('data_set_name'))
            file_path = resolve_resource(
                os.path.join(folder_path, 'height_list_values.txt'))

        if 'mesh' in bpy.data.objects:
            mesh = MeshObject(bpy.data.objects['mesh'])
        else:
            raise Exception("Mesh object is not defined!")

        # Find floor object
        if 'floor' in bpy.data.objects:
            floor_object = MeshObject(bpy.data.objects['floor'])
        else:
            raise Exception("No floor object is defined!")

        self.point_sampler = ReplicaPointInRoomSampler(mesh, floor_object,
                                                       file_path)
        super().run()
示例#23
0
    def _transform_and_colorize_object(object: MeshObject,
                                       material_adjustments: List[Dict[str,
                                                                       str]],
                                       transform: Optional[Matrix] = None,
                                       parent: Optional[Entity] = None):
        """ Applies the given transformation to the object and refactors its materials.

        Material is replaced with an existing material if possible or is changed according to the material_adjustments

        :param object: The object to use.
        :param material_adjustments: A list of adjustments to make. (Each element i corresponds to material_i)
        :param transform: The transformation matrix to apply
        :param parent: The parent object to which the object should be linked
        """
        if parent is not None:
            object.set_parent(parent)

        if transform is not None:
            # Apply transformation
            object.apply_T(transform)

        for i, mat in enumerate(object.get_materials()):
            if mat is None:
                continue
            # the material name of an object contains a nr, which is mentioned in the material_adjustments
            index = mat.get_name()[mat.get_name().find("_") + 1:]
            if "." in index:
                index = index[:index.find(".")]
            index = int(index)

            # check if this index is mentioned in material_adjustments and if a texture is necessary
            force_texture = index < len(
                material_adjustments
            ) and "texture" in material_adjustments[index]
            SuncgLoader._recreate_material_nodes(mat, force_texture)

            if index < len(material_adjustments):
                SuncgLoader._adjust_material_nodes(mat,
                                                   material_adjustments[index])
            mat_type, value = SuncgLoader._get_type_and_value_from_mat(mat)
            current_mats = SuncgLoader._collection_of_loaded_mats[mat_type]
            if value in current_mats:
                object.set_material(i, current_mats[value])
            else:
                # save the current material for later
                current_mats[value] = mat
    def check_spacing(obj: MeshObject, placed_objects: List[MeshObject], min_distance: float, max_distance: float) \
            -> bool:
        """ Check if object is not too close or too far from previous objects.

        :param obj: Object for which the check is carried out.
        :param placed_objects: A list of already placed objects that should be used for checking spacing.
        :param min_distance: Minimum distance to the closest other object from placed_objects. Center to center.
        :param max_distance: Maximum distance to the closest other object from placed_objects. Center to center.
        :return: True, if the spacing is correct
        """
        closest_distance = None

        for already_placed in placed_objects:
            distance = np.linalg.norm(already_placed.get_location() -
                                      obj.get_location())
            if closest_distance is None or distance < closest_distance:
                closest_distance = distance

        return closest_distance is None or (min_distance <= closest_distance <=
                                            max_distance)
    def is_point_inside_object(obj: MeshObject,
                               obj_BVHtree: mathutils.bvhtree.BVHTree,
                               point: Union[Vector, np.ndarray]) -> bool:
        """ Checks whether the given point is inside the given object.

        This only works if the given object is watertight and has correct normals

        :param obj: The object
        :param obj_BVHtree: A bvh tree of the object
        :param point: The point to check
        :return: True, if the point is inside the object
        """
        point = Vector(point)
        # Look for closest point on object
        nearest, normal, _, _ = obj_BVHtree.find_nearest(point)
        # Compute direction
        p2 = nearest - point
        # Compute dot product between direction and normal vector
        a = p2.normalized().dot(
            (Euler(obj.get_rotation()).to_matrix() @ normal).normalized())
        return a >= 0.0
def visible_objects(cam2world_matrix: Union[Matrix, np.ndarray], sqrt_number_of_rays: int = 10) -> Set[MeshObject]:
    """ Returns a set of objects visible from the given camera pose.

    Sends a grid of rays through the camera frame and returns all objects hit by at least one ray.

    :param cam2world_matrix: The world matrix which describes the camera orientation to check.
    :param sqrt_number_of_rays: The square root of the number of rays which will be used to determine the visible objects.
    :return: A set of objects visible hit by the sent rays.
    """
    cam2world_matrix = Matrix(cam2world_matrix)

    visible_objects = set()
    cam_ob = bpy.context.scene.camera
    cam = cam_ob.data

    # Get position of the corners of the near plane
    frame = cam.view_frame(scene=bpy.context.scene)
    # Bring to world space
    frame = [cam2world_matrix @ v for v in frame]

    # Compute vectors along both sides of the plane
    vec_x = frame[1] - frame[0]
    vec_y = frame[3] - frame[0]

    # Go in discrete grid-like steps over plane
    position = cam2world_matrix.to_translation()
    for x in range(0, sqrt_number_of_rays):
        for y in range(0, sqrt_number_of_rays):
            # Compute current point on plane
            end = frame[0] + vec_x * x / float(sqrt_number_of_rays - 1) + vec_y * y / float(sqrt_number_of_rays - 1)
            # Send ray from the camera position through the current point on the plane
            _, _, _, _, hit_object, _ = bpy.context.scene.ray_cast(bpy.context.evaluated_depsgraph_get(), position, end - position)
            # Add hit object to set
            visible_objects.add(MeshObject(hit_object))

    return visible_objects
def sample_poses_on_surface(
        objects_to_sample: List[MeshObject],
        surface: MeshObject,
        sample_pose_func: Callable[[MeshObject], None],
        max_tries: int = 100,
        min_distance: float = 0.25,
        max_distance: float = 0.6,
        up_direction: Optional[np.ndarray] = None) -> List[MeshObject]:
    """ Samples objects poses on a surface.

    The objects are positioned slightly above the surface due to the non-axis aligned nature of used bounding boxes
    and possible non-alignment of the sampling surface (i.e. on the X-Y hyperplane, can be somewhat mitigated with
    precise "up_direction" value), which leads to the objects hovering slightly above the surface. So it is
    recommended to use the PhysicsPositioning module afterwards for realistically looking placements of objects on
    the sampling surface.

    :param objects_to_sample: A list of objects that should be sampled above the surface.
    :param surface: Object to place objects_to_sample on.
    :param sample_pose_func: The function to use for sampling the pose of a given object.
    :param max_tries: Amount of tries before giving up on an object (deleting it) and moving to the next one.
    :param min_distance: Minimum distance to the closest other object from objects_to_sample. Center to center.
    :param max_distance: Maximum distance to the closest other object from objects_to_sample. Center to center.
    :param up_direction: Normal vector of the side of surface the objects should be placed on.
    :return: The list of placed objects.
    """
    if up_direction is None:
        up_direction = np.array([0., 0., 1.])
    else:
        up_direction /= np.linalg.norm(up_direction)

    surface_bounds = surface.get_bound_box()
    surface_height = max(
        [up_direction.dot(corner) for corner in surface_bounds])

    # cache to fasten collision detection
    bvh_cache: Dict[str, mathutils.bvhtree.BVHTree] = {}

    placed_objects: List[MeshObject] = []
    for obj in objects_to_sample:
        print("Trying to put ", obj.get_name())

        placed_successfully = False

        for i in range(max_tries):
            sample_pose_func(obj)
            # Remove bvh cache, as object has changed
            if obj.get_name() in bvh_cache:
                del bvh_cache[obj.get_name()]

            if not CollisionUtility.check_intersections(
                    obj, bvh_cache, placed_objects, []):
                print("Collision detected, retrying!")
                continue

            if not OnSurfaceSampler.check_above_surface(
                    obj, surface, up_direction):
                print("Not above surface, retrying!")
                continue

            OnSurfaceSampler.drop(obj, up_direction, surface_height)
            # Remove bvh cache, as object has changed
            if obj.get_name() in bvh_cache:
                del bvh_cache[obj.get_name()]

            if not OnSurfaceSampler.check_above_surface(
                    obj, surface, up_direction):
                print("Not above surface after drop, retrying!")
                continue

            if not OnSurfaceSampler.check_spacing(obj, placed_objects,
                                                  min_distance, max_distance):
                print("Bad spacing after drop, retrying!")
                continue

            if not CollisionUtility.check_intersections(
                    obj, bvh_cache, placed_objects, []):
                print("Collision detected after drop, retrying!")
                continue

            print(
                "Placed object \"{}\" successfully at {} after {} iterations!".
                format(obj.get_name(), obj.get_location(), i + 1))
            placed_objects.append(obj)

            placed_successfully = True
            break

        if not placed_successfully:
            print("Giving up on {}, deleting...".format(obj.get_name()))
            obj.delete()

    return placed_objects
示例#28
0
 def sample_pose(obj: MeshObject):
     obj.set_location(self.config.get_vector3d("pos_sampler"))
     obj.set_rotation_euler(self.config.get_vector3d("rot_sampler"))
示例#29
0
def load_blend(path: str, obj_types: Optional[Union[List[str], str]] = None, name_regrex: Optional[str] = None,
               data_blocks: Union[List[str], str] = "objects") -> List[Entity]:
    """
    Loads entities (everything that can be stored in a .blend file's folders, see Blender's documentation for
    bpy.types.ID for more info) that match a name pattern from a specified .blend file's section/datablock.

    :param path: Path to a .blend file.
    :param obj_types: The type of objects to load. This parameter is only relevant when `data_blocks` is set to `"objects"`.
                      Available options are: ['mesh', 'curve', 'hair', 'armature', 'empty', 'light', 'camera']
    :param name_regrex: Regular expression representing a name pattern of entities' (everything that can be stored in a .blend
                     file's folders, see Blender's documentation for bpy.types.ID for more info) names.
    :param data_blocks: The datablock or a list of datablocks which should be loaded from the given .blend file.
                        Available options are: ['armatures', 'cameras', 'curves', 'hairs', 'images', 'lights', 'materials', 'meshes', 'objects', 'textures']
    :return: The list of loaded mesh objects.
    """
    if obj_types is None:
        obj_types = ["mesh", "empty"]
    # get a path to a .blend file
    path = resolve_path(path)
    data_blocks = BlendLoader._validate_and_standardizes_configured_list(data_blocks, BlendLoader.valid_datablocks,
                                                                         "data block")
    obj_types = BlendLoader._validate_and_standardizes_configured_list(obj_types, BlendLoader.valid_object_types,
                                                                       "object type")

    # Remember which orphans existed beforehand
    orphans_before = collect_all_orphan_datablocks()

    # Start importing blend file. All objects that should be imported need to be copied from "data_from" to "data_to"
    with bpy.data.libraries.load(path) as (data_from, data_to):
        for data_block in data_blocks:
            # Verify that the given data block is valid
            if hasattr(data_from, data_block):
                # Find all entities of this data block that match the specified pattern
                data_to_entities = []
                for entity_name in getattr(data_from, data_block):
                    if not name_regrex or re.fullmatch(name_regrex, entity_name) is not None:
                        data_to_entities.append(entity_name)
                # Import them
                setattr(data_to, data_block, data_to_entities)
                print("Imported " + str(len(data_to_entities)) + " " + data_block)
            else:
                raise Exception("No such data block: " + data_block)

    # Go over all imported objects again
    loaded_objects: List[Entity] = []
    for data_block in data_blocks:
        # Some adjustments that only affect objects
        if data_block == "objects":
            for obj in getattr(data_to, data_block):
                # Check that the object type is desired
                if obj.type.lower() in obj_types:
                    # Link objects to the scene
                    bpy.context.collection.objects.link(obj)
                    if obj.type == 'MESH':
                        loaded_objects.append(MeshObject(obj))
                    elif obj.type == 'LIGHT':
                        loaded_objects.append(Light(blender_obj=obj))
                    else:
                        loaded_objects.append(Entity(obj))

                    # If a camera was imported
                    if obj.type == 'CAMERA':
                        # Make it the active camera in the scene
                        bpy.context.scene.camera = obj

                        # Find the maximum frame number of its key frames
                        max_keyframe = -1
                        if obj.animation_data is not None:
                            fcurves = obj.animation_data.action.fcurves
                            for curve in fcurves:
                                keyframe_points = curve.keyframe_points
                                for keyframe in keyframe_points:
                                    max_keyframe = max(max_keyframe, keyframe.co[0])

                        # Set frame_end to the next free keyframe
                        bpy.context.scene.frame_end = max_keyframe + 1
                else:
                    # Remove object again if its type is not desired
                    bpy.data.objects.remove(obj, do_unlink=True)
            print("Selected " + str(len(loaded_objects)) + " of the loaded objects by type")
        else:
            loaded_objects.extend(getattr(data_to, data_block))

    # As some loaded objects were deleted again due to their type, we need also to remove the dependent datablocks that were also loaded and are now orphans
    BlendLoader._purge_added_orphans(orphans_before, data_to)
    return loaded_objects
    def check_mesh_intersection(obj1: MeshObject, obj2: MeshObject, skip_inside_check: bool = False,
                                bvh_cache: Optional[Dict[str, mathutils.bvhtree.BVHTree]] = None) \
            -> Tuple[bool, Dict[str, mathutils.bvhtree.BVHTree]]:
        """
        Checks if the two objects are intersecting.

        This will use BVH trees to check whether the objects are overlapping.

        It is further also checked if one object is completely inside the other.
        This check requires that both objects are watertight, have correct normals and are coherent.
        If this is not the case it can be disabled via the parameter skip_inside_check.

        :param obj1: object 1 to check for intersection, must be a mesh
        :param obj2: object 2 to check for intersection, must be a mesh
        :param skip_inside_check: Disables checking whether one object is completely inside the other.
        :param bvh_cache: Dict of all the bvh trees, removes the `obj` from the cache before adding it again.
        :return: True, if they are intersecting
        """

        if bvh_cache is None:
            bvh_cache = {}

        # If one of the objects has no vertices, collision is impossible
        if len(obj1.get_mesh().vertices) == 0 or len(
                obj2.get_mesh().vertices) == 0:
            return False, bvh_cache

        # create bvhtree for obj1
        if obj1.get_name() not in bvh_cache:
            obj1_BVHtree = obj1.create_bvh_tree()
            bvh_cache[obj1.get_name()] = obj1_BVHtree
        else:
            obj1_BVHtree = bvh_cache[obj1.get_name()]

        # create bvhtree for obj2
        if obj2.get_name() not in bvh_cache:
            obj2_BVHtree = obj2.create_bvh_tree()
            bvh_cache[obj2.get_name()] = obj2_BVHtree
        else:
            obj2_BVHtree = bvh_cache[obj2.get_name()]

        # Check whether both meshes intersect
        inter = len(obj1_BVHtree.overlap(obj2_BVHtree)) > 0

        # Optionally check whether obj2 is contained in obj1
        if not inter and not skip_inside_check:
            inter = CollisionUtility.is_point_inside_object(
                obj1, obj1_BVHtree,
                Matrix(obj2.get_local2world_mat())
                @ obj2.get_mesh().vertices[0].co)
            if inter:
                print(
                    "Warning: Detected that " + obj2.get_name() +
                    " is completely inside " + obj1.get_name() +
                    ". This might be wrong, if " + obj1.get_name() +
                    " is not water tight or has incorrect normals. If that is the case, consider setting "
                    "skip_inside_check to True.")

        # Optionally check whether obj1 is contained in obj2
        if not inter and not skip_inside_check:
            inter = CollisionUtility.is_point_inside_object(
                obj2, obj2_BVHtree,
                Matrix(obj1.get_local2world_mat())
                @ obj1.get_mesh().vertices[0].co)
            if inter:
                print("Warning: Detected that " + obj1.get_name() +
                      " is completely inside " + obj2.get_name() +
                      ". This might be wrong, if " + obj2.get_name() +
                      " is not water tight or has incorrect "
                      "normals. If that is the case, consider "
                      "setting skip_inside_check to True.")

        return inter, bvh_cache