shaderflow 0.4.0.dev1__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of shaderflow might be problematic. Click here for more details.

Files changed (51) hide show
  1. ShaderFlow/Common/Imgui.py +0 -0
  2. ShaderFlow/Common/Notes.py +134 -0
  3. ShaderFlow/Common/__init__.py +0 -0
  4. ShaderFlow/Message.py +147 -0
  5. ShaderFlow/Module.py +162 -0
  6. ShaderFlow/Modules/Audio.py +431 -0
  7. ShaderFlow/Modules/Bouncing.py +94 -0
  8. ShaderFlow/Modules/Camera.py +451 -0
  9. ShaderFlow/Modules/Depth.py +0 -0
  10. ShaderFlow/Modules/Dynamics.py +288 -0
  11. ShaderFlow/Modules/Frametimer.py +79 -0
  12. ShaderFlow/Modules/Keyboard.py +43 -0
  13. ShaderFlow/Modules/Noise.py +80 -0
  14. ShaderFlow/Modules/OpticalFlow.py +1 -0
  15. ShaderFlow/Modules/Piano.py +362 -0
  16. ShaderFlow/Modules/Spectrogram.py +315 -0
  17. ShaderFlow/Modules/Video.py +234 -0
  18. ShaderFlow/Modules/Waveform.py +100 -0
  19. ShaderFlow/Modules/__init__.py +0 -0
  20. ShaderFlow/Resources/Images/ShaderFlow.png +0 -0
  21. ShaderFlow/Resources/Images/ShaderFlow.svg +1 -0
  22. ShaderFlow/Resources/Scenes/Examples/Demo.py +284 -0
  23. ShaderFlow/Resources/Scenes/Examples/GLSL/Bars.frag +20 -0
  24. ShaderFlow/Resources/Scenes/Examples/GLSL/Bouncing.frag +50 -0
  25. ShaderFlow/Resources/Scenes/Examples/GLSL/Life/Simulation.glsl +49 -0
  26. ShaderFlow/Resources/Scenes/Examples/GLSL/Life/Visuals.glsl +31 -0
  27. ShaderFlow/Resources/Scenes/Examples/GLSL/MotionBlur.frag +20 -0
  28. ShaderFlow/Resources/Scenes/Examples/GLSL/Multipass.frag +43 -0
  29. ShaderFlow/Resources/Scenes/Examples/GLSL/RayMarch.frag +59 -0
  30. ShaderFlow/Resources/Scenes/Examples/GLSL/ShaderToy.frag +65 -0
  31. ShaderFlow/Resources/Scenes/Examples/GLSL/Video.frag +5 -0
  32. ShaderFlow/Resources/Scenes/Examples/GLSL/Visualizer.frag +75 -0
  33. ShaderFlow/Resources/Scenes/Examples/GLSL/Waveform.frag +18 -0
  34. ShaderFlow/Resources/Shaders/Fragment/Default.glsl +65 -0
  35. ShaderFlow/Resources/Shaders/Fragment/Final.glsl +7 -0
  36. ShaderFlow/Resources/Shaders/Fragment/Loading.frag +40 -0
  37. ShaderFlow/Resources/Shaders/Fragment/Missing.glsl +23 -0
  38. ShaderFlow/Resources/Shaders/Include/Camera.glsl +166 -0
  39. ShaderFlow/Resources/Shaders/Include/Complex.glsl +0 -0
  40. ShaderFlow/Resources/Shaders/Include/ShaderFlow.glsl +444 -0
  41. ShaderFlow/Resources/Shaders/Vertex/Default.glsl +21 -0
  42. ShaderFlow/Scene.py +1124 -0
  43. ShaderFlow/Shader.py +391 -0
  44. ShaderFlow/Texture.py +528 -0
  45. ShaderFlow/Variable.py +128 -0
  46. ShaderFlow/__init__.py +11 -0
  47. ShaderFlow/__main__.py +22 -0
  48. shaderflow-0.4.0.dev1.dist-info/METADATA +79 -0
  49. shaderflow-0.4.0.dev1.dist-info/RECORD +51 -0
  50. shaderflow-0.4.0.dev1.dist-info/WHEEL +5 -0
  51. shaderflow-0.4.0.dev1.dist-info/entry_points.txt +2 -0
ShaderFlow/Scene.py ADDED
@@ -0,0 +1,1124 @@
1
+ import importlib
2
+ import inspect
3
+ import math
4
+ import os
5
+ from abc import abstractmethod
6
+ from collections import deque
7
+ from pathlib import Path
8
+ from subprocess import PIPE
9
+ from time import perf_counter
10
+ from typing import (
11
+ Annotated,
12
+ Any,
13
+ Deque,
14
+ Dict,
15
+ Iterable,
16
+ List,
17
+ Optional,
18
+ Self,
19
+ Tuple,
20
+ Union,
21
+ )
22
+
23
+ import glfw
24
+ import imgui
25
+ import moderngl
26
+ import PIL
27
+ import tqdm
28
+ from attr import Factory, define, field
29
+ from dotmap import DotMap
30
+ from moderngl_window.context.base import BaseWindow as ModernglWindow
31
+ from moderngl_window.integrations.imgui import ModernglWindowRenderer as ModernglImgui
32
+ from typer import Option
33
+
34
+ import Broken
35
+ from Broken import (
36
+ BrokenEnum,
37
+ BrokenPath,
38
+ BrokenResolution,
39
+ BrokenScheduler,
40
+ BrokenTask,
41
+ BrokenThread,
42
+ BrokenTyper,
43
+ OnceTracker,
44
+ PlainTracker,
45
+ clamp,
46
+ denum,
47
+ flatten,
48
+ hyphen_range,
49
+ limited_integer_ratio,
50
+ log,
51
+ )
52
+ from Broken.Externals.FFmpeg import BrokenFFmpeg
53
+ from Broken.Loaders import LoaderBytes, LoaderString
54
+ from Broken.Types import Hertz, Seconds, Unchanged
55
+ from ShaderFlow import SHADERFLOW
56
+ from ShaderFlow.Message import ShaderMessage
57
+ from ShaderFlow.Module import ShaderModule
58
+ from ShaderFlow.Modules.Camera import ShaderCamera
59
+ from ShaderFlow.Modules.Dynamics import DynamicNumber
60
+ from ShaderFlow.Modules.Frametimer import ShaderFrametimer
61
+ from ShaderFlow.Modules.Keyboard import ShaderKeyboard
62
+ from ShaderFlow.Shader import ShaderObject
63
+ from ShaderFlow.Variable import ShaderVariable
64
+
65
+
66
+ class WindowBackend(BrokenEnum):
67
+ Headless = "headless"
68
+ GLFW = "glfw"
69
+
70
+ @define
71
+ class ShaderScene(ShaderModule):
72
+ __name__ = "Scene"
73
+
74
+ # # Base modules
75
+
76
+ modules: Deque[ShaderModule] = Factory(deque)
77
+ """Deque of all Modules on the Scene, not a set for order preservation"""
78
+
79
+ # Scheduling
80
+ scheduler: BrokenScheduler = Factory(BrokenScheduler)
81
+ vsync: BrokenTask = None
82
+
83
+ # ShaderFlow modules
84
+ frametimer: ShaderFrametimer = None
85
+ keyboard: ShaderKeyboard = None
86
+ camera: ShaderCamera = None
87
+ ffmpeg: BrokenFFmpeg = None
88
+
89
+ # # Fractional SSAA
90
+
91
+ shader: ShaderObject = None
92
+ """The main ShaderObject of the Scene, the visible content of the Window"""
93
+
94
+ _final: ShaderObject = None
95
+ """Internal ShaderObject used for a Fractional Super-Sampling Anti-Aliasing (SSAA). This shader
96
+ samples the texture from the user's final self.shader, which is rendered at SSAA resolution"""
97
+
98
+ alpha: bool = False
99
+ """Makes the final texture have an alpha channel, useful for transparent windows. Exporting
100
+ videos might fail, perhaps output a Chroma Key compatible video - add this to the shader:
101
+ - `fragColor.rgb = mix(vec3(0, 1, 0), fragColor.rgb, fragColor.a);`"""
102
+
103
+ quality: float = field(default=50, converter=lambda x: clamp(x, 0, 100))
104
+ """Rendering Quality, if implemented - either on the GPU Shader or CPU Python side"""
105
+
106
+ typer: BrokenTyper = Factory(lambda: BrokenTyper(chain=True))
107
+ """This Scene's BrokenTyper instance for the CLI. Commands are added by any module in the
108
+ `self.commands` method. The `self.main` is always added to it"""
109
+
110
+ def __post__(self):
111
+ self.typer.description = (self.typer.description or self.__class__.__doc__)
112
+ self.typer._panel = "🔥 Scene Commands"
113
+ self.typer.command(self.main, context=True)
114
+ self.build()
115
+
116
+ def cli(self, *args: List[Union[Any, str]]):
117
+ """Interpret a list of arguments as actions, defined by the Scene's `self.commands` plus
118
+ the `main` method. Must not start with `sys.executable`, so send `sys.argv[1:]` or direct"""
119
+ self.typer(flatten(args))
120
+
121
+ __built__: OnceTracker = Factory(OnceTracker)
122
+
123
+ def build(self):
124
+ if self.__built__():
125
+ return
126
+
127
+ # Init Imgui
128
+ imgui.create_context()
129
+ self.imguio = imgui.get_io()
130
+ self.imguio.font_global_scale = 1
131
+ self.imguio.fonts.add_font_from_file_ttf(
132
+ str(Broken.BROKEN.RESOURCES.FONTS/"DejaVuSans.ttf"),
133
+ 16*self.imguio.font_global_scale,
134
+ )
135
+
136
+ # Default modules
137
+ self.init_window()
138
+ self.frametimer = ShaderFrametimer(scene=self)
139
+ self.keyboard = ShaderKeyboard(scene=self)
140
+ self.camera = ShaderCamera(scene=self)
141
+
142
+ # Create the SSAA Workaround engines
143
+ self._final = ShaderObject(scene=self, name="iFinal")
144
+ self._final.texture.components = 3 + int(self.alpha)
145
+ self._final.texture.dtype = "f1"
146
+ self._final.texture.final = True
147
+ self._final.texture.track = True
148
+ self._final.fragment = (SHADERFLOW.RESOURCES.FRAGMENT/"Final.glsl")
149
+ self.shader = ShaderObject(scene=self, name="iScreen")
150
+ self.shader.texture.track = True
151
+ self.shader.texture.repeat(False)
152
+
153
+ # ---------------------------------------------------------------------------------------------|
154
+ # Temporal
155
+
156
+ time: Seconds = field(default=0.0, converter=float)
157
+ """Current time in seconds. Ideally, everything should depend on time, for flexibility"""
158
+
159
+ tempo: float = Factory(lambda: DynamicNumber(value=1, frequency=3))
160
+ """Time scale factor, used for `dt`, which integrates to `time`"""
161
+
162
+ runtime: Seconds = field(default=10.0, converter=float)
163
+ """The longest module duration; overriden by the user; or default length of 10s"""
164
+
165
+ fps: Hertz = field(default=60.0, converter=lambda x: max(float(x), 1.0))
166
+ """Target frames per second rendering speed"""
167
+
168
+ dt: Seconds = field(default=0.0, converter=float)
169
+ """Virtual delta time since last frame, time scaled by `tempo`. Use `self.rdt` for real delta"""
170
+
171
+ rdt: Seconds = field(default=0.0, converter=float)
172
+ """Real life, physical delta time since last frame. Use `self.dt` for time scaled version"""
173
+
174
+ @property
175
+ def tau(self) -> float:
176
+ """Normalized time value relative to runtime between 0 and 1"""
177
+ return (self.time / self.runtime)
178
+
179
+ @property
180
+ def cycle(self) -> float:
181
+ """A number from 0 to 2pi that ends on the runtime ('normalized angular time')"""
182
+ return (2 * math.pi * self.tau)
183
+
184
+ @property
185
+ def frametime(self) -> Seconds:
186
+ """Ideal time between two frames. This value is coupled with `fps`"""
187
+ return (1 / self.fps)
188
+
189
+ @frametime.setter
190
+ def frametime(self, value: Seconds):
191
+ self.fps = (1 / value)
192
+
193
+ @property
194
+ def frame(self) -> int:
195
+ """Current frame being rendered. This value is coupled with 'time' and 'fps'"""
196
+ return round(self.time * self.fps)
197
+
198
+ @frame.setter
199
+ def frame(self, value: int):
200
+ self.time = (value / self.fps)
201
+
202
+ # Total Duration
203
+
204
+ @property
205
+ def duration(self) -> Seconds:
206
+ """Alias to self.runtime. Set both with `.set_duration()`"""
207
+ return self.runtime
208
+
209
+ def set_duration(self, override: Seconds=None, *, minimum: Seconds=10) -> Seconds:
210
+ """Either force the runtime to be 'override' or find the longest module lower bounded"""
211
+ self.runtime = (override or minimum)
212
+ for module in (not bool(override)) * self.modules:
213
+ self.runtime = max(self.runtime, module.duration)
214
+ self.runtime /= self.tempo.value
215
+ return self.runtime
216
+
217
+ @property
218
+ def total_frames(self) -> int:
219
+ """The total frames this scene should render when exporting, if 'runtime' isn't changed"""
220
+ return round(self.runtime * self.fps)
221
+
222
+ # ---------------------------------------------------------------------------------------------|
223
+ # Window synchronized properties
224
+
225
+ # # Title
226
+
227
+ _title: str = "ShaderFlow"
228
+
229
+ @property
230
+ def title(self) -> str:
231
+ """Realtime window 'title' property"""
232
+ return self._title
233
+
234
+ @title.setter
235
+ def title(self, value: str):
236
+ log.debug(f"{self.who} Changing Window Title to ({value})")
237
+ self.window.title = value
238
+ self._title = value
239
+
240
+ # # Resizable
241
+
242
+ _resizable: bool = True
243
+
244
+ @property
245
+ def resizable(self) -> bool:
246
+ """Realtime window 'is resizable' property"""
247
+ return self._resizable
248
+
249
+ @resizable.setter
250
+ def resizable(self, value: bool):
251
+ log.debug(f"{self.who} Changing Window Resizable to ({value})")
252
+ self.window.resizable = value
253
+ self._resizable = value
254
+
255
+ # # Visible
256
+
257
+ _visible: bool = False
258
+
259
+ @property
260
+ def visible(self) -> bool:
261
+ """Realtime window 'is visible' property"""
262
+ return self._visible
263
+
264
+ @visible.setter
265
+ def visible(self, value: bool):
266
+ log.debug(f"{self.who} Changing Window Visibility to ({value})")
267
+ self.window.visible = value
268
+ self._visible = value
269
+
270
+ # # Window Fullscreen
271
+
272
+ _fullscreen: bool = False
273
+
274
+ @property
275
+ def fullscreen(self) -> bool:
276
+ """Window 'is fullscreen' property"""
277
+ return self._fullscreen
278
+
279
+ @fullscreen.setter
280
+ def fullscreen(self, value: bool):
281
+ log.debug(f"{self.who} Changing Window Fullscreen to ({value})")
282
+ self._fullscreen = value
283
+ try:
284
+ self.window.fullscreen = value
285
+ except AttributeError:
286
+ pass
287
+
288
+ # # Window Exclusive
289
+
290
+ _exclusive: bool = False
291
+
292
+ @property
293
+ def exclusive(self) -> bool:
294
+ """Window 'mouse exclusivity' property: Is the mouse cursor be locked to the window"""
295
+ return self._exclusive
296
+
297
+ @exclusive.setter
298
+ def exclusive(self, value: bool):
299
+ log.debug(f"{self.who} Changing Window Exclusive to ({value})")
300
+ self.window.mouse_exclusivity = value
301
+ self._exclusive = value
302
+
303
+ # # Video modes and monitor
304
+
305
+ monitor: int = os.environ.get("MONITOR", 0)
306
+
307
+ @property
308
+ def glfw_monitor(self) -> Optional[glfw._GLFWmonitor]:
309
+ if (monitors := glfw.get_monitors()):
310
+ return monitors[self.monitor]
311
+
312
+ @property
313
+ def glfw_video_mode(self) -> Optional[Dict]:
314
+ if (monitor := self.glfw_monitor):
315
+ return glfw.get_video_mode(monitor)
316
+
317
+ @property
318
+ def monitor_framerate(self) -> float:
319
+ """Note: Defaults to 60 if no monitor is found"""
320
+ if (mode := self.glfw_video_mode):
321
+ return mode.refresh_rate or 60.0
322
+ return 60.0
323
+
324
+ @property
325
+ def monitor_size(self) -> Optional[Tuple[int, int]]:
326
+ if self.exporting:
327
+ return None
328
+ if (mode := self.glfw_video_mode):
329
+ return (mode.size.width, mode.size.height)
330
+
331
+ @property
332
+ def monitor_width(self) -> Optional[int]:
333
+ if (resolution := self.monitor_size):
334
+ return resolution[0]
335
+
336
+ @property
337
+ def monitor_height(self) -> Optional[int]:
338
+ if (resolution := self.monitor_size):
339
+ return resolution[1]
340
+
341
+ # ---------------------------------------------------------------------------------------------|
342
+ # Resolution
343
+
344
+ # # Scale
345
+
346
+ _scale: float = field(default=1.0, converter=lambda x: max(0.01, x))
347
+
348
+ @property
349
+ def scale(self) -> float:
350
+ """Resolution scale factor, the `self.width` and `self.height` are multiplied by this"""
351
+ return self._scale
352
+
353
+ @scale.setter
354
+ def scale(self, value: float):
355
+ log.debug(f"{self.who} Changing Resolution Scale to ({value})")
356
+ self.resize(scale=value)
357
+
358
+ # # Width
359
+
360
+ _width: int = field(default=1920, converter=lambda x: int(max(1, x)))
361
+
362
+ @property
363
+ def width(self) -> int:
364
+ """Rendering width (horizontal size) of the Scene in pixels"""
365
+ return round(self._width * self._scale)
366
+
367
+ @width.setter
368
+ def width(self, value: int):
369
+ self.resize(width=value)
370
+
371
+ # # Height
372
+
373
+ _height: int = field(default=1080, converter=lambda x: int(max(1, x)))
374
+
375
+ @property
376
+ def height(self) -> int:
377
+ """Rendering height (vertical size) of the Scene in pixels"""
378
+ return round(self._height * self._scale)
379
+
380
+ @height.setter
381
+ def height(self, value: int):
382
+ self.resize(height=value)
383
+
384
+ # # SSAA
385
+
386
+ _ssaa: float = field(default=1.0, converter=lambda x: max(0.01, x))
387
+
388
+ @property
389
+ def ssaa(self) -> float:
390
+ """Fractional Super Sampling Anti-Aliasing (SSAA) factor. Improves the image quality (>1) by
391
+ rendering at a higher resolution and then downsampling, resulting in smoother edges at a
392
+ significant GPU computational cost of O(N^2). Values lower than 1 (yield worse quality, but)
393
+ are useful when the GPU can't keep up: when the resolution is too high or FPS is too low"""
394
+ return self._ssaa
395
+
396
+ @ssaa.setter
397
+ def ssaa(self, value: float):
398
+ log.debug(f"{self.who} Changing Fractional SSAA to {value}")
399
+ self._ssaa = value
400
+ self.relay(ShaderMessage.Shader.RecreateTextures)
401
+
402
+ # # Resolution (With, Height)
403
+
404
+ @property
405
+ def resolution(self) -> Tuple[int, int]:
406
+ """The resolution the Scene is rendering in pixels"""
407
+ return (self.width, self.height)
408
+
409
+ @resolution.setter
410
+ def resolution(self, value: Tuple[int, int]):
411
+ self.resize(*value)
412
+
413
+ @property
414
+ def render_resolution(self) -> Tuple[int, int]:
415
+ """Internal 'true' rendering resolution for SSAA. Same as `self.resolution*self.ssaa`"""
416
+ return (round(self.width*self.ssaa), round(self.height*self.ssaa))
417
+
418
+ # # Aspect Ratio
419
+
420
+ _aspect_ratio: float = None
421
+
422
+ @property
423
+ def aspect_ratio(self) -> float:
424
+ """Either the forced `self._aspect_ratio` or dynamic from `self.width/self.height`. When set
425
+ and resizing, the logic of `BrokenResolution.fit` is applied to enforce ratios"""
426
+ return self._aspect_ratio or (self._width/self._height)
427
+
428
+ @aspect_ratio.setter
429
+ def aspect_ratio(self, value: Union[float, str]):
430
+ log.debug(f"{self.who} Changing Aspect Ratio to {value}")
431
+
432
+ # The aspect ratio can be sent as a fraction or "none", "false"
433
+ if isinstance(value, str):
434
+ value = eval(value.replace(":", "/").capitalize())
435
+
436
+ # Optimization: Only change if different
437
+ if (self._aspect_ratio == value):
438
+ return
439
+
440
+ self._aspect_ratio = value
441
+
442
+ if (self.backend == WindowBackend.GLFW):
443
+ num, den = limited_integer_ratio(self._aspect_ratio, limit=2**20) or (glfw.DONT_CARE, glfw.DONT_CARE)
444
+ glfw.set_window_aspect_ratio(self.window._window, num, den)
445
+
446
+ def resize(self,
447
+ width: Union[int, float]=Unchanged,
448
+ height: Union[int, float]=Unchanged,
449
+ *,
450
+ aspect_ratio: Union[Unchanged, float, str]=Unchanged,
451
+ scale: float=Unchanged
452
+ ) -> Tuple[int, int]:
453
+ """
454
+ Resize the true final rendering resolution of the Scene. Rounded to nearest multiple of 2,
455
+ so FFmpeg is happy, and limited by the monitor resolution if realtime
456
+
457
+ Args:
458
+ width: New width of the Scene, None to not change
459
+ height: New height of the Scene, None to not change
460
+
461
+ Returns:
462
+ Self: Fluent interface
463
+ """
464
+
465
+ # Maybe update auxiliary properties
466
+ self.aspect_ratio = (aspect_ratio or self._aspect_ratio)
467
+ self._scale = (scale or self._scale)
468
+
469
+ # The parameters aren't trivial. The idea is to fit resolution from the scale-less components,
470
+ # so scaling isn't carried over, then to apply scaling (self.resolution)
471
+ resolution = BrokenResolution.fit(
472
+ old=(self._width, self._height),
473
+ new=(width, height),
474
+ max=(self.monitor_size),
475
+ ar=self._aspect_ratio
476
+ )
477
+
478
+ # Optimization: Only resize when resolution changes
479
+ if (resolution != (self.width, self.height)):
480
+ self._width, self._height = resolution
481
+ self.window.size = self.resolution
482
+ self.relay(ShaderMessage.Shader.RecreateTextures)
483
+ log.info(f"{self.who} Resized Window to {self.resolution}")
484
+
485
+ return self.resolution
486
+
487
+ # ---------------------------------------------------------------------------------------------|
488
+ # Window, OpenGL, Backend
489
+
490
+ backend: WindowBackend = WindowBackend.get(os.environ.get("WINDOW_BACKEND", WindowBackend.GLFW))
491
+ """The ModernGL Window Backend. **Cannot be changed after creation**. Can also be set with the
492
+ environment variable `WINDOW_BACKEND=<backend>`, where `backend = {glfw, headless}`"""
493
+
494
+ opengl: moderngl.Context = None
495
+ """ModernGL Context of this Scene. The thread accessing this MUST own or ENTER its context for
496
+ creating, changing, deleting objects; more often than not, it's the Main thread"""
497
+
498
+ window: ModernglWindow = None
499
+ """ModernGL Window instance at `site-packages/moderngl_window.context.<self.backend>.Window`"""
500
+
501
+ imgui: ModernglImgui = None
502
+ """ModernGL Imgui integration class bound to the Window"""
503
+
504
+ imguio: Any = None
505
+ """Imgui IO object"""
506
+
507
+ def init_window(self) -> None:
508
+ """Create the window and the OpenGL context"""
509
+ if self.window:
510
+ raise RuntimeError("Window backend cannot be changed after creation")
511
+
512
+ # Use EGL for creating a OpenGL context, allows true headless with GPU acceleration
513
+ # https://forums.developer.nvidia.com/t/81412 - Comments 2 and 6
514
+ backend = "egl" if (os.environ.get("WINDOW_EGL", "1") == "1") else None
515
+
516
+ # Dynamically import the ModernGL Window Backend and instantiate it. Vsync is on our side 😉
517
+ module = f"moderngl_window.context.{denum(self.backend).lower()}"
518
+ self.window = importlib.import_module(module).Window(
519
+ size=self.resolution,
520
+ title=self.title,
521
+ resizable=self.resizable,
522
+ visible=self.visible,
523
+ fullscreen=self.fullscreen,
524
+ vsync=False,
525
+ backend=backend
526
+ )
527
+ ShaderKeyboard.set_keymap(self.window.keys)
528
+ self.imgui = ModernglImgui(self.window)
529
+ self.opengl = self.window.ctx
530
+
531
+ # Bind window events to relay
532
+ self.window.resize_func = self.__window_resize__
533
+ self.window.close_func = self.__window_close__
534
+ self.window.iconify_func = self.__window_iconify__
535
+ self.window.key_event_func = self.__window_key_event__
536
+ self.window.mouse_position_event_func = self.__window_mouse_position_event__
537
+ self.window.mouse_press_event_func = self.__window_mouse_press_event__
538
+ self.window.mouse_release_event_func = self.__window_mouse_release_event__
539
+ self.window.mouse_drag_event_func = self.__window_mouse_drag_event__
540
+ self.window.mouse_scroll_event_func = self.__window_mouse_scroll_event__
541
+ self.window.unicode_char_entered_func = self.__window_unicode_char_entered__
542
+ self.window.files_dropped_event_func = self.__window_files_dropped_event__
543
+
544
+ if (self.backend == WindowBackend.GLFW):
545
+ BrokenThread.new(target=self.window.set_icon, icon_path=Broken.PROJECT.RESOURCES.ICON, daemon=True)
546
+ glfw.set_cursor_enter_callback(self.window._window, lambda _, enter: self.__window_mouse_enter_event__(inside=enter))
547
+ glfw.set_drop_callback(self.window._window, self.__window_files_dropped_event__)
548
+ ShaderKeyboard.Keys.LEFT_SHIFT = glfw.KEY_LEFT_SHIFT
549
+ ShaderKeyboard.Keys.LEFT_CTRL = glfw.KEY_LEFT_CONTROL
550
+ ShaderKeyboard.Keys.LEFT_ALT = glfw.KEY_LEFT_ALT
551
+
552
+ log.debug(f"{self.who} Finished Window creation")
553
+
554
+ def read_screen(self) -> bytes:
555
+ """Take a screenshot of the screen and return raw bytes. Length `width*height*components`"""
556
+ return self.window.fbo.read(viewport=(0, 0, self.width, self.height))
557
+
558
+ # ---------------------------------------------------------------------------------------------|
559
+ # User actions
560
+
561
+ @property
562
+ def directory(self) -> Path:
563
+ """Path of the current Scene file Python script. This works by searching up the call stack
564
+ for the first context whose filename isn't the local __file__ (of ShaderFlow.Scene)"""
565
+ # Idea: Maybe `type(self).mro()[0]` could help
566
+ for frame in inspect.stack():
567
+ if (frame.filename != __file__):
568
+ return Path(frame.filename).parent
569
+
570
+ def read_file(self, file: Path, bytes: bool=False) -> Union[str, bytes]:
571
+ """
572
+ Read a file relative to the current Scene Python script
573
+
574
+ Args:
575
+ file: File to read, relative to the current Scene script directory
576
+ bytes: Whether to read the file as bytes, defaults to text
577
+
578
+ Returns:
579
+ File contents as text or bytes
580
+ """
581
+ file = (self.directory/file)
582
+ log.info(f"{self.who} Reading file ({file})")
583
+ return LoaderBytes(file) if bytes else LoaderString(file)
584
+
585
+ # ---------------------------------------------------------------------------------------------|
586
+ # Main event loop
587
+
588
+ quit: PlainTracker = Factory(lambda: PlainTracker(False))
589
+ """Should the scene end the main event loop? Use as `if scene.quit():`"""
590
+
591
+ def next(self, dt: float) -> Self:
592
+ """Integrate time, update all modules and render the next frame"""
593
+
594
+ # Fixme: Windows: https://github.com/glfw/glfw/pull/1426
595
+ # Immediately swap the buffer with previous frame for vsync
596
+ if (self.backend != WindowBackend.Headless):
597
+ self.window.swap_buffers()
598
+
599
+ # Temporal logic
600
+ dt = min(dt, 1)
601
+ self.tempo.next(dt=abs(dt))
602
+ self.rdt = dt
603
+ self.dt = dt * self.tempo
604
+ self.time += self.dt
605
+ self.vsync.fps = self.fps
606
+
607
+ # Note: Updates in reverse order of addition (child -> parent)
608
+ # Note: Update non-engine first, as the pipeline might change
609
+ for module in self.modules:
610
+ if not isinstance(module, ShaderObject):
611
+ module.update()
612
+ for module in reversed(self.modules):
613
+ if isinstance(module, ShaderObject):
614
+ module.update()
615
+
616
+ self._render_ui()
617
+ return self
618
+
619
+ exporting: bool = True
620
+ """Is this Scene exporting to a video file?"""
621
+
622
+ rendering: bool = False
623
+ """Either Exporting, Rendering or Benchmarking. 'Not Realtime' mode"""
624
+
625
+ realtime: bool = False
626
+ """Running with a window and user interaction"""
627
+
628
+ headless: bool = False
629
+ """Running Headlessly, without a window and user interaction"""
630
+
631
+ benchmark: bool = False
632
+ """Stress test the rendering speed of the Scene"""
633
+
634
+ repeat: int = field(default=1, converter=int)
635
+ """Number of times to loop the exported video. One '1' is no repeat, two '2' doubles the length.
636
+ Ideally have seamless transitions on the shader based on self.tau and/or/no audio input"""
637
+
638
+ # Batch exporting
639
+
640
+ export_batch: Iterable[int] = field(factory=lambda: [0], converter=lambda x: list(x) or [0])
641
+ """Batch indices iterable to export"""
642
+
643
+ export_index: int = 0
644
+ """Current Batch exporting video index"""
645
+
646
+ export_format: str = field(default="mp4", converter=lambda x: str(denum(x)))
647
+ """The last (or only) video export format (extension) to use"""
648
+
649
+ export_base: Path = field(default=Broken.PROJECT.DIRECTORIES.DATA, converter=lambda x: Path(x))
650
+ """The last (or only) video export base directory. Videos should render to ($base/$name) if $name
651
+ is plain, that is, the path isn't absolute"""
652
+
653
+ @abstractmethod
654
+ def export_name(self, path: Path) -> Path:
655
+ """Change the video file name being exported based on the current batch index. By default,
656
+ the name is unchanged in single export, else the stem is appended with the batch index"""
657
+ if (len(self.export_batch) > 1):
658
+ return path.with_stem(f"{path.stem}_{self.export_index}")
659
+ return path
660
+
661
+ def main(self,
662
+ width: Annotated[int, Option("--width", "-w", help="[bold][red](🔴 Basic )[/red][/bold] Width of the rendering resolution [medium_purple3](None to keep or find by Aspect Ratio)[/medium_purple3] [dim](1920 on init)[/dim]")]=None,
663
+ height: Annotated[int, Option("--height", "-h", help="[bold][red](🔴 Basic )[/red][/bold] Height of the rendering resolution [medium_purple3](None to keep or find by Aspect Ratio)[/medium_purple3] [dim](1080 on init)[/dim]")]=None,
664
+ scale: Annotated[float, Option("--scale", "-x", help="[bold][red](🔴 Basic )[/red][/bold] Post-multiply width and height by a scale factor [medium_purple3](None to keep)[/medium_purple3] [dim](1.0 on init)[dim]")]=None,
665
+ aspect: Annotated[str, Option("--ar", "-R", help="[bold][red](🔴 Basic )[/red][/bold] Force resolution aspect ratio [green](Examples: '16:9', '16/9', '1.777')[/green] [medium_purple3](None for dynamic)[/medium_purple3]")]=None,
666
+ fps: Annotated[float, Option("--fps", "-f", help="[bold][red](🔴 Basic )[/red][/bold] Target frames per second [medium_purple3](Defaults to the monitor framerate on realtime else 60)[/medium_purple3]")]=None,
667
+ fullscreen: Annotated[bool, Option("--fullscreen", help="[bold][red](🔴 Basic )[/red][/bold] Start the real time window in fullscreen mode [medium_purple3](Toggle with F11)[/medium_purple3]")]=False,
668
+ maximize: Annotated[bool, Option("--maximize", "-M", help="[bold][red](🔴 Basic )[/red][/bold] Start the real time window in maximized mode")]=False,
669
+ quality: Annotated[float, Option("--quality", "-q", help="[bold][yellow](🟡 Quality)[/yellow][/bold] Shader quality level [green](0-100%)[/green] [yellow](If supported by the shader)[/yellow] [medium_purple3](None to keep, default 50%)[/medium_purple3]")]=None,
670
+ ssaa: Annotated[float, Option("--ssaa", "-s", help="[bold][yellow](🟡 Quality)[/yellow][/bold] Fractional super sampling anti aliasing factor [yellow](O(N^2) GPU cost)[/yellow] [medium_purple3](None to keep, default 1.0)[/medium_purple3]")]=None,
671
+ render: Annotated[bool, Option("--render", "-r", help="[green](🟢 Export )[/green] Export the Scene to a video file [medium_purple3](defined on --output, and implicit if so)[/medium_purple3]")]=False,
672
+ output: Annotated[str, Option("--output", "-o", help="[green](🟢 Export )[/green] Output file name [green]('Absolute', 'Relative path', 'Plain Name')[/green] [dim]($base/$(plain_name or $scene-$date))[/dim]")]=None,
673
+ time: Annotated[float, Option("--time", "-t", help="[green](🟢 Export )[/green] The duration of exported videos [dim](Loop duration)[/dim] [medium_purple3](defaults to 10 or longest module's duration)[/medium_purple3]")]=None,
674
+ tempo: Annotated[float, Option("--tempo", "-T", help="[green](🟢 Export )[/green] Set the time speed factor of the Scene [yellow](Final duration is stretched by [italic]1/tempo[/italic])[/yellow] [dim](1 on init)[/dim]")]=None,
675
+ repeat: Annotated[int, Option("--repeat", help="[green](🟢 Export )[/green] Number of exported videos loop copies [yellow](Final duration is stretched by [italic]repeat[/italic])[/yellow] [dim](1 on init)[/dim]")]=None,
676
+ format: Annotated[str, Option("--format", "-F", help="[green](🟢 Export )[/green] Output video container [green]('mp4', 'mkv', 'webm', 'avi, '...')[/green] [yellow](Overrides --output one)[/yellow]")]="mp4",
677
+ base: Annotated[Path, Option("--base", "-D", help="[green](🟢 Export )[/green] Output file base directory")]=Broken.PROJECT.DIRECTORIES.DATA,
678
+ vcodec: Annotated[str, Option("--vcodec", "-c", help="[green](🟢 Export )[/green] Video codec [green]('h264', 'h264-nvenc', 'h265, 'hevc-nvenc', 'vp9', 'av1-{aom,svt,nvenc,rav1e}')[/green]")]="h264",
679
+ acodec: Annotated[str, Option("--acodec", "-a", help="[green](🟢 Export )[/green] Audio codec [green]('aac', 'mp3', 'flac', 'wav', 'opus', 'ogg', 'copy', 'none', 'empty')[/green]")]="copy",
680
+ batch: Annotated[str, Option("--batch", "-b", help="[bold][blue](🔵 Special)[/blue][/bold] [dim][WIP] Hyphenated indices range to export multiple videos, if implemented. (1,5-7,10)[/dim]")]="0",
681
+ benchmark: Annotated[bool, Option("--benchmark", "-B", help="[bold][blue](🔵 Special)[/blue][/bold] Benchmark the Scene's speed on raw rendering [medium_purple3](Use SKIP_GPU=1 for CPU only benchmark)[/medium_purple3]")]=False,
682
+ raw: Annotated[bool, Option("--raw", help="[bold][blue](🔵 Special)[/blue][/bold] Send raw OpenGL frames before GPU SSAA to FFmpeg [medium_purple3](Enabled if SSAA < 1)[/medium_purple3] [dim](CPU Downsampling)[/dim]")]=False,
683
+ open: Annotated[bool, Option("--open", help="[bold][blue](🔵 Special)[/blue][/bold] Open the directory of video exports after all rendering finishes")]=False,
684
+ ) -> Optional[List[Path]]:
685
+ """
686
+ Main event loop of this ShaderFlow Scene. Start a realtime window, exports to video, stress test speeds
687
+
688
+ • Note: For advanced video or audio codec configuration, modify it on 'self.ffmpeg' maybe inheriting this base class
689
+ """
690
+ outputs: List[Path] = []
691
+
692
+ from arrow import now as arrow_now
693
+ export_started = arrow_now().format("YYYY-MM-DD HH-mm-ss")
694
+
695
+ # Maybe update indices of exporting videos
696
+ self.export_batch = hyphen_range(batch) or self.export_batch
697
+ self.export_format = format
698
+ self.export_base = base
699
+
700
+ for index in self.export_batch:
701
+ self.export_index = index
702
+ self.exporting = (render or bool(output))
703
+ self.rendering = (self.exporting or benchmark)
704
+ self.realtime = (not self.rendering)
705
+ self.benchmark = benchmark
706
+ self.headless = (self.rendering)
707
+ self.fps = (fps or self.monitor_framerate)
708
+ self.title = f"ShaderFlow | {self.__name__}"
709
+ self.fullscreen = fullscreen
710
+ self.quality = quality or self.quality
711
+ self.repeat = repeat or self.repeat
712
+ self.ssaa = ssaa or self.ssaa
713
+ self.time = 0
714
+ self.tempo.set(tempo or self.tempo.value)
715
+
716
+ for module in self.modules:
717
+ module.setup()
718
+
719
+ self.relay(ShaderMessage.Shader.Compile)
720
+ self.set_duration(time)
721
+
722
+ # Maybe keep or force aspect ratio, and find best resolution
723
+ width, height = self.resize(width=width, height=height, scale=scale, aspect_ratio=aspect)
724
+
725
+ # Optimization: Save bandwidth by piping native frames on ssaa < 1
726
+ if self.rendering and (raw or self.ssaa < 1):
727
+ self.resolution = self.render_resolution
728
+ self.ssaa = 1
729
+
730
+ # Configure FFmpeg and Popen it
731
+ if (self.rendering):
732
+
733
+ # Get video output path - if not absolute, save to data directory
734
+ export = Path(output or f"({export_started}) {self.__name__}")
735
+ export = export if export.is_absolute() else (self.export_base/export)
736
+ export = export.with_suffix(export.suffix or f".{self.export_format}")
737
+ export = self.export_name(export)
738
+
739
+ self.ffmpeg = (BrokenFFmpeg(time=self.runtime).quiet()
740
+ .pipe_input(pixel_format=("rgba" if self.alpha else "rgb24"),
741
+ width=self.width, height=self.height, framerate=self.fps)
742
+ .scale(width=width, height=height)
743
+ .output(path=export)
744
+ )
745
+
746
+ # Apply default good codec options on the video
747
+ self.ffmpeg.apply_vcodec_str(vcodec)
748
+ self.ffmpeg.apply_acodec_str(acodec)
749
+
750
+ for module in self.modules:
751
+ if module is self: continue
752
+ module.ffmpeg(self.ffmpeg)
753
+
754
+ if self.exporting:
755
+ buffer = self.opengl.buffer(reserve=self._final.texture.size_t)
756
+ self.ffmpeg = self.ffmpeg.popen(stdin=PIPE, wrapper=True)
757
+
758
+ # Status tracker
759
+ status = DotMap(
760
+ start=perf_counter(),
761
+ bar=tqdm.tqdm(
762
+ total=self.total_frames,
763
+ desc=f"Scene #{self.export_index} ({type(self).__name__}) → Video",
764
+ dynamic_ncols=True,
765
+ colour="#43BFEF",
766
+ leave=False,
767
+ unit=" frames",
768
+ mininterval=1/30,
769
+ maxinterval=0.1,
770
+ smoothing=0.1,
771
+ )
772
+ )
773
+
774
+ # Some scenes might take a while to setup
775
+ self.visible = not self.headless
776
+
777
+ if (self.backend == WindowBackend.GLFW and maximize):
778
+ glfw.maximize_window(self.window._window)
779
+
780
+ # Add self.next to the event loop
781
+ self.vsync = self.scheduler.new(
782
+ task=self.next,
783
+ frequency=self.fps,
784
+ freewheel=self.rendering,
785
+ precise=True,
786
+ )
787
+
788
+ # Main rendering loop
789
+ while (self.rendering) or (not self.quit()):
790
+ task = self.scheduler.next()
791
+
792
+ # Only continue if exporting
793
+ if (task.output is not self):
794
+ continue
795
+ if self.realtime:
796
+ continue
797
+ status.bar.update(1)
798
+
799
+ # Write a new frame to FFmpeg
800
+ if self.exporting:
801
+ if (_STANDARD_METHOD := False):
802
+ frame = self._final.texture.fbo().read()
803
+ self.ffmpeg.stdin.write(frame)
804
+ elif (_BUFFER_PROXY := True):
805
+ self._final.texture.fbo().read_into(buffer)
806
+ self.ffmpeg.stdin.write(buffer.read())
807
+ elif (_MGL_FORK_METHOD := False):
808
+ self._final.texture.fbo().read_into(buffer)
809
+ buffer.read_into(self.ffmpeg.stdin)
810
+ else:
811
+ raise RuntimeError("No read method selected")
812
+
813
+ # Finish exporting condition
814
+ if (status.bar.n < self.total_frames):
815
+ continue
816
+ status.bar.close()
817
+
818
+ if self.exporting:
819
+ log.info("Waiting for FFmpeg process to finish (Queue writes, codecs lookahead, etc)")
820
+ self.ffmpeg.stdin.close()
821
+ outputs.append(export)
822
+
823
+ # Repeat the video N times
824
+ if (self.repeat > 1):
825
+ log.info(f"Repeating video ({self.repeat-1} times)")
826
+ export.rename(temporary := export.with_stem(f"{export.stem}-repeat"))
827
+ (BrokenFFmpeg(stream_loop=(self.repeat-1)).quiet().copy_audio().copy_video()
828
+ .input(temporary).output(export, pixel_format=None).run())
829
+ temporary.unlink()
830
+
831
+ # Log stats
832
+ status.took = (perf_counter() - status.start)
833
+ log.info(f"Finished rendering ({export})", echo=not self.benchmark)
834
+ log.info((
835
+ f"• Stats: "
836
+ f"(Took {status.took:.2f} s) at "
837
+ f"({self.frame/status.took:.2f} FPS | "
838
+ f"{self.runtime/status.took:.2f} x Realtime) with "
839
+ f"({status.bar.n} Total Frames)"
840
+ ))
841
+
842
+ if self.benchmark:
843
+ return None
844
+ break
845
+
846
+ BrokenPath.open_in_file_explorer(outputs[0].parent) if open else None
847
+ return outputs
848
+
849
+ # ---------------------------------------------------------------------------------------------|
850
+ # Module
851
+
852
+ def handle(self, message: ShaderMessage) -> None:
853
+
854
+ if isinstance(message, ShaderMessage.Window.Close):
855
+ log.info(f"{self.who} Received Window Close Event")
856
+ self.quit(True)
857
+
858
+ elif isinstance(message, ShaderMessage.Keyboard.KeyDown):
859
+ if message.key == ShaderKeyboard.Keys.O:
860
+ log.info(f"{self.who} (O ) Resetting the Scene")
861
+ for module in self.modules:
862
+ module.setup()
863
+ self.time = 0
864
+
865
+ elif message.key == ShaderKeyboard.Keys.R:
866
+ log.info(f"{self.who} (R ) Reloading Shaders")
867
+ for module in self.modules:
868
+ if isinstance(module, ShaderObject):
869
+ module.compile()
870
+
871
+ elif message.key == ShaderKeyboard.Keys.TAB:
872
+ log.info(f"{self.who} (TAB) Toggling Menu")
873
+ self.render_ui = not self.render_ui
874
+
875
+ elif message.key == ShaderKeyboard.Keys.F1:
876
+ log.info(f"{self.who} (F1 ) Toggling Exclusive Mode")
877
+ self.exclusive = not self.exclusive
878
+
879
+ elif message.key == ShaderKeyboard.Keys.F2:
880
+ import arrow
881
+ time = arrow.now().format("YYYY-MM-DD_HH-mm-ss")
882
+ image = PIL.Image.frombytes("RGB", self.resolution, self.read_screen())
883
+ image = image.transpose(PIL.Image.FLIP_TOP_BOTTOM)
884
+ path = Broken.PROJECT.DIRECTORIES.SCREENSHOTS/f"({time}) {self.__name__}.png"
885
+ log.minor(f"{self.who} (F2 ) Saving Screenshot to ({path})")
886
+ BrokenThread.new(target=image.save, fp=path)
887
+
888
+ elif message.key == ShaderKeyboard.Keys.F11:
889
+ log.info(f"{self.who} (F11) Toggling Fullscreen")
890
+ self.fullscreen = not self.fullscreen
891
+
892
+ elif isinstance(message, (ShaderMessage.Mouse.Drag, ShaderMessage.Mouse.Position)):
893
+ self.mouse_gluv = (message.u, message.v)
894
+
895
+ def pipeline(self) -> Iterable[ShaderVariable]:
896
+ yield ShaderVariable("uniform", "float", "iTime", self.time)
897
+ yield ShaderVariable("uniform", "float", "iDuration", self.duration)
898
+ yield ShaderVariable("uniform", "float", "iDeltaTime", self.dt)
899
+ yield ShaderVariable("uniform", "vec2", "iResolution", self.resolution)
900
+ yield ShaderVariable("uniform", "float", "iQuality", self.quality/100)
901
+ yield ShaderVariable("uniform", "float", "iSSAA", self.ssaa)
902
+ yield ShaderVariable("uniform", "float", "iFrameRate", self.fps)
903
+ yield ShaderVariable("uniform", "int", "iFrame", self.frame)
904
+ yield ShaderVariable("uniform", "bool", "iRealtime", self.realtime)
905
+ yield ShaderVariable("uniform", "vec2", "iMouse", self.mouse_gluv)
906
+ yield ShaderVariable("uniform", "bool", "iMouseInside", self.mouse_inside)
907
+ for i in range(1, 6):
908
+ yield ShaderVariable("uniform", "bool", f"iMouse{i}", self.mouse_buttons[i])
909
+
910
+ # ---------------------------------------------------------------------------------------------|
911
+ # Internal window events
912
+
913
+ def __window_resize__(self, width: int, height: int) -> None:
914
+
915
+ # Don't listen to resizes when exporting, as the final resolution might be
916
+ # greater than the monitor and the window will resize down to fit
917
+ if self.exporting:
918
+ return
919
+ self.imgui.resize(width, height)
920
+ self._width, self._height = width, height
921
+ self.relay(ShaderMessage.Shader.RecreateTextures)
922
+ self.relay(ShaderMessage.Shader.Render)
923
+
924
+ def __window_close__(self) -> None:
925
+ self.relay(ShaderMessage.Window.Close())
926
+
927
+ def __window_iconify__(self, state: bool) -> None:
928
+ self.relay(ShaderMessage.Window.Iconify(state=state))
929
+
930
+ def __window_files_dropped_event__(self, *stuff: list[str]) -> None:
931
+ self.relay(ShaderMessage.Window.FileDrop(files=stuff[1]))
932
+
933
+ # # Keyboard related events
934
+
935
+ def __window_key_event__(self, key: int, action: int, modifiers: int) -> None:
936
+ self.imgui.key_event(key, action, modifiers)
937
+ if self.imguio.want_capture_keyboard and self.render_ui:
938
+ return
939
+ if action == ShaderKeyboard.Keys.ACTION_PRESS:
940
+ self.relay(ShaderMessage.Keyboard.KeyDown(key=key, modifiers=modifiers))
941
+ elif action == ShaderKeyboard.Keys.ACTION_RELEASE:
942
+ self.relay(ShaderMessage.Keyboard.KeyUp(key=key, modifiers=modifiers))
943
+ self.relay(ShaderMessage.Keyboard.Press(key=key, action=action, modifiers=modifiers))
944
+
945
+ def __window_unicode_char_entered__(self, char: str) -> None:
946
+ if self.imguio.want_capture_keyboard and self.render_ui:
947
+ return
948
+ self.relay(ShaderMessage.Keyboard.Unicode(char=char))
949
+
950
+ # # Mouse related events
951
+
952
+ mouse_gluv: Tuple[float, float] = Factory(lambda: (0, 0))
953
+
954
+ def __xy2uv__(self, x: int=0, y: int=0) -> dict[str, float]:
955
+ """Convert a XY pixel coordinate into a Center-UV normalized coordinate"""
956
+ return dict(
957
+ u=2*(x/self.width - 0.5),
958
+ v=2*(y/self.height - 0.5)*(-1),
959
+ x=x, y=y,
960
+ )
961
+
962
+ def __dxdy2dudv__(self, dx: int=0, dy: int=0) -> dict[str, float]:
963
+ """Convert a dx dy pixel coordinate into a Center-UV normalized coordinate"""
964
+ return dict(
965
+ du=2*(dx/self.width)*(self.width/self.height),
966
+ dv=2*(dy/self.height)*(-1),
967
+ dx=dx, dy=dy,
968
+ )
969
+
970
+ mouse_buttons: Dict[int, bool] = Factory(lambda: {k: False for k in range(1, 6)})
971
+
972
+ def __window_mouse_press_event__(self, x: int, y: int, button: int) -> None:
973
+ self.imgui.mouse_press_event(x, y, button)
974
+ if self.imguio.want_capture_mouse and self.render_ui:
975
+ return
976
+ self.mouse_buttons[button] = True
977
+ self.relay(ShaderMessage.Mouse.Press(
978
+ **self.__xy2uv__(x, y),
979
+ button=button
980
+ ))
981
+
982
+ def __window_mouse_release_event__(self, x: int, y: int, button: int) -> None:
983
+ self.imgui.mouse_release_event(x, y, button)
984
+ if self.imguio.want_capture_mouse and self.render_ui:
985
+ return
986
+ self.mouse_buttons[button] = False
987
+ self.relay(ShaderMessage.Mouse.Release(
988
+ **self.__xy2uv__(x, y),
989
+ button=button
990
+ ))
991
+
992
+ mouse_inside: bool = False
993
+
994
+ def __window_mouse_enter_event__(self, inside: bool) -> None:
995
+ self.mouse_inside = inside
996
+ self.relay(ShaderMessage.Mouse.Enter(state=inside))
997
+
998
+ def __window_mouse_scroll_event__(self, dx: int, dy: int) -> None:
999
+ self.imgui.mouse_scroll_event(dx, dy)
1000
+ if self.imguio.want_capture_mouse and self.render_ui:
1001
+ return
1002
+ elif self.keyboard(ShaderKeyboard.Keys.LEFT_ALT):
1003
+ self.tempo.target += (dy)*0.2
1004
+ return
1005
+ self.relay(ShaderMessage.Mouse.Scroll(
1006
+ **self.__dxdy2dudv__(dx=dx, dy=dy)
1007
+ ))
1008
+
1009
+ def __window_mouse_position_event__(self, x: int, y: int, dx: int, dy: int) -> None:
1010
+ self.imgui.mouse_position_event(x, y, dx, dy)
1011
+ if self.imguio.want_capture_mouse and self.render_ui:
1012
+ return
1013
+ self.relay(ShaderMessage.Mouse.Position(
1014
+ **self.__dxdy2dudv__(dx=dx, dy=dy),
1015
+ **self.__xy2uv__(x=x, y=y)
1016
+ ))
1017
+
1018
+ _mouse_drag_time_factor: float = 4
1019
+ """How much seconds to scroll in time when the mouse moves the full window height"""
1020
+
1021
+ def __window_mouse_drag_event__(self, x: int, y: int, dx: int, dy: int) -> None:
1022
+ self.imgui.mouse_drag_event(x, y, dx, dy)
1023
+ if self.imguio.want_capture_mouse and self.render_ui:
1024
+ return
1025
+
1026
+ # Rotate the camera on Shift
1027
+ if self.keyboard(ShaderKeyboard.Keys.LEFT_CTRL):
1028
+ cx, cy = (x-self.width/2), (y-self.height/2)
1029
+ angle = math.atan2(cy+dy, cx+dx) - math.atan2(cy, cx)
1030
+ if (abs(angle) > math.pi): angle -= 2*math.pi
1031
+ self.camera.rotate(self.camera.base_z, angle=math.degrees(angle))
1032
+ return
1033
+
1034
+ elif self.exclusive:
1035
+ self.camera.apply_zoom(-dy/500)
1036
+ self.camera.rotate(self.camera.base_z, angle=-dx/10)
1037
+ return
1038
+
1039
+ # Time Travel on Alt
1040
+ elif self.keyboard(ShaderKeyboard.Keys.LEFT_ALT):
1041
+ self.time -= self._mouse_drag_time_factor * (dy/self.height)
1042
+ return
1043
+
1044
+ self.relay(ShaderMessage.Mouse.Drag(
1045
+ **self.__dxdy2dudv__(dx=dx, dy=dy),
1046
+ **self.__xy2uv__(x=x, y=y)
1047
+ ))
1048
+
1049
+ # ---------------------------------------------------------------------------------------------|
1050
+ # Todo: Move UI to own class: For main menu, settings, exporting, etc
1051
+
1052
+ render_ui: bool = False
1053
+ """Whether to render the Main UI"""
1054
+
1055
+ # Fixme: Move to somewhere better
1056
+ def _render_ui(self):
1057
+ if not self.render_ui:
1058
+ return
1059
+
1060
+ self._final.texture.fbo().use()
1061
+ imgui.push_style_var(imgui.STYLE_WINDOW_BORDERSIZE, 0.0)
1062
+ imgui.push_style_var(imgui.STYLE_WINDOW_ROUNDING, 8)
1063
+ imgui.push_style_var(imgui.STYLE_TAB_ROUNDING, 8)
1064
+ imgui.push_style_var(imgui.STYLE_GRAB_ROUNDING, 8)
1065
+ imgui.push_style_var(imgui.STYLE_FRAME_ROUNDING, 8)
1066
+ imgui.push_style_var(imgui.STYLE_CHILD_ROUNDING, 8)
1067
+ imgui.push_style_color(imgui.COLOR_FRAME_BACKGROUND, 0.1, 0.1, 0.1, 0.5)
1068
+ imgui.new_frame()
1069
+ imgui.set_next_window_position(0, 0)
1070
+ imgui.set_next_window_bg_alpha(0.6)
1071
+ imgui.begin(f"{self.__name__}", False, imgui.WINDOW_NO_MOVE | imgui.WINDOW_NO_RESIZE | imgui.WINDOW_NO_COLLAPSE | imgui.WINDOW_ALWAYS_AUTO_RESIZE)
1072
+
1073
+ # Render every module
1074
+ for module in self.modules:
1075
+ if imgui.tree_node(f"{module.uuid:>2} - {type(module).__name__.replace('ShaderFlow', '')}", imgui.TREE_NODE_BULLET | imgui.TREE_NODE_DEFAULT_OPEN):
1076
+ module.__shaderflow_ui__()
1077
+ imgui.spacing()
1078
+ imgui.tree_pop()
1079
+
1080
+ imgui.end()
1081
+ imgui.pop_style_color()
1082
+ imgui.pop_style_var(6)
1083
+ imgui.render()
1084
+ self.imgui.render(imgui.get_draw_data())
1085
+
1086
+ def __ui__(self) -> None:
1087
+
1088
+ # Render status
1089
+ imgui.text(f"Resolution: {self.render_resolution} -> {self.resolution} @ {self.ssaa:.2f}x SSAA")
1090
+
1091
+ # Framerate
1092
+ imgui.spacing()
1093
+ if (state := imgui.slider_float("Framerate", self.fps, 10, 240, "%.0f"))[0]:
1094
+ self.fps = round(state[1])
1095
+ for fps in (options := [24, 30, 60, 120, 144, 240]):
1096
+ if (state := imgui.button(f"{fps} Hz")):
1097
+ self.fps = fps
1098
+ if fps != options[-1]:
1099
+ imgui.same_line()
1100
+
1101
+ # Temporal
1102
+ imgui.spacing()
1103
+ if (state := imgui.slider_float("Time Scale", self.tempo.target, -2, 2, "%.2f"))[0]:
1104
+ self.tempo.target = state[1]
1105
+ for scale in (options := [-10, -5, -2, -1, 0, 1, 2, 5, 10]):
1106
+ if (state := imgui.button(f"{scale}x")):
1107
+ self.tempo.target = scale
1108
+ if scale != options[-1]:
1109
+ imgui.same_line()
1110
+
1111
+ # SSAA
1112
+ imgui.spacing()
1113
+ if (state := imgui.slider_float("SSAA", self.ssaa, 0.01, 2, "%.2f"))[0]:
1114
+ self.ssaa = state[1]
1115
+ for ssaa in (options := [0.1, 0.25, 0.5, 1.0, 1.25, 1.5, 2.0]):
1116
+ if (state := imgui.button(f"{ssaa}x")):
1117
+ self.ssaa = ssaa
1118
+ if ssaa != options[-1]:
1119
+ imgui.same_line()
1120
+
1121
+ # Quality
1122
+ imgui.spacing()
1123
+ if (state := imgui.slider_float("Quality", self.quality, 0, 100, "%.0f%%"))[0]:
1124
+ self.quality = state[1]