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.
- ShaderFlow/Common/Imgui.py +0 -0
- ShaderFlow/Common/Notes.py +134 -0
- ShaderFlow/Common/__init__.py +0 -0
- ShaderFlow/Message.py +147 -0
- ShaderFlow/Module.py +162 -0
- ShaderFlow/Modules/Audio.py +431 -0
- ShaderFlow/Modules/Bouncing.py +94 -0
- ShaderFlow/Modules/Camera.py +451 -0
- ShaderFlow/Modules/Depth.py +0 -0
- ShaderFlow/Modules/Dynamics.py +288 -0
- ShaderFlow/Modules/Frametimer.py +79 -0
- ShaderFlow/Modules/Keyboard.py +43 -0
- ShaderFlow/Modules/Noise.py +80 -0
- ShaderFlow/Modules/OpticalFlow.py +1 -0
- ShaderFlow/Modules/Piano.py +362 -0
- ShaderFlow/Modules/Spectrogram.py +315 -0
- ShaderFlow/Modules/Video.py +234 -0
- ShaderFlow/Modules/Waveform.py +100 -0
- ShaderFlow/Modules/__init__.py +0 -0
- ShaderFlow/Resources/Images/ShaderFlow.png +0 -0
- ShaderFlow/Resources/Images/ShaderFlow.svg +1 -0
- ShaderFlow/Resources/Scenes/Examples/Demo.py +284 -0
- ShaderFlow/Resources/Scenes/Examples/GLSL/Bars.frag +20 -0
- ShaderFlow/Resources/Scenes/Examples/GLSL/Bouncing.frag +50 -0
- ShaderFlow/Resources/Scenes/Examples/GLSL/Life/Simulation.glsl +49 -0
- ShaderFlow/Resources/Scenes/Examples/GLSL/Life/Visuals.glsl +31 -0
- ShaderFlow/Resources/Scenes/Examples/GLSL/MotionBlur.frag +20 -0
- ShaderFlow/Resources/Scenes/Examples/GLSL/Multipass.frag +43 -0
- ShaderFlow/Resources/Scenes/Examples/GLSL/RayMarch.frag +59 -0
- ShaderFlow/Resources/Scenes/Examples/GLSL/ShaderToy.frag +65 -0
- ShaderFlow/Resources/Scenes/Examples/GLSL/Video.frag +5 -0
- ShaderFlow/Resources/Scenes/Examples/GLSL/Visualizer.frag +75 -0
- ShaderFlow/Resources/Scenes/Examples/GLSL/Waveform.frag +18 -0
- ShaderFlow/Resources/Shaders/Fragment/Default.glsl +65 -0
- ShaderFlow/Resources/Shaders/Fragment/Final.glsl +7 -0
- ShaderFlow/Resources/Shaders/Fragment/Loading.frag +40 -0
- ShaderFlow/Resources/Shaders/Fragment/Missing.glsl +23 -0
- ShaderFlow/Resources/Shaders/Include/Camera.glsl +166 -0
- ShaderFlow/Resources/Shaders/Include/Complex.glsl +0 -0
- ShaderFlow/Resources/Shaders/Include/ShaderFlow.glsl +444 -0
- ShaderFlow/Resources/Shaders/Vertex/Default.glsl +21 -0
- ShaderFlow/Scene.py +1124 -0
- ShaderFlow/Shader.py +391 -0
- ShaderFlow/Texture.py +528 -0
- ShaderFlow/Variable.py +128 -0
- ShaderFlow/__init__.py +11 -0
- ShaderFlow/__main__.py +22 -0
- shaderflow-0.4.0.dev1.dist-info/METADATA +79 -0
- shaderflow-0.4.0.dev1.dist-info/RECORD +51 -0
- shaderflow-0.4.0.dev1.dist-info/WHEEL +5 -0
- shaderflow-0.4.0.dev1.dist-info/entry_points.txt +2 -0
ShaderFlow/Texture.py
ADDED
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
from collections import deque
|
|
2
|
+
from typing import Any, Deque, Iterable, List, Optional, Self, Tuple, Type, Union
|
|
3
|
+
|
|
4
|
+
import moderngl
|
|
5
|
+
import numpy
|
|
6
|
+
import PIL
|
|
7
|
+
from attr import Factory, define, field
|
|
8
|
+
|
|
9
|
+
from Broken import BrokenEnum, Ignore
|
|
10
|
+
from Broken.Loaders import LoadableImage, LoaderImage
|
|
11
|
+
from ShaderFlow.Message import ShaderMessage
|
|
12
|
+
from ShaderFlow.Module import ShaderModule
|
|
13
|
+
from ShaderFlow.Variable import ShaderVariable
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TextureType(BrokenEnum):
|
|
17
|
+
|
|
18
|
+
# Float
|
|
19
|
+
f1 = numpy.uint8
|
|
20
|
+
"""Alias for uint8, GL_UNSIGNED_BYTE"""
|
|
21
|
+
f2 = numpy.float16
|
|
22
|
+
"""Float16 bits = 2 bytes, GL_HALF_FLOAT"""
|
|
23
|
+
f4 = numpy.float32
|
|
24
|
+
"""Float32 bits = 4 bytes, GL_FLOAT"""
|
|
25
|
+
|
|
26
|
+
# # Integers
|
|
27
|
+
|
|
28
|
+
# Normal
|
|
29
|
+
u1 = numpy.uint8
|
|
30
|
+
u2 = numpy.uint16
|
|
31
|
+
u4 = numpy.uint32
|
|
32
|
+
i1 = numpy.int8
|
|
33
|
+
i2 = numpy.int16
|
|
34
|
+
i4 = numpy.int32
|
|
35
|
+
|
|
36
|
+
# Normalized
|
|
37
|
+
nu1 = numpy.uint8
|
|
38
|
+
nu2 = numpy.uint16
|
|
39
|
+
nu4 = numpy.uint32
|
|
40
|
+
ni1 = numpy.int8
|
|
41
|
+
ni2 = numpy.int16
|
|
42
|
+
ni4 = numpy.int32
|
|
43
|
+
|
|
44
|
+
class TextureFilter(BrokenEnum):
|
|
45
|
+
# Fixme: Disallow bad combinations of filter and types
|
|
46
|
+
Nearest = "nearest"
|
|
47
|
+
Linear = "linear"
|
|
48
|
+
|
|
49
|
+
class Anisotropy(BrokenEnum):
|
|
50
|
+
"""Anisotropy levels to use on textures"""
|
|
51
|
+
x1 = 1
|
|
52
|
+
x2 = 2
|
|
53
|
+
x4 = 4
|
|
54
|
+
x8 = 8
|
|
55
|
+
x16 = 16
|
|
56
|
+
|
|
57
|
+
@define
|
|
58
|
+
class TextureBox:
|
|
59
|
+
"""Holds a Framebuffer and bound Texture on the TextureMatrix"""
|
|
60
|
+
texture: moderngl.Texture = None
|
|
61
|
+
fbo: moderngl.Framebuffer = None
|
|
62
|
+
clear: bool = False
|
|
63
|
+
data: bytes = None
|
|
64
|
+
empty: bool = True
|
|
65
|
+
|
|
66
|
+
def release(self) -> Self:
|
|
67
|
+
(self.texture or Ignore()).release()
|
|
68
|
+
(self.fbo or Ignore()).release()
|
|
69
|
+
|
|
70
|
+
@define
|
|
71
|
+
class ShaderTexture(ShaderModule):
|
|
72
|
+
name: str = None
|
|
73
|
+
|
|
74
|
+
def __post__(self):
|
|
75
|
+
self.make()
|
|
76
|
+
self.apply()
|
|
77
|
+
|
|
78
|
+
# ------------------------------------------|
|
|
79
|
+
# Special
|
|
80
|
+
|
|
81
|
+
final: bool = field(default=False, converter=bool)
|
|
82
|
+
"""Is this bound to the final FSSAA ShaderObject?"""
|
|
83
|
+
|
|
84
|
+
_track: bool = field(default=False, converter=bool)
|
|
85
|
+
"""Should this ShaderTexture match the resolution of the Scene?"""
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def track(self) -> bool:
|
|
89
|
+
return self._track
|
|
90
|
+
|
|
91
|
+
@track.setter
|
|
92
|
+
def track(self, value: bool):
|
|
93
|
+
if (self._track == value):
|
|
94
|
+
return
|
|
95
|
+
self._track = value
|
|
96
|
+
self.make()
|
|
97
|
+
|
|
98
|
+
# ------------------------------------------|
|
|
99
|
+
|
|
100
|
+
# Filter
|
|
101
|
+
|
|
102
|
+
_filter: TextureFilter = TextureFilter.Linear
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def filter(self) -> TextureFilter:
|
|
106
|
+
return TextureFilter.get(self._filter)
|
|
107
|
+
|
|
108
|
+
@filter.setter
|
|
109
|
+
def filter(self, value: TextureFilter):
|
|
110
|
+
self._filter = TextureFilter.get(value)
|
|
111
|
+
self.apply()
|
|
112
|
+
|
|
113
|
+
# Anisotropy
|
|
114
|
+
|
|
115
|
+
_anisotropy: Anisotropy = Anisotropy.x16
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def anisotropy(self) -> Anisotropy:
|
|
119
|
+
return Anisotropy.get(self._anisotropy)
|
|
120
|
+
|
|
121
|
+
@anisotropy.setter
|
|
122
|
+
def anisotropy(self, value: Anisotropy):
|
|
123
|
+
self._anisotropy = Anisotropy.get(value)
|
|
124
|
+
self.apply()
|
|
125
|
+
|
|
126
|
+
# Mipmaps
|
|
127
|
+
|
|
128
|
+
_mipmaps: bool = field(default=False, converter=bool)
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def mipmaps(self) -> bool:
|
|
132
|
+
return self._mipmaps
|
|
133
|
+
|
|
134
|
+
@mipmaps.setter
|
|
135
|
+
def mipmaps(self, value: bool):
|
|
136
|
+
if (self._mipmaps == value):
|
|
137
|
+
return
|
|
138
|
+
self._mipmaps = value
|
|
139
|
+
self.apply()
|
|
140
|
+
|
|
141
|
+
# ModernGL Filter
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def moderngl_filter(self) -> int:
|
|
145
|
+
return dict(
|
|
146
|
+
linear=moderngl.LINEAR,
|
|
147
|
+
nearest=moderngl.NEAREST,
|
|
148
|
+
linear_mipmap=moderngl.LINEAR_MIPMAP_LINEAR,
|
|
149
|
+
nearest_mipmap=moderngl.NEAREST_MIPMAP_NEAREST,
|
|
150
|
+
).get(self.filter.value + ("_mipmap"*self.mipmaps))
|
|
151
|
+
|
|
152
|
+
# Dtype
|
|
153
|
+
|
|
154
|
+
_dtype: TextureType = TextureType.f4
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def dtype(self) -> TextureType:
|
|
158
|
+
return TextureType.get(self._dtype)
|
|
159
|
+
|
|
160
|
+
@dtype.setter
|
|
161
|
+
def dtype(self, value: TextureType):
|
|
162
|
+
self._dtype = TextureType.get(value)
|
|
163
|
+
self.make()
|
|
164
|
+
|
|
165
|
+
# Repeat X
|
|
166
|
+
|
|
167
|
+
_repeat_x: bool = field(default=True, converter=bool)
|
|
168
|
+
|
|
169
|
+
@property
|
|
170
|
+
def repeat_x(self) -> bool:
|
|
171
|
+
return self._repeat_x
|
|
172
|
+
|
|
173
|
+
@repeat_x.setter
|
|
174
|
+
def repeat_x(self, value: bool):
|
|
175
|
+
if (self._repeat_x == value):
|
|
176
|
+
return
|
|
177
|
+
self._repeat_x = value
|
|
178
|
+
self.apply()
|
|
179
|
+
|
|
180
|
+
# Repeat Y
|
|
181
|
+
|
|
182
|
+
_repeat_y: bool = field(default=True, converter=bool)
|
|
183
|
+
|
|
184
|
+
@property
|
|
185
|
+
def repeat_y(self) -> bool:
|
|
186
|
+
return self._repeat_y
|
|
187
|
+
|
|
188
|
+
@repeat_y.setter
|
|
189
|
+
def repeat_y(self, value: bool):
|
|
190
|
+
if (self._repeat_y == value):
|
|
191
|
+
return
|
|
192
|
+
self._repeat_y = value
|
|
193
|
+
self.apply()
|
|
194
|
+
|
|
195
|
+
# Repeat XY
|
|
196
|
+
|
|
197
|
+
def repeat(self, value: bool) -> Self:
|
|
198
|
+
self.repeat_x = self.repeat_y = bool(value)
|
|
199
|
+
return self.apply()
|
|
200
|
+
|
|
201
|
+
# ------------------------------------------|
|
|
202
|
+
|
|
203
|
+
# Width
|
|
204
|
+
|
|
205
|
+
_width: int = field(default=1, converter=int)
|
|
206
|
+
|
|
207
|
+
@property
|
|
208
|
+
def width(self) -> int:
|
|
209
|
+
if not self.track:
|
|
210
|
+
return self._width
|
|
211
|
+
return self.resolution[0]
|
|
212
|
+
|
|
213
|
+
@width.setter
|
|
214
|
+
def width(self, value: int):
|
|
215
|
+
value = int(value)
|
|
216
|
+
if (self._width == value):
|
|
217
|
+
return
|
|
218
|
+
self._width = value
|
|
219
|
+
self.make()
|
|
220
|
+
|
|
221
|
+
# Height
|
|
222
|
+
|
|
223
|
+
_height: int = field(default=1, converter=int)
|
|
224
|
+
|
|
225
|
+
@property
|
|
226
|
+
def height(self) -> int:
|
|
227
|
+
if not self.track:
|
|
228
|
+
return self._height
|
|
229
|
+
return self.resolution[1]
|
|
230
|
+
|
|
231
|
+
@height.setter
|
|
232
|
+
def height(self, value: int):
|
|
233
|
+
value = int(value)
|
|
234
|
+
if (self._height == value):
|
|
235
|
+
return
|
|
236
|
+
self._height = value
|
|
237
|
+
self.make()
|
|
238
|
+
|
|
239
|
+
# Size (Width, Height)
|
|
240
|
+
|
|
241
|
+
@property
|
|
242
|
+
def resolution(self) -> Tuple[int, int]:
|
|
243
|
+
if not self.track:
|
|
244
|
+
return (self.width, self.height)
|
|
245
|
+
if self.final:
|
|
246
|
+
return self.scene.resolution
|
|
247
|
+
return self.scene.render_resolution
|
|
248
|
+
|
|
249
|
+
@resolution.setter
|
|
250
|
+
def resolution(self, value: Tuple[int, int]):
|
|
251
|
+
if not self.track:
|
|
252
|
+
self.width, self.height = value
|
|
253
|
+
|
|
254
|
+
@property
|
|
255
|
+
def size(self) -> Tuple[int, int]:
|
|
256
|
+
return self.resolution
|
|
257
|
+
|
|
258
|
+
@size.setter
|
|
259
|
+
def size(self, value: Tuple[int, int]):
|
|
260
|
+
self.resolution = value
|
|
261
|
+
|
|
262
|
+
@property
|
|
263
|
+
def aspect_ratio(self) -> float:
|
|
264
|
+
return self.width/(self.height or 1)
|
|
265
|
+
|
|
266
|
+
# Components
|
|
267
|
+
|
|
268
|
+
_components: int = field(default=4, converter=int)
|
|
269
|
+
|
|
270
|
+
@property
|
|
271
|
+
def components(self) -> int:
|
|
272
|
+
"""Number of color channels per pixel (1 Grayscale, 2 RG, 3 RGB, 4 RGBA)"""
|
|
273
|
+
return self._components
|
|
274
|
+
|
|
275
|
+
@components.setter
|
|
276
|
+
def components(self, value: int):
|
|
277
|
+
if (self._components == value):
|
|
278
|
+
return
|
|
279
|
+
self._components = value
|
|
280
|
+
self.make()
|
|
281
|
+
|
|
282
|
+
# Bytes size and Zero filling
|
|
283
|
+
|
|
284
|
+
@property
|
|
285
|
+
def zeros(self) -> numpy.ndarray:
|
|
286
|
+
return numpy.zeros((*self.size, self.components), dtype=self.dtype.value)
|
|
287
|
+
|
|
288
|
+
@property
|
|
289
|
+
def size_t(self) -> int:
|
|
290
|
+
"""Size of the texture data in bytes (width * height * components)"""
|
|
291
|
+
return self.width * self.height * self.components
|
|
292
|
+
|
|
293
|
+
# ------------------------------------------|
|
|
294
|
+
|
|
295
|
+
# Matrix
|
|
296
|
+
|
|
297
|
+
_matrix: Deque[Deque[TextureBox]] = Factory(deque)
|
|
298
|
+
"""Matrix of previous frames (temporal) and their layers (layers)"""
|
|
299
|
+
|
|
300
|
+
@property
|
|
301
|
+
def matrix(self) -> Deque[Deque[TextureBox]]:
|
|
302
|
+
return self._matrix
|
|
303
|
+
|
|
304
|
+
# Temporal
|
|
305
|
+
|
|
306
|
+
_temporal: int = field(default=1, converter=int)
|
|
307
|
+
"""Number of previous frames to be stored"""
|
|
308
|
+
|
|
309
|
+
@property
|
|
310
|
+
def temporal(self) -> int:
|
|
311
|
+
return self._temporal
|
|
312
|
+
|
|
313
|
+
@temporal.setter
|
|
314
|
+
def temporal(self, value: int):
|
|
315
|
+
if (self._temporal == value):
|
|
316
|
+
return
|
|
317
|
+
self._temporal = value
|
|
318
|
+
self.make()
|
|
319
|
+
|
|
320
|
+
# Layers
|
|
321
|
+
|
|
322
|
+
_layers: int = field(default=1, converter=int)
|
|
323
|
+
"""Number of layers to be stored, useful in single-shader multipass"""
|
|
324
|
+
|
|
325
|
+
@property
|
|
326
|
+
def layers(self) -> int:
|
|
327
|
+
return self._layers
|
|
328
|
+
|
|
329
|
+
@layers.setter
|
|
330
|
+
def layers(self, value: int):
|
|
331
|
+
if (self._layers == value):
|
|
332
|
+
return
|
|
333
|
+
self._layers = value
|
|
334
|
+
self.make()
|
|
335
|
+
|
|
336
|
+
# ------------------------------------------|
|
|
337
|
+
|
|
338
|
+
def _pop_fill(self, list: Union[List, Deque], fill: Type[Any], length: int) -> List:
|
|
339
|
+
"""Pop right or fill until a list's length is met"""
|
|
340
|
+
while len(list) > length:
|
|
341
|
+
list.pop()
|
|
342
|
+
while len(list) < length:
|
|
343
|
+
list.append(fill())
|
|
344
|
+
return list
|
|
345
|
+
|
|
346
|
+
def _populate(self) -> Self:
|
|
347
|
+
self._pop_fill(self.matrix, deque, self.temporal)
|
|
348
|
+
for row in self.matrix:
|
|
349
|
+
self._pop_fill(row, TextureBox, self.layers)
|
|
350
|
+
|
|
351
|
+
@property
|
|
352
|
+
def boxes(self) -> Iterable[Tuple[int, int, TextureBox]]:
|
|
353
|
+
for t, temporal in enumerate(self.matrix):
|
|
354
|
+
for b, box in enumerate(temporal):
|
|
355
|
+
yield (t, b, box)
|
|
356
|
+
|
|
357
|
+
def row(self, n: int=0) -> Iterable[TextureBox]:
|
|
358
|
+
yield from self.matrix[n]
|
|
359
|
+
|
|
360
|
+
def make(self) -> Self:
|
|
361
|
+
if (max(self.size) > 2**15):
|
|
362
|
+
raise Exception(f"Texture size likely too large {self.size}")
|
|
363
|
+
|
|
364
|
+
self._populate()
|
|
365
|
+
for (_, _, box) in self.boxes:
|
|
366
|
+
box.release()
|
|
367
|
+
box.texture = self.scene.opengl.texture(
|
|
368
|
+
components=self.components,
|
|
369
|
+
dtype=self.dtype.name,
|
|
370
|
+
size=self.size,
|
|
371
|
+
)
|
|
372
|
+
box.fbo = self.scene.opengl.framebuffer(
|
|
373
|
+
color_attachments=[box.texture]
|
|
374
|
+
)
|
|
375
|
+
return self.apply()
|
|
376
|
+
|
|
377
|
+
def apply(self) -> Self:
|
|
378
|
+
for (_, _, box) in self.boxes:
|
|
379
|
+
if self.mipmaps:
|
|
380
|
+
box.texture.build_mipmaps()
|
|
381
|
+
box.texture.filter = (self.moderngl_filter, self.moderngl_filter)
|
|
382
|
+
box.texture.anisotropy = self.anisotropy.value
|
|
383
|
+
box.texture.repeat_x = self.repeat_x
|
|
384
|
+
box.texture.repeat_y = self.repeat_y
|
|
385
|
+
return self
|
|
386
|
+
|
|
387
|
+
def box(self, temporal: int=0, layer: int=-1) -> Optional[TextureBox]:
|
|
388
|
+
"""Note: Points to the current final box"""
|
|
389
|
+
if (self.temporal <= temporal):
|
|
390
|
+
return None
|
|
391
|
+
if (self.layers <= layer):
|
|
392
|
+
return None
|
|
393
|
+
return self.matrix[temporal][layer]
|
|
394
|
+
|
|
395
|
+
def fbo(self) -> moderngl.Framebuffer:
|
|
396
|
+
"""Final and most Recent FBO of this Texture"""
|
|
397
|
+
if self.final and self.scene.realtime:
|
|
398
|
+
return self.scene.window.fbo
|
|
399
|
+
return self.box().fbo
|
|
400
|
+
|
|
401
|
+
def texture(self) -> moderngl.Texture:
|
|
402
|
+
"""Final and most Recent Texture of this Texture"""
|
|
403
|
+
return self.box().texture
|
|
404
|
+
|
|
405
|
+
def roll(self, n: int=1) -> Self:
|
|
406
|
+
"""Rotate the temporal layers by $n times"""
|
|
407
|
+
self.matrix.rotate(n)
|
|
408
|
+
return self
|
|
409
|
+
|
|
410
|
+
# ------------------------------------------|
|
|
411
|
+
# Input and Output
|
|
412
|
+
|
|
413
|
+
def from_image(self, image: LoadableImage) -> Self:
|
|
414
|
+
image = LoaderImage(image)
|
|
415
|
+
image = image.transpose(PIL.Image.FLIP_TOP_BOTTOM)
|
|
416
|
+
self.width, self.height = image.size
|
|
417
|
+
self.components = len(image.getbands())
|
|
418
|
+
self.dtype = TextureType.get(numpy.array(image).dtype.str[1:].replace("u", "f"))
|
|
419
|
+
self.make()
|
|
420
|
+
self.write(image.tobytes())
|
|
421
|
+
return self
|
|
422
|
+
|
|
423
|
+
def from_numpy(self, data: numpy.ndarray) -> Self:
|
|
424
|
+
size = data.shape
|
|
425
|
+
if len(size) == 3:
|
|
426
|
+
components = size[2]
|
|
427
|
+
size = size[:2][::-1]
|
|
428
|
+
else:
|
|
429
|
+
components = 1
|
|
430
|
+
self.dtype = TextureType.get(data.dtype)
|
|
431
|
+
self.width, self.height = size
|
|
432
|
+
self.components = components
|
|
433
|
+
self.make()
|
|
434
|
+
self.write(numpy.flip(data, axis=0).tobytes())
|
|
435
|
+
return self
|
|
436
|
+
|
|
437
|
+
def write(self,
|
|
438
|
+
data: bytes=None,
|
|
439
|
+
*,
|
|
440
|
+
temporal: int=0,
|
|
441
|
+
layer: int=-1,
|
|
442
|
+
viewport: Tuple[int, int, int, int]=None,
|
|
443
|
+
) -> Self:
|
|
444
|
+
box = self.box(temporal, layer)
|
|
445
|
+
box.texture.write(data, viewport=viewport)
|
|
446
|
+
if not viewport:
|
|
447
|
+
box.data = bytes(data)
|
|
448
|
+
box.empty = False
|
|
449
|
+
return self
|
|
450
|
+
|
|
451
|
+
def clear(self, temporal: int=0, layer: int=-1) -> Self:
|
|
452
|
+
return self.write(self.zeros, temporal=temporal, layer=layer)
|
|
453
|
+
|
|
454
|
+
def is_empty(self, temporal: int=0, layer: int=-1) -> bool:
|
|
455
|
+
return self.box(temporal, layer).empty
|
|
456
|
+
|
|
457
|
+
# ------------------------------------------|
|
|
458
|
+
|
|
459
|
+
@property
|
|
460
|
+
def nbytes(self) -> int:
|
|
461
|
+
return self.dtype.value().nbytes * self.components
|
|
462
|
+
|
|
463
|
+
def sample_xy(self, x: float, y: float, temporal: int=0, layer: int=-1) -> numpy.ndarray:
|
|
464
|
+
"""Get the Pixel at a XY coordinate: Origin at Top Right (0, 0); Bottom Left (width, height)"""
|
|
465
|
+
box = self.box(temporal=temporal, layer=layer)
|
|
466
|
+
data = (box.data or box.texture.read())
|
|
467
|
+
start = int((y*self.width + x) * self.nbytes)
|
|
468
|
+
return numpy.frombuffer(data, dtype=self.dtype.value)[start:start + self.nbytes]
|
|
469
|
+
|
|
470
|
+
def sample_stxy(self, x: float, y: float, temporal: int=0, layer: int=-1) -> numpy.ndarray:
|
|
471
|
+
"""Get the Pixel at a XY coordinate: Origin at Bottom left (0, 0); Top right (width, height)"""
|
|
472
|
+
return self.sample_xy(x=x, y=(self.height - y - 1), temporal=temporal, layer=layer)
|
|
473
|
+
|
|
474
|
+
def sample_glxy(self, x: float, y: float, temporal: int=0, layer: int=-1) -> numpy.ndarray:
|
|
475
|
+
"""Get the Pixel at a XY coordinate: Origin at Center (0, 0); Any Edge either (±w/2, 0), (0, ±h/2)"""
|
|
476
|
+
return self.sample_xy(x=int(x + (self.width/2)), y=int(y + (self.height/2)), temporal=temporal, layer=layer)
|
|
477
|
+
|
|
478
|
+
def sample_uv(self, u: float, v: float, temporal: int=0, layer: int=-1) -> numpy.ndarray:
|
|
479
|
+
"""Get the Pixel at a UV coordinate: Origin at Top Right (0, 0); Bottom Left (1, 1)"""
|
|
480
|
+
return self.sample_xy(u*self.width, v*self.height, temporal=temporal, layer=layer)
|
|
481
|
+
|
|
482
|
+
def sample_stuv(self, u: float, v: float, temporal: int=0, layer: int=-1) -> numpy.ndarray:
|
|
483
|
+
"""Get the Pixel at a UV coordinate: Origin at Bottom Left (0, 0); Top Right (1, 1)"""
|
|
484
|
+
return self.sample_uv(u=u, v=(1-v), temporal=temporal, layer=layer)
|
|
485
|
+
|
|
486
|
+
def sample_gluv(self, u: float, v: float, temporal: int=0, layer: int=-1) -> numpy.ndarray:
|
|
487
|
+
"""Get the Pixel at a UV coordinate: Origin at Center (0, 0); Any Edge either (±1, 0), (0, ±1)"""
|
|
488
|
+
return self.sample_uv(u=(u/2 + 0.5), v=(v/2 + 0.5), temporal=temporal, layer=layer)
|
|
489
|
+
|
|
490
|
+
# ------------------------------------------|
|
|
491
|
+
# Module
|
|
492
|
+
|
|
493
|
+
def _coord2name(self, old: int, layer: int) -> str:
|
|
494
|
+
return f"{self.name}{old}x{layer}"
|
|
495
|
+
|
|
496
|
+
def defines(self) -> Iterable[str]:
|
|
497
|
+
if not self.name:
|
|
498
|
+
return
|
|
499
|
+
|
|
500
|
+
# Define last frames as plain name (iTex0x(-1) -> iTex, iTex1x(-1) -> iTex1)
|
|
501
|
+
for temporal in range(self.temporal):
|
|
502
|
+
yield f"#define {self.name}{temporal or ''} {self.name}{temporal}x{self.layers-1}"
|
|
503
|
+
|
|
504
|
+
# Function to sample a dynamic temporal, layer
|
|
505
|
+
yield f"\nvec4 {self.name}Texture(int temporal, int layer, vec2 astuv) {{"
|
|
506
|
+
yield " if (false) return vec4(0);"
|
|
507
|
+
for temporal in range(self.temporal):
|
|
508
|
+
for layer in range(self.layers):
|
|
509
|
+
yield f" else if (temporal == {temporal} && layer == {layer}) return texture({self._coord2name(temporal, layer)}, astuv);"
|
|
510
|
+
yield " else {return vec4(0);}"
|
|
511
|
+
yield "}"
|
|
512
|
+
|
|
513
|
+
def handle(self, message: ShaderMessage):
|
|
514
|
+
if self.track:
|
|
515
|
+
if isinstance(message, ShaderMessage.Shader.RecreateTextures):
|
|
516
|
+
self.make()
|
|
517
|
+
|
|
518
|
+
def pipeline(self) -> Iterable[ShaderVariable]:
|
|
519
|
+
if not self.name:
|
|
520
|
+
return
|
|
521
|
+
yield ShaderVariable("uniform", "int", "iLayer", None)
|
|
522
|
+
yield ShaderVariable("uniform", "vec2", f"{self.name}Size", self.size)
|
|
523
|
+
yield ShaderVariable("uniform", "float", f"{self.name}AspectRatio", self.aspect_ratio)
|
|
524
|
+
yield ShaderVariable("uniform", "int", f"{self.name}Layers", self.layers)
|
|
525
|
+
yield ShaderVariable("uniform", "int", f"{self.name}Temporal", self.temporal)
|
|
526
|
+
for (t, b, box) in self.boxes:
|
|
527
|
+
yield ShaderVariable("uniform", "sampler2D", self._coord2name(t, b), box.texture)
|
|
528
|
+
|
ShaderFlow/Variable.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from attr import define
|
|
6
|
+
|
|
7
|
+
from Broken import BrokenEnum, BrokenFluentBuilder, log
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ShaderVariableQualifier(BrokenEnum):
|
|
11
|
+
"""Guidance enum for GLSL variable qualifiers options"""
|
|
12
|
+
Uniform = "uniform"
|
|
13
|
+
Attribute = "attribute"
|
|
14
|
+
Varying = "varying"
|
|
15
|
+
|
|
16
|
+
class ShaderVariableType(BrokenEnum):
|
|
17
|
+
"""Guidance enum for GLSL variable types"""
|
|
18
|
+
Float = "float"
|
|
19
|
+
Int = "int"
|
|
20
|
+
Bool = "bool"
|
|
21
|
+
Vec2 = "vec2"
|
|
22
|
+
Vec3 = "vec3"
|
|
23
|
+
Vec4 = "vec4"
|
|
24
|
+
Mat2 = "mat2"
|
|
25
|
+
Mat3 = "mat3"
|
|
26
|
+
Mat4 = "mat4"
|
|
27
|
+
Sampler2D = "sampler2D"
|
|
28
|
+
|
|
29
|
+
class ShaderVariableInterpolation(BrokenEnum):
|
|
30
|
+
"""Guidance enum for GLSL variable interpolation options"""
|
|
31
|
+
Flat = "flat"
|
|
32
|
+
Smooth = "smooth"
|
|
33
|
+
NoPerspective = "noperspective"
|
|
34
|
+
|
|
35
|
+
class ShaderVariableDirection(BrokenEnum):
|
|
36
|
+
"""Guidance enum for GLSL variable direction options"""
|
|
37
|
+
In = "in"
|
|
38
|
+
Out = "out"
|
|
39
|
+
|
|
40
|
+
@define(eq=False)
|
|
41
|
+
class ShaderVariable(BrokenFluentBuilder):
|
|
42
|
+
"""
|
|
43
|
+
Metaprogramming class to define a shader variable
|
|
44
|
+
|
|
45
|
+
"uniform vec2 resolution;"
|
|
46
|
+
• interpolation: "flat", "smooth", "noperspective" (smooth)
|
|
47
|
+
• qualifier: "uniform"
|
|
48
|
+
• type: "vec2"
|
|
49
|
+
• name: "resolution"
|
|
50
|
+
• value: Any (of self.size)
|
|
51
|
+
"""
|
|
52
|
+
qualifier: ShaderVariableQualifier = None
|
|
53
|
+
type: ShaderVariableType = None
|
|
54
|
+
name: str = None
|
|
55
|
+
value: Any = None
|
|
56
|
+
direction: ShaderVariableDirection = None
|
|
57
|
+
interpolation: ShaderVariableInterpolation = None
|
|
58
|
+
|
|
59
|
+
def __hash__(self) -> int:
|
|
60
|
+
return hash(self.name)
|
|
61
|
+
|
|
62
|
+
def __eq__(self, other: ShaderVariable) -> bool:
|
|
63
|
+
return self.name == other.name
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def size_string(self) -> str:
|
|
67
|
+
"""Get the size string for this variable"""
|
|
68
|
+
return {
|
|
69
|
+
ShaderVariableType.Float: "f",
|
|
70
|
+
ShaderVariableType.Int: "i",
|
|
71
|
+
ShaderVariableType.Bool: "i",
|
|
72
|
+
ShaderVariableType.Vec2: "2f",
|
|
73
|
+
ShaderVariableType.Vec3: "3f",
|
|
74
|
+
ShaderVariableType.Vec4: "4f",
|
|
75
|
+
}.get(ShaderVariableType.get(self.type))
|
|
76
|
+
|
|
77
|
+
# # To string methods
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def declaration(self) -> str:
|
|
81
|
+
"""String to declared this variable in GLSL"""
|
|
82
|
+
return (" ".join(filter(None, (
|
|
83
|
+
self.interpolation,
|
|
84
|
+
self.direction,
|
|
85
|
+
self.qualifier,
|
|
86
|
+
self.type,
|
|
87
|
+
self.name,
|
|
88
|
+
)))).strip() + ";"
|
|
89
|
+
|
|
90
|
+
@staticmethod
|
|
91
|
+
def smart(definition: str | ShaderVariable) -> ShaderVariable:
|
|
92
|
+
"""Smartly creates variables from item or list of variables or string definition"""
|
|
93
|
+
|
|
94
|
+
# Return if already instance of self
|
|
95
|
+
if isinstance(definition, ShaderVariable):
|
|
96
|
+
return definition
|
|
97
|
+
|
|
98
|
+
# Attempt to smartly parse the string
|
|
99
|
+
elif isinstance(definition, str):
|
|
100
|
+
variable = ShaderVariable()
|
|
101
|
+
string = definition.replace(";", "").strip().split()
|
|
102
|
+
|
|
103
|
+
# Iterate on each line item split, attribute if an option, else name
|
|
104
|
+
for i, item in enumerate(string):
|
|
105
|
+
|
|
106
|
+
# Match against known types on enums
|
|
107
|
+
if item in ShaderVariableDirection.values():
|
|
108
|
+
variable.direction = item
|
|
109
|
+
elif item in ShaderVariableQualifier.values():
|
|
110
|
+
variable.qualifier = item
|
|
111
|
+
elif item in ShaderVariableType.values():
|
|
112
|
+
variable.type = item
|
|
113
|
+
elif item in ShaderVariableInterpolation.values():
|
|
114
|
+
variable.interpolation = item
|
|
115
|
+
|
|
116
|
+
# Item -2 can be a custom type which is not in the enum
|
|
117
|
+
elif i == len(string) - 2:
|
|
118
|
+
variable.type = item
|
|
119
|
+
|
|
120
|
+
# Item -1 is assumed to be the name
|
|
121
|
+
elif i == len(string) - 1:
|
|
122
|
+
variable.name = item
|
|
123
|
+
|
|
124
|
+
# Unknown item
|
|
125
|
+
else:
|
|
126
|
+
log.warning(f"Unknown item ({item}) on ShaderVariable Smart from ({string})")
|
|
127
|
+
|
|
128
|
+
return variable
|
ShaderFlow/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import ShaderFlow.Resources as ShaderFlowResources
|
|
2
|
+
from Broken import BrokenPath, BrokenProject
|
|
3
|
+
|
|
4
|
+
SHADERFLOW = BrokenProject(
|
|
5
|
+
PACKAGE=__file__,
|
|
6
|
+
APP_NAME="ShaderFlow",
|
|
7
|
+
APP_AUTHOR="BrokenSource",
|
|
8
|
+
RESOURCES=ShaderFlowResources,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
BrokenPath.resetdir(SHADERFLOW.DIRECTORIES.DUMP, echo=False)
|
ShaderFlow/__main__.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from Broken import BrokenApp
|
|
2
|
+
from ShaderFlow import SHADERFLOW
|
|
3
|
+
|
|
4
|
+
SHADERFLOW_ABOUT = """
|
|
5
|
+
🌵 Imagine ShaderToy, on a Manim-like architecture. That's ShaderFlow.\n
|
|
6
|
+
• Tip: run "shaderflow (scene) --help" for More Options ✨
|
|
7
|
+
• Warn: Make sure you trust the file you are running
|
|
8
|
+
|
|
9
|
+
©️ Broken Source Software, AGPL-3.0 License.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
class ShaderFlowApp(BrokenApp):
|
|
13
|
+
def main(self):
|
|
14
|
+
self.typer.description = SHADERFLOW_ABOUT
|
|
15
|
+
self.find_projects(tag="Scene")
|
|
16
|
+
self.typer()
|
|
17
|
+
|
|
18
|
+
def main():
|
|
19
|
+
ShaderFlowApp(PROJECT=SHADERFLOW)
|
|
20
|
+
|
|
21
|
+
if __name__ == "__main__":
|
|
22
|
+
main()
|