vis3d.py 15.8 KB
Newer Older
Karol Actun's avatar
Karol Actun committed
1
import importlib
Karol Actun's avatar
Karol Actun committed
2
from threading import Thread
Karol Actun's avatar
Karol Actun committed
3

Karol Actun's avatar
Karol Actun committed
4
from PyQt5.QtWidgets import QApplication, QSplitter, QWidget
5
6

from lib.swarm_sim_header import eprint
Karol Actun's avatar
Karol Actun committed
7
8
9
from lib.visualization.OGLWidget import OGLWidget
import time
from lib.visualization.camera import Camera
10
from lib.visualization.utils import LoadingWindow
Karol Actun's avatar
Karol Actun committed
11

12
import OpenGL.GL as GL
Karol Actun's avatar
Karol Actun committed
13

Karol Actun's avatar
Karol Actun committed
14
15
def close(_):
    exit(0)
Karol Actun's avatar
Karol Actun committed
16
17
18


class Visualization:
Karol Actun's avatar
Karol Actun committed
19
    def __init__(self, world):
Karol Actun's avatar
Karol Actun committed
20
21
22
23
24
25
26
27
28
29
30
31
32
33
        """
        Main Interface between the OpenGL stuff and the simulator.
        Initializes the camera, and the opengl-widget.
        controlls the speed of the simulation.
        :param world: the world class
        """
        self._world = world
        self._last_light_rotation = 0
        self._rounds_per_second = 10
        self._running = False
        self._app = None
        self._viewer = None
        self._gui = None
        self._splitter = None
34
        self.light_rotation = False
Karol Actun's avatar
Karol Actun committed
35

Karol Actun's avatar
Karol Actun committed
36
        # create the QApplication
Karol Actun's avatar
Karol Actun committed
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
        self._app = QApplication([])

        # create camera for the visualization
        # if grid is 2D, set to ortho (ortho is better for 2D)
        if self._world.grid.get_dimension_count() == 2:
            self._camera = Camera(self._world.config_data.window_size_x, self._world.config_data.window_size_y,
                                  self._world.config_data.look_at, self._world.config_data.phi,
                                  self._world.config_data.theta, self._world.config_data.radius,
                                  self._world.config_data.fov, self._world.config_data.cursor_offset,
                                  self._world.config_data.render_distance, "ortho", self._world.grid.get_scaling())
        else:
            self._camera = Camera(self._world.config_data.window_size_x, self._world.config_data.window_size_y,
                                  self._world.config_data.look_at, self._world.config_data.phi,
                                  self._world.config_data.theta, self._world.config_data.radius,
                                  self._world.config_data.fov, self._world.config_data.cursor_offset,
                                  self._world.config_data.render_distance,
                                  "perspective", self._world.grid.get_scaling())
Karol Actun's avatar
Karol Actun committed
54
55

        # create the opengl widget
Karol Actun's avatar
Karol Actun committed
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
        self._viewer = OGLWidget(self._world, self._camera)
        self._viewer.glInit()

        # create and show the main Window
        self._splitter = QSplitter()
        self._splitter.closeEvent = close
        self._splitter.setMinimumWidth(self._world.config_data.window_size_x)
        self._splitter.setMinimumHeight(self._world.config_data.window_size_y)
        self._splitter.setWindowTitle("Simulator")

        self._splitter.show()

        # create gui
        # creating the gui has to happen after showing the window, so the gui can access
        # opengl variables and programs during creation
        gui_module = importlib.import_module('gui.' + self._world.config_data.gui)

        # the key press handler
        def key_press_event(event):
            gui_module.key_handler(event.key(), self._world, self)

        # loading key handler from gui module
        if "key_handler" in dir(gui_module):
            self._viewer.keyPressEventHandler = key_press_event
            self._splitter.keyPressEvent = key_press_event
        else:
            eprint("Warning: no key_handler in gui module => no key handler added.")

        # loading gui from gui module
        if "create_gui" in dir(gui_module):
            self._gui = gui_module.create_gui(self._world, self)
            if self._gui is not None and issubclass(self._gui.__class__, QWidget):
                self._splitter.addWidget(self._gui)
                self._splitter.keyPressEvent = self._viewer.keyPressEvent
                self._splitter.keyReleaseEvent = self._viewer.keyReleaseEvent
                self._splitter.addWidget(self._viewer)
                self._splitter.setSizes(
                    [self._world.config_data.window_size_x * 0.25, self._world.config_data.window_size_x * 0.75])
            else:
                eprint("warning: create_gui in gui module didn't return a QWidget. expected subclass of QWidget, "
                       "but got %s => no gui added." % self._gui.__class__.__name__)
                self._splitter.addWidget(self._viewer)

        else:
            eprint("warning: no create_gui in gui module => no gui added.")
            self._splitter.addWidget(self._viewer)

    def reset(self):
        """
        stops the simulation.
        deletes all data in the visualization.
        resets the camera
        :return:
        """
        self._running = False
        self._viewer.particle_offset_data = {}
        self._viewer.particle_update_flag = True
        self._viewer.tile_offset_data = {}
        self._viewer.tile_update_flag = True
115
116
        self._viewer.location_offset_data = {}
        self._viewer.location_update_flag = True
Karol Actun's avatar
Karol Actun committed
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
        self._viewer.update_data()
        self._camera.reset()
        self._viewer.update_scene()

    def wait_for_thread(self, thread: Thread, window_message, window_title):
        """
        executes a thread and shows a loading window till the thread stops.
        blocks the gui, while thread runs.
        :param thread: the thread
        :param window_message: the displayed message
        :param window_title: the title of the loading window
        :return:
        """

        loading_window = LoadingWindow(window_message, window_title)
        if self._gui is not None and issubclass(self._gui.__class__, QWidget):
            self._gui.setDisabled(True)
        thread.start()
        while thread.is_alive():
            self._app.processEvents()
        thread.join()
        loading_window.close()
        self._gui.setDisabled(False)

    def rotate_light(self):
        """
        rotates the light direction at a steady degrees/second velocity independent of the CPU-clock or framerate.
        :return:
        """
        # rotation of light only in 3d
        if self._world.grid.get_dimension_count() > 2:
            # 20° per second rotation
            if self._last_light_rotation == 0:
                self._last_light_rotation = time.perf_counter()
            else:
                angle = (time.perf_counter() - self._last_light_rotation) * 20
                self._last_light_rotation = time.perf_counter()
                self._viewer.rotate_light(angle)
                self._viewer.glDraw()

    def start_stop(self):
        """
        starts and pauses the simulation
        :return:
        """
        self._running = not self._running

    def _wait_while_not_running(self):
        """
        helper function.
        waits until the running flag is set.
        :return:
        """
        sleep_time = 1.0 / 120.0
        self._app.processEvents()
        if self.light_rotation:
            self.rotate_light()
        while not self._running:
            # sleeping for 1/120 secs, for responsive GUI
            time.sleep(sleep_time)
            if self.light_rotation:
                self.rotate_light()
            self._app.processEvents()
Karol Actun's avatar
Karol Actun committed
180
181

    def run(self, round_start_timestamp):
Karol Actun's avatar
Karol Actun committed
182
183
184
185
186
187
        """
        main function for running the simulation with the visualization.
        Controlls the waiting time, so the rounds_per_second value is being kept.
        :param round_start_timestamp: timestamp of the start of the round.
        :return:
        """
188
        # update and draw scene
Karol Actun's avatar
Karol Actun committed
189
190
        self._viewer.update_data()
        self._viewer.glDraw()
Karol Actun's avatar
Karol Actun committed
191
        # waiting until simulation starts
Karol Actun's avatar
Karol Actun committed
192
        self._wait_while_not_running()
Karol Actun's avatar
Karol Actun committed
193
194
        # waiting until enough time passed to do the next simulation round.
        time_elapsed = time.perf_counter() - round_start_timestamp
Karol Actun's avatar
Karol Actun committed
195
196
        # sleeping time - max 1/120 for a responsive GUI
        sleep_time = min(1.0 / 120, (1.0 / self._rounds_per_second) / 10.0)
197
198
        max_wait_time = 1 / self._rounds_per_second
        while time_elapsed < max_wait_time:
Karol Actun's avatar
Karol Actun committed
199
            # waiting for 1/100 of the round_time
Karol Actun's avatar
Karol Actun committed
200
201
202
            time.sleep(sleep_time)
            # check if still running... if not wait (important for low rounds_per_second values)
            self._wait_while_not_running()
Karol Actun's avatar
Karol Actun committed
203
            time_elapsed = time.perf_counter() - round_start_timestamp
Karol Actun's avatar
Karol Actun committed
204

205
206
207
        GL.glFinish()
        end = time.perf_counter_ns()

Karol Actun's avatar
Karol Actun committed
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
    def remove_particle(self, particle):
        """
        removes a particle from the visualization.
        it wont be deleted immediately! not until the next round.
        if you want an immediate deletion of the particle, then call this function, then, update_data and after that
        glDraw of the OpenGLWidget.

        :param particle: the particle (not the id, the instance) to be deleted
        :return:
        """
        self._viewer.particle_update_flag = True
        if particle in self._viewer.particle_offset_data:
            del self._viewer.particle_offset_data[particle]

    def particle_changed(self, particle):
        """
        updates the offset, color and carry data of the particle in the visualization.
        it wont be an immediate update. it will update in the beginning of the next "run" call / after current round.
        :param particle: the particle that has changed (the instance)
        :return:
        """
        self._viewer.particle_update_flag = True
        self._viewer.particle_offset_data[particle] = (particle.coordinates, particle.color,
                                                       1.0 if particle.get_carried_status() else 0.0)

    def remove_tile(self, tile):
        """
        removes a tile from the visualization.
        :param tile: the tile (not the id, the instance) to be deleted
        :return:
        """
        self._viewer.tile_update_flag = True
        if tile in self._viewer.tile_offset_data:
            del self._viewer.tile_offset_data[tile]

    def tile_changed(self, tile):
        """
        updates the offset, color and carry data of the tile in the visualization.
        :param tile: the tile ( not the id, the instance) to be deleted
        :return:
        """
        self._viewer.tile_update_flag = True
        self._viewer.tile_offset_data[tile] = (tile.coordinates, tile.color, 1.0 if tile.get_tile_status() else 0.0)

252
    def remove_location(self, location):
Karol Actun's avatar
Karol Actun committed
253
        """
254
255
        removes a location from the visualization.
        :param location: the location (not the id, the instance) to be deleted
Karol Actun's avatar
Karol Actun committed
256
257
        :return:
        """
258
259
260
        self._viewer.location_update_flag = True
        if location in self._viewer.location_offset_data:
            del self._viewer.location_offset_data[location]
Karol Actun's avatar
Karol Actun committed
261

262
    def location_changed(self, location):
Karol Actun's avatar
Karol Actun committed
263
        """
264
265
        updates the offset and color data of the location in the visualization.
        :param location: the location ( not the id, the instance) to be deleted
Karol Actun's avatar
Karol Actun committed
266
267
        :return:
        """
268
269
        self._viewer.location_update_flag = True
        self._viewer.location_offset_data[location] = (location.coordinates, location.color)
Karol Actun's avatar
Karol Actun committed
270

Karol Actun's avatar
Karol Actun committed
271
272
273
    def update_visualization_data(self):
        self._viewer.update_data()

Karol Actun's avatar
Karol Actun committed
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
    # setters and getters for various variables in the visualization

    def set_rounds_per_second(self, rounds_per_second):
        self._rounds_per_second = rounds_per_second

    def get_rounds_per_second(self):
        return self._rounds_per_second

    def reset_camera_position(self):
        self._camera.reset()
        self._viewer.update_scene()

    def set_field_of_view(self, fov: float):
        self._camera.set_fov(fov)
        self._viewer.update_programs_projection_matrix()
        self._viewer.update_cursor_data()
        self._viewer.glDraw()

    def get_field_of_view(self):
        return self._camera.get_fov()

    def set_drag_sensitivity(self, s: float):
        self._viewer.drag_sensitivity = s

    def get_drag_sensitivity(self):
        return self._viewer.drag_sensitivity

    def set_zoom_sensitivity(self, s: float):
        self._viewer.zoom_sensitivity = s

    def get_zoom_sensitivity(self):
        return self._viewer.zoom_sensitivity

    def set_rotation_sensitivity(self, s: float):
        self._viewer.rotation_sensitivity = s

    def get_rotation_sensitivity(self):
        return self._viewer.rotation_sensitivity

    def get_projection_type(self):
        return self._camera.get_projection_type()

    def set_projection_type(self, projection_type):
        self._camera.set_projection_type(projection_type)
        self._viewer.update_programs_projection_matrix()
        self._viewer.glDraw()

    def get_background_color(self):
        return self._viewer.background

    def set_background_color(self, color):
325
        self._viewer.set_background_color(color)
Karol Actun's avatar
Karol Actun committed
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346

    def get_grid_line_color(self):
        return self._viewer.programs["grid"].get_line_color()

    def set_grid_line_color(self, color):
        self._viewer.programs["grid"].set_line_color(color)

    def get_grid_line_width(self):
        return self._viewer.programs["grid"].width

    def set_grid_line_width(self, width):
        self._viewer.programs["grid"].set_width(width)
        self._viewer.glDraw()

    def get_grid_line_scaling(self):
        return self._viewer.programs["grid"].get_line_scaling()

    def set_grid_line_scaling(self, scaling):
        self._viewer.programs["grid"].set_line_scaling(scaling)
        self._viewer.glDraw()

347
    def get_grid_coordinates_color(self):
Karol Actun's avatar
Karol Actun committed
348
349
        return self._viewer.programs["grid"].get_model_color()

350
    def set_grid_coordinates_color(self, color):
Karol Actun's avatar
Karol Actun committed
351
352
353
        self._viewer.programs["grid"].set_model_color(color)
        self._viewer.glDraw()

354
    def get_grid_coordinates_scaling(self):
Karol Actun's avatar
Karol Actun committed
355
356
        return self._viewer.programs["grid"].get_model_scaling()

357
    def set_grid_coordinates_scaling(self, scaling):
Karol Actun's avatar
Karol Actun committed
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
        self._viewer.programs["grid"].set_model_scaling(scaling)
        self._viewer.glDraw()

    def get_render_distance(self):
        return self._camera.get_render_distance()

    def set_render_distance(self, render_distance):
        self._camera.set_render_distance(render_distance)
        self._viewer.update_programs_projection_matrix()
        self._viewer.glDraw()

    def get_show_lines(self):
        return self._viewer.programs["grid"].show_lines

    def set_show_lines(self, show_lines: bool):
        self._viewer.programs["grid"].show_lines = show_lines
        self._viewer.glDraw()

376
377
    def get_show_coordinates(self):
        return self._viewer.programs["grid"].show_coordinates
Karol Actun's avatar
Karol Actun committed
378

379
380
    def set_show_coordinates(self, show_coordinates: bool):
        self._viewer.programs["grid"].show_coordinates = show_coordinates
Karol Actun's avatar
Karol Actun committed
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
        self._viewer.glDraw()

    def get_show_center(self):
        return self._viewer.show_center

    def set_show_center(self, show_center: bool):
        self._viewer.show_center = show_center

    def get_show_focus(self):
        return self._viewer.show_focus

    def set_show_focus(self, show_focus: bool):
        self._viewer.show_focus = show_focus

    def take_screenshot(self):
        self._viewer.take_screenshot()

    def recalculate_grid(self, size):
        self._viewer.programs["grid"].update_offsets(self._world.grid.get_box(size))
400
401
402
403
404
405
406
407
408
409
410
411
412
413
        self._viewer.glDraw()

    def get_particle_scaling(self):
        return self._viewer.programs["particle"].get_model_scaling()

    def set_particle_scaling(self, scaling):
        self._viewer.programs["particle"].set_model_scaling(scaling)

    def get_tile_scaling(self):
        return self._viewer.programs["tile"].get_model_scaling()

    def set_tile_scaling(self, scaling):
        self._viewer.programs["tile"].set_model_scaling(scaling)

414
415
    def get_location_scaling(self):
        return self._viewer.programs["location"].get_model_scaling()
416

417
    def set_location_scaling(self, scaling):
418
419
420
421
422
423
        self._viewer.programs["location"].set_model_scaling(scaling)

    def set_on_cursor_click_matter_type(self, matter_type):
        if matter_type == 'tile' or matter_type == 'particle' or matter_type == 'location':
            self._viewer.cursor_type = matter_type
            self._viewer.update_cursor_data()