-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathbsp_to_obj.py
185 lines (175 loc) · 7.84 KB
/
bsp_to_obj.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
"""Converts .bsp files into .obj files"""
import time
from typing import Dict, Generator, List
import sys
import bsp_tool
import vector
def source_bsp_to_obj(bsp) -> Generator[str, None, None]: # TODO: write .mtl for each vmt
"""yields an .obj file, one line at a time"""
start_time = time.time()
yield f"# Generated by bsp_tool_examples/objs/obj_model_from_bsp.py from {bsp.filename}\n"
yield "# Y+ Forward, Z+ Up\n"
for vertex in bsp.VERTICES:
yield f"v {vertex.x} {vertex.y} {vertex.z}\n"
for plane in bsp.PLANES:
yield f"vn {plane.normal.x} {plane.normal.y} {plane.normal.z}\n"
faces_by_material: Dict[str, List[int]] = {}
# ^ {material: [face_index]}
disps_by_material: Dict[str, List[int]] = {}
# ^ {material: [face_index]}
# for model in bsp.MODELS
# model_faces = slice(model.first_face, model.first_face + model.num_faces)
# --- for face in bsp.FACES[modeL_faces]
# for vertex in model: vertex = vertex + model.origin
for face_index, face in enumerate(bsp.FACES):
tex_info = bsp.TEXINFO[face.tex_info]
tex_data = bsp.TEXDATA[tex_info.tex_data]
material = bsp.TEXDATA_STRING_DATA[tex_data.tex_data_string_index]
if face.disp_info == -1:
if material not in faces_by_material:
faces_by_material[material] = []
faces_by_material[material].append(face_index)
else:
if material not in disps_by_material:
disps_by_material[material] = []
disps_by_material[material].append(face_index)
def uvs_of(vertex_index, face):
vertex = bsp.VERTICES[vertex_index]
tex_info = bsp.TEXINFO[face.tex_info]
tex_data = bsp.TEXDATA[tex_info.tex_data]
texture = tex_info.texture
u = vector.dot(vertex, (texture.s.x, texture.s.y, texture.s.z)) + texture.s.offset
v = -vector.dot(vertex, (texture.t.x, texture.t.y, texture.t.z)) + texture.t.offset
view_width = tex_data.view_width
view_height = tex_data.view_height
u /= view_width if view_width != 0 else 1
v /= view_height if view_height != 0 else 1
return u, v
# FACES
face_number = 0
current_progress = 0.1
print("0...", end="")
vt_count = 0
for material in faces_by_material:
if "SKYBOX" in material: # assumes skybox is last
yield f"o {material}"
yield f"usemtl {material}\n"
for face_index in faces_by_material[material]:
face = bsp.FACES[face_index]
surfedges = bsp.SURFEDGES[face.first_edge:(face.first_edge + face.num_edges)]
edges = [(bsp.EDGES[se] if se > -1 else bsp.EDGES[-se][::-1]) for se in surfedges]
vs = [e[0] for e in edges]
vn = face.plane
f = list()
for v in vs:
vt_u, vt_v = uvs_of(v, face)
yield f"vt {vt_u} {vt_v}\n"
vt = vt_count
vt_count += 1
f.append((v + 1, vt + 1, vn + 1))
yield "f " + " ".join([f"{v}/{vt}/{vn}" for v, vt, vn in reversed(f)]) + "\n"
face_number += 1
if face_number >= len(bsp.FACES) * current_progress:
print(f"{current_progress * 10:.0f}...", end="")
current_progress += 0.1
# DISPLACEMENTS
v_count = len(bsp.VERTICES) + 1
vn_count = len(bsp.PLANES) + 1
disp_no = 0
yield "g displacements\n"
for material in disps_by_material:
yield f"usemtl {material}\n"
for face_index in disps_by_material[material]:
yield f"o displacement_{disp_no}\n"
disp_no += 1
disp_vs = bsp.vertices_of_displacement(face_index)
f = []
for v, vn, vt, vt2, colour in disp_vs:
yield f"v {vector.vec3(*v):}\n"
yield f"vt {vector.vec2(*vt):}\n"
vn = bsp.FACES[face_index].plane + 1
power = bsp.DISP_INFO[bsp.FACES[face_index].disp_info].power
tris = bsp.mod.displacement_indices(power)
for A, B, C in zip(tris[::3], tris[1::3], tris[2::3]):
A = (A + v_count, A + vt_count, vn)
B = (B + v_count, B + vt_count, vn)
C = (C + v_count, C + vt_count, vn)
A, B, C = [map(str, i) for i in (C, B, A)] # CCW FLIP
yield f"f {'/'.join(A)} {'/'.join(B)} {'/'.join(C)}\n"
disp_size = (2 ** power + 1) ** 2
v_count += disp_size
vt_count += disp_size
vn_count += 1
face_number += 1
if face_number >= len(bsp.FACES) * current_progress:
print(f'{current_progress * 10:.0f}...', end='')
current_progress += 0.1
total_time = time.time() - start_time
minutes = total_time // 60
seconds = total_time - minutes * 60
yield f"# file generated in {minutes:.0f} minutes {seconds:2.3f} seconds"
print("Done!")
print(f"Generated in {minutes:.0f} minutes {seconds:2.3f} seconds")
def respawn_bsp_to_obj(bsp): # TODO: write .mtl for each vmt
"""yields an .obj file, one line at a time"""
start_time = time.time()
yield f"# Generated by bsp_tool from {bsp.filename}\n"
vts = []
current_progress = 0.1
print("0...", end="")
for vertex in bsp.VERTICES:
yield f"v {vertex.x} {vertex.y} {vertex.z}\n"
for normal in bsp.VERTEX_NORMALS:
yield f"vn {normal.x} {normal.y} {normal.z}\n"
for mesh_index, mesh in enumerate(bsp.MESHES):
yield f"o MESH_{mesh_index}\n"
triangles = [] # [(v, vt, vn)]
for vertex in bsp.vertices_of_mesh(mesh_index):
if vertex.uv not in vts:
yield f"vt {vertex.uv.u} {vertex.uv.v}\n"
vt = len(vts)
else:
vt = vts.index(vertex.uv) + 1
triangles.append((vertex.position_index + 1, vt, vertex.normal_index + 1))
for A, B, C in zip(triangles[::3], triangles[1::3], triangles[2::3]):
A, B, C = [map(str, i) for i in (C, B, A)] # CCW FLIP
yield f"f {'/'.join(A)} {'/'.join(B)} {'/'.join(C)}\n"
if mesh_index >= len(bsp.MESHES) * current_progress:
print(f'{current_progress * 10:.0f}...', end='')
current_progress += 0.1
total_time = time.time() - start_time
minutes = total_time // 60
seconds = total_time - minutes * 60
yield f"# file generated in {minutes:.0f} minutes {seconds:2.3f} seconds"
print("Done!")
print(f"Generated in {minutes:.0f} minutes {seconds:2.3f} seconds")
if __name__ == "__main__":
# import argparse
# parser = argparse.ArgumentParser(description="Converts .bsp files into .obj files")
# parser.add_argument("files", metavar="FILENAME", nargs="+",
# help=".bsp files to convert")
# supported_games = list(bsp_tool.mods.by_name) # allow incomplete names
# # substitute spaces with dashes / underscores
# # perhaps a function that maps a string to the nearest game name?
# parser.add_argument("--game", default="Team Fortress 2",
# help=f"supported games: {supported_games}")
# # parser.add_argument("--outfile", default="")
# # name an output, how will it work for multiple files?
# parser.print_help()
sys.argv.append("hightower_assets.bsp")
if len(sys.argv) > 1: # drag & drop obj converter
# if game not in ("Titanfall 2", "Apex Legends"):
write_obj = source_bsp_to_obj
for map_path in sys.argv[1:]:
bsp = bsp_tool.Bsp(map_path)
obj_file = open(map_path + ".obj", "w")
buffer = ""
for line in write_obj(bsp):
buffer += line
if len(buffer) > 2048:
obj_file.write(buffer)
buffer = ""
obj_file.write(buffer)
obj_file.close()
else:
... # do nothing (tests can go here)