def _load_room(node: dict, metadata: dict, material_adjustments: list, transform: Matrix, house_id: str, parent: MeshObject, room_per_object: dict) -> List[MeshObject]: """ Load the room specified in the given node. :param node: The node dict which contains information from house.json.. :param metadata: A dict of metadata which will be written into the object's custom data. :param material_adjustments: Adjustments to the materials which were specified inside house.json. :param transform: The transformation that should be applied to the loaded objects. :param house_id: The id of the current house. :param parent: The parent object to which the room should be linked :param room_per_object: A dict for object -> room lookup (Will be written into) :return: The list of loaded mesh objects. """ # Build empty room object which acts as a parent for all objects inside room_obj = MeshObject.create_empty("Room#" + node["id"]) room_obj.set_cp("type", "Room") room_obj.set_cp("bbox", SuncgLoader._correct_bbox_frame(node["bbox"])) room_obj.set_cp("roomTypes", node["roomTypes"]) room_obj.set_parent(parent) loaded_objects = [room_obj] # Store indices of all contained objects in if "nodeIndices" in node: for child_id in node["nodeIndices"]: room_per_object[child_id] = room_obj if "hideFloor" not in node or node["hideFloor"] != 1: metadata["type"] = "Floor" metadata["category_id"] = LabelIdMapping.label_id_map["floor"] metadata["fine_grained_class"] = "floor" loaded_objects += SuncgLoader._load_obj(os.path.join(SuncgLoader._suncg_dir, "room", house_id, node["modelId"] + "f.obj"), metadata, material_adjustments, transform, room_obj) if "hideCeiling" not in node or node["hideCeiling"] != 1: metadata["type"] = "Ceiling" metadata["category_id"] = LabelIdMapping.label_id_map["ceiling"] metadata["fine_grained_class"] = "ceiling" loaded_objects += SuncgLoader._load_obj(os.path.join(SuncgLoader._suncg_dir, "room", house_id, node["modelId"] + "c.obj"), metadata, material_adjustments, transform, room_obj) if "hideWalls" not in node or node["hideWalls"] != 1: metadata["type"] = "Wall" metadata["category_id"] = LabelIdMapping.label_id_map["wall"] metadata["fine_grained_class"] = "wall" loaded_objects += SuncgLoader._load_obj(os.path.join(SuncgLoader._suncg_dir, "room", house_id, node["modelId"] + "w.obj"), metadata, material_adjustments, transform, room_obj) return loaded_objects
def _create_mesh_objects_from_file(data: dict, ceiling_light_strength: float, mapping: dict, json_path: str) -> List[MeshObject]: """ This creates for a given data json block all defined meshes and assigns the correct materials. This means that the json file contains some mesh, like walls and floors, which have to built up manually. It also already adds the lighting for the ceiling :param data: json data dir. Must contain "material" and "mesh" :param ceiling_light_strength: Strength of the emission shader used in the ceiling. :param mapping: A dict which maps the names of the objects to ids. :param json_path: Path to the json file, where the house information is stored. :return: The list of loaded mesh objects. """ # extract all used materials -> there are more materials defined than used used_materials = [] for mat in data["material"]: used_materials.append({"uid": mat["uid"], "texture": mat["texture"], "normaltexture": mat["normaltexture"], "color": mat["color"]}) created_objects = [] for mesh_data in data["mesh"]: # extract the obj name, which also is used as the category_id name used_obj_name = mesh_data["type"].strip() if used_obj_name == "": used_obj_name = "void" if "material" not in mesh_data: warnings.warn(f"Material is not defined for {used_obj_name} in this file: {json_path}") continue # create a new mesh obj = MeshObject.create_empty(used_obj_name, used_obj_name + "_mesh") created_objects.append(obj) # set two custom properties, first that it is a 3D_future object and second the category_id obj.set_cp("is_3D_future", True) obj.set_cp("category_id", mapping[used_obj_name.lower()]) # get the material uid of the current mesh data current_mat = mesh_data["material"] used_mat = None # search in the used materials after this uid for u_mat in used_materials: if u_mat["uid"] == current_mat: used_mat = u_mat break # If there should be a material used if used_mat: if used_mat["texture"]: raise Exception("The material should use a texture, this was not implemented yet!") if used_mat["normaltexture"]: raise Exception("The material should use a normal texture, this was not implemented yet!") # if there is a normal color used if used_mat["color"]: # Create a new material mat = bpy.data.materials.new(name=used_obj_name + "_material") mat.use_nodes = True nodes = mat.node_tree.nodes # create a principled node and set the default color principled_node = Utility.get_the_one_node_with_type(nodes, "BsdfPrincipled") principled_node.inputs["Base Color"].default_value = mathutils.Vector(used_mat["color"]) / 255.0 # if the object is a ceiling add some light output if "ceiling" in used_obj_name.lower(): links = mat.node_tree.links mix_node = nodes.new(type='ShaderNodeMixShader') output = Utility.get_the_one_node_with_type(nodes, 'OutputMaterial') Utility.insert_node_instead_existing_link(links, principled_node.outputs['BSDF'], mix_node.inputs[2], mix_node.outputs['Shader'], output.inputs['Surface']) # The light path node returns 1, if the material is hit by a ray coming from the camera, # else it returns 0. In this way the mix shader will use the principled shader for rendering # the color of the lightbulb itself, while using the emission shader for lighting the scene. light_path_node = nodes.new(type='ShaderNodeLightPath') links.new(light_path_node.outputs['Is Camera Ray'], mix_node.inputs['Fac']) emission_node = nodes.new(type='ShaderNodeEmission') # use the same color for the emission light then for the ceiling itself emission_node.inputs["Color"].default_value = mathutils.Vector(used_mat["color"]) / 255.0 emission_node.inputs["Strength"].default_value = ceiling_light_strength links.new(emission_node.outputs["Emission"], mix_node.inputs[1]) # as this material was just created the material is just append it to the empty list obj.add_material(mat) # extract the vertices from the mesh_data vert = [float(ele) for ele in mesh_data["xyz"]] # extract the faces from the mesh_data faces = mesh_data["faces"] # extract the normals from the mesh_data normal = [float(ele) for ele in mesh_data["normal"]] # map those to the blender coordinate system num_vertices = int(len(vert) / 3) vertices = np.reshape(np.array(vert), [num_vertices, 3]) normal = np.reshape(np.array(normal), [num_vertices, 3]) # flip the first and second value vertices[:, 1], vertices[:, 2] = vertices[:, 2], vertices[:, 1].copy() normal[:, 1], normal[:, 2] = normal[:, 2], normal[:, 1].copy() # reshape back to a long list vertices = np.reshape(vertices, [num_vertices * 3]) normal = np.reshape(normal, [num_vertices * 3]) # add this new data to the mesh object mesh = obj.get_mesh() mesh.vertices.add(num_vertices) mesh.vertices.foreach_set("co", vertices) mesh.vertices.foreach_set("normal", normal) # link the faces as vertex indices num_vertex_indicies = len(faces) mesh.loops.add(num_vertex_indicies) mesh.loops.foreach_set("vertex_index", faces) # the loops are set based on how the faces are a ranged num_loops = int(num_vertex_indicies / 3) mesh.polygons.add(num_loops) # always 3 vertices form one triangle loop_start = np.arange(0, num_vertex_indicies, 3) # the total size of each triangle is therefore 3 loop_total = [3] * num_loops mesh.polygons.foreach_set("loop_start", loop_start) mesh.polygons.foreach_set("loop_total", loop_total) # the uv coordinates are reshaped then the face coords are extracted uv_mesh_data = [float(ele) for ele in mesh_data["uv"] if ele is not None] # bb1737bf-dae6-4215-bccf-fab6f584046b.json includes one mesh which only has no UV mapping if uv_mesh_data: uv = np.reshape(np.array(uv_mesh_data), [num_vertices, 2]) used_uvs = uv[faces, :] # and again reshaped back to the long list used_uvs = np.reshape(used_uvs, [2 * num_vertex_indicies]) mesh.uv_layers.new(name="new_uv_layer") mesh.uv_layers[-1].data.foreach_set("uv", used_uvs) else: warnings.warn(f"This mesh {obj.name} does not have a specified uv map!") # this update converts the upper data into a mesh mesh.update() # the generation might fail if the data does not line up # this is not used as even if the data does not line up it is still able to render the objects # We assume that not all meshes in the dataset do conform with the mesh standards set in blender #result = mesh.validate(verbose=False) #if result: # raise Exception("The generation of the mesh: {} failed!".format(used_obj_name)) return created_objects
def load(house_path: str, suncg_dir: str) -> List[MeshObject]: """ Loads a house.json file into blender. - Loads all objects files specified in the house.json file. - Orders them hierarchically (level -> room -> object) - Writes metadata into the custom properties of each object :param house_path: The path to the house.json file which should be loaded. :param suncg_dir: The path to the suncg root directory which should be used for loading objects, rooms, textures etc. :return: The list of loaded mesh objects. """ SuncgLoader._suncg_dir = suncg_dir SuncgLoader._collection_of_loaded_objs = {} # there are only two types of materials, textures and diffuse SuncgLoader._collection_of_loaded_mats = {"texture": {}, "diffuse": {}} with open(Utility.resolve_path(house_path), "r") as f: config = json.load(f) object_label_map, object_fine_grained_label_map, object_coarse_grained_label_map = SuncgLoader._read_model_category_mapping(os.path.join('resources','suncg','Better_labeling_for_NYU.csv')) house_id = config["id"] loaded_objects = [] for level in config["levels"]: # Build empty level object which acts as a parent for all rooms on the level level_obj = MeshObject.create_empty("Level#" + level["id"]) level_obj.set_cp("type", "Level") if "bbox" in level: level_obj.set_cp("bbox", SuncgLoader._correct_bbox_frame(level["bbox"])) else: print("Warning: The level with id " + level["id"] + " is missing the bounding box attribute in the given house.json file!") loaded_objects.append(level_obj) room_per_object = {} for node in level["nodes"]: # Skip invalid nodes (This is the same behavior as in the SUNCG Toolbox) if "valid" in node and node["valid"] == 0: continue # Metadata is directly stored in the objects custom data metadata = { "type": node["type"], "is_suncg": True } if "modelId" in node: metadata["modelId"] = node["modelId"] if node["modelId"] in object_fine_grained_label_map: metadata["fine_grained_class"] = object_fine_grained_label_map[node["modelId"]] metadata["coarse_grained_class"] = object_coarse_grained_label_map[node["modelId"]] metadata["category_id"] = SuncgLoader._get_label_id(node["modelId"], object_label_map) if "bbox" in node: metadata["bbox"] = SuncgLoader._correct_bbox_frame(node["bbox"]) if "transform" in node: transform = Matrix([node["transform"][i*4:(i+1)*4] for i in range(4)]) # Transpose, as given transform matrix was col-wise, but blender expects row-wise transform.transpose() else: transform = None if "materials" in node: material_adjustments = node["materials"] else: material_adjustments = [] # Lookup if the object belongs to a room object_id = int(node["id"].split("_")[-1]) if object_id in room_per_object: parent = room_per_object[object_id] else: parent = level_obj if node["type"] == "Room": loaded_objects += SuncgLoader._load_room(node, metadata, material_adjustments, transform, house_id, level_obj, room_per_object) elif node["type"] == "Ground": loaded_objects += SuncgLoader._load_ground(node, metadata, material_adjustments, transform, house_id, parent) elif node["type"] == "Object": loaded_objects += SuncgLoader._load_object(node, metadata, material_adjustments, transform, parent) elif node["type"] == "Box": loaded_objects += SuncgLoader._load_box(node, material_adjustments, transform, parent) SuncgLoader._rename_materials() return loaded_objects