Skip to content

Commit 649e902

Browse files
committed
feat: implement automated CS2 cubemap generation tool with multi-exposure support and image stitching
1 parent dfba471 commit 649e902

5 files changed

Lines changed: 92 additions & 56 deletions

File tree

makefile.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ def build_app_pyinstaller(fast=False, channel='stable') -> None:
230230

231231
'--hidden-import=vpk',
232232
'--collect-all=velopack',
233+
'--collect-all=imageio',
233234

234235
'--noconfirm',
235236

src/app_core.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,7 @@ def setup_buttons(self):
430430
self.ui.documentation_button.clicked.connect(self.open_about)
431431
self.ui.mapbuilder.clicked.connect(self.open_mapbuilder_dialog)
432432
self.ui.open_dialog_button.clicked.connect(self.open_selected_dialog)
433+
self.ui.dialog_selection_combobox.currentTextChanged.connect(self.save_last_tool)
433434
self.updateLaunchAddonButton()
434435

435436
def updateLaunchAddonButton(self):
@@ -531,7 +532,12 @@ def open_export_and_import_addon(self):
531532

532533
def open_my_twitter(self): webbrowser.open("https://twitter.com/dertwist")
533534
def open_discord(self): webbrowser.open("https://discord.gg/6X88yX8Y")
534-
def _restore_user_prefs(self): pass
535+
def save_last_tool(self, text):
536+
set_settings_value('APP', 'last_tool', text)
537+
def _restore_user_prefs(self):
538+
last_tool = get_settings_value('APP', 'last_tool')
539+
if last_tool:
540+
self.ui.dialog_selection_combobox.setCurrentText(last_tool)
535541
def show_minimize_message_once(self): pass
536542

537543
def handle_new_connection(server, widget):

src/forms/cubemap_maker/main.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def init_ui(self):
4747
# Mode
4848
config_layout.addWidget(QLabel("Layout Mode:"))
4949
self.mode_combo = QComboBox()
50-
self.mode_combo.addItems(["CrossHLayout", "Equirectangular"])
50+
self.mode_combo.addItems(["CrossHLayout", "Equirectangular", "Individual Faces"])
5151
config_layout.addWidget(self.mode_combo)
5252

5353
# Game Resolution
@@ -69,6 +69,12 @@ def init_ui(self):
6969
self.pos_x = QDoubleSpinBox(); self.pos_x.setRange(-1e6, 1e6); self.pos_x.setDecimals(2); self.pos_x.setButtonSymbols(QSpinBox.NoButtons)
7070
self.pos_y = QDoubleSpinBox(); self.pos_y.setRange(-1e6, 1e6); self.pos_y.setDecimals(2); self.pos_y.setButtonSymbols(QSpinBox.NoButtons)
7171
self.pos_z = QDoubleSpinBox(); self.pos_z.setRange(-1e6, 1e6); self.pos_z.setDecimals(2); self.pos_z.setButtonSymbols(QSpinBox.NoButtons)
72+
73+
# Load last position
74+
self.pos_x.setValue(float(get_settings_value('CUBEMAP', 'last_pos_x', "0")))
75+
self.pos_y.setValue(float(get_settings_value('CUBEMAP', 'last_pos_y', "0")))
76+
self.pos_z.setValue(float(get_settings_value('CUBEMAP', 'last_pos_z', "0")))
77+
7278
pos_h.addWidget(self.pos_x); pos_h.addWidget(self.pos_y); pos_h.addWidget(self.pos_z)
7379

7480
paste_btn = QPushButton("Paste")
@@ -175,6 +181,10 @@ def start_capture(self):
175181
if addon_dir:
176182
out_path = os.path.join(addon_dir, out_path)
177183

184+
set_settings_value('CUBEMAP', 'last_pos_x', str(self.pos_x.value()))
185+
set_settings_value('CUBEMAP', 'last_pos_y', str(self.pos_y.value()))
186+
set_settings_value('CUBEMAP', 'last_pos_z', str(self.pos_z.value()))
187+
178188
config = {
179189
'hdr': self.hdr_check.isChecked(),
180190
'game_res': self.game_res_edit.text(),

src/forms/cubemap_maker/stitcher.py

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import os
2+
os.environ["OPENCV_IO_ENABLE_OPENEXR"] = "1"
23
import numpy as np
34
from PIL import Image
4-
import imageio
5+
import cv2
56
import pyopencl as cl
67

78
class CubemapStitcher:
@@ -16,25 +17,42 @@ def __init__(self, face_size=1024):
1617
except:
1718
self.ctx = None
1819

20+
def _save_image(self, img_bgr, output_path):
21+
"""Internal helper to save images with correct type for extension."""
22+
if output_path.lower().endswith('.exr'):
23+
# OpenEXR requires float16 or float32, does not support uint8
24+
img_float = img_bgr.astype(np.float32) / 255.0
25+
cv2.imwrite(output_path, img_float)
26+
else:
27+
# Standard formats like .jpg, .png, .tga support uint8
28+
cv2.imwrite(output_path, img_bgr)
29+
1930
def stitch_cross(self, faces, output_path):
2031
"""Creates a 3x4 horizontal cross layout for CS2."""
21-
# Standard cross layout doesn't need flipping if captured correctly
32+
# New CS2 Layout:
33+
# R0: [Empty] [Up 90rot] [Empty] [Empty]
34+
# R1: [Back] [Left] [Forward] [Right]
35+
# R2: [Empty] [Down 90rot] [Empty] [Empty]
2236

2337
cross_img = np.zeros((self.face_size * 3, self.face_size * 4, 3), dtype=np.uint8)
2438

2539
# Row 0
26-
cross_img[0:self.face_size, self.face_size:self.face_size*2] = faces[4] # Up
40+
up_face = np.rot90(faces[4], k=1) # -90 rotate (clockwise)
41+
cross_img[0:self.face_size, self.face_size:self.face_size*2] = up_face
42+
2743
# Row 1
28-
cross_img[self.face_size:self.face_size*2, 0:self.face_size] = faces[3] # Left
29-
cross_img[self.face_size:self.face_size*2, self.face_size:self.face_size*2] = faces[0] # Forward
30-
cross_img[self.face_size:self.face_size*2, self.face_size*2:self.face_size*3] = faces[1] # Right
31-
cross_img[self.face_size:self.face_size*2, self.face_size*3:self.face_size*4] = faces[2] # Back
44+
cross_img[self.face_size:self.face_size*2, 0:self.face_size] = faces[2] # Back
45+
cross_img[self.face_size:self.face_size*2, self.face_size:self.face_size*2] = faces[3] # Left
46+
cross_img[self.face_size:self.face_size*2, self.face_size*2:self.face_size*3] = faces[0] # Forward
47+
cross_img[self.face_size:self.face_size*2, self.face_size*3:self.face_size*4] = faces[1] # Right
48+
3249
# Row 2
33-
cross_img[self.face_size*2:self.face_size*3, self.face_size:self.face_size*2] = faces[5] # Down
50+
down_face = np.rot90(faces[5], k=-5) # 90 rotate (counter-clockwise)
51+
cross_img[self.face_size*2:self.face_size*3, self.face_size:self.face_size*2] = down_face
3452

35-
# Save as EXR (normalized float32)
36-
cross_float = cross_img.astype(np.float32) / 255.0
37-
imageio.imwrite(output_path, cross_float)
53+
# Convert RGB to BGR for OpenCV
54+
cross_bgr = cv2.cvtColor(cross_img, cv2.COLOR_RGB2BGR)
55+
self._save_image(cross_bgr, output_path)
3856
return output_path
3957

4058
def stitch_equirectangular(self, faces, output_path, out_w=4096, out_h=2048):
@@ -107,5 +125,8 @@ def stitch_equirectangular(self, faces, output_path, out_w=4096, out_h=2048):
107125
output = np.empty((out_h, out_w, 3), dtype=np.uint8)
108126
cl.enqueue_copy(self.queue, output, dest_buf)
109127

110-
Image.fromarray(output).save(output_path, quality=95)
128+
# Convert RGB to BGR for OpenCV
129+
output_bgr = cv2.cvtColor(output, cv2.COLOR_RGB2BGR)
130+
self._save_image(output_bgr, output_path)
131+
111132
return output_path

src/forms/cubemap_maker/worker.py

Lines changed: 40 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -132,46 +132,48 @@ def run(self):
132132
time.sleep(0.5)
133133

134134
# Build command list to avoid semicolon issues in netcon
135+
t = 0.1 # Tick interval
135136
cmds = [
136137
"sv_cheats 1", "noclip 1", "r_drawviewmodel 0", "cl_drawhud 0", "r_drawpanorama 0", "cl_firstperson_legs 0",
137138
"fov_cs_debug 106.260205", "ent_fire cmd kill", "ent_create point_servercommand {targetname cmd}",
138139
"screenshot_subdir screenshots\\\\cubemap",
139140
'ent_fire worldent addoutput "OnUser1>cmd>command>r_always_render_all_windows true>0.01>1"',
140141
# Forward
141-
f'ent_fire worldent addoutput "OnUser1>cmd>command>setpos_exact {x} {y} {z}>{1*0.5}>1"',
142-
f'ent_fire worldent addoutput "OnUser1>cmd>command>setang 0 0 0>{1*0.5}>1"',
143-
f'ent_fire worldent addoutput "OnUser1>cmd>command>screenshot_prefix {session_id}_cube_{ev_suffix}_forward>{1*0.5 + 0.1}>1"',
144-
f'ent_fire worldent addoutput "OnUser1>cmd>command>png_screenshot>{1*0.5 + 0.2}>1"',
142+
f'ent_fire worldent addoutput "OnUser1>cmd>command>setpos_exact {x} {y} {z}>{1*t}>1"',
143+
f'ent_fire worldent addoutput "OnUser1>cmd>command>setang_exact 1 0 0>{1*t}>1"',
144+
f'ent_fire worldent addoutput "OnUser1>cmd>command>screenshot_prefix {session_id}_cube_{ev_suffix}_forward>{1*t + 0.01}>1"',
145+
f'ent_fire worldent addoutput "OnUser1>cmd>command>png_screenshot>{1*t + 0.02}>1"',
145146
# Right
146-
f'ent_fire worldent addoutput "OnUser1>cmd>command>setpos_exact {x} {y} {z}>{2*0.5}>1"',
147-
f'ent_fire worldent addoutput "OnUser1>cmd>command>setang 0 270 0>{2*0.5}>1"',
148-
f'ent_fire worldent addoutput "OnUser1>cmd>command>screenshot_prefix {session_id}_cube_{ev_suffix}_right>{2*0.5 + 0.1}>1"',
149-
f'ent_fire worldent addoutput "OnUser1>cmd>command>png_screenshot>{2*0.5 + 0.2}>1"',
147+
f'ent_fire worldent addoutput "OnUser1>cmd>command>setpos_exact {x} {y} {z}>{2*t}>1"',
148+
f'ent_fire worldent addoutput "OnUser1>cmd>command>setang_exact 0 270 -1>{2*t}>1"',
149+
f'ent_fire worldent addoutput "OnUser1>cmd>command>screenshot_prefix {session_id}_cube_{ev_suffix}_right>{2*t + 0.01}>1"',
150+
f'ent_fire worldent addoutput "OnUser1>cmd>command>png_screenshot>{2*t + 0.02}>1"',
150151
# Back
151-
f'ent_fire worldent addoutput "OnUser1>cmd>command>setpos_exact {x} {y} {z}>{3*0.5}>1"',
152-
f'ent_fire worldent addoutput "OnUser1>cmd>command>setang 0 180 0>{3*0.5}>1"',
153-
f'ent_fire worldent addoutput "OnUser1>cmd>command>screenshot_prefix {session_id}_cube_{ev_suffix}_back>{3*0.5 + 0.1}>1"',
154-
f'ent_fire worldent addoutput "OnUser1>cmd>command>png_screenshot>{3*0.5 + 0.2}>1"',
152+
f'ent_fire worldent addoutput "OnUser1>cmd>command>setpos_exact {x} {y} {z}>{3*t}>1"',
153+
f'ent_fire worldent addoutput "OnUser1>cmd>command>setang_exact -1 180 0>{3*t}>1"',
154+
f'ent_fire worldent addoutput "OnUser1>cmd>command>screenshot_prefix {session_id}_cube_{ev_suffix}_back>{3*t + 0.01}>1"',
155+
f'ent_fire worldent addoutput "OnUser1>cmd>command>png_screenshot>{3*t + 0.02}>1"',
155156
# Left
156-
f'ent_fire worldent addoutput "OnUser1>cmd>command>setpos_exact {x} {y} {z}>{4*0.5}>1"',
157-
f'ent_fire worldent addoutput "OnUser1>cmd>command>setang 0 90 0>{4*0.5}>1"',
158-
f'ent_fire worldent addoutput "OnUser1>cmd>command>screenshot_prefix {session_id}_cube_{ev_suffix}_left>{4*0.5 + 0.1}>1"',
159-
f'ent_fire worldent addoutput "OnUser1>cmd>command>png_screenshot>{4*0.5 + 0.2}>1"',
157+
f'ent_fire worldent addoutput "OnUser1>cmd>command>setpos_exact {x} {y} {z}>{4*t}>1"',
158+
f'ent_fire worldent addoutput "OnUser1>cmd>command>setang_exact 0 90 1>{4*t}>1"',
159+
f'ent_fire worldent addoutput "OnUser1>cmd>command>screenshot_prefix {session_id}_cube_{ev_suffix}_left>{4*t + 0.01}>1"',
160+
f'ent_fire worldent addoutput "OnUser1>cmd>command>png_screenshot>{4*t + 0.02}>1"',
160161
# Up
161-
f'ent_fire worldent addoutput "OnUser1>cmd>command>setpos_exact {x} {y} {z}>{5*0.5}>1"',
162-
f'ent_fire worldent addoutput "OnUser1>cmd>command>setang -90 0 0>{5*0.5}>1"',
163-
f'ent_fire worldent addoutput "OnUser1>cmd>command>screenshot_prefix {session_id}_cube_{ev_suffix}_up>{5*0.5 + 0.1}>1"',
164-
f'ent_fire worldent addoutput "OnUser1>cmd>command>png_screenshot>{5*0.5 + 0.2}>1"',
162+
f'ent_fire worldent addoutput "OnUser1>cmd>command>fov_cs_debug 106.260205>{5*t}>1"',
163+
f'ent_fire worldent addoutput "OnUser1>cmd>command>setpos_exact {x} {y} {z}>{5*t}>1"',
164+
f'ent_fire worldent addoutput "OnUser1>cmd>command>setang_exact -89 0 0>{5*t}>1"',
165+
f'ent_fire worldent addoutput "OnUser1>cmd>command>screenshot_prefix {session_id}_cube_{ev_suffix}_up>{5*t + 0.01}>1"',
166+
f'ent_fire worldent addoutput "OnUser1>cmd>command>png_screenshot>{5*t + 0.02}>1"',
165167
# Down
166-
f'ent_fire worldent addoutput "OnUser1>cmd>command>setpos_exact {x} {y} {z}>{6*0.5}>1"',
167-
f'ent_fire worldent addoutput "OnUser1>cmd>command>setang 90 0 0>{6*0.5}>1"',
168-
f'ent_fire worldent addoutput "OnUser1>cmd>command>screenshot_prefix {session_id}_cube_{ev_suffix}_down>{6*0.5 + 0.1}>1"',
169-
f'ent_fire worldent addoutput "OnUser1>cmd>command>png_screenshot>{6*0.5 + 0.2}>1"',
168+
f'ent_fire worldent addoutput "OnUser1>cmd>command>setpos_exact {x} {y} {z}>{6*t}>1"',
169+
f'ent_fire worldent addoutput "OnUser1>cmd>command>setang_exact 89 180 180>{6*t}>1"',
170+
f'ent_fire worldent addoutput "OnUser1>cmd>command>screenshot_prefix {session_id}_cube_{ev_suffix}_down>{6*t + 0.01}>1"',
171+
f'ent_fire worldent addoutput "OnUser1>cmd>command>png_screenshot>{6*t + 0.02}>1"',
170172
# Cleanup
171-
f'ent_fire worldent addoutput "OnUser1>cmd>command>cl_drawhud 1;r_drawviewmodel 1;r_drawpanorama 1;cl_firstperson_legs 1;fov_cs_debug 0;noclip 0>4.0>1"',
172-
f'ent_fire worldent addoutput "OnUser1>cmd>command>r_always_render_all_windows {original_render_all}>4.2>1"',
173-
'ent_fire worldent addoutput "OnUser1>cmd>command>screenshot_subdir \"\">4.2>1"',
174-
'ent_fire worldent addoutput "OnUser1>cmd>command>echo [Cubemap Done]>4.5>1"'
173+
f'ent_fire worldent addoutput "OnUser1>cmd>command>cl_drawhud 1;r_drawviewmodel 1;r_drawpanorama 1;cl_firstperson_legs 1;fov_cs_debug 0;noclip 1>{7*t}>1"',
174+
f'ent_fire worldent addoutput "OnUser1>cmd>command>r_always_render_all_windows {original_render_all}>{7*t + 0.1}>1"',
175+
f'ent_fire worldent addoutput "OnUser1>cmd>command>screenshot_subdir \"\">{7*t + 0.1}>1"',
176+
'ent_fire worldent addoutput "OnUser1>cmd>command>echo [Cubemap Done]>5.0>1"'
175177
]
176178

177179
CS2Netcon.send_many(cmds) # Send all setup commands including the sentinel echo
@@ -202,28 +204,24 @@ def run(self):
202204

203205
if self.config['mode'] == "CrossHLayout":
204206
stitcher.stitch_cross(faces, output_path)
207+
elif self.config['mode'] == "Individual Faces":
208+
# Save individual faces to a subfolder
209+
indiv_dir = os.path.join(self.config['out'], f"individual_{ev_suffix}")
210+
os.makedirs(indiv_dir, exist_ok=True)
211+
face_names = ["forward", "right", "back", "left", "up", "down"]
212+
for i, name in enumerate(face_names):
213+
face_img = Image.fromarray(faces[i])
214+
face_img.save(os.path.join(indiv_dir, f"{name}.png"))
215+
self.progress.emit(f"Saved individual faces to {indiv_dir}")
205216
else:
206217
stitcher.stitch_equirectangular(faces, output_path)
207218

208219
except Exception as e:
209220
self.error.emit(f"Error stitching EV {ev}: {e}")
210221
return
211222

212-
self.finished.emit(f"Success! Cubemap saved to: {self.config['out']}")
213-
214-
# Ensure output folder exists
215-
os.makedirs(self.config['out'], exist_ok=True)
216-
217-
out_name = "output_cubemap"
218-
if self.config['mode'] == "CrossHLayout":
219-
out_path = os.path.join(self.config['out'], f"{out_name}.exr")
220-
stitcher.stitch_cross(faces, out_path)
221-
else:
222-
out_path = os.path.join(self.config['out'], f"{out_name}.jpg")
223-
stitcher.stitch_equirectangular(faces, out_path)
224-
225223
# Success cleanup
226224
try: shutil.rmtree(cs2_ss_dir)
227225
except: pass
228226

229-
self.finished.emit(f"Success! Output saved to: {out_path}")
227+
self.finished.emit(f"Success! Cubemap saved to: {self.config['out']}")

0 commit comments

Comments
 (0)