yta-video-opengl 0.0.5__py3-none-any.whl → 0.0.6__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.
@@ -0,0 +1,1091 @@
1
+ """
2
+ TODO: Please, rename, refactor and move.
3
+
4
+ Opengl doesn't know how to draw a quad
5
+ or any other complex shape. The basics
6
+ that opengl can handle are triangles,
7
+ so we use different triangles to build
8
+ our shapes (quad normally).
9
+ """
10
+ from yta_validation.parameter import ParameterValidator
11
+ from yta_video_opengl.utils import frame_to_texture
12
+ from abc import ABC, abstractmethod
13
+ from typing import Union
14
+
15
+ import av
16
+ import moderngl
17
+ import numpy as np
18
+
19
+
20
+ class _Uniforms:
21
+ """
22
+ Class to wrap the functionality related to
23
+ handling the opengl program uniforms.
24
+ """
25
+
26
+ @property
27
+ def program(
28
+ self
29
+ ):
30
+ """
31
+ Shortcut to the FrameShader program.
32
+ """
33
+ return self._shader_instance.program
34
+
35
+ def __init__(
36
+ self,
37
+ shader_instance: 'FrameShaderBase'
38
+ ):
39
+ self._shader_instance: 'FrameShaderBase' = shader_instance
40
+ """
41
+ The instance of the FrameShader class these
42
+ uniforms belong to.
43
+ """
44
+
45
+ def set(
46
+ self,
47
+ name: str,
48
+ value
49
+ ) -> 'FrameShaderBase':
50
+ """
51
+ Set the provided 'value' to the normal type
52
+ uniform with the given 'name'. Here you have
53
+ some examples of defined uniforms we can set
54
+ with this method:
55
+ - `uniform float name;`
56
+
57
+ TODO: Add more examples
58
+ """
59
+ if name in self.program:
60
+ self.program[name].value = value
61
+
62
+ return self._shader_instance
63
+
64
+ def set_vec(
65
+ self,
66
+ name: str,
67
+ values
68
+ ) -> 'FrameShaderBase':
69
+ """
70
+ Set the provided 'value' to the normal type
71
+ uniform with the given 'name'. Here you have
72
+ some examples of defined uniforms we can set
73
+ with this method:
74
+ - `uniform vec2 name;`
75
+
76
+ TODO: Is this example ok? I didn't use it yet
77
+ """
78
+ if name in self.program:
79
+ self.program[name].write(np.array(values, dtype = 'f4').tobytes())
80
+
81
+ def set_mat(
82
+ self,
83
+ name: str,
84
+ value
85
+ ) -> 'FrameShaderBase':
86
+ """
87
+ Set the provided 'value' to a `matN` type
88
+ uniform with the given 'name'. The 'value'
89
+ must be a NxN matrix (maybe numpy array)
90
+ transformed to bytes ('.tobytes()').
91
+
92
+ This uniform must be defined in the vertex
93
+ like this:
94
+ - `uniform matN name;`
95
+
96
+ TODO: Maybe we can accept a NxN numpy
97
+ array and do the .tobytes() by ourselves...
98
+ """
99
+ if name in self.program:
100
+ self.program[name].write(value)
101
+
102
+ return self._shader_instance
103
+
104
+ class FrameShaderBase(ABC):
105
+ """
106
+ Class to be inherited by any of our own
107
+ custom opengl program classes.
108
+
109
+ This shader base class must be used by all
110
+ the classes that are modifying the frames
111
+ one by one.
112
+ """
113
+
114
+ @property
115
+ @abstractmethod
116
+ def vertex_shader(
117
+ self
118
+ ) -> str:
119
+ """
120
+ Source code of the vertex shader.
121
+ """
122
+ pass
123
+
124
+ @property
125
+ @abstractmethod
126
+ def fragment_shader(
127
+ self
128
+ ) -> str:
129
+ """
130
+ Source code of the fragment shader.
131
+ """
132
+ pass
133
+
134
+ @property
135
+ def vertices(
136
+ self
137
+ ) -> 'np.ndarray':
138
+ """
139
+ The UV coordinates to build the quad we
140
+ will use to represent the frame by
141
+ applying it as a texture.
142
+ """
143
+ return np.array([
144
+ # vertex 0 - bottom left
145
+ -1.0, -1.0, 0.0, 0.0,
146
+ # vertex 1 - bottom right
147
+ 1.0, -1.0, 1.0, 0.0,
148
+ # vertex 2 - top left
149
+ -1.0, 1.0, 0.0, 1.0,
150
+ # vertex 3 - top right
151
+ 1.0, 1.0, 1.0, 1.0
152
+ ], dtype = 'f4')
153
+
154
+ @property
155
+ def indexes(
156
+ self
157
+ ) -> 'np.ndarray':
158
+ """
159
+ The indexes of the vertices (see 'vertices'
160
+ property) to build the 2 opengl triangles
161
+ that will represent the quad we need for
162
+ the frame.
163
+ """
164
+ return np.array(
165
+ [
166
+ 0, 1, 2,
167
+ 2, 1, 3
168
+ ],
169
+ dtype = 'i4'
170
+ )
171
+
172
+ def __init__(
173
+ self,
174
+ size: tuple[int, int],
175
+ first_frame: Union['VideoFrame', 'np.ndarray'],
176
+ context: Union[moderngl.Context, None] = None,
177
+ ):
178
+ context = (
179
+ moderngl.create_context(standalone = True)
180
+ if context is None else
181
+ context
182
+ )
183
+
184
+ self.size: tuple[int, int] = size
185
+ """
186
+ The size we want to use for the frame buffer
187
+ in a (width, height) format.
188
+ """
189
+ self.first_frame: Union['VideoFrame', 'np.ndarray'] = first_frame
190
+ """
191
+ The first frame of the video in which we will
192
+ apply the effect. Needed to build the texture.
193
+ """
194
+ self.context: moderngl.Context = context
195
+ """
196
+ The context of the program.
197
+ """
198
+ self.program: moderngl.Program = None
199
+ """
200
+ The opengl program.
201
+ """
202
+ self.fbo: moderngl.Framebuffer = None
203
+ """
204
+ The frame buffer object.
205
+ """
206
+ self.vbo: moderngl.Buffer = None
207
+ """
208
+ The vertices buffer object.
209
+ """
210
+ self.ibo: moderngl.Buffer = None
211
+ """
212
+ The indexes buffer object.
213
+ """
214
+ self.uniforms: _Uniforms = None
215
+ """
216
+ Shortcut to the uniforms functionality.
217
+ """
218
+
219
+ self._initialize_program()
220
+
221
+ def _initialize_program(
222
+ self
223
+ ):
224
+ # Compile shaders within the program
225
+ self.program: moderngl.Program = self.context.program(
226
+ vertex_shader = self.vertex_shader,
227
+ fragment_shader = self.fragment_shader
228
+ )
229
+
230
+ # Create frame buffer
231
+ self.fbo = self.context.simple_framebuffer(self.size)
232
+
233
+ # Create buffers
234
+ # TODO: I could have more than 1 vertices,
235
+ # so more than 1 vbo and more than 1 vao...
236
+ self.vbo: moderngl.Buffer = self.context.buffer(self.vertices.tobytes())
237
+ self.ibo: moderngl.Buffer = self.context.buffer(self.indexes.tobytes())
238
+ vao_content = [
239
+ (self.vbo, "2f 2f", "in_vert", "in_texcoord")
240
+ ]
241
+ self.vao: moderngl.VertexArray = self.context.vertex_array(self.program, vao_content, self.ibo)
242
+
243
+ self.uniforms: _Uniforms = _Uniforms(self)
244
+
245
+ # TODO: How do I manage these textures (?)
246
+ self.textures = {}
247
+
248
+ # TODO: Should we do this here (?)
249
+ texture: moderngl.Texture = frame_to_texture(self.first_frame, self.context)
250
+ texture.build_mipmaps()
251
+
252
+ # TODO: I'm not using this method, but sounds
253
+ # interesting to simplify the 'process_frame'
254
+ # method in different mini actions
255
+ def load_texture(
256
+ self,
257
+ image: np.ndarray,
258
+ uniform_name: str,
259
+ texture_unit = 0
260
+ ):
261
+ """
262
+ Load a texture with the given 'image' and set
263
+ it to the uniform with the given 'uniform_name'.
264
+
265
+ TODO: Understand better the 'texture_unit'
266
+ """
267
+ # This is to receive a path (str) to an image
268
+ #img = Image.open(path).transpose(Image.FLIP_TOP_BOTTOM).convert("RGBA")
269
+ image = np.flipud(image)
270
+ tex = self.context.texture((image.shape[1], image.shape[0]), 4, image.tobytes())
271
+ tex.use(texture_unit)
272
+ self.textures[uniform_name] = tex
273
+ if uniform_name in self.program:
274
+ self.program[uniform_name].value = texture_unit
275
+
276
+ @abstractmethod
277
+ def _prepare_frame(
278
+ self,
279
+ t: float
280
+ ):
281
+ """
282
+ Set the uniforms we need to process that
283
+ specific frame and the code to calculate
284
+ those uniforms we need.
285
+ """
286
+ pass
287
+
288
+ def process_frame(
289
+ self,
290
+ frame: Union['VideoFrame', np.ndarray],
291
+ t: float,
292
+ numpy_format: str = 'rgb24'
293
+ ) -> 'VideoFrame':
294
+ # TODO: This method accepts 'np.ndarray' to
295
+ # prepare it to frames coming from other source
296
+ # different than reading a video here (that
297
+ # will be processed as VideoFrame). Check the
298
+ # sizes and [0], [1] indexes.
299
+ ParameterValidator.validate_mandatory_instance_of('frame', frame, ['VideoFrame', 'ndarray'])
300
+
301
+ # By now I call this here because I don't need
302
+ # to send nothing specific when calculating the
303
+ # frame...
304
+ self._prepare_frame(t)
305
+
306
+ # Set frame as a texture
307
+ texture = frame_to_texture(frame, self.context, numpy_format)
308
+ # TODO: Why 0 (?)
309
+ #texture.use(0)
310
+ texture.use()
311
+
312
+ # # TODO: Check this
313
+ # if 'u_texture' in self.program:
314
+ # self.program['u_texture'].value = 0
315
+
316
+ # Set the frame buffer a a whole black frame
317
+ self.context.clear(0.0, 0.0, 0.0)
318
+ # TODO: No 'self.fbo.use()' here (?)
319
+ self.fbo.use()
320
+ self.vao.render(moderngl.TRIANGLE_STRIP)
321
+
322
+ # Read output of fbo
323
+ output = np.flipud(
324
+ np.frombuffer(
325
+ self.fbo.read(components = 3, alignment = 1),
326
+ dtype = np.uint8
327
+ ).reshape((texture.size[1], texture.size[0], 3))
328
+ #).reshape((self.size[1], self.size[0], 3))
329
+ )
330
+
331
+ # We want a VideoFrame instance because we
332
+ # we can send it directly to the mux to
333
+ # write
334
+ output: 'VideoFrame' = av.VideoFrame.from_ndarray(output, format = numpy_format)
335
+
336
+ return output
337
+
338
+ # Example classes below
339
+ class WavingFrame(FrameShaderBase):
340
+ """
341
+ The frame but waving as a flag.
342
+ """
343
+
344
+ @property
345
+ def vertex_shader(
346
+ self
347
+ ) -> str:
348
+ return (
349
+ '''
350
+ #version 330
351
+ in vec2 in_vert;
352
+ in vec2 in_texcoord;
353
+ out vec2 v_uv;
354
+ void main() {
355
+ v_uv = in_texcoord;
356
+ gl_Position = vec4(in_vert, 0.0, 1.0);
357
+ }
358
+ '''
359
+ )
360
+
361
+ @property
362
+ def fragment_shader(
363
+ self
364
+ ) -> str:
365
+ return (
366
+ '''
367
+ #version 330
368
+ uniform sampler2D tex;
369
+ uniform float time;
370
+ uniform float amp;
371
+ uniform float freq;
372
+ uniform float speed;
373
+ in vec2 v_uv;
374
+ out vec4 f_color;
375
+ void main() {
376
+ float wave = sin(v_uv.x * freq + time * speed) * amp;
377
+ vec2 uv = vec2(v_uv.x, v_uv.y + wave);
378
+ f_color = texture(tex, uv);
379
+ }
380
+ '''
381
+ )
382
+
383
+ def __init__(
384
+ self,
385
+ size,
386
+ first_frame,
387
+ context = None,
388
+ amplitude: float = 0.05,
389
+ frequency: float = 10.0,
390
+ speed: float = 2.0
391
+ ):
392
+ super().__init__(size, first_frame, context)
393
+
394
+ # TODO: Use automatic way of detecting the
395
+ # parameters that are not 'self', 'size',
396
+ # 'first_frame' nor 'context' and set those
397
+ # as uniforms automatically
398
+
399
+ self.uniforms.set('amp', amplitude)
400
+ self.uniforms.set('freq', frequency)
401
+ self.uniforms.set('speed', speed)
402
+
403
+ def _prepare_frame(
404
+ self,
405
+ t: float
406
+ ) -> 'WavingFrame':
407
+ """
408
+ Precalculate all the things we need to process
409
+ a frame, like the uniforms, etc.
410
+ """
411
+ self.uniforms.set('time', t)
412
+
413
+ return self
414
+
415
+ class BreathingFrame(FrameShaderBase):
416
+ """
417
+ The frame but as if it was breathing.
418
+ """
419
+
420
+ @property
421
+ def vertex_shader(
422
+ self
423
+ ) -> str:
424
+ return (
425
+ '''
426
+ #version 330
427
+ in vec2 in_vert;
428
+ in vec2 in_texcoord;
429
+ out vec2 v_text;
430
+ void main() {
431
+ gl_Position = vec4(in_vert, 0.0, 1.0);
432
+ v_text = in_texcoord;
433
+ }
434
+ '''
435
+ )
436
+
437
+ @property
438
+ def fragment_shader(
439
+ self
440
+ ) -> str:
441
+ return (
442
+ '''
443
+ #version 330
444
+ uniform sampler2D tex;
445
+ uniform float time;
446
+ in vec2 v_text;
447
+ out vec4 f_color;
448
+ // Use uniforms to be customizable
449
+
450
+ void main() {
451
+ // Dynamic zoom scaled with t
452
+ float scale = 1.0 + 0.05 * sin(time * 2.0); // 5% de zoom
453
+ vec2 center = vec2(0.5, 0.5);
454
+
455
+ // Recalculate coords according to center
456
+ vec2 uv = (v_text - center) / scale + center;
457
+
458
+ // Clamp to avoid artifacts
459
+ uv = clamp(uv, 0.0, 1.0);
460
+
461
+ f_color = texture(tex, uv);
462
+ }
463
+ '''
464
+ )
465
+
466
+ def _prepare_frame(
467
+ self,
468
+ t: float
469
+ ) -> 'BreathingFrame':
470
+ # TODO: Use automatic way of detecting the
471
+ # parameters that are not 'self', 'size',
472
+ # 'first_frame' nor 'context' and set those
473
+ # as uniforms automatically
474
+
475
+ self.uniforms.set('time', t)
476
+
477
+ return self
478
+
479
+ class HandheldFrame(FrameShaderBase):
480
+ """
481
+ The frame but as if it was being recorder by
482
+ someone holding a camera, that is not 100%
483
+ stable.
484
+ """
485
+
486
+ @property
487
+ def vertex_shader(
488
+ self
489
+ ) -> str:
490
+ return (
491
+ '''
492
+ #version 330
493
+ in vec2 in_vert;
494
+ in vec2 in_texcoord;
495
+ out vec2 v_text;
496
+
497
+ uniform mat3 transform;
498
+
499
+ void main() {
500
+ vec3 pos = vec3(in_vert, 1.0);
501
+ pos = transform * pos;
502
+ gl_Position = vec4(pos.xy, 0.0, 1.0);
503
+ v_text = in_texcoord;
504
+ }
505
+ '''
506
+ )
507
+
508
+ @property
509
+ def fragment_shader(
510
+ self
511
+ ) -> str:
512
+ return (
513
+ '''
514
+ #version 330
515
+ uniform sampler2D tex;
516
+ in vec2 v_text;
517
+ out vec4 f_color;
518
+
519
+ void main() {
520
+ f_color = texture(tex, v_text);
521
+ }
522
+ '''
523
+ )
524
+
525
+ def _prepare_frame(
526
+ self,
527
+ t: float
528
+ ) -> 'HandheldFrame':
529
+ import math
530
+ def handheld_matrix_exaggerated(t):
531
+ # Rotación más notoria
532
+ angle = smooth_noise(t, freq=0.8, scale=0.05) # antes 0.02
533
+
534
+ # Traslaciones más grandes
535
+ tx = smooth_noise(t, freq=1.1, scale=0.04) # antes 0.015
536
+ ty = smooth_noise(t, freq=1.4, scale=0.04)
537
+
538
+ # Zoom más agresivo
539
+ zoom = 1.0 + smooth_noise(t, freq=0.5, scale=0.06) # antes 0.02
540
+
541
+ cos_a, sin_a = math.cos(angle), math.sin(angle)
542
+
543
+ return np.array([
544
+ [ cos_a * zoom, -sin_a * zoom, tx],
545
+ [ sin_a * zoom, cos_a * zoom, ty],
546
+ [ 0.0, 0.0, 1.0]
547
+ ], dtype="f4")
548
+
549
+ def smooth_noise(t, freq=1.5, scale=1.0):
550
+ """Pequeño ruido orgánico usando senos y cosenos mezclados"""
551
+ return (
552
+ math.sin(t * freq) +
553
+ 0.5 * math.cos(t * freq * 0.5 + 1.7) +
554
+ 0.25 * math.sin(t * freq * 0.25 + 2.5)
555
+ ) * scale
556
+
557
+ def handheld_matrix(t):
558
+ # Rotación ligera (en radianes)
559
+ angle = smooth_noise(t, freq=0.8, scale=0.02)
560
+
561
+ # Traslación horizontal/vertical
562
+ tx = smooth_noise(t, freq=1.1, scale=0.015)
563
+ ty = smooth_noise(t, freq=1.4, scale=0.015)
564
+
565
+ # Zoom (escala)
566
+ zoom = 1.0 + smooth_noise(t, freq=0.5, scale=0.02)
567
+
568
+ cos_a, sin_a = math.cos(angle), math.sin(angle)
569
+
570
+ # Matriz de transformación: Zoom * Rotación + Traslación
571
+ return np.array([
572
+ [ cos_a * zoom, -sin_a * zoom, tx],
573
+ [ sin_a * zoom, cos_a * zoom, ty],
574
+ [ 0.0, 0.0, 1.0]
575
+ ], dtype = "f4")
576
+
577
+ self.uniforms.set_mat('transform', handheld_matrix_exaggerated(t).tobytes())
578
+
579
+ return self
580
+
581
+ class OrbitingFrame(FrameShaderBase):
582
+ """
583
+ The frame but orbiting around the camera.
584
+ """
585
+
586
+ @property
587
+ def vertex_shader(
588
+ self
589
+ ) -> str:
590
+ return (
591
+ '''
592
+ #version 330
593
+
594
+ in vec2 in_vert;
595
+ in vec2 in_texcoord;
596
+
597
+ out vec2 v_uv;
598
+
599
+ uniform mat4 mvp; // Model-View-Projection matrix
600
+
601
+ void main() {
602
+ v_uv = in_texcoord;
603
+ // El quad está en XY, lo pasamos a XYZ con z=0
604
+ vec4 pos = vec4(in_vert, 0.0, 1.0);
605
+ gl_Position = mvp * pos;
606
+ }
607
+ '''
608
+ )
609
+
610
+ @property
611
+ def fragment_shader(
612
+ self
613
+ ) -> str:
614
+ return (
615
+ '''
616
+ #version 330
617
+
618
+ uniform sampler2D tex;
619
+ in vec2 v_uv;
620
+ out vec4 f_color;
621
+
622
+ void main() {
623
+ f_color = texture(tex, v_uv);
624
+ }
625
+ '''
626
+ )
627
+
628
+ def _prepare_frame(
629
+ self,
630
+ t: float
631
+ ) -> 'OrbitingFrame':
632
+ def perspective(fov_y_rad, aspect, near, far):
633
+ f = 1.0 / np.tan(fov_y_rad / 2.0)
634
+ m = np.zeros((4,4), dtype='f4')
635
+ m[0,0] = f / aspect
636
+ m[1,1] = f
637
+ m[2,2] = (far + near) / (near - far)
638
+ m[2,3] = (2 * far * near) / (near - far)
639
+ m[3,2] = -1.0
640
+ return m
641
+
642
+ def look_at(eye, target, up=(0,1,0)):
643
+ eye = np.array(eye, dtype='f4')
644
+ target = np.array(target, dtype='f4')
645
+ up = np.array(up, dtype='f4')
646
+
647
+ f = target - eye
648
+ f = f / np.linalg.norm(f)
649
+ s = np.cross(f, up)
650
+ s = s / np.linalg.norm(s)
651
+ u = np.cross(s, f)
652
+
653
+ m = np.eye(4, dtype='f4')
654
+ m[0,0:3] = s
655
+ m[1,0:3] = u
656
+ m[2,0:3] = -f
657
+ m[0,3] = -np.dot(s, eye)
658
+ m[1,3] = -np.dot(u, eye)
659
+ m[2,3] = np.dot(f, eye)
660
+ return m
661
+
662
+ def translate(x, y, z):
663
+ m = np.eye(4, dtype='f4')
664
+ m[0,3] = x
665
+ m[1,3] = y
666
+ m[2,3] = z
667
+ return m
668
+
669
+ def rotate_y(angle):
670
+ c, s = np.cos(angle), np.sin(angle)
671
+ m = np.eye(4, dtype='f4')
672
+ m[0,0], m[0,2] = c, s
673
+ m[2,0], m[2,2] = -s, c
674
+ return m
675
+
676
+ def scale_uniform(k):
677
+ m = np.eye(4, dtype='f4')
678
+ m[0,0] = m[1,1] = m[2,2] = k
679
+ return m
680
+
681
+ def carousel_mvp(t, *,
682
+ aspect,
683
+ fov_deg=60.0,
684
+ radius=4.0,
685
+ center_z=-6.0,
686
+ speed=1.0,
687
+ face_center_strength=1.0,
688
+ extra_scale=1.0):
689
+ """
690
+ t: tiempo en segundos
691
+ aspect: width/height del framebuffer
692
+ radius: radio en XZ
693
+ center_z: desplaza el carrusel entero hacia -Z para que esté frente a cámara
694
+ speed: velocidad angular
695
+ face_center_strength: 1.0 = panel mira al centro; 0.0 = no gira con la órbita
696
+ """
697
+
698
+ # Proyección y vista (cámara en el origen mirando hacia -Z)
699
+ proj = perspective(np.radians(fov_deg), aspect, 0.1, 100.0)
700
+ view = np.eye(4, dtype='f4') # o look_at((0,0,0), (0,0,-1))
701
+
702
+ # Ángulo de órbita (elige el offset para que "entre" por la izquierda)
703
+ theta = speed * t - np.pi * 0.5
704
+
705
+ # Órbita en XZ con el centro desplazado a center_z
706
+ # x = radius * np.cos(theta)
707
+ # z = radius * np.sin(theta) + center_z
708
+ x = radius * np.cos(theta)
709
+ z = (radius * 0.2) * np.sin(theta) + center_z
710
+
711
+ # Yaw para que el panel apunte al centro (0,0,center_z)
712
+ # El vector desde panel -> centro es (-x, 0, center_z - z)
713
+ yaw_to_center = np.arctan2(-x, (center_z - z)) # atan2(X, Z)
714
+ yaw = face_center_strength * yaw_to_center
715
+
716
+ model = translate(x, 0.0, z) @ rotate_y(yaw) @ scale_uniform(extra_scale)
717
+
718
+ # ¡IMPORTANTE! OpenGL espera column-major: transponemos al escribir
719
+ mvp = proj @ view @ model
720
+ return mvp
721
+
722
+ aspect = self.size[0] / self.size[1]
723
+ mvp = carousel_mvp(t, aspect=aspect, radius=4.0, center_z=-4.0, speed=1.2, face_center_strength=1.0, extra_scale = 1.0)
724
+
725
+ self.uniforms.set_mat('mvp', mvp.T.tobytes())
726
+
727
+ return self
728
+
729
+ class RotatingInCenterFrame(FrameShaderBase):
730
+ """
731
+ The frame but orbiting around the camera.
732
+ """
733
+
734
+ @property
735
+ def vertex_shader(
736
+ self
737
+ ) -> str:
738
+ return (
739
+ '''
740
+ #version 330
741
+
742
+ in vec2 in_vert;
743
+ in vec2 in_texcoord;
744
+ out vec2 v_uv;
745
+
746
+ uniform float time;
747
+ uniform float speed;
748
+
749
+ void main() {
750
+ v_uv = in_texcoord;
751
+
752
+ // Rotación alrededor del eje Y
753
+ float angle = time * speed; // puedes usar time directamente, o time * speed
754
+ float cosA = cos(angle);
755
+ float sinA = sin(angle);
756
+
757
+ // Convertimos el quad a 3D (x, y, z)
758
+ vec3 pos = vec3(in_vert.xy, 0.0);
759
+
760
+ // Rotación Y
761
+ mat3 rotY = mat3(
762
+ cosA, 0.0, sinA,
763
+ 0.0 , 1.0, 0.0,
764
+ -sinA, 0.0, cosA
765
+ );
766
+
767
+ pos = rotY * pos;
768
+
769
+ gl_Position = vec4(pos, 1.0);
770
+ }
771
+ '''
772
+ )
773
+
774
+ @property
775
+ def fragment_shader(
776
+ self
777
+ ) -> str:
778
+ return (
779
+ '''
780
+ #version 330
781
+
782
+ in vec2 v_uv;
783
+ out vec4 f_color;
784
+
785
+ uniform sampler2D tex;
786
+
787
+ void main() {
788
+ f_color = texture(tex, v_uv);
789
+ }
790
+ '''
791
+ )
792
+
793
+ def __init__(
794
+ self,
795
+ size,
796
+ first_frame,
797
+ context = None,
798
+ speed: float = 30
799
+ ):
800
+ super().__init__(size, first_frame, context)
801
+
802
+ self.uniforms.set('speed', speed)
803
+
804
+ def _prepare_frame(
805
+ self,
806
+ t: float
807
+ ) -> 'BreathingFrame':
808
+ self.uniforms.set('time', t)
809
+
810
+ return self
811
+
812
+ class StrangeTvFrame(FrameShaderBase):
813
+ """
814
+ Nice effect like a tv screen or something...
815
+ """
816
+
817
+ @property
818
+ def vertex_shader(
819
+ self
820
+ ) -> str:
821
+ return (
822
+ '''
823
+ #version 330
824
+ in vec2 in_vert;
825
+ in vec2 in_texcoord;
826
+ out vec2 v_uv;
827
+
828
+ void main() {
829
+ v_uv = in_texcoord;
830
+ gl_Position = vec4(in_vert, 0.0, 1.0);
831
+ }
832
+ '''
833
+ )
834
+
835
+ @property
836
+ def fragment_shader(
837
+ self
838
+ ) -> str:
839
+ return (
840
+ '''
841
+ #version 330
842
+
843
+ uniform sampler2D tex;
844
+ uniform float time;
845
+
846
+ // ---- Parámetros principales (ajústalos en runtime) ----
847
+ uniform float aberr_strength; // 0..3 (fuerza del RGB split radial)
848
+ uniform float barrel_k; // -0.5..0.5 (distorsión de lente; positivo = barrel)
849
+ uniform float blur_radius; // 0..0.02 (radio de motion blur en UV)
850
+ uniform float blur_angle; // en radianes (dirección del arrastre)
851
+ uniform int blur_samples; // 4..24 (taps del blur)
852
+ uniform float vignette_strength; // 0..2
853
+ uniform float grain_amount; // 0..0.1
854
+ uniform float flicker_amount; // 0..0.2
855
+ uniform float scanline_amount; // 0..0.2
856
+
857
+ in vec2 v_uv;
858
+ out vec4 f_color;
859
+
860
+ // --- helpers ---
861
+ float rand(vec2 co){
862
+ return fract(sin(dot(co, vec2(12.9898,78.233))) * 43758.5453);
863
+ }
864
+
865
+ // Barrel distortion (simple, k>0 curva hacia fuera)
866
+ vec2 barrel(vec2 uv, float k){
867
+ // map to [-1,1]
868
+ vec2 p = uv * 2.0 - 1.0;
869
+ float r2 = dot(p, p);
870
+ p *= (1.0 + k * r2);
871
+ // back to [0,1]
872
+ return p * 0.5 + 0.5;
873
+ }
874
+
875
+ // Aberración cromática radial
876
+ vec3 sample_chromatic(sampler2D t, vec2 uv, vec2 center, float strength){
877
+ // Offset radial según distancia al centro
878
+ vec2 d = uv - center;
879
+ float r = length(d);
880
+ vec2 dir = (r > 1e-5) ? d / r : vec2(0.0);
881
+ // Cada canal se desplaza un poco distinto
882
+ float s = strength * r * 0.005; // escala fina
883
+ float sr = s * 1.0;
884
+ float sg = s * 0.5;
885
+ float sb = s * -0.5; // azul hacia dentro para contraste
886
+
887
+ float rC = texture(t, uv + dir * sr).r;
888
+ float gC = texture(t, uv + dir * sg).g;
889
+ float bC = texture(t, uv + dir * sb).b;
890
+ return vec3(rC, gC, bC);
891
+ }
892
+
893
+ void main(){
894
+ vec2 uv = v_uv;
895
+ vec2 center = vec2(0.5, 0.5);
896
+
897
+ // Lente (barrel/pincushion)
898
+ uv = barrel(uv, barrel_k);
899
+
900
+ // Early out si nos salimos mucho (fade de bordes)
901
+ vec2 uv_clamped = clamp(uv, 0.0, 1.0);
902
+ float edge = smoothstep(0.0, 0.02, 1.0 - max(max(-uv.x, uv.x-1.0), max(-uv.y, uv.y-1.0)));
903
+
904
+ // Dirección del motion blur
905
+ vec2 dir = vec2(cos(blur_angle), sin(blur_angle));
906
+ // Pequeña variación temporal para que “respire”
907
+ float jitter = (sin(time * 13.0) * 0.5 + 0.5) * 0.4 + 0.6;
908
+
909
+ // Acumulación de blur con pesos
910
+ vec3 acc = vec3(0.0);
911
+ float wsum = 0.0;
912
+
913
+ int N = max(1, blur_samples);
914
+ for(int i = 0; i < 64; ++i){ // hard cap de seguridad
915
+ if(i >= N) break;
916
+ // t de -1..1 distribuye muestras a ambos lados
917
+ float fi = float(i);
918
+ float t = (fi / float(N - 1)) * 2.0 - 1.0;
919
+
920
+ // curva de pesos (gauss approx)
921
+ float w = exp(-t*t * 2.5);
922
+ // offset base
923
+ vec2 ofs = dir * t * blur_radius * jitter;
924
+
925
+ // micro-jitter por muestra para romper banding
926
+ ofs += vec2(rand(uv + fi)*0.0005, rand(uv + fi + 3.14)*0.0005) * blur_radius;
927
+
928
+ // muestreo con aberración cromática
929
+ vec3 c = sample_chromatic(tex, uv + ofs, center, aberr_strength);
930
+
931
+ acc += c * w;
932
+ wsum += w;
933
+ }
934
+ vec3 col = acc / max(wsum, 1e-6);
935
+
936
+ // Scanlines + flicker
937
+ float scan = 1.0 - scanline_amount * (0.5 + 0.5 * sin((uv.y + time*1.7)*3.14159*480.0));
938
+ float flick = 1.0 + flicker_amount * (sin(time*60.0 + uv.x*10.0) * 0.5 + 0.5);
939
+ col *= scan * flick;
940
+
941
+ // Vignette (radial)
942
+ float r = distance(uv, center);
943
+ float vig = 1.0 - smoothstep(0.7, 1.0, r * (1.0 + 0.5*vignette_strength));
944
+ col *= mix(1.0, vig, vignette_strength);
945
+
946
+ // Grano
947
+ float g = (rand(uv * (time*37.0 + 1.0)) - 0.5) * 2.0 * grain_amount;
948
+ col += g;
949
+
950
+ // Fade de bordes por clamp/warp
951
+ col *= edge;
952
+
953
+ f_color = vec4(col, 1.0);
954
+ }
955
+ '''
956
+ )
957
+
958
+ def __init__(
959
+ self,
960
+ size,
961
+ first_frame,
962
+ context = None,
963
+ aberr_strength = 1.5,
964
+ barrel_k = 0.08,
965
+ blur_radius = 0.006,
966
+ blur_angle = 0.0, # (0 = horizontal, 1.57 ≈ vertical)
967
+ blur_samples = 12,
968
+ vignette_strength = 0.8,
969
+ grain_amount = 0.02,
970
+ flicker_amount = 0.05,
971
+ scanline_amount = 0.05
972
+ ):
973
+ super().__init__(size, first_frame, context)
974
+
975
+ self.uniforms.set('aberr_strength', aberr_strength)
976
+ self.uniforms.set('barrel_k', barrel_k)
977
+ self.uniforms.set('blur_radius', blur_radius)
978
+ self.uniforms.set('blur_angle', blur_angle)
979
+ self.uniforms.set('blur_samples', blur_samples)
980
+ self.uniforms.set('vignette_strength', vignette_strength)
981
+ self.uniforms.set('grain_amount', grain_amount)
982
+ self.uniforms.set('flicker_amount', flicker_amount)
983
+ self.uniforms.set('scanline_amount', scanline_amount)
984
+
985
+ def _prepare_frame(
986
+ self,
987
+ t: float
988
+ ) -> 'BreathingFrame':
989
+ self.uniforms.set('time', t)
990
+
991
+ return self
992
+
993
+ class GlitchRgbFrame(FrameShaderBase):
994
+ """
995
+ Nice effect like a tv screen or something...
996
+ """
997
+
998
+ @property
999
+ def vertex_shader(
1000
+ self
1001
+ ) -> str:
1002
+ return (
1003
+ '''
1004
+ #version 330
1005
+
1006
+ // ----------- Vertex Shader -----------
1007
+ in vec2 in_vert;
1008
+ in vec2 in_texcoord;
1009
+
1010
+ out vec2 v_uv;
1011
+
1012
+ void main() {
1013
+ v_uv = in_texcoord;
1014
+ gl_Position = vec4(in_vert, 0.0, 1.0);
1015
+ }
1016
+ '''
1017
+ )
1018
+
1019
+ @property
1020
+ def fragment_shader(
1021
+ self
1022
+ ) -> str:
1023
+ return (
1024
+ '''
1025
+ #version 330
1026
+
1027
+ // ----------- Fragment Shader -----------
1028
+ uniform sampler2D tex;
1029
+ uniform float time;
1030
+
1031
+ // Intensidades del efecto
1032
+ uniform float amp; // amplitud de distorsión
1033
+ uniform float freq; // frecuencia de la onda
1034
+ uniform float glitchAmp; // fuerza del glitch
1035
+ uniform float glitchSpeed;
1036
+
1037
+ in vec2 v_uv;
1038
+ out vec4 f_color;
1039
+
1040
+ void main() {
1041
+ // Distorsión sinusoidal en Y
1042
+ float wave = sin(v_uv.x * freq + time * 2.0) * amp;
1043
+
1044
+ // Pequeño desplazamiento aleatorio (shake)
1045
+ float shakeX = (fract(sin(time * 12.9898) * 43758.5453) - 0.5) * 0.01;
1046
+ float shakeY = (fract(sin(time * 78.233) * 12345.6789) - 0.5) * 0.01;
1047
+
1048
+ // Coordenadas base con distorsión
1049
+ vec2 uv = vec2(v_uv.x + shakeX, v_uv.y + wave + shakeY);
1050
+
1051
+ // Glitch con separación RGB
1052
+ float glitch = sin(time * glitchSpeed) * glitchAmp;
1053
+ vec2 uv_r = uv + vec2(glitch, 0.0);
1054
+ vec2 uv_g = uv + vec2(-glitch * 0.5, glitch * 0.5);
1055
+ vec2 uv_b = uv + vec2(0.0, -glitch);
1056
+
1057
+ // Muestreo canales desplazados
1058
+ float r = texture(tex, uv_r).r;
1059
+ float g = texture(tex, uv_g).g;
1060
+ float b = texture(tex, uv_b).b;
1061
+
1062
+ f_color = vec4(r, g, b, 1.0);
1063
+ }
1064
+ '''
1065
+ )
1066
+
1067
+ def __init__(
1068
+ self,
1069
+ size,
1070
+ first_frame,
1071
+ context = None,
1072
+ amp = 0.02,
1073
+ freq = 25.0,
1074
+ glitchAmp = 0.02,
1075
+ glitchSpeed = 30.0
1076
+ ):
1077
+ super().__init__(size, first_frame, context)
1078
+
1079
+ self.uniforms.set('amp', amp)
1080
+ self.uniforms.set('freq', freq)
1081
+ self.uniforms.set('glitchAmp', glitchAmp)
1082
+ self.uniforms.set('glitchSpeed', glitchSpeed)
1083
+
1084
+ def _prepare_frame(
1085
+ self,
1086
+ t: float
1087
+ ) -> 'BreathingFrame':
1088
+ self.uniforms.set('time', t)
1089
+
1090
+ return self
1091
+