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