Skip to content

File: ShaderFlow/Module.py

ShaderFlow.Module

ShaderModule

Bases: BrokenFluent, BrokenAttrs

Source code in Projects/ShaderFlow/ShaderFlow/Module.py
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
@define
class ShaderModule(BrokenFluent, BrokenAttrs):

    scene: ShaderScene = field(default=None, repr=False)
    """The ShaderScene this module belongs to. Must be set on initialization of any module with
    `ShaderModule(scene=...)` (even though it's `default=None` for MRO reasons)"""

    uuid: int = Factory(itertools.count(1).__next__)
    """A module identifier, mostly used for differentiating log statements of same type modules"""

    name: str = None
    """The base name for exported GLSL variables, textures, etc. It is technically optional, but
    it's not a bad idea for all modules to have a default value for this attribute than None"""

    def __post__(self):

        # Post-import to avoid circular reference for type checking
        from ShaderFlow.Scene import ShaderScene

        # The first module initialized is the Scene itself
        self.scene = smartproxy(self.scene or self)

        # Module must be part of a 'scene=instance(ShaderScene)'
        if not isinstance(self.scene, ShaderScene):
            raise RuntimeError(log.error('\n'.join((
                f"Module of type '{type(self).__name__}' must be added to a 'ShaderScene' instance",
                f"• Initialize it with {type(self).__name__}(scene='instance(ShaderScene)', ...)",
            ))))

        self.log_debug("Module added to the Scene")
        self.scene.modules.append(self)
        self.commands()

        if not isinstance(self, ShaderScene):
            self.build()

    def __del__(self) -> None:
        self.destroy()

    @abstractmethod
    def destroy(self) -> None:
        """Similar to __del__, potentially intentional, but automatic when the Scene is gc'd"""
        pass

    @abstractmethod
    def build(self) -> None:
        """Create Textures, child ShaderModules, load base shaders, etc. Happens only once, and it's
        a good place to set default values for attributes, such as a background image that can be
        later changed on `self.setup()` or, better yet, on the CLI of the module/custom Scene"""
        pass

    @abstractmethod
    def setup(self) -> None:
        """Called every time before the initialization (rendering) of the Scene. Useful for managing
        the behavior of batch exporting per export index; also a good place to reset values to their
        defaults or create procedural objects (seeds) after `self.build()`"""
        pass

    @abstractmethod
    def update(self) -> None:
        """Called every frame. This defines the main behavior of the module inside the event loop.
        All non-ShaderObjects are called first, then regular Modules. Access state data directly
        on the Scene with `self.scene.{dt,time,width,height,...}`"""
        pass

    @abstractmethod
    def pipeline(self) -> Iterable[ShaderVariable]:
        """Returns the list of variables that will be exported to all Shaders of the scene. The
        first compilation happens after `self.build()`, where all variables are metaprogrammed into
        the GLSL code. Subsequent calls happens after all `self.update()` on every frame and the
        variables are updated to the yielded values here"""
        return []

    def full_pipeline(self) -> Iterable[ShaderVariable]:
        """Yield all pipelines from all modules in the scene"""
        for module in self.scene.modules:
            yield from module.pipeline()

    def relay(self, message: Union[ShaderMessage, type[ShaderMessage]]) -> Self:
        """Send a message to all modules in the scene. Handle it defining a `self.handle(message)`"""
        if isinstance(message, type):
            message = message()
        for module in self.scene.modules:
            module.handle(message)
        return self

    @abstractmethod
    def handle(self, message: ShaderMessage) -> None:
        """Whenever a module relays a message on the scene, all modules are signaled via this method
        for potentially acting on it. A Camera might move on WASD keys, for example"""
        ...

    def find(self, type: type[ShaderModule]) -> Iterable[ShaderModule]:
        """Find all modules of a certain type in the scene. Note that this function is a generator,
        so it must be consumed on a loop or a `list(self.find(...))`"""
        for module in self.scene.modules:
            if isinstance(module, type):
                yield module

    @property
    @abstractmethod
    def duration(self) -> float:
        """Self-reported 'time for completion'. A ShaderAudio shall return the input audio duration,
        for example. The scene will determine or override the final duration"""
        return 0.0

    @abstractmethod
    def ffhook(self, ffmpeg: BrokenFFmpeg) -> None:
        """When exporting the Scene, after the initial CLI configuration of FFmpeg by the Scene's
        `self.main` method, all modules have an option to change the FFmpeg settings on the fly.
        Note that this can also be implemented on a custom Scene itself, and behavior _can_ be
        changed per batch exporting"""
        pass

    @abstractmethod
    def commands(self) -> None:
        """Add commands to the scene with `self.scene.cli.command(...)`"""
        ...

    # -------------------------------------------|
    # Logging

    @property
    def panel_module_type(self) -> str:
        """Suggested typer panel for identifying this module type"""
        return f"→ (Module) {type(self).__name__}"

    @property
    def panel_module_self(self) -> str:
        """Suggested typer panel for identifying this unique module"""
        return f"{self.panel_module_type}: {self.name or 'Unnamed'}"

    @property
    def who(self) -> str:
        return f"[bold dim](Module {self.uuid:>2}{type(self).__name__[:12].ljust(12)})[/]"

    def log_info(self, *args, **kwargs) -> str:
        return log.info(self.who, *args, **kwargs)

    def log_success(self, *args, **kwargs) -> str:
        return log.success(self.who, *args, **kwargs)

    def log_warning(self, *args, **kwargs) -> str:
        return log.warning(self.who, *args, **kwargs)

    def log_error(self, *args, **kwargs) -> str:
        return log.error(self.who, *args, **kwargs)

    def log_debug(self, *args, **kwargs) -> str:
        return log.debug(self.who, *args, **kwargs)

    def log_minor(self, *args, **kwargs) -> str:
        return log.minor(self.who, *args, **kwargs)

    # -------------------------------------------|
    # Stuff pending a remaster

    @abstractmethod
    def includes(self) -> Iterable[dict[str, str]]:
        yield ""

    @abstractmethod
    def defines(self) -> Iterable[str]:
        yield None

    # # User interface

    def __shaderflow_ui__(self) -> None:
        """Basic info of a Module"""
        # Todo: Make automatic Imgui methods

        # Module - self.__ui__ must be implemented
        if not getattr(self.__ui__, "__isabstractmethod__", False):
            self.__ui__()

        # Module - self.ui must be implemented
        if not getattr(self.ui, "__isabstractmethod__", False):
            self.ui()

    @abstractmethod
    def __ui__(self) -> None:
        """Internal method for self.ui"""
        pass

    @abstractmethod
    def ui(self) -> None:
        """
        Draw the UI for this module
        """
        pass

scene

scene: ShaderScene = field(default=None, repr=False)

The ShaderScene this module belongs to. Must be set on initialization of any module with ShaderModule(scene=...) (even though it's default=None for MRO reasons)

uuid

uuid: int = Factory(itertools.count(1).__next__)

A module identifier, mostly used for differentiating log statements of same type modules

name

name: str = None

The base name for exported GLSL variables, textures, etc. It is technically optional, but it's not a bad idea for all modules to have a default value for this attribute than None

__post__

__post__()
Source code in Projects/ShaderFlow/ShaderFlow/Module.py
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
def __post__(self):

    # Post-import to avoid circular reference for type checking
    from ShaderFlow.Scene import ShaderScene

    # The first module initialized is the Scene itself
    self.scene = smartproxy(self.scene or self)

    # Module must be part of a 'scene=instance(ShaderScene)'
    if not isinstance(self.scene, ShaderScene):
        raise RuntimeError(log.error('\n'.join((
            f"Module of type '{type(self).__name__}' must be added to a 'ShaderScene' instance",
            f"• Initialize it with {type(self).__name__}(scene='instance(ShaderScene)', ...)",
        ))))

    self.log_debug("Module added to the Scene")
    self.scene.modules.append(self)
    self.commands()

    if not isinstance(self, ShaderScene):
        self.build()

__del__

__del__() -> None
Source code in Projects/ShaderFlow/ShaderFlow/Module.py
54
55
def __del__(self) -> None:
    self.destroy()

destroy

destroy() -> None

Similar to del, potentially intentional, but automatic when the Scene is gc'd

Source code in Projects/ShaderFlow/ShaderFlow/Module.py
57
58
59
60
@abstractmethod
def destroy(self) -> None:
    """Similar to __del__, potentially intentional, but automatic when the Scene is gc'd"""
    pass

build

build() -> None

Create Textures, child ShaderModules, load base shaders, etc. Happens only once, and it's a good place to set default values for attributes, such as a background image that can be later changed on self.setup() or, better yet, on the CLI of the module/custom Scene

Source code in Projects/ShaderFlow/ShaderFlow/Module.py
62
63
64
65
66
67
@abstractmethod
def build(self) -> None:
    """Create Textures, child ShaderModules, load base shaders, etc. Happens only once, and it's
    a good place to set default values for attributes, such as a background image that can be
    later changed on `self.setup()` or, better yet, on the CLI of the module/custom Scene"""
    pass

setup

setup() -> None

Called every time before the initialization (rendering) of the Scene. Useful for managing the behavior of batch exporting per export index; also a good place to reset values to their defaults or create procedural objects (seeds) after self.build()

Source code in Projects/ShaderFlow/ShaderFlow/Module.py
69
70
71
72
73
74
@abstractmethod
def setup(self) -> None:
    """Called every time before the initialization (rendering) of the Scene. Useful for managing
    the behavior of batch exporting per export index; also a good place to reset values to their
    defaults or create procedural objects (seeds) after `self.build()`"""
    pass

update

update() -> None

Called every frame. This defines the main behavior of the module inside the event loop. All non-ShaderObjects are called first, then regular Modules. Access state data directly on the Scene with self.scene.{dt,time,width,height,...}

Source code in Projects/ShaderFlow/ShaderFlow/Module.py
76
77
78
79
80
81
@abstractmethod
def update(self) -> None:
    """Called every frame. This defines the main behavior of the module inside the event loop.
    All non-ShaderObjects are called first, then regular Modules. Access state data directly
    on the Scene with `self.scene.{dt,time,width,height,...}`"""
    pass

pipeline

pipeline() -> Iterable[ShaderVariable]

Returns the list of variables that will be exported to all Shaders of the scene. The first compilation happens after self.build(), where all variables are metaprogrammed into the GLSL code. Subsequent calls happens after all self.update() on every frame and the variables are updated to the yielded values here

Source code in Projects/ShaderFlow/ShaderFlow/Module.py
83
84
85
86
87
88
89
@abstractmethod
def pipeline(self) -> Iterable[ShaderVariable]:
    """Returns the list of variables that will be exported to all Shaders of the scene. The
    first compilation happens after `self.build()`, where all variables are metaprogrammed into
    the GLSL code. Subsequent calls happens after all `self.update()` on every frame and the
    variables are updated to the yielded values here"""
    return []

full_pipeline

full_pipeline() -> Iterable[ShaderVariable]

Yield all pipelines from all modules in the scene

Source code in Projects/ShaderFlow/ShaderFlow/Module.py
91
92
93
94
def full_pipeline(self) -> Iterable[ShaderVariable]:
    """Yield all pipelines from all modules in the scene"""
    for module in self.scene.modules:
        yield from module.pipeline()

relay

relay(
    message: Union[ShaderMessage, type[ShaderMessage]],
) -> Self

Send a message to all modules in the scene. Handle it defining a self.handle(message)

Source code in Projects/ShaderFlow/ShaderFlow/Module.py
 96
 97
 98
 99
100
101
102
def relay(self, message: Union[ShaderMessage, type[ShaderMessage]]) -> Self:
    """Send a message to all modules in the scene. Handle it defining a `self.handle(message)`"""
    if isinstance(message, type):
        message = message()
    for module in self.scene.modules:
        module.handle(message)
    return self

handle

handle(message: ShaderMessage) -> None

Whenever a module relays a message on the scene, all modules are signaled via this method for potentially acting on it. A Camera might move on WASD keys, for example

Source code in Projects/ShaderFlow/ShaderFlow/Module.py
104
105
106
107
108
@abstractmethod
def handle(self, message: ShaderMessage) -> None:
    """Whenever a module relays a message on the scene, all modules are signaled via this method
    for potentially acting on it. A Camera might move on WASD keys, for example"""
    ...

find

find(type: type[ShaderModule]) -> Iterable[ShaderModule]

Find all modules of a certain type in the scene. Note that this function is a generator, so it must be consumed on a loop or a list(self.find(...))

Source code in Projects/ShaderFlow/ShaderFlow/Module.py
110
111
112
113
114
115
def find(self, type: type[ShaderModule]) -> Iterable[ShaderModule]:
    """Find all modules of a certain type in the scene. Note that this function is a generator,
    so it must be consumed on a loop or a `list(self.find(...))`"""
    for module in self.scene.modules:
        if isinstance(module, type):
            yield module

duration

duration: float

Self-reported 'time for completion'. A ShaderAudio shall return the input audio duration, for example. The scene will determine or override the final duration

ffhook

ffhook(ffmpeg: BrokenFFmpeg) -> None

When exporting the Scene, after the initial CLI configuration of FFmpeg by the Scene's self.main method, all modules have an option to change the FFmpeg settings on the fly. Note that this can also be implemented on a custom Scene itself, and behavior can be changed per batch exporting

Source code in Projects/ShaderFlow/ShaderFlow/Module.py
124
125
126
127
128
129
130
@abstractmethod
def ffhook(self, ffmpeg: BrokenFFmpeg) -> None:
    """When exporting the Scene, after the initial CLI configuration of FFmpeg by the Scene's
    `self.main` method, all modules have an option to change the FFmpeg settings on the fly.
    Note that this can also be implemented on a custom Scene itself, and behavior _can_ be
    changed per batch exporting"""
    pass

commands

commands() -> None

Add commands to the scene with self.scene.cli.command(...)

Source code in Projects/ShaderFlow/ShaderFlow/Module.py
132
133
134
135
@abstractmethod
def commands(self) -> None:
    """Add commands to the scene with `self.scene.cli.command(...)`"""
    ...

panel_module_type

panel_module_type: str

Suggested typer panel for identifying this module type

panel_module_self

panel_module_self: str

Suggested typer panel for identifying this unique module

who

who: str

log_info

log_info(*args, **kwargs) -> str
Source code in Projects/ShaderFlow/ShaderFlow/Module.py
154
155
def log_info(self, *args, **kwargs) -> str:
    return log.info(self.who, *args, **kwargs)

log_success

log_success(*args, **kwargs) -> str
Source code in Projects/ShaderFlow/ShaderFlow/Module.py
157
158
def log_success(self, *args, **kwargs) -> str:
    return log.success(self.who, *args, **kwargs)

log_warning

log_warning(*args, **kwargs) -> str
Source code in Projects/ShaderFlow/ShaderFlow/Module.py
160
161
def log_warning(self, *args, **kwargs) -> str:
    return log.warning(self.who, *args, **kwargs)

log_error

log_error(*args, **kwargs) -> str
Source code in Projects/ShaderFlow/ShaderFlow/Module.py
163
164
def log_error(self, *args, **kwargs) -> str:
    return log.error(self.who, *args, **kwargs)

log_debug

log_debug(*args, **kwargs) -> str
Source code in Projects/ShaderFlow/ShaderFlow/Module.py
166
167
def log_debug(self, *args, **kwargs) -> str:
    return log.debug(self.who, *args, **kwargs)

log_minor

log_minor(*args, **kwargs) -> str
Source code in Projects/ShaderFlow/ShaderFlow/Module.py
169
170
def log_minor(self, *args, **kwargs) -> str:
    return log.minor(self.who, *args, **kwargs)

includes

includes() -> Iterable[dict[str, str]]
Source code in Projects/ShaderFlow/ShaderFlow/Module.py
175
176
177
@abstractmethod
def includes(self) -> Iterable[dict[str, str]]:
    yield ""

defines

defines() -> Iterable[str]
Source code in Projects/ShaderFlow/ShaderFlow/Module.py
179
180
181
@abstractmethod
def defines(self) -> Iterable[str]:
    yield None

__shaderflow_ui__

__shaderflow_ui__() -> None

Basic info of a Module

Source code in Projects/ShaderFlow/ShaderFlow/Module.py
185
186
187
188
189
190
191
192
193
194
195
def __shaderflow_ui__(self) -> None:
    """Basic info of a Module"""
    # Todo: Make automatic Imgui methods

    # Module - self.__ui__ must be implemented
    if not getattr(self.__ui__, "__isabstractmethod__", False):
        self.__ui__()

    # Module - self.ui must be implemented
    if not getattr(self.ui, "__isabstractmethod__", False):
        self.ui()

__ui__

__ui__() -> None

Internal method for self.ui

Source code in Projects/ShaderFlow/ShaderFlow/Module.py
197
198
199
200
@abstractmethod
def __ui__(self) -> None:
    """Internal method for self.ui"""
    pass

ui

ui() -> None

Draw the UI for this module

Source code in Projects/ShaderFlow/ShaderFlow/Module.py
202
203
204
205
206
207
@abstractmethod
def ui(self) -> None:
    """
    Draw the UI for this module
    """
    pass