yta-editor-nodes-gpu 0.0.1__tar.gz → 0.0.2__tar.gz
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.
- {yta_editor_nodes_gpu-0.0.1 → yta_editor_nodes_gpu-0.0.2}/PKG-INFO +5 -1
- {yta_editor_nodes_gpu-0.0.1 → yta_editor_nodes_gpu-0.0.2}/pyproject.toml +8 -2
- yta_editor_nodes_gpu-0.0.2/src/yta_editor_nodes_gpu/processor/__init__.py +162 -0
- yta_editor_nodes_gpu-0.0.2/src/yta_editor_nodes_gpu/processor/blender/__init__.py +447 -0
- yta_editor_nodes_gpu-0.0.2/src/yta_editor_nodes_gpu/processor/blender/utils.py +44 -0
- yta_editor_nodes_gpu-0.0.2/src/yta_editor_nodes_gpu/processor/video/__init__.py +231 -0
- yta_editor_nodes_gpu-0.0.2/src/yta_editor_nodes_gpu/processor/video/experimental.py +723 -0
- yta_editor_nodes_gpu-0.0.2/src/yta_editor_nodes_gpu/processor/video/transitions/__init__.py +680 -0
- {yta_editor_nodes_gpu-0.0.1 → yta_editor_nodes_gpu-0.0.2}/LICENSE +0 -0
- {yta_editor_nodes_gpu-0.0.1 → yta_editor_nodes_gpu-0.0.2}/README.md +0 -0
- {yta_editor_nodes_gpu-0.0.1 → yta_editor_nodes_gpu-0.0.2}/src/yta_editor_nodes_gpu/__init__.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: yta-editor-nodes-gpu
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.2
|
4
4
|
Summary: Youtube Autonomous Main Editor Nodes GPU module
|
5
5
|
License-File: LICENSE
|
6
6
|
Author: danialcala94
|
@@ -8,6 +8,10 @@ Author-email: danielalcalavalera@gmail.com
|
|
8
8
|
Requires-Python: ==3.9
|
9
9
|
Classifier: Programming Language :: Python :: 3
|
10
10
|
Classifier: Programming Language :: Python :: 3.9
|
11
|
+
Requires-Dist: moderngl (>=0.0.1,<9.0.0)
|
12
|
+
Requires-Dist: yta_programming (>=0.0.1,<1.0.0)
|
13
|
+
Requires-Dist: yta_validation (>=0.0.1,<1.0.0)
|
14
|
+
Requires-Dist: yta_video_opengl (>=0.0.1,<1.0.0)
|
11
15
|
Description-Content-Type: text/markdown
|
12
16
|
|
13
17
|
# Youtube Autonomous Main Editor Nodes GPU module
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[project]
|
2
2
|
name = "yta-editor-nodes-gpu"
|
3
|
-
version = "0.0.
|
3
|
+
version = "0.0.2"
|
4
4
|
description = "Youtube Autonomous Main Editor Nodes GPU module"
|
5
5
|
authors = [
|
6
6
|
{name = "danialcala94",email = "danielalcalavalera@gmail.com"}
|
@@ -8,7 +8,10 @@ authors = [
|
|
8
8
|
readme = "README.md"
|
9
9
|
requires-python = "==3.9"
|
10
10
|
dependencies = [
|
11
|
-
|
11
|
+
"yta_validation (>=0.0.1,<1.0.0)",
|
12
|
+
"yta_programming (>=0.0.1,<1.0.0)",
|
13
|
+
"yta_video_opengl (>=0.0.1,<1.0.0)",
|
14
|
+
"moderngl (>=0.0.1,<9.0.0)",
|
12
15
|
]
|
13
16
|
|
14
17
|
[tool.poetry]
|
@@ -17,6 +20,9 @@ packages = [{include = "yta_editor_nodes_gpu", from = "src"}]
|
|
17
20
|
[tool.poetry.group.dev.dependencies]
|
18
21
|
pytest = "^8.3.5"
|
19
22
|
yta_testing = ">=0.0.1"
|
23
|
+
yta_editor_utils = ">=0.0.1"
|
24
|
+
pillow = ">=0.0.1"
|
25
|
+
# opencv-python = ">=0.0.1"
|
20
26
|
|
21
27
|
[build-system]
|
22
28
|
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
@@ -0,0 +1,162 @@
|
|
1
|
+
"""
|
2
|
+
The nodes that modify inputs by using static parameters
|
3
|
+
and not 't' time moments, with GPU.
|
4
|
+
"""
|
5
|
+
from yta_video_opengl.abstract import _OpenGLBase
|
6
|
+
from yta_validation.parameter import ParameterValidator
|
7
|
+
from typing import Union
|
8
|
+
|
9
|
+
import moderngl
|
10
|
+
|
11
|
+
|
12
|
+
class _NodeProcessorGPU(_OpenGLBase):
|
13
|
+
"""
|
14
|
+
*Singleton class*
|
15
|
+
|
16
|
+
*For internal use only*
|
17
|
+
|
18
|
+
Class to represent a node processor that uses GPU
|
19
|
+
to transform the input.
|
20
|
+
|
21
|
+
The basic class of a node to manipulate frames as
|
22
|
+
opengl textures. This node will process the frame
|
23
|
+
as an input texture and will generate also a
|
24
|
+
texture as the output.
|
25
|
+
|
26
|
+
Nodes can be chained and the result from one node
|
27
|
+
can be applied on another node.
|
28
|
+
"""
|
29
|
+
|
30
|
+
# TODO: Do we really need this class just to add
|
31
|
+
# one step on the hierarchy? It is to differentiate
|
32
|
+
# between any class that uses OpenGL on the base
|
33
|
+
# from the ones that are nodes to process and to
|
34
|
+
# be added to a general NodeProcessor class
|
35
|
+
|
36
|
+
def __init__(
|
37
|
+
self,
|
38
|
+
opengl_context: Union[moderngl.Context, None],
|
39
|
+
output_size: tuple[int, int],
|
40
|
+
**kwargs
|
41
|
+
):
|
42
|
+
"""
|
43
|
+
Provide all the variables you want to be initialized
|
44
|
+
as uniforms at the begining for the global OpenGL
|
45
|
+
animation in the `**kwargs`.
|
46
|
+
|
47
|
+
The `output_size` is the size (width, height) of the
|
48
|
+
texture that will be obtained as result. This size
|
49
|
+
can be modified when processing a specific input, but
|
50
|
+
be consider the cost of resources of modifying the
|
51
|
+
size, that will regenerate the output texture.
|
52
|
+
"""
|
53
|
+
super().__init__(
|
54
|
+
opengl_context = opengl_context,
|
55
|
+
output_size = output_size,
|
56
|
+
**kwargs
|
57
|
+
)
|
58
|
+
|
59
|
+
def __reinit__(
|
60
|
+
self,
|
61
|
+
opengl_context: Union[moderngl.Context, None] = None,
|
62
|
+
output_size: Union[tuple[int, int], None] = None,
|
63
|
+
**kwargs
|
64
|
+
):
|
65
|
+
# TODO: We ignore the 'opengl_context' as we don't need
|
66
|
+
# to reset it
|
67
|
+
super().__reinit__(
|
68
|
+
output_size = output_size,
|
69
|
+
**kwargs
|
70
|
+
)
|
71
|
+
|
72
|
+
class SelectionMaskProcessorGPU(_NodeProcessorGPU):
|
73
|
+
"""
|
74
|
+
Class to use a mask selection (from which we will
|
75
|
+
read the red color to determine if the pixel must
|
76
|
+
be applied or not) to apply the `processed_texture`
|
77
|
+
on the `original_texture`.
|
78
|
+
|
79
|
+
If the selection mask is completely full, the
|
80
|
+
result will be the `processed_texture`.
|
81
|
+
"""
|
82
|
+
|
83
|
+
@property
|
84
|
+
def fragment_shader(
|
85
|
+
self
|
86
|
+
):
|
87
|
+
return (
|
88
|
+
'''
|
89
|
+
#version 330
|
90
|
+
|
91
|
+
uniform sampler2D original_texture;
|
92
|
+
uniform sampler2D processed_texture;
|
93
|
+
// White = apply, black = ignore
|
94
|
+
uniform sampler2D selection_mask_texture;
|
95
|
+
in vec2 v_uv;
|
96
|
+
out vec4 output_color;
|
97
|
+
|
98
|
+
void main() {
|
99
|
+
vec4 original_color = texture(original_texture, v_uv);
|
100
|
+
vec4 processed_color = texture(processed_texture, v_uv);
|
101
|
+
// We use the red as the value
|
102
|
+
float mask = texture(selection_mask_texture, v_uv).r;
|
103
|
+
|
104
|
+
output_color = mix(original_color, processed_color, mask);
|
105
|
+
}
|
106
|
+
'''
|
107
|
+
)
|
108
|
+
|
109
|
+
def _prepare_input_textures(
|
110
|
+
self
|
111
|
+
) -> '_OpenGLBase':
|
112
|
+
"""
|
113
|
+
*For internal use only*
|
114
|
+
|
115
|
+
Set the input texture variables and handlers
|
116
|
+
we need to manage this. This method has to be
|
117
|
+
called only once, just to set the slot for
|
118
|
+
the different textures we will use (and are
|
119
|
+
registered as textures in the shader).
|
120
|
+
"""
|
121
|
+
self.textures.add('original_texture', 0)
|
122
|
+
self.textures.add('processed_texture', 1)
|
123
|
+
self.textures.add('selection_mask_texture', 2)
|
124
|
+
|
125
|
+
return self
|
126
|
+
|
127
|
+
def process(
|
128
|
+
self,
|
129
|
+
original_input: Union[moderngl.Texture, 'np.ndarray'],
|
130
|
+
processed_input: Union[moderngl.Texture, 'np.ndarray'],
|
131
|
+
selection_mask_input: Union[moderngl.Texture, 'np.ndarray'],
|
132
|
+
output_size: Union[tuple[int, int], None] = None,
|
133
|
+
**kwargs
|
134
|
+
) -> moderngl.Texture:
|
135
|
+
"""
|
136
|
+
Mix the `processed_input` with the
|
137
|
+
`original_input` as the `selecction_mask_input`
|
138
|
+
says.
|
139
|
+
|
140
|
+
You can provide any additional parameter
|
141
|
+
in the **kwargs, but be careful because
|
142
|
+
this could overwrite other uniforms that
|
143
|
+
were previously set.
|
144
|
+
|
145
|
+
We use and return textures to maintain
|
146
|
+
the process in GPU and optimize it.
|
147
|
+
"""
|
148
|
+
ParameterValidator.validate_mandatory_instance_of('original_input', original_input, [moderngl.Texture, 'np.ndarray'])
|
149
|
+
ParameterValidator.validate_mandatory_instance_of('processed_input', processed_input, [moderngl.Texture, 'np.ndarray'])
|
150
|
+
ParameterValidator.validate_mandatory_instance_of('selection_mask_input', selection_mask_input, [moderngl.Texture, 'np.ndarray'])
|
151
|
+
|
152
|
+
textures_map = {
|
153
|
+
'original_texture': original_input,
|
154
|
+
'processed_texture': processed_input,
|
155
|
+
'selection_mask_texture': selection_mask_input
|
156
|
+
}
|
157
|
+
|
158
|
+
return self._process_common(
|
159
|
+
textures_map = textures_map,
|
160
|
+
output_size = output_size,
|
161
|
+
**kwargs
|
162
|
+
)
|
@@ -0,0 +1,447 @@
|
|
1
|
+
"""
|
2
|
+
This entire module is working with OpenGL.
|
3
|
+
|
4
|
+
We don't name it 'blend' instead of 'process' because
|
5
|
+
the GPU classes have the 'process' name due to the
|
6
|
+
inheritance from OpenGL classes, and we want to avoid
|
7
|
+
misunderstandings.
|
8
|
+
|
9
|
+
(!) IMPORTANT:
|
10
|
+
In all our blenders we need to include a platform
|
11
|
+
called `mix_weight` to determine the percentage of
|
12
|
+
effect we want to apply to the output result:
|
13
|
+
- `uniform float mix_weight; // 0.0 → only base, 1.0 → only overlay`
|
14
|
+
- `output_color = mix(base_color, overlay_color, mix_weight);`
|
15
|
+
"""
|
16
|
+
from yta_video_opengl.abstract import _OpenGLBase
|
17
|
+
from yta_editor_nodes_gpu.processor.blender.utils import _validate_inputs_and_mix_weights
|
18
|
+
from yta_video_opengl.utils import frame_to_texture
|
19
|
+
from yta_validation.parameter import ParameterValidator
|
20
|
+
from yta_validation import PythonValidator
|
21
|
+
from typing import Union
|
22
|
+
|
23
|
+
import numpy as np
|
24
|
+
import moderngl
|
25
|
+
|
26
|
+
|
27
|
+
# TODO: I think this one should inherit from
|
28
|
+
# '_NodeProcessorGPU'
|
29
|
+
class _BlenderGPU(_OpenGLBase):
|
30
|
+
"""
|
31
|
+
*Singleton class*
|
32
|
+
|
33
|
+
*For internal use only*
|
34
|
+
|
35
|
+
*This class has to be inherited*
|
36
|
+
|
37
|
+
Class to represent a blender that uses GPU to
|
38
|
+
blend the inputs. Base class for an OpenGL
|
39
|
+
blender, that will be inherited by the specific
|
40
|
+
implementations.
|
41
|
+
|
42
|
+
The basic class of a blender to blend frames as
|
43
|
+
opengl textures. This blender will process the
|
44
|
+
inputs as textures and will generate also a
|
45
|
+
texture as the output.
|
46
|
+
|
47
|
+
Blender results can be chained and the result
|
48
|
+
from one node can be applied on another node.
|
49
|
+
|
50
|
+
This blender is able to work acting as an
|
51
|
+
opacity blender, but will be used as the base
|
52
|
+
of the classes that we will implement actually.
|
53
|
+
|
54
|
+
The `opacity` value must be set as an uniform
|
55
|
+
each time we are processing the 2 textures.
|
56
|
+
"""
|
57
|
+
|
58
|
+
@property
|
59
|
+
def fragment_shader(
|
60
|
+
self
|
61
|
+
) -> str:
|
62
|
+
"""
|
63
|
+
The code of the fragment shader.
|
64
|
+
"""
|
65
|
+
return (
|
66
|
+
'''
|
67
|
+
#version 330
|
68
|
+
uniform sampler2D base_texture;
|
69
|
+
uniform sampler2D overlay_texture;
|
70
|
+
uniform float mix_weight; // 0.0 → only base, 1.0 → only overlay
|
71
|
+
in vec2 v_uv;
|
72
|
+
out vec4 output_color;
|
73
|
+
|
74
|
+
void main() {
|
75
|
+
vec4 base_color = texture(base_texture, v_uv);
|
76
|
+
vec4 overlay_color = texture(overlay_texture, v_uv);
|
77
|
+
|
78
|
+
// Apply global mix intensity
|
79
|
+
output_color = mix(base_color, overlay_color, mix_weight);
|
80
|
+
}
|
81
|
+
'''
|
82
|
+
)
|
83
|
+
|
84
|
+
def __init__(
|
85
|
+
self,
|
86
|
+
opengl_context: Union[moderngl.Context, None],
|
87
|
+
output_size: tuple[int, int],
|
88
|
+
mix_weight: float = 1.0,
|
89
|
+
**kwargs
|
90
|
+
):
|
91
|
+
ParameterValidator.validate_mandatory_number_between('mix_weight', mix_weight, 0.0, 1.0)
|
92
|
+
|
93
|
+
super().__init__(
|
94
|
+
opengl_context = opengl_context,
|
95
|
+
output_size = output_size,
|
96
|
+
mix_weight = mix_weight,
|
97
|
+
**kwargs
|
98
|
+
)
|
99
|
+
|
100
|
+
# TODO: Overwrite this method
|
101
|
+
def _prepare_input_textures(
|
102
|
+
self
|
103
|
+
) -> '_BlenderGPU':
|
104
|
+
"""
|
105
|
+
*For internal use only*
|
106
|
+
|
107
|
+
Set the input texture variables and handlers
|
108
|
+
we need to manage this. This method has to be
|
109
|
+
called only once, just to set the slot for
|
110
|
+
the different textures we will use (and are
|
111
|
+
registered as textures in the shader).
|
112
|
+
"""
|
113
|
+
self.textures.add('base_texture', 0)
|
114
|
+
self.textures.add('overlay_texture', 1)
|
115
|
+
|
116
|
+
return self
|
117
|
+
|
118
|
+
# TODO: Overwrite this method
|
119
|
+
def process(
|
120
|
+
self,
|
121
|
+
base_input: Union[moderngl.Texture, np.ndarray],
|
122
|
+
overlay_input: Union[moderngl.Texture, np.ndarray],
|
123
|
+
output_size: Union[tuple[int, int], None] = None,
|
124
|
+
mix_weight: float = 1.0,
|
125
|
+
**kwargs
|
126
|
+
) -> moderngl.Texture:
|
127
|
+
"""
|
128
|
+
Validate the parameters, set the textures map, process
|
129
|
+
the mix by applying the blender logic, and return the
|
130
|
+
result but only applied as much as the `mix_weight`
|
131
|
+
parameter is indicating.
|
132
|
+
|
133
|
+
Apply the shader to the given 'base_input'
|
134
|
+
and 'overlay_input', that must be frames or
|
135
|
+
textures, and return the new resulting
|
136
|
+
texture.
|
137
|
+
|
138
|
+
You can provide any additional parameter
|
139
|
+
in the **kwargs, but be careful because
|
140
|
+
this could overwrite other uniforms that
|
141
|
+
were previously set.
|
142
|
+
|
143
|
+
We use and return textures to maintain
|
144
|
+
the process in GPU and optimize it.
|
145
|
+
"""
|
146
|
+
ParameterValidator.validate_mandatory_instance_of('base_input', base_input, [moderngl.Texture, np.ndarray])
|
147
|
+
ParameterValidator.validate_mandatory_instance_of('overlay_input', overlay_input, [moderngl.Texture, np.ndarray])
|
148
|
+
ParameterValidator.validate_mandatory_number_between('mix_weight', mix_weight, 0.0, 1.0)
|
149
|
+
|
150
|
+
if mix_weight == 0.0:
|
151
|
+
# If the mix_weight is 0.0 we don't want to affect
|
152
|
+
# the base so we don't even need to process the
|
153
|
+
# mix but return the original base input as a
|
154
|
+
# Texture
|
155
|
+
return (
|
156
|
+
base_input
|
157
|
+
if PythonValidator.is_instance_of(base_input, moderngl.Texture) else
|
158
|
+
frame_to_texture(base_input, self.context)
|
159
|
+
)
|
160
|
+
|
161
|
+
textures_map = {
|
162
|
+
'base_texture': base_input,
|
163
|
+
'overlay_texture': overlay_input
|
164
|
+
}
|
165
|
+
|
166
|
+
return self._process_common(
|
167
|
+
textures_map = textures_map,
|
168
|
+
output_size = output_size,
|
169
|
+
mix_weight = mix_weight,
|
170
|
+
**kwargs
|
171
|
+
)
|
172
|
+
|
173
|
+
# TODO: This method can change according to the way
|
174
|
+
# we need to process the inputs, that could be
|
175
|
+
# different than by pairs
|
176
|
+
def process_multiple_inputs(
|
177
|
+
self,
|
178
|
+
inputs: list[Union[np.ndarray, moderngl.Texture]],
|
179
|
+
mix_weights: Union[list[float], float] = 1.0,
|
180
|
+
output_size: Union[tuple[int, int], None] = None,
|
181
|
+
) -> moderngl.Texture:
|
182
|
+
"""
|
183
|
+
Blend all the `inputs` provided, one after another,
|
184
|
+
applying the `mix_weights` provided, and forcing the
|
185
|
+
result to the `dtype` if provided.
|
186
|
+
|
187
|
+
The `mix_weights` can be a single float value, that
|
188
|
+
will be used for all the mixings, or a list of as
|
189
|
+
many float values as `inputs` received, to be
|
190
|
+
applied individually to each mixing.
|
191
|
+
"""
|
192
|
+
_validate_inputs_and_mix_weights(
|
193
|
+
inputs = inputs,
|
194
|
+
mix_weights = mix_weights
|
195
|
+
)
|
196
|
+
|
197
|
+
# We process all the 'inputs' as 'base' and 'overlay'
|
198
|
+
# and accumulate the result
|
199
|
+
|
200
|
+
# Use the first one as the base
|
201
|
+
output = inputs[0]
|
202
|
+
|
203
|
+
# TODO: How do we handle the additional parameters that
|
204
|
+
# could be an array? Maybe if it is an array, check that
|
205
|
+
# the number of elements is the same as the number of
|
206
|
+
# inputs, and if a single value just use it...
|
207
|
+
|
208
|
+
for i in range(1, len(inputs)):
|
209
|
+
overlay_input = inputs[i]
|
210
|
+
mix_weight = mix_weights[i]
|
211
|
+
|
212
|
+
output = self.process(
|
213
|
+
base_input = output,
|
214
|
+
overlay_input = overlay_input,
|
215
|
+
output_size = output_size,
|
216
|
+
mix_weight = mix_weight,
|
217
|
+
)
|
218
|
+
|
219
|
+
return output
|
220
|
+
|
221
|
+
# Specific implementations below
|
222
|
+
class MixBlenderGPU(_BlenderGPU):
|
223
|
+
"""
|
224
|
+
Blender to blend 2 textures by using a mix float
|
225
|
+
parameter that determines the amount of the result
|
226
|
+
we want to mix with the input.
|
227
|
+
"""
|
228
|
+
pass
|
229
|
+
|
230
|
+
class AlphaBlenderGPU(_BlenderGPU):
|
231
|
+
"""
|
232
|
+
Blender to blend 2 textures by using the most common
|
233
|
+
blending method, which is the alpha.
|
234
|
+
|
235
|
+
This blender will use the alpha channel of the
|
236
|
+
overlay input, multiplied by the `blend_strength`
|
237
|
+
parameter provided, to use it as the mixer factor
|
238
|
+
between the base and the overlay inputs.
|
239
|
+
"""
|
240
|
+
|
241
|
+
@property
|
242
|
+
def fragment_shader(
|
243
|
+
self
|
244
|
+
) -> str:
|
245
|
+
"""
|
246
|
+
The code of the fragment shader.
|
247
|
+
"""
|
248
|
+
# TODO: What if no 'overlay_color.a' (?)
|
249
|
+
return (
|
250
|
+
'''
|
251
|
+
#version 330
|
252
|
+
uniform sampler2D base_texture;
|
253
|
+
uniform sampler2D overlay_texture;
|
254
|
+
uniform float mix_weight; // 0.0 → only base, 1.0 → only overlay
|
255
|
+
uniform float blend_strength;
|
256
|
+
in vec2 v_uv;
|
257
|
+
out vec4 output_color;
|
258
|
+
|
259
|
+
void main() {
|
260
|
+
vec4 base_color = texture(base_texture, v_uv);
|
261
|
+
vec4 overlay_color = texture(overlay_texture, v_uv);
|
262
|
+
|
263
|
+
// Use alpha channel of overlay if available
|
264
|
+
float alpha = overlay_color.a * blend_strength;
|
265
|
+
|
266
|
+
// Classic blending
|
267
|
+
output_color = mix(base_color, overlay_color, alpha);
|
268
|
+
|
269
|
+
// Apply global mix intensity
|
270
|
+
output_color = mix(base_color, output_color, mix_weight);
|
271
|
+
}
|
272
|
+
'''
|
273
|
+
)
|
274
|
+
|
275
|
+
def __init__(
|
276
|
+
self,
|
277
|
+
opengl_context: Union[moderngl.Context, None],
|
278
|
+
output_size: tuple[int, int]
|
279
|
+
):
|
280
|
+
super().__init__(
|
281
|
+
opengl_context = opengl_context,
|
282
|
+
output_size = output_size
|
283
|
+
)
|
284
|
+
|
285
|
+
def process(
|
286
|
+
self,
|
287
|
+
base_input: Union[moderngl.Texture, np.ndarray],
|
288
|
+
overlay_input: Union[moderngl.Texture, np.ndarray],
|
289
|
+
output_size: Union[tuple[int, int], None] = None,
|
290
|
+
mix_weight: float = 1.0,
|
291
|
+
blend_strength: float = 0.5
|
292
|
+
) -> moderngl.Texture:
|
293
|
+
"""
|
294
|
+
Validate the parameters, set the textures map, process
|
295
|
+
the mix by applying the blender logic, and return the
|
296
|
+
result but only applied as much as the `mix_weight`
|
297
|
+
parameter is indicating.
|
298
|
+
|
299
|
+
Apply the shader to the given 'base_input'
|
300
|
+
and 'overlay_input', that must be frames or
|
301
|
+
textures, and return the new resulting
|
302
|
+
texture.
|
303
|
+
|
304
|
+
You can provide any additional parameter
|
305
|
+
in the **kwargs, but be careful because
|
306
|
+
this could overwrite other uniforms that
|
307
|
+
were previously set.
|
308
|
+
|
309
|
+
We use and return textures to maintain
|
310
|
+
the process in GPU and optimize it.
|
311
|
+
"""
|
312
|
+
ParameterValidator.validate_mandatory_instance_of('base_input', base_input, [moderngl.Texture, np.ndarray])
|
313
|
+
ParameterValidator.validate_mandatory_instance_of('overlay_input', overlay_input, [moderngl.Texture, np.ndarray])
|
314
|
+
ParameterValidator.validate_mandatory_number_between('mix_weight', mix_weight, 0.0, 1.0)
|
315
|
+
ParameterValidator.validate_mandatory_number_between('blend_strength', blend_strength, 0.0, 1.0)
|
316
|
+
|
317
|
+
if mix_weight == 0.0:
|
318
|
+
# If the mix_weight is 0.0 we don't want to affect
|
319
|
+
# the base so we don't even need to process the
|
320
|
+
# mix but return the original base input as a
|
321
|
+
# Texture
|
322
|
+
return (
|
323
|
+
base_input
|
324
|
+
if PythonValidator.is_instance_of(base_input, moderngl.Texture) else
|
325
|
+
frame_to_texture(base_input, self.context)
|
326
|
+
)
|
327
|
+
|
328
|
+
textures_map = {
|
329
|
+
'base_texture': base_input,
|
330
|
+
'overlay_texture': overlay_input
|
331
|
+
}
|
332
|
+
|
333
|
+
return self._process_common(
|
334
|
+
textures_map = textures_map,
|
335
|
+
output_size = output_size,
|
336
|
+
mix_weight = mix_weight,
|
337
|
+
blend_strength = blend_strength
|
338
|
+
)
|
339
|
+
|
340
|
+
class AddBlenderGPU(_BlenderGPU):
|
341
|
+
"""
|
342
|
+
Blender to blend 2 textures by adding their values.
|
343
|
+
|
344
|
+
This blender will increase the brightness by
|
345
|
+
combining the colors of the base and the overlay
|
346
|
+
inputs, using the overlay as much as the
|
347
|
+
`stregth` parameter is indicating.
|
348
|
+
"""
|
349
|
+
|
350
|
+
@property
|
351
|
+
def fragment_shader(
|
352
|
+
self
|
353
|
+
) -> str:
|
354
|
+
"""
|
355
|
+
The code of the fragment shader.
|
356
|
+
"""
|
357
|
+
return (
|
358
|
+
'''
|
359
|
+
#version 330
|
360
|
+
uniform sampler2D base_texture;
|
361
|
+
uniform sampler2D overlay_texture;
|
362
|
+
uniform float mix_weight; // 0.0 → only base, 1.0 → only overlay
|
363
|
+
uniform float strength; // 1.0 = full additive, 0.5 = subtle
|
364
|
+
in vec2 v_uv;
|
365
|
+
out vec4 output_color;
|
366
|
+
|
367
|
+
void main() {
|
368
|
+
vec4 base_color = texture(base_texture, v_uv);
|
369
|
+
vec4 overlay_color = texture(overlay_texture, v_uv);
|
370
|
+
|
371
|
+
vec4 result = base_color + overlay_color * strength;
|
372
|
+
|
373
|
+
// Clamp to avoid overflow (values > 1.0)
|
374
|
+
output_color = clamp(result, 0.0, 1.0);
|
375
|
+
|
376
|
+
// Apply global mix intensity
|
377
|
+
output_color = mix(base_color, output_color, mix_weight);
|
378
|
+
}
|
379
|
+
'''
|
380
|
+
)
|
381
|
+
|
382
|
+
def __init__(
|
383
|
+
self,
|
384
|
+
opengl_context: Union[moderngl.Context, None],
|
385
|
+
output_size: tuple[int, int],
|
386
|
+
):
|
387
|
+
super().__init__(
|
388
|
+
opengl_context = opengl_context,
|
389
|
+
output_size = output_size,
|
390
|
+
)
|
391
|
+
|
392
|
+
def process(
|
393
|
+
self,
|
394
|
+
base_input: Union[moderngl.Texture, np.ndarray],
|
395
|
+
overlay_input: Union[moderngl.Texture, np.ndarray],
|
396
|
+
output_size: Union[tuple[int, int], None] = None,
|
397
|
+
mix_weight: float = 1.0,
|
398
|
+
strength: float = 0.5
|
399
|
+
) -> moderngl.Texture:
|
400
|
+
"""
|
401
|
+
Validate the parameters, set the textures map, process
|
402
|
+
the mix by applying the blender logic, and return the
|
403
|
+
result but only applied as much as the `mix_weight`
|
404
|
+
parameter is indicating.
|
405
|
+
|
406
|
+
Apply the shader to the given 'base_input'
|
407
|
+
and 'overlay_input', that must be frames or
|
408
|
+
textures, and return the new resulting
|
409
|
+
texture.
|
410
|
+
|
411
|
+
You can provide any additional parameter
|
412
|
+
in the **kwargs, but be careful because
|
413
|
+
this could overwrite other uniforms that
|
414
|
+
were previously set.
|
415
|
+
|
416
|
+
We use and return textures to maintain
|
417
|
+
the process in GPU and optimize it.
|
418
|
+
"""
|
419
|
+
ParameterValidator.validate_mandatory_instance_of('base_input', base_input, [moderngl.Texture, np.ndarray])
|
420
|
+
ParameterValidator.validate_mandatory_instance_of('overlay_input', overlay_input, [moderngl.Texture, np.ndarray])
|
421
|
+
ParameterValidator.validate_mandatory_number_between('mix_weight', mix_weight, 0.0, 1.0)
|
422
|
+
ParameterValidator.validate_mandatory_number_between('strength', strength, 0.0, 1.0)
|
423
|
+
|
424
|
+
if mix_weight == 0.0:
|
425
|
+
# If the mix_weight is 0.0 we don't want to affect
|
426
|
+
# the base so we don't even need to process the
|
427
|
+
# mix but return the original base input as a
|
428
|
+
# Texture
|
429
|
+
return (
|
430
|
+
base_input
|
431
|
+
if PythonValidator.is_instance_of(base_input, moderngl.Texture) else
|
432
|
+
frame_to_texture(base_input, self.context)
|
433
|
+
)
|
434
|
+
|
435
|
+
textures_map = {
|
436
|
+
'base_texture': base_input,
|
437
|
+
'overlay_texture': overlay_input
|
438
|
+
}
|
439
|
+
|
440
|
+
return self._process_common(
|
441
|
+
textures_map = textures_map,
|
442
|
+
output_size = output_size,
|
443
|
+
mix_weight = mix_weight,
|
444
|
+
strength = strength
|
445
|
+
)
|
446
|
+
|
447
|
+
# TODO: Create more
|
@@ -0,0 +1,44 @@
|
|
1
|
+
"""
|
2
|
+
TODO: This module is duplicated as it is repeated
|
3
|
+
in the 'yta-editor-nodes-gpu' library. It should
|
4
|
+
be in a 'yta-editor-nodes-common' library.
|
5
|
+
"""
|
6
|
+
from yta_validation.parameter import ParameterValidator
|
7
|
+
from yta_validation.number import NumberValidator
|
8
|
+
from yta_validation import PythonValidator
|
9
|
+
from typing import Union
|
10
|
+
|
11
|
+
import numpy as np
|
12
|
+
|
13
|
+
|
14
|
+
def _validate_inputs_and_mix_weights(
|
15
|
+
inputs: list[Union[np.ndarray, 'moderngl.Texture']],
|
16
|
+
mix_weights: Union[list[float], float] = 1.0,
|
17
|
+
) -> None:
|
18
|
+
"""
|
19
|
+
*For internal use only*
|
20
|
+
|
21
|
+
Validate the provided 'inputs' and 'mix_weights' raising
|
22
|
+
an exception if something wrong was found.
|
23
|
+
"""
|
24
|
+
ParameterValidator.validate_mandatory_list_of_these_instances('inputs', inputs, [np.ndarray, 'moderngl.Texture'])
|
25
|
+
|
26
|
+
if len(inputs) < 2:
|
27
|
+
raise Exception('The amount of "inputs" provided is less than 2.')
|
28
|
+
|
29
|
+
if not all(input.size == inputs[0].size for input in inputs):
|
30
|
+
# TODO: Should we raise this exception (?)
|
31
|
+
raise Exception('All the inputs do not have the same size.')
|
32
|
+
|
33
|
+
# Validate 'mix_weights' as a single or list of floats
|
34
|
+
# in the [0.0, 1.0] range
|
35
|
+
if PythonValidator.is_list_of_float(mix_weights):
|
36
|
+
if len(mix_weights) != (len(inputs) - 1):
|
37
|
+
raise Exception('The number of "mix_weights" values provided is not the same as "inputs".')
|
38
|
+
|
39
|
+
for mix_weight in mix_weights:
|
40
|
+
ParameterValidator.validate_mandatory_number_between('mix_weight', mix_weight, 0.0, 1.0)
|
41
|
+
elif NumberValidator.is_number_between(mix_weights, 0.0, 1.0):
|
42
|
+
mix_weights = [mix_weights] * len(inputs)
|
43
|
+
else:
|
44
|
+
raise Exception('The "mix_weights" parameter provided is not valid.')
|