def test_open(): with WalImageFile.open(TEST_FILE) as im: assert im.format == "WAL" assert im.format_description == "Quake2 Texture" assert im.mode == "P" assert im.size == (128, 128) assert isinstance(im, WalImageFile.WalImageFile) assert_image_equal_tofile(im, "Tests/images/hopper_wal.png")
def test_open(self): # Arrange TEST_FILE = "Tests/images/hopper.wal" # Act im = WalImageFile.open(TEST_FILE) # Assert self.assertEqual(im.format, "WAL") self.assertEqual(im.format_description, "Quake2 Texture") self.assertEqual(im.mode, "P") self.assertEqual(im.size, (128, 128))
def test_open(): # Arrange TEST_FILE = "Tests/images/hopper.wal" # Act im = WalImageFile.open(TEST_FILE) # Assert assert im.format == "WAL" assert im.format_description == "Quake2 Texture" assert im.mode == "P" assert im.size == (128, 128)
def load_texture(pball_path: str, texture: str) -> Optional[Image.Image]: """ Loads Image object based on texture name and subdir :param pball_path: path to game media folder :param texture: texture name the way it is stored in the bsp file (relative to pball/textures and without extension) :return: RGBA Image object """ if not os.path.exists(pball_path + "/textures/" + "/".join(texture.lower().split("/")[:-1])): print( f"Info: no such path {pball_path + '/textures/' + '/'.join(texture.lower().split('/')[:-1])}" ) return # list of all files in stored subdirectory texture_options = os.listdir(pball_path + "/textures/" + "/".join(texture.lower().split("/")[:-1])) texture_path = "" # iterate through texture options until one name matches stored texture name for idx, tex_option in enumerate(texture_options): if texture.split("/")[-1].lower() == os.path.splitext(tex_option)[0]: texture_path = "/".join( texture.lower().split("/")[:-1]) + "/" + tex_option break # texture was not found in specified subdirectory if not texture_path: print("Missing texture: ", texture) return if os.path.splitext(texture_path)[1] in [".png", ".jpg", ".tga"]: img = Image.open(pball_path + "/textures/" + texture_path) img2 = img.convert("RGBA") return img2 elif os.path.splitext(texture_path)[1] == ".wal": # wal files are 8 bit and require a palette with open("pb2e.pal", "r") as pal: conts = (pal.read().split("\n")[3:]) conts = [b.split(" ") for b in conts] conts = [c for b in conts for c in b] conts.pop(len(conts) - 1) conts = list(map(int, conts)) img3 = WalImageFile.open(pball_path + "/textures/" + texture_path) img3.putpalette(conts) img3 = img3.convert("RGBA") return img3 else: print( f"Error: unsupported format {os.path.splitext(texture_path)[1]} in {texture_path}" f"\nsupported formats are .png, .jpg, .tga, .wal") return
def get_polygons(path: str, pball_path: str) -> Tuple[List[Polygon], List[Tuple[int]]]: """ Converts information from Q2BSP object into List of Polygon objects Calculates mean color of all used textures and builds list of all unique colors :param path: full path to map :param pball_path: path to pball / game media directory, needed to get full texture path :return: list of Polygon objects, list of RGB colors """ # instead of directly reading all information from file, the Q2BSP class is used for reading temp_map = Q2BSP(path) # get a list of unique texture names (which are stored without an extension -> multiple ones must be tested) texture_list = [x.get_texture_name() for x in temp_map.tex_infos] texture_list_cleaned = list(dict.fromkeys(texture_list)) # iterate through texture list, look which one exists, load, rescale to 1×1 pixel = color is mean color average_colors = list() skip_faces = list() for texture in texture_list_cleaned: color = (0, 0, 0) if not os.path.exists(pball_path + "/textures/" + "/".join(texture.lower().split("/")[:-1])): print( f"Info: no such path {pball_path+'/textures/'+'/'.join(texture.lower().split('/')[:-1])}" ) # sets (0,0,0) as default color for missing textures average_colors.append((0, 0, 0)) continue # list of all files in stored subdirectory texture_options = os.listdir(pball_path + "/textures/" + "/".join(texture.lower().split("/")[:-1])) texture_path = "" # iterate through texture options until one name matches stored texture name for idx, tex_option in enumerate(texture_options): if texture.split("/")[-1].lower() == os.path.splitext( tex_option)[0]: texture_path = "/".join( texture.lower().split("/")[:-1]) + "/" + tex_option break # texture was not found in specified subdirectory if not texture_path: print("Missing texture: ", texture) average_colors.append((0, 0, 0)) continue if os.path.splitext(texture_path)[1] in [".png", ".jpg", ".tga"]: img = Image.open(pball_path + "/textures/" + texture_path) img2 = img.resize((1, 1)) img2 = img2.convert("RGBA") img2 = img2.load() color = img2[0, 0] elif os.path.splitext(texture_path)[1] == ".wal": # wal files are 8 bit and require a palette with open("pb2e.pal", "r") as pal: conts = (pal.read().split("\n")[3:]) conts = [b.split(" ") for b in conts] conts = [c for b in conts for c in b] conts.pop(len(conts) - 1) conts = list(map(int, conts)) img3 = WalImageFile.open(pball_path + "/textures/" + texture_path) img3.putpalette(conts) img3 = img3.convert("RGBA") img2 = img3.resize((1, 1)) color = img2.getpixel((0, 0)) else: print( f"Error: unsupported format {os.path.splitext(texture_path)[1]} in {texture_path}" f"\nsupported formats are .png, .jpg, .tga, .wal") color_rgb = color[:3] if color_rgb == (0, 0, 0): print(texture) if True in [ x in texture.lower() for x in ["origin", "clip", "skip", "hint", "trigger"] ]: print(texture) color_rgb = (0, 0, 0, 0) # actually rgba average_colors.append(color_rgb) # instead of storing face color directly in the Polygon object, store an index so that you can easily change one # color for all faces using the same one tex_indices = [x.texture_info for x in temp_map.faces] tex_ids = [ texture_list_cleaned.index(texture_list[tex_index]) for tex_index in tex_indices ] # each face is a list of vertices stored as Tuples faces: List[List[Tuple]] = list() skip_surfaces = [] for idx, face in enumerate(temp_map.faces): flags = temp_map.tex_infos[face.texture_info].flags if flags.hint or flags.nodraw or flags.sky or flags.skip: skip_surfaces.append(idx) current_face: List[Tuple] = list() for i in range(face.num_edges): face_edge = temp_map.face_edges[face.first_edge + i] if face_edge > 0: edge = temp_map.edge_list[face_edge] else: edge = temp_map.edge_list[abs(face_edge)][::-1] for vert in edge: if not temp_map.vertices[vert] in current_face: current_face.append(temp_map.vertices[vert]) faces.append(current_face) # get minimal of all x y and z values and move all vertices so they all have coordinate values >= 0 min_x = min([a[0] for b in faces for a in b]) min_y = min([a[1] for b in faces for a in b]) min_z = min([a[2] for b in faces for a in b]) polys_normalized = [[[ vertex[0] - min_x, vertex[1] - min_y, vertex[2] - min_z ] for vertex in edge] for edge in faces] # get normals out of the Q2BSP object, if face.plane_side != 0, flip it (invert signs of coordinates) normal_list = [x.normal for x in temp_map.planes] normals = list() for face in temp_map.faces: # print(temp_map.tex_infos[face.texture_info].flags) if not face.plane_side == 0: # -1*0.0 returns -0.0 which is prevented by this expression # TODO: Does -0.0 do any harm here? normal = [ -1 * x if not x == 0.0 else x for x in normal_list[face.plane] ] else: normal = list(normal_list[face.plane]) normals.append(normal) # construct polygon list out of the faces, indices into unique textures aka colors (two different textures could # have the same mean color), normals polygons: List[Polygon] = list() for idx, poly in enumerate(polys_normalized): polygon = Polygon(poly, tex_ids[idx], point3f(*normals[idx])) polygons.append(polygon) print(skip_surfaces, "skip") for i in skip_surfaces[::-1]: polygons.pop(i) # for idx, poly in enumerate(polygons): # print(average_colors[poly.tex_id]) # if average_colors[poly.tex_id] == (0,0,0,0): # print("here") # polygons.pop(len(polygons)-1-idx) return polygons, average_colors
def get_polys(path, pball_path): with open(path, "rb") as f: # bsps are binary files bytes1 = f.read() # stores all bytes in bytes1 variable (named like that to not interfere with builtin names # get offset (position of entity block begin) and length of entity block -> see bsp quake 2 format documentation offset_faces = int.from_bytes(bytes1[56:60], byteorder='little', signed=False) length_faces = int.from_bytes(bytes1[60:64], byteorder='little', signed=False) offset_verts = int.from_bytes(bytes1[24:28], byteorder='little', signed=False) length_verts = int.from_bytes(bytes1[28:32], byteorder='little', signed=False) vertices = list() for i in range(int(length_verts / 12)): (vert_x,) = struct.unpack('<f', (bytes1[offset_verts + 12 * i + 0:offset_verts + 12 * i + 4])) (vert_y,) = struct.unpack('<f', (bytes1[offset_verts + 12 * i + 4:offset_verts + 12 * i + 8])) (vert_z,) = struct.unpack('<f', (bytes1[offset_verts + 12 * i + 8:offset_verts + 12 * i + 12])) vertices.append([vert_x, vert_y, vert_z]) # print(f"{vert_x} - {vert_y} - {vert_z}") offset_edges = int.from_bytes(bytes1[96:100], byteorder='little', signed=False) length_edges = int.from_bytes(bytes1[100:104], byteorder='little', signed=False) edges = list() for i in range(int(length_edges / 4)): # texture information lump is 76 bytes large vert_1 = int.from_bytes(bytes1[offset_edges + 4 * i + 0:offset_edges + 4 * i + 2], byteorder='little', signed=False) vert_2 = int.from_bytes(bytes1[offset_edges + 4 * i + 2:offset_edges + 4 * i + 4], byteorder='little', signed=False) edges.append([vertices[vert_1], vertices[vert_2]]) offset_face_edges = int.from_bytes(bytes1[104:108], byteorder='little', signed=False) length_face_edges = int.from_bytes(bytes1[108:112], byteorder='little', signed=False) face_edges = list() for i in range(int(length_face_edges / 4)): # texture information lump is 76 bytes large edge_index = int.from_bytes(bytes1[offset_face_edges + 4 * i + 0:offset_face_edges + 4 * i + 4], byteorder='little', signed=True) if edge_index > 0: face_edges.append([edges[abs(edge_index)][0], edges[abs(edge_index)][1]]) elif edge_index < 0: face_edges.append([edges[abs(edge_index)][1], edges[abs(edge_index)][0]]) offset_textures = int.from_bytes(bytes1[48:52], byteorder='little', signed=False) length_textures = int.from_bytes(bytes1[52:56], byteorder='little', signed=False) texture_list = list() for i in range(int(length_textures/76)): tex = (bytes1[offset_textures+76*i+40:offset_textures+76*i+72]) tex = [x for x in tex if x] tex_name = struct.pack("b" * len(tex), *tex).decode('ascii', "ignore") print(tex_name) texture_list.append(tex_name) faces = list() tex_ids = list() texture_list_cleaned=list(dict.fromkeys(texture_list)) average_colors=list() for texture in texture_list_cleaned: color = (0, 0, 0) if os.path.isfile(pball_path+"/textures/"+texture+".png"): img = Image.open((pball_path+"/textures/"+texture+".png")) img2 = img.resize((1, 1)) color = img2.getpixel((0, 0)) elif os.path.isfile(pball_path+"/textures/"+texture+".jpg"): img = Image.open((pball_path+"/textures/"+texture+".jpg")) img.save("1.png") img2 = img.resize((1, 1)) # break color = img2.getpixel((0, 0)) # print(f"texture: {texture} - color: {color}") elif os.path.isfile(pball_path + "/textures/" + texture + ".tga"): img = Image.open((pball_path + "/textures/" + texture + ".tga")) img2 = img.resize((1, 1)) color = img2.getpixel((0, 0)) # print(f"texture: {texture} - color: {color}") elif os.path.isfile(pball_path+"/textures/"+texture+".wal"): with open("pb2e.pal", "r") as pal: conts = (pal.read().split("\n")[3:]) conts = [b.split(" ") for b in conts] conts = [c for b in conts for c in b] conts.pop(len(conts)-1) conts=list(map(int, conts)) img3 = WalImageFile.open((pball_path+"/textures/"+texture+".wal")) img3.putpalette(conts) img3=img3.convert("RGBA") print(img3.mode) img2 = img3.resize((1, 1)) color = img2.getpixel((0, 0)) print(f"texture: {texture} - color: {color}") color_rgb = color[:3] average_colors.append(color_rgb) for i in range(int(length_faces / 20)): # texture information lump is 76 bytes large # get sum of flags / transform flag bit field to uint32 first_edge = (bytes1[offset_faces + 20 * i + 4:offset_faces + 20 * i + 8]) (num_edges,) = struct.unpack('<H', (bytes1[offset_faces + 20 * i + 8:offset_faces + 20 * i + 10])) (tex_index,) = struct.unpack('<H', (bytes1[offset_faces + 20 * i + 10:offset_faces + 20 * i + 12])) print(tex_index) tex_ids.append(texture_list_cleaned.index(texture_list[tex_index])) print(tex_ids[len(tex_ids)-1]) first_edge = int.from_bytes(first_edge, byteorder='little', signed=True) next_edges = list() for j in range(num_edges): if face_edges[first_edge+j][0] not in next_edges: next_edges.append(face_edges[first_edge+j][0]) if face_edges[first_edge + j][1] not in next_edges: next_edges.append(face_edges[first_edge + j][1]) faces.append(next_edges) print(texture_list_cleaned) print(tex_ids) print(average_colors) min_x = min([p for i in [[vertex[0] for vertex in edge] for edge in faces] for p in i]) min_y = min([p for i in [[vertex[1] for vertex in edge] for edge in faces] for p in i]) min_z = min([p for i in [[vertex[2] for vertex in edge] for edge in faces] for p in i]) return [[[round(vertex[0]-min_x), round(vertex[1]-min_y), round(vertex[2]-min_z)] for vertex in edge] for edge in faces], tex_ids, average_colors
def test_load(): with WalImageFile.open(TEST_FILE) as im: assert im.load()[0, 0] == 122 # Test again now that it has already been loaded once assert im.load()[0, 0] == 122