diff --git a/Doxyfile b/Doxyfile
index a657ede81c43b3cae1fc5c17f071aa7dc65add04..249fed47d3c09ceca1e570b3be4f1a20cb1b13c3 100644
--- a/Doxyfile
+++ b/Doxyfile
@@ -1,4 +1,4 @@
-# Doxyfile 1.9.1
+# Doxyfile 1.9.3
 
 # This file describes the settings to be used by the documentation system
 # doxygen (www.doxygen.org) for a project.
@@ -93,14 +93,6 @@ ALLOW_UNICODE_NAMES    = NO
 
 OUTPUT_LANGUAGE        = English
 
-# The OUTPUT_TEXT_DIRECTION tag is used to specify the direction in which all
-# documentation generated by doxygen is written. Doxygen will use this
-# information to generate all generated output in the proper direction.
-# Possible values are: None, LTR, RTL and Context.
-# The default value is: None.
-
-OUTPUT_TEXT_DIRECTION  = None
-
 # If the BRIEF_MEMBER_DESC tag is set to YES, doxygen will include brief member
 # descriptions after the members that are listed in the file and class
 # documentation (similar to Javadoc). Set to NO to disable this.
@@ -258,16 +250,16 @@ TAB_SIZE               = 4
 # the documentation. An alias has the form:
 # name=value
 # For example adding
-# "sideeffect=@par Side Effects:\n"
+# "sideeffect=@par Side Effects:^^"
 # will allow you to put the command \sideeffect (or @sideeffect) in the
 # documentation, which will result in a user-defined paragraph with heading
-# "Side Effects:". You can put \n's in the value part of an alias to insert
-# newlines (in the resulting output). You can put ^^ in the value part of an
-# alias to insert a newline as if a physical newline was in the original file.
-# When you need a literal { or } or , in the value part of an alias you have to
-# escape them by means of a backslash (\), this can lead to conflicts with the
-# commands \{ and \} for these it is advised to use the version @{ and @} or use
-# a double escape (\\{ and \\})
+# "Side Effects:". Note that you cannot put \n's in the value part of an alias
+# to insert newlines (in the resulting output). You can put ^^ in the value part
+# of an alias to insert a newline as if a physical newline was in the original
+# file. When you need a literal { or } or , in the value part of an alias you
+# have to escape them by means of a backslash (\), this can lead to conflicts
+# with the commands \{ and \} for these it is advised to use the version @{ and
+# @} or use a double escape (\\{ and \\})
 
 ALIASES                =
 
@@ -312,8 +304,8 @@ OPTIMIZE_OUTPUT_SLICE  = NO
 # extension. Doxygen has a built-in mapping, but you can override or extend it
 # using this tag. The format is ext=language, where ext is a file extension, and
 # language is one of the parsers supported by doxygen: IDL, Java, JavaScript,
-# Csharp (C#), C, C++, D, PHP, md (Markdown), Objective-C, Python, Slice, VHDL,
-# Fortran (fixed format Fortran: FortranFixed, free formatted Fortran:
+# Csharp (C#), C, C++, Lex, D, PHP, md (Markdown), Objective-C, Python, Slice,
+# VHDL, Fortran (fixed format Fortran: FortranFixed, free formatted Fortran:
 # FortranFree, unknown formatted Fortran: Fortran. In the later case the parser
 # tries to guess whether the code is fixed or free formatted code, this is the
 # default for Fortran type files). For instance to make doxygen treat .inc files
@@ -466,12 +458,12 @@ LOOKUP_CACHE_SIZE      = 0
 # than 0 to get more control over the balance between CPU load and processing
 # speed. At this moment only the input processing can be done using multiple
 # threads. Since this is still an experimental feature the default is set to 1,
-# which efficively disables parallel processing. Please report any issues you
+# which effectively disables parallel processing. Please report any issues you
 # encounter. Generating dot graphs in parallel is controlled by the
 # DOT_NUM_THREADS setting.
 # Minimum value: 0, maximum value: 32, default value: 1.
 
-NUM_PROC_THREADS       = 1
+NUM_PROC_THREADS       = 0
 
 #---------------------------------------------------------------------------
 # Build related configuration options
@@ -610,6 +602,12 @@ HIDE_SCOPE_NAMES       = NO
 
 HIDE_COMPOUND_REFERENCE= NO
 
+# If the SHOW_HEADERFILE tag is set to YES then the documentation for a class
+# will show which file needs to be included to use the class.
+# The default value is: YES.
+
+SHOW_HEADERFILE        = YES
+
 # If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of
 # the files that are included by a file in the documentation of that file.
 # The default value is: YES.
@@ -767,7 +765,8 @@ FILE_VERSION_FILTER    =
 # output files in an output format independent way. To create the layout file
 # that represents doxygen's defaults, run doxygen with the -l option. You can
 # optionally specify a file name after the option, if omitted DoxygenLayout.xml
-# will be used as the name of the layout file.
+# will be used as the name of the layout file. See also section "Changing the
+# layout of pages" for information.
 #
 # Note that if you run doxygen from a directory containing a file called
 # DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE
@@ -813,18 +812,26 @@ WARNINGS               = YES
 WARN_IF_UNDOCUMENTED   = YES
 
 # If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for
-# potential errors in the documentation, such as not documenting some parameters
-# in a documented function, or documenting parameters that don't exist or using
-# markup commands wrongly.
+# potential errors in the documentation, such as documenting some parameters in
+# a documented function twice, or documenting parameters that don't exist or
+# using markup commands wrongly.
 # The default value is: YES.
 
 WARN_IF_DOC_ERROR      = YES
 
+# If WARN_IF_INCOMPLETE_DOC is set to YES, doxygen will warn about incomplete
+# function parameter documentation. If set to NO, doxygen will accept that some
+# parameters have no documentation without warning.
+# The default value is: YES.
+
+WARN_IF_INCOMPLETE_DOC = YES
+
 # This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that
 # are documented, but have no documentation for their parameters or return
-# value. If set to NO, doxygen will only warn about wrong or incomplete
-# parameter documentation, but not about the absence of documentation. If
-# EXTRACT_ALL is set to YES then this flag will automatically be disabled.
+# value. If set to NO, doxygen will only warn about wrong parameter
+# documentation, but not about the absence of documentation. If EXTRACT_ALL is
+# set to YES then this flag will automatically be disabled. See also
+# WARN_IF_INCOMPLETE_DOC
 # The default value is: NO.
 
 WARN_NO_PARAMDOC       = NO
@@ -850,7 +857,10 @@ WARN_FORMAT            = "$file:$line: $text"
 
 # The WARN_LOGFILE tag can be used to specify a file to which warning and error
 # messages should be written. If left blank the output is written to standard
-# error (stderr).
+# error (stderr). In case the file specified cannot be opened for writing the
+# warning and error messages are written to standard error. When as file - is
+# specified the warning and error messages are written to standard output
+# (stdout).
 
 WARN_LOGFILE           =
 
@@ -890,10 +900,10 @@ INPUT_ENCODING         = UTF-8
 #
 # If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp,
 # *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h,
-# *.hh, *.hxx, *.hpp, *.h++, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, *.inc,
-# *.m, *.markdown, *.md, *.mm, *.dox (to be provided as doxygen C comment),
-# *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, *.f18, *.f, *.for, *.vhd, *.vhdl,
-# *.ucf, *.qsf and *.ice.
+# *.hh, *.hxx, *.hpp, *.h++, *.l, *.cs, *.d, *.php, *.php4, *.php5, *.phtml,
+# *.inc, *.m, *.markdown, *.md, *.mm, *.dox (to be provided as doxygen C
+# comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, *.f18, *.f, *.for, *.vhd,
+# *.vhdl, *.ucf, *.qsf and *.ice.
 
 FILE_PATTERNS          = *.c \
                          *.cc \
@@ -976,7 +986,7 @@ EXCLUDE_PATTERNS       = */lib/*
 # (namespaces, classes, functions, etc.) that should be excluded from the
 # output. The symbol name can be a fully qualified name, a word, or if the
 # wildcard * is used, a substring. Examples: ANamespace, AClass,
-# AClass::ANamespace, ANamespace::*Test
+# ANamespace::AClass, ANamespace::*Test
 #
 # Note that the wildcards are matched against the file with absolute path, so to
 # exclude all test directories use the pattern */test/*
@@ -1261,7 +1271,7 @@ HTML_EXTRA_FILES       =
 
 # The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen
 # will adjust the colors in the style sheet and background images according to
-# this color. Hue is specified as an angle on a colorwheel, see
+# this color. Hue is specified as an angle on a color-wheel, see
 # https://en.wikipedia.org/wiki/Hue for more information. For instance the value
 # 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300
 # purple, and 360 is red again.
@@ -1271,7 +1281,7 @@ HTML_EXTRA_FILES       =
 HTML_COLORSTYLE_HUE    = 220
 
 # The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors
-# in the HTML output. For a value of 0 the output will use grayscales only. A
+# in the HTML output. For a value of 0 the output will use gray-scales only. A
 # value of 255 will produce the most vivid colors.
 # Minimum value: 0, maximum value: 255, default value: 100.
 # This tag requires that the tag GENERATE_HTML is set to YES.
@@ -1353,6 +1363,13 @@ GENERATE_DOCSET        = NO
 
 DOCSET_FEEDNAME        = "Doxygen generated docs"
 
+# This tag determines the URL of the docset feed. A documentation feed provides
+# an umbrella under which multiple documentation sets from a single provider
+# (such as a company or product suite) can be grouped.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_FEEDURL         =
+
 # This tag specifies a string that should uniquely identify the documentation
 # set bundle. This should be a reverse domain-name style string, e.g.
 # com.mycompany.MyDocSet. Doxygen will append .docset to the name.
@@ -1378,8 +1395,12 @@ DOCSET_PUBLISHER_NAME  = Publisher
 # If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three
 # additional HTML index files: index.hhp, index.hhc, and index.hhk. The
 # index.hhp is a project file that can be read by Microsoft's HTML Help Workshop
-# (see:
-# https://www.microsoft.com/en-us/download/details.aspx?id=21138) on Windows.
+# on Windows. In the beginning of 2021 Microsoft took the original page, with
+# a.o. the download links, offline the HTML help workshop was already many years
+# in maintenance mode). You can download the HTML help workshop from the web
+# archives at Installation executable (see:
+# http://web.archive.org/web/20160201063255/http://download.microsoft.com/downlo
+# ad/0/A/9/0A939EF6-E31C-430F-A3DF-DFAE7960D564/htmlhelp.exe).
 #
 # The HTML Help Workshop contains a compiler that can convert all HTML output
 # generated by doxygen into a single compiled HTML file (.chm). Compiled HTML
@@ -1538,16 +1559,28 @@ DISABLE_INDEX          = NO
 # to work a browser that supports JavaScript, DHTML, CSS and frames is required
 # (i.e. any modern browser). Windows users are probably better off using the
 # HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can
-# further fine-tune the look of the index. As an example, the default style
-# sheet generated by doxygen has an example that shows how to put an image at
-# the root of the tree instead of the PROJECT_NAME. Since the tree basically has
-# the same information as the tab index, you could consider setting
-# DISABLE_INDEX to YES when enabling this option.
+# further fine tune the look of the index (see "Fine-tuning the output"). As an
+# example, the default style sheet generated by doxygen has an example that
+# shows how to put an image at the root of the tree instead of the PROJECT_NAME.
+# Since the tree basically has the same information as the tab index, you could
+# consider setting DISABLE_INDEX to YES when enabling this option.
 # The default value is: NO.
 # This tag requires that the tag GENERATE_HTML is set to YES.
 
 GENERATE_TREEVIEW      = YES
 
+# When both GENERATE_TREEVIEW and DISABLE_INDEX are set to YES, then the
+# FULL_SIDEBAR option determines if the side bar is limited to only the treeview
+# area (value NO) or if it should extend to the full height of the window (value
+# YES). Setting this to YES gives a layout similar to
+# https://docs.readthedocs.io with more room for contents, but less room for the
+# project logo, title, and description. If either GENERATE_TREEVIEW or
+# DISABLE_INDEX is set to NO, this option has no effect.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+FULL_SIDEBAR           = NO
+
 # The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that
 # doxygen will group on one line in the generated HTML documentation.
 #
@@ -1572,6 +1605,13 @@ TREEVIEW_WIDTH         = 250
 
 EXT_LINKS_IN_WINDOW    = NO
 
+# If the OBFUSCATE_EMAILS tag is set to YES, doxygen will obfuscate email
+# addresses.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+OBFUSCATE_EMAILS       = YES
+
 # If the HTML_FORMULA_FORMAT option is set to svg, doxygen will use the pdf2svg
 # tool (see https://github.com/dawbarton/pdf2svg) or inkscape (see
 # https://inkscape.org) to generate formulas as SVG images instead of PNGs for
@@ -1620,11 +1660,29 @@ FORMULA_MACROFILE      =
 
 USE_MATHJAX            = NO
 
+# With MATHJAX_VERSION it is possible to specify the MathJax version to be used.
+# Note that the different versions of MathJax have different requirements with
+# regards to the different settings, so it is possible that also other MathJax
+# settings have to be changed when switching between the different MathJax
+# versions.
+# Possible values are: MathJax_2 and MathJax_3.
+# The default value is: MathJax_2.
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_VERSION        = MathJax_2
+
 # When MathJax is enabled you can set the default output format to be used for
-# the MathJax output. See the MathJax site (see:
-# http://docs.mathjax.org/en/v2.7-latest/output.html) for more details.
+# the MathJax output. For more details about the output format see MathJax
+# version 2 (see:
+# http://docs.mathjax.org/en/v2.7-latest/output.html) and MathJax version 3
+# (see:
+# http://docs.mathjax.org/en/latest/web/components/output.html).
 # Possible values are: HTML-CSS (which is slower, but has the best
-# compatibility), NativeMML (i.e. MathML) and SVG.
+# compatibility. This is the name for Mathjax version 2, for MathJax version 3
+# this will be translated into chtml), NativeMML (i.e. MathML. Only supported
+# for NathJax 2. For MathJax version 3 chtml will be used instead.), chtml (This
+# is the name for Mathjax version 3, for MathJax version 2 this will be
+# translated into HTML-CSS) and SVG.
 # The default value is: HTML-CSS.
 # This tag requires that the tag USE_MATHJAX is set to YES.
 
@@ -1637,15 +1695,21 @@ MATHJAX_FORMAT         = HTML-CSS
 # MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax
 # Content Delivery Network so you can quickly see the result without installing
 # MathJax. However, it is strongly recommended to install a local copy of
-# MathJax from https://www.mathjax.org before deployment.
-# The default value is: https://cdn.jsdelivr.net/npm/mathjax@2.
+# MathJax from https://www.mathjax.org before deployment. The default value is:
+# - in case of MathJax version 2: https://cdn.jsdelivr.net/npm/mathjax@2
+# - in case of MathJax version 3: https://cdn.jsdelivr.net/npm/mathjax@3
 # This tag requires that the tag USE_MATHJAX is set to YES.
 
 MATHJAX_RELPATH        = https://cdn.jsdelivr.net/npm/mathjax@2
 
 # The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax
 # extension names that should be enabled during MathJax rendering. For example
+# for MathJax version 2 (see
+# https://docs.mathjax.org/en/v2.7-latest/tex.html#tex-and-latex-extensions):
 # MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols
+# For example for MathJax version 3 (see
+# http://docs.mathjax.org/en/latest/input/tex/extensions/index.html):
+# MATHJAX_EXTENSIONS = ams
 # This tag requires that the tag USE_MATHJAX is set to YES.
 
 MATHJAX_EXTENSIONS     =
@@ -1825,29 +1889,31 @@ PAPER_TYPE             = a4
 
 EXTRA_PACKAGES         =
 
-# The LATEX_HEADER tag can be used to specify a personal LaTeX header for the
-# generated LaTeX document. The header should contain everything until the first
-# chapter. If it is left blank doxygen will generate a standard header. See
-# section "Doxygen usage" for information on how to let doxygen write the
-# default header to a separate file.
+# The LATEX_HEADER tag can be used to specify a user-defined LaTeX header for
+# the generated LaTeX document. The header should contain everything until the
+# first chapter. If it is left blank doxygen will generate a standard header. It
+# is highly recommended to start with a default header using
+# doxygen -w latex new_header.tex new_footer.tex new_stylesheet.sty
+# and then modify the file new_header.tex. See also section "Doxygen usage" for
+# information on how to generate the default header that doxygen normally uses.
 #
-# Note: Only use a user-defined header if you know what you are doing! The
-# following commands have a special meaning inside the header: $title,
-# $datetime, $date, $doxygenversion, $projectname, $projectnumber,
-# $projectbrief, $projectlogo. Doxygen will replace $title with the empty
-# string, for the replacement values of the other commands the user is referred
-# to HTML_HEADER.
+# Note: Only use a user-defined header if you know what you are doing!
+# Note: The header is subject to change so you typically have to regenerate the
+# default header when upgrading to a newer version of doxygen. The following
+# commands have a special meaning inside the header (and footer): For a
+# description of the possible markers and block names see the documentation.
 # This tag requires that the tag GENERATE_LATEX is set to YES.
 
 LATEX_HEADER           =
 
-# The LATEX_FOOTER tag can be used to specify a personal LaTeX footer for the
-# generated LaTeX document. The footer should contain everything after the last
-# chapter. If it is left blank doxygen will generate a standard footer. See
+# The LATEX_FOOTER tag can be used to specify a user-defined LaTeX footer for
+# the generated LaTeX document. The footer should contain everything after the
+# last chapter. If it is left blank doxygen will generate a standard footer. See
 # LATEX_HEADER for more information on how to generate a default footer and what
-# special commands can be used inside the footer.
-#
-# Note: Only use a user-defined footer if you know what you are doing!
+# special commands can be used inside the footer. See also section "Doxygen
+# usage" for information on how to generate the default footer that doxygen
+# normally uses. Note: Only use a user-defined footer if you know what you are
+# doing!
 # This tag requires that the tag GENERATE_LATEX is set to YES.
 
 LATEX_FOOTER           =
@@ -1892,8 +1958,7 @@ USE_PDFLATEX           = YES
 
 # If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \batchmode
 # command to the generated LaTeX files. This will instruct LaTeX to keep running
-# if errors occur, instead of asking the user for help. This option is also used
-# when generating formulas in HTML.
+# if errors occur, instead of asking the user for help.
 # The default value is: NO.
 # This tag requires that the tag GENERATE_LATEX is set to YES.
 
@@ -1906,16 +1971,6 @@ LATEX_BATCHMODE        = NO
 
 LATEX_HIDE_INDICES     = NO
 
-# If the LATEX_SOURCE_CODE tag is set to YES then doxygen will include source
-# code with syntax highlighting in the LaTeX output.
-#
-# Note that which sources are shown also depends on other settings such as
-# SOURCE_BROWSER.
-# The default value is: NO.
-# This tag requires that the tag GENERATE_LATEX is set to YES.
-
-LATEX_SOURCE_CODE      = NO
-
 # The LATEX_BIB_STYLE tag can be used to specify the style to use for the
 # bibliography, e.g. plainnat, or ieeetr. See
 # https://en.wikipedia.org/wiki/BibTeX and \cite for more info.
@@ -1996,16 +2051,6 @@ RTF_STYLESHEET_FILE    =
 
 RTF_EXTENSIONS_FILE    =
 
-# If the RTF_SOURCE_CODE tag is set to YES then doxygen will include source code
-# with syntax highlighting in the RTF output.
-#
-# Note that which sources are shown also depends on other settings such as
-# SOURCE_BROWSER.
-# The default value is: NO.
-# This tag requires that the tag GENERATE_RTF is set to YES.
-
-RTF_SOURCE_CODE        = NO
-
 #---------------------------------------------------------------------------
 # Configuration options related to the man page output
 #---------------------------------------------------------------------------
@@ -2102,15 +2147,6 @@ GENERATE_DOCBOOK       = NO
 
 DOCBOOK_OUTPUT         = docbook
 
-# If the DOCBOOK_PROGRAMLISTING tag is set to YES, doxygen will include the
-# program listings (including syntax highlighting and cross-referencing
-# information) to the DOCBOOK output. Note that enabling this will significantly
-# increase the size of the DOCBOOK output.
-# The default value is: NO.
-# This tag requires that the tag GENERATE_DOCBOOK is set to YES.
-
-DOCBOOK_PROGRAMLISTING = NO
-
 #---------------------------------------------------------------------------
 # Configuration options for the AutoGen Definitions output
 #---------------------------------------------------------------------------
@@ -2293,15 +2329,6 @@ EXTERNAL_PAGES         = YES
 # Configuration options related to the dot tool
 #---------------------------------------------------------------------------
 
-# If the CLASS_DIAGRAMS tag is set to YES, doxygen will generate a class diagram
-# (in HTML and LaTeX) for classes with base or super classes. Setting the tag to
-# NO turns the diagrams off. Note that this option also works with HAVE_DOT
-# disabled, but it is recommended to install and use dot, since it yields more
-# powerful graphs.
-# The default value is: YES.
-
-CLASS_DIAGRAMS         = NO
-
 # You can include diagrams made with dia in doxygen documentation. Doxygen will
 # then run dia to produce the diagram and insert it in the documentation. The
 # DIA_PATH tag allows you to specify the directory where the dia binary resides.
@@ -2358,11 +2385,14 @@ DOT_FONTSIZE           = 10
 
 DOT_FONTPATH           =
 
-# If the CLASS_GRAPH tag is set to YES then doxygen will generate a graph for
-# each documented class showing the direct and indirect inheritance relations.
-# Setting this tag to YES will force the CLASS_DIAGRAMS tag to NO.
+# If the CLASS_GRAPH tag is set to YES (or GRAPH) then doxygen will generate a
+# graph for each documented class showing the direct and indirect inheritance
+# relations. In case HAVE_DOT is set as well dot will be used to draw the graph,
+# otherwise the built-in generator will be used. If the CLASS_GRAPH tag is set
+# to TEXT the direct and indirect inheritance relations will be shown as texts /
+# links.
+# Possible values are: NO, YES, TEXT and GRAPH.
 # The default value is: YES.
-# This tag requires that the tag HAVE_DOT is set to YES.
 
 CLASS_GRAPH            = YES
 
@@ -2491,6 +2521,13 @@ GRAPHICAL_HIERARCHY    = YES
 
 DIRECTORY_GRAPH        = YES
 
+# The DIR_GRAPH_MAX_DEPTH tag can be used to limit the maximum number of levels
+# of child directories generated in directory dependency graphs by dot.
+# Minimum value: 1, maximum value: 25, default value: 1.
+# This tag requires that the tag DIRECTORY_GRAPH is set to YES.
+
+DIR_GRAPH_MAX_DEPTH    = 1
+
 # The DOT_IMAGE_FORMAT tag can be used to set the image format of the images
 # generated by dot. For an explanation of the image formats see the section
 # output formats in the documentation of the dot tool (Graphviz (see:
@@ -2544,10 +2581,10 @@ MSCFILE_DIRS           =
 DIAFILE_DIRS           =
 
 # When using plantuml, the PLANTUML_JAR_PATH tag should be used to specify the
-# path where java can find the plantuml.jar file. If left blank, it is assumed
-# PlantUML is not used or called during a preprocessing step. Doxygen will
-# generate a warning when it encounters a \startuml command in this case and
-# will not generate output for the diagram.
+# path where java can find the plantuml.jar file or to the filename of jar file
+# to be used. If left blank, it is assumed PlantUML is not used or called during
+# a preprocessing step. Doxygen will generate a warning when it encounters a
+# \startuml command in this case and will not generate output for the diagram.
 
 PLANTUML_JAR_PATH      =
 
@@ -2609,6 +2646,8 @@ DOT_MULTI_TARGETS      = NO
 # If the GENERATE_LEGEND tag is set to YES doxygen will generate a legend page
 # explaining the meaning of the various boxes and arrows in the dot generated
 # graphs.
+# Note: This tag requires that UML_LOOK isn't set, i.e. the doxygen internal
+# graphical representation for inheritance and collaboration diagrams is used.
 # The default value is: YES.
 # This tag requires that the tag HAVE_DOT is set to YES.
 
@@ -2617,8 +2656,8 @@ GENERATE_LEGEND        = YES
 # If the DOT_CLEANUP tag is set to YES, doxygen will remove the intermediate
 # files that are used to generate the various graphs.
 #
-# Note: This setting is not only used for dot files but also for msc and
-# plantuml temporary files.
+# Note: This setting is not only used for dot files but also for msc temporary
+# files.
 # The default value is: YES.
 
 DOT_CLEANUP            = YES
diff --git a/include/vkcv/Core.hpp b/include/vkcv/Core.hpp
index 77370fb39b1a5a300614fe9cfb0b505e5ef2dd67..cac5bdf5d6e4d1cb182d8b9036695378165a5e65 100644
--- a/include/vkcv/Core.hpp
+++ b/include/vkcv/Core.hpp
@@ -265,7 +265,7 @@ namespace vkcv
 		 * @return swapchain
 		 */
 		[[nodiscard]]
-		Swapchain& getSwapchain(const SwapchainHandle& handle);
+		Swapchain& getSwapchain(const SwapchainHandle &handle);
 
 		/**
 		 * gets the swapchain handle from the window
@@ -273,7 +273,7 @@ namespace vkcv
 		 * @return the swapchain from getSwapchain( SwapchainHandle )
 		 */
 		[[nodiscard]]
-		Swapchain& getSwapchain(const WindowHandle& handle);
+		Swapchain& getSwapchain(const WindowHandle &handle);
 
 		/**
 		 * returns the image width
@@ -281,7 +281,7 @@ namespace vkcv
 		 * @return imageWidth
 		 */
         [[nodiscard]]
-        uint32_t getImageWidth(const ImageHandle& image);
+        uint32_t getImageWidth(const ImageHandle &image);
 
         /**
          * returns the image height
@@ -289,7 +289,7 @@ namespace vkcv
          * @return imageHeight
          */
         [[nodiscard]]
-        uint32_t getImageHeight(const ImageHandle& image);
+        uint32_t getImageHeight(const ImageHandle &image);
 
         /**
          * returns the image format of the image
@@ -297,7 +297,16 @@ namespace vkcv
          * @return imageFormat
          */
 		[[nodiscard]]
-		vk::Format getImageFormat(const ImageHandle& image);
+		vk::Format getImageFormat(const ImageHandle &image);
+		
+		/**
+		 * @brief Returns the images amount of mip levels.
+		 *
+		 * @param image Image handle
+		 * @return Amount of mip levels
+		 */
+		[[nodiscard]]
+		uint32_t getImageMipLevels(const ImageHandle &image);
 
 		/** TODO:
 		 * @param bindings
diff --git a/modules/CMakeLists.txt b/modules/CMakeLists.txt
index 4d89c52a039e9d5de9efb276396158e987f52118..0a7f0f0ab1b71f78c6a548caf3d82442a05dcb2b 100644
--- a/modules/CMakeLists.txt
+++ b/modules/CMakeLists.txt
@@ -5,6 +5,7 @@ set(vkcv_modules_libraries)
 # Add new modules here:
 add_subdirectory(asset_loader)
 add_subdirectory(camera)
+add_subdirectory(effects)
 add_subdirectory(gui)
 add_subdirectory(material)
 add_subdirectory(meshlet)
diff --git a/modules/effects/CMakeLists.txt b/modules/effects/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..8de9ea539cfbe9c22607a9b6deb07f683a56bbdd
--- /dev/null
+++ b/modules/effects/CMakeLists.txt
@@ -0,0 +1,54 @@
+cmake_minimum_required(VERSION 3.16)
+project(vkcv_effects)
+
+# setting c++ standard for the project
+set(CMAKE_CXX_STANDARD 20)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+
+set(vkcv_effects_source ${PROJECT_SOURCE_DIR}/src)
+set(vkcv_effects_include ${PROJECT_SOURCE_DIR}/include)
+
+set(vkcv_effects_sources
+		${vkcv_effects_include}/vkcv/effects/Effect.hpp
+		${vkcv_effects_source}/vkcv/effects/Effect.cpp
+		
+		${vkcv_effects_include}/vkcv/effects/BloomAndFlaresEffect.hpp
+		${vkcv_effects_source}/vkcv/effects/BloomAndFlaresEffect.cpp
+)
+
+set(vkcv_effects_shaders ${PROJECT_SOURCE_DIR}/shaders)
+
+include_shader(${vkcv_effects_shaders}/bloomDownsample.comp ${vkcv_effects_include} ${vkcv_effects_source})
+include_shader(${vkcv_effects_shaders}/bloomFlaresComposite.comp ${vkcv_effects_include} ${vkcv_effects_source})
+include_shader(${vkcv_effects_shaders}/bloomUpsample.comp ${vkcv_effects_include} ${vkcv_effects_source})
+include_shader(${vkcv_effects_shaders}/lensFlares.comp ${vkcv_effects_include} ${vkcv_effects_source})
+
+list(APPEND vkcv_effects_sources ${vkcv_effects_source}/bloomDownsample.comp.cxx)
+list(APPEND vkcv_effects_sources ${vkcv_effects_source}/bloomFlaresComposite.comp.cxx)
+list(APPEND vkcv_effects_sources ${vkcv_effects_source}/bloomUpsample.comp.cxx)
+list(APPEND vkcv_effects_sources ${vkcv_effects_source}/lensFlares.comp.cxx)
+
+list(APPEND vkcv_effects_sources ${vkcv_effects_include}/bloomDownsample.comp.hxx)
+list(APPEND vkcv_effects_sources ${vkcv_effects_include}/bloomFlaresComposite.comp.hxx)
+list(APPEND vkcv_effects_sources ${vkcv_effects_include}/bloomUpsample.comp.hxx)
+list(APPEND vkcv_effects_sources ${vkcv_effects_include}/lensFlares.comp.hxx)
+
+# adding source files to the project
+add_library(vkcv_effects ${vkcv_build_attribute} ${vkcv_effects_sources})
+
+# link the required libraries to the module
+target_link_libraries(vkcv_effects ${vkcv_effects_libraries} vkcv vkcv_shader_compiler vkcv_camera vkcv_asset_loader)
+
+# including headers of dependencies and the VkCV framework
+target_include_directories(vkcv_effects SYSTEM BEFORE PRIVATE ${vkcv_effects_includes} ${vkcv_include} ${vkcv_shader_compiler_include} ${vkcv_camera_include} {vkcv_asset_loader_include})
+
+# add the own include directory for public headers
+target_include_directories(vkcv_effects BEFORE PUBLIC ${vkcv_effects_include})
+
+if (vkcv_parent_scope)
+	list(APPEND vkcv_modules_includes ${vkcv_effects_include})
+	list(APPEND vkcv_modules_libraries vkcv_effects)
+	
+	set(vkcv_modules_includes ${vkcv_modules_includes} PARENT_SCOPE)
+	set(vkcv_modules_libraries ${vkcv_modules_libraries} PARENT_SCOPE)
+endif()
diff --git a/modules/effects/README.md b/modules/effects/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..4e035831381f50a66cf405c06fadbb63f4273bc0
--- /dev/null
+++ b/modules/effects/README.md
@@ -0,0 +1,15 @@
+# Effects
+
+A VkCV module to add post-processing effects to images in realtime after rendering
+
+## Build
+
+### Dependencies (required):
+
+| Name of dependency | Used as submodule |
+|--------------------|-------------------|
+|                    |                   |
+
+## Docs
+
+Here is a [link](https://vkcv.de/develop/group__vkcv__effects.html) to this module.
diff --git a/modules/effects/include/vkcv/effects/BloomAndFlaresEffect.hpp b/modules/effects/include/vkcv/effects/BloomAndFlaresEffect.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..f4a9d03d8afcaf3532570a0807b246a0d641e02d
--- /dev/null
+++ b/modules/effects/include/vkcv/effects/BloomAndFlaresEffect.hpp
@@ -0,0 +1,73 @@
+#pragma once
+
+#include <vector>
+#include <vkcv/camera/Camera.hpp>
+
+#include "Effect.hpp"
+
+namespace vkcv::effects {
+	
+	class BloomAndFlaresEffect : public Effect {
+	private:
+		bool m_advanced;
+		
+		ComputePipelineHandle m_downsamplePipeline;
+		ComputePipelineHandle m_upsamplePipeline;
+		ComputePipelineHandle m_lensFlaresPipeline;
+		ComputePipelineHandle m_compositePipeline;
+		
+		DescriptorSetLayoutHandle m_downsampleDescriptorSetLayout;
+		std::vector<DescriptorSetHandle> m_downsampleDescriptorSets;
+		
+		DescriptorSetLayoutHandle m_upsampleDescriptorSetLayout;
+		std::vector<DescriptorSetHandle> m_upsampleDescriptorSets;
+		std::vector<DescriptorSetHandle> m_flaresDescriptorSets;
+		
+		DescriptorSetLayoutHandle m_lensFlaresDescriptorSetLayout;
+		DescriptorSetHandle m_lensFlaresDescriptorSet;
+		
+		DescriptorSetLayoutHandle m_compositeDescriptorSetLayout;
+		DescriptorSetHandle m_compositeDescriptorSet;
+		
+		ImageHandle m_blurImage;
+		ImageHandle m_flaresImage;
+		
+		SamplerHandle m_linearSampler;
+		SamplerHandle m_radialLutSampler;
+		
+		ImageHandle m_radialLut;
+		ImageHandle m_lensDirt;
+		
+		glm::vec3 m_cameraDirection;
+		uint32_t m_upsampleLimit;
+		
+		void recordDownsampling(const CommandStreamHandle &cmdStream,
+								const ImageHandle &input,
+								const ImageHandle &sample,
+								const std::vector<DescriptorSetHandle> &mipDescriptorSets);
+		
+		void recordUpsampling(const CommandStreamHandle &cmdStream,
+							  const ImageHandle &sample,
+							  const std::vector<DescriptorSetHandle> &mipDescriptorSets);
+		
+		void recordLensFlares(const CommandStreamHandle &cmdStream,
+							  uint32_t mipLevel);
+		
+		void recordComposition(const CommandStreamHandle &cmdStream,
+							   const ImageHandle &output);
+		
+	public:
+		BloomAndFlaresEffect(Core& core,
+							 bool advanced = false);
+		
+		void recordEffect(const CommandStreamHandle &cmdStream,
+						  const ImageHandle &input,
+						  const ImageHandle &output) override;
+		
+		void updateCameraDirection(const camera::Camera &camera);
+		
+		void setUpsamplingLimit(uint32_t limit);
+		
+	};
+	
+}
diff --git a/modules/effects/include/vkcv/effects/Effect.hpp b/modules/effects/include/vkcv/effects/Effect.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..d068fb59404080aa421e49f6a183844e731c1c5e
--- /dev/null
+++ b/modules/effects/include/vkcv/effects/Effect.hpp
@@ -0,0 +1,23 @@
+#pragma once
+
+#include <vkcv/Core.hpp>
+#include <vkcv/Handles.hpp>
+
+namespace vkcv::effects {
+	
+	class Effect {
+	protected:
+		Core& m_core;
+	
+	public:
+		explicit Effect(Core& core);
+		
+		~Effect() = default;
+		
+		virtual void recordEffect(const CommandStreamHandle& cmdStream,
+								  const ImageHandle& input,
+								  const ImageHandle& output) = 0;
+		
+	};
+	
+}
diff --git a/projects/voxelization/assets/shaders/bloomDownsample.comp b/modules/effects/shaders/bloomDownsample.comp
similarity index 100%
rename from projects/voxelization/assets/shaders/bloomDownsample.comp
rename to modules/effects/shaders/bloomDownsample.comp
diff --git a/projects/voxelization/assets/shaders/bloomFlaresComposite.comp b/modules/effects/shaders/bloomFlaresComposite.comp
similarity index 97%
rename from projects/voxelization/assets/shaders/bloomFlaresComposite.comp
rename to modules/effects/shaders/bloomFlaresComposite.comp
index 57174b73ae3b58023d01defd26f636e13cb4709c..21d67393b634c8e639c0b81669e96070c380e6f6 100644
--- a/projects/voxelization/assets/shaders/bloomFlaresComposite.comp
+++ b/modules/effects/shaders/bloomFlaresComposite.comp
@@ -5,12 +5,17 @@ layout(set=0, binding=0) uniform texture2D                          blurImage;
 layout(set=0, binding=1) uniform texture2D                          lensImage;
 layout(set=0, binding=2) uniform sampler                            linearSampler;
 layout(set=0, binding=3, r11f_g11f_b10f) uniform image2D            colorBuffer;
+
+#ifdef ADVANCED_FEATURES
 layout(set=0, binding=4) uniform texture2D                          radialLUT;
 layout(set=0, binding=5) uniform sampler                            radialLUTSampler;
 layout(set=0, binding=6) uniform texture2D                          dirtTexture;
+#endif
 
 layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
 
+#ifdef ADVANCED_FEATURES
+
 layout( push_constant ) uniform constants{
     vec3 cameraForward;
 };
@@ -52,6 +57,8 @@ float getLensDirtWeight(vec2 uv){
     return mix(1, dirt, dirtStrength);
 }
 
+#endif
+
 void main()
 {
     if(any(greaterThanEqual(gl_GlobalInvocationID.xy, imageSize(colorBuffer)))){
@@ -73,17 +80,17 @@ void main()
     float lens_weight  = 0.02f;
     float main_weight = 1 - (bloom_weight + lens_weight);
 
+#ifdef ADVANCED_FEATURES
     lens_color *= starburst(UV);
     
     float lensDirtWeight = getLensDirtWeight(UV);
     bloom_weight        *= lensDirtWeight;
     lens_weight         *= lensDirtWeight;
+#endif
     
     composite_color.rgb = blur_color * bloom_weight +
                           lens_color * lens_weight  +
                           main_color * main_weight;
-                          
-    //composite_color.rgb = vec3(1) * starburst(UV);
 
     imageStore(colorBuffer, pixel_coord, composite_color);
 }
\ No newline at end of file
diff --git a/projects/voxelization/assets/shaders/bloomUpsample.comp b/modules/effects/shaders/bloomUpsample.comp
similarity index 100%
rename from projects/voxelization/assets/shaders/bloomUpsample.comp
rename to modules/effects/shaders/bloomUpsample.comp
diff --git a/projects/voxelization/assets/shaders/lensFlares.comp b/modules/effects/shaders/lensFlares.comp
similarity index 100%
rename from projects/voxelization/assets/shaders/lensFlares.comp
rename to modules/effects/shaders/lensFlares.comp
diff --git a/modules/effects/src/vkcv/effects/BloomAndFlaresEffect.cpp b/modules/effects/src/vkcv/effects/BloomAndFlaresEffect.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..78456da209233c67735b3fb1abe1a11ad8be5736
--- /dev/null
+++ b/modules/effects/src/vkcv/effects/BloomAndFlaresEffect.cpp
@@ -0,0 +1,558 @@
+
+#include "vkcv/effects/BloomAndFlaresEffect.hpp"
+
+#include <vkcv/DrawcallRecording.hpp>
+#include <vkcv/PushConstants.hpp>
+
+#include <vkcv/shader/GLSLCompiler.hpp>
+#include <vkcv/asset/asset_loader.hpp>
+
+#include "bloomDownsample.comp.hxx"
+#include "bloomFlaresComposite.comp.hxx"
+#include "bloomUpsample.comp.hxx"
+#include "lensFlares.comp.hxx"
+
+namespace vkcv::effects {
+	
+	static DescriptorBindings getDescriptorBindings() {
+		DescriptorBindings descriptorBindings = {};
+		
+		auto binding_0 = DescriptorBinding {
+				0,
+				DescriptorType::IMAGE_SAMPLED,
+				1,
+				ShaderStage::COMPUTE,
+				false
+		};
+		
+		auto binding_1 = DescriptorBinding {
+				1,
+				DescriptorType::SAMPLER,
+				1,
+				ShaderStage::COMPUTE,
+				false
+		};
+		
+		auto binding_2 = DescriptorBinding{
+				2,
+				DescriptorType::IMAGE_STORAGE,
+				1,
+				ShaderStage::COMPUTE,
+				false
+		};
+		
+		descriptorBindings.insert(std::make_pair(0, binding_0));
+		descriptorBindings.insert(std::make_pair(1, binding_1));
+		descriptorBindings.insert(std::make_pair(2, binding_2));
+		
+		return descriptorBindings;
+	}
+	
+	static DescriptorBindings getCompositeDescriptorBindings(bool advanced) {
+		DescriptorBindings descriptorBindings = {};
+		
+		auto binding_0 = DescriptorBinding {
+				0,
+				DescriptorType::IMAGE_SAMPLED,
+				1,
+				ShaderStage::COMPUTE,
+				false
+		};
+		
+		auto binding_1 = DescriptorBinding {
+				1,
+				DescriptorType::IMAGE_SAMPLED,
+				1,
+				ShaderStage::COMPUTE,
+				false
+		};
+		
+		auto binding_2 = DescriptorBinding{
+				2,
+				DescriptorType::SAMPLER,
+				1,
+				ShaderStage::COMPUTE,
+				false
+		};
+		
+		auto binding_3 = DescriptorBinding{
+				3,
+				DescriptorType::IMAGE_STORAGE,
+				1,
+				ShaderStage::COMPUTE,
+				false
+		};
+		
+		descriptorBindings.insert(std::make_pair(0, binding_0));
+		descriptorBindings.insert(std::make_pair(1, binding_1));
+		descriptorBindings.insert(std::make_pair(2, binding_2));
+		descriptorBindings.insert(std::make_pair(3, binding_3));
+		
+		if (advanced) {
+			auto binding_4 = DescriptorBinding{
+					4,
+					DescriptorType::IMAGE_SAMPLED,
+					1,
+					ShaderStage::COMPUTE,
+					false
+			};
+			
+			auto binding_5 = DescriptorBinding{
+					5,
+					DescriptorType::SAMPLER,
+					1,
+					ShaderStage::COMPUTE,
+					false
+			};
+			
+			auto binding_6 = DescriptorBinding{
+					6,
+					DescriptorType::IMAGE_SAMPLED,
+					1,
+					ShaderStage::COMPUTE,
+					false
+			};
+			
+			descriptorBindings.insert(std::make_pair(4, binding_4));
+			descriptorBindings.insert(std::make_pair(5, binding_5));
+			descriptorBindings.insert(std::make_pair(6, binding_6));
+		}
+		
+		return descriptorBindings;
+	}
+	
+	static ImageHandle loadTexture(Core &core,
+								   const std::string &texturePath) {
+		const auto texture = vkcv::asset::loadTexture(texturePath);
+		
+		Image image = core.createImage(
+				vk::Format::eR8G8B8A8Unorm,
+				texture.width,
+				texture.height
+		);
+		
+		image.fill(texture.data.data(), texture.data.size());
+		return image.getHandle();
+	}
+	
+	static ComputePipelineHandle compilePipeline(Core &core,
+												 const std::string &shaderSource,
+												 const DescriptorSetLayoutHandle &descriptorSetLayout,
+												 bool advanced = false) {
+		vkcv::shader::GLSLCompiler compiler;
+		
+		if (advanced) {
+			compiler.setDefine("ADVANCED_FEATURES", "1");
+		}
+		
+		ShaderProgram program;
+		compiler.compileSource(
+				vkcv::ShaderStage::COMPUTE,
+				shaderSource.c_str(),
+				[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+					program.addShader(shaderStage, path);
+				},
+				""
+		);
+		
+		ComputePipelineHandle pipeline = core.createComputePipeline({
+			program, { descriptorSetLayout }
+		});
+		
+		return pipeline;
+	}
+	
+	BloomAndFlaresEffect::BloomAndFlaresEffect(Core &core,
+											   bool advanced) :
+	Effect(core),
+	m_advanced(advanced),
+	
+	m_downsamplePipeline(),
+	m_upsamplePipeline(),
+	m_lensFlaresPipeline(),
+	m_compositePipeline(),
+	
+	m_downsampleDescriptorSetLayout(m_core.createDescriptorSetLayout(getDescriptorBindings())),
+	m_downsampleDescriptorSets({}),
+	
+	m_upsampleDescriptorSetLayout(m_core.createDescriptorSetLayout(getDescriptorBindings())),
+	m_upsampleDescriptorSets({}),
+	m_flaresDescriptorSets({}),
+	
+	m_lensFlaresDescriptorSetLayout(m_core.createDescriptorSetLayout(getDescriptorBindings())),
+	m_lensFlaresDescriptorSet(m_core.createDescriptorSet(m_lensFlaresDescriptorSetLayout)),
+	
+	m_compositeDescriptorSetLayout(),
+	m_compositeDescriptorSet(),
+	
+	m_blurImage(),
+	m_flaresImage(),
+	
+	m_linearSampler(m_core.createSampler(
+			vkcv::SamplerFilterType::LINEAR,
+			vkcv::SamplerFilterType::LINEAR,
+			vkcv::SamplerMipmapMode::LINEAR,
+			vkcv::SamplerAddressMode::CLAMP_TO_EDGE
+	)),
+	
+	m_radialLutSampler(),
+	
+	m_radialLut(),
+	m_lensDirt(),
+	
+	m_cameraDirection(),
+	m_upsampleLimit(5) {
+		m_downsamplePipeline = compilePipeline(
+				m_core,
+				BLOOMDOWNSAMPLE_COMP_SHADER,
+				m_downsampleDescriptorSetLayout
+		);
+		
+		m_upsamplePipeline = compilePipeline(
+				m_core,
+				BLOOMUPSAMPLE_COMP_SHADER,
+				m_upsampleDescriptorSetLayout
+		);
+		
+		m_lensFlaresPipeline = compilePipeline(
+				m_core,
+				LENSFLARES_COMP_SHADER,
+				m_lensFlaresDescriptorSetLayout
+		);
+		
+		m_compositeDescriptorSetLayout = m_core.createDescriptorSetLayout(
+				getCompositeDescriptorBindings(m_advanced)
+		);
+		
+		m_compositeDescriptorSet = m_core.createDescriptorSet(
+				m_compositeDescriptorSetLayout
+		);
+		
+		m_compositePipeline = compilePipeline(
+				m_core,
+				BLOOMFLARESCOMPOSITE_COMP_SHADER,
+				m_compositeDescriptorSetLayout,
+				m_advanced
+		);
+		
+		if (m_advanced) {
+			m_radialLutSampler = m_core.createSampler(
+					vkcv::SamplerFilterType::LINEAR,
+					vkcv::SamplerFilterType::LINEAR,
+					vkcv::SamplerMipmapMode::LINEAR,
+					vkcv::SamplerAddressMode::REPEAT
+			);
+			
+			m_radialLut = loadTexture(m_core, "assets/RadialLUT.png");
+			m_lensDirt = loadTexture(m_core, "assets/lensDirt.jpg");
+		}
+	}
+	
+	static uint32_t calcDispatchSize(float sampleSizeDim, uint32_t threadGroupWorkRegionDim) {
+		return std::max<uint32_t>(
+				static_cast<uint32_t>(std::ceil(
+						sampleSizeDim / static_cast<float>(threadGroupWorkRegionDim)
+				)),
+				1
+		);
+	}
+	
+	void BloomAndFlaresEffect::recordDownsampling(const CommandStreamHandle &cmdStream,
+												  const ImageHandle &input,
+												  const ImageHandle &sample,
+												  const std::vector<DescriptorSetHandle> &mipDescriptorSets) {
+		const uint32_t sampleWidth = m_core.getImageWidth(sample);
+		const uint32_t sampleHeight = m_core.getImageHeight(sample);
+		
+		m_core.prepareImageForSampling(cmdStream, input);
+		m_core.prepareImageForStorage(cmdStream, m_blurImage);
+		
+		for (uint32_t mipLevel = 0; mipLevel < mipDescriptorSets.size(); mipLevel++) {
+			// mip descriptor writes
+			DescriptorWrites mipDownsampleWrites;
+			
+			if (mipLevel > 0) {
+				mipDownsampleWrites.sampledImageWrites = {SampledImageDescriptorWrite(0, sample, mipLevel - 1, true)};
+			} else {
+				mipDownsampleWrites.sampledImageWrites = {SampledImageDescriptorWrite(0, input)};
+			}
+			
+			mipDownsampleWrites.samplerWrites      = {SamplerDescriptorWrite(1, m_linearSampler)};
+			mipDownsampleWrites.storageImageWrites = {StorageImageDescriptorWrite(2, sample, mipLevel) };
+			
+			m_core.writeDescriptorSet(mipDescriptorSets[mipLevel], mipDownsampleWrites);
+			
+			float mipDivisor = 1.0f;
+			
+			for (uint32_t i = 0; i < mipLevel; i++) {
+				mipDivisor *= 2.0f;
+			}
+			
+			const auto downsampleSizeX  = static_cast<float>(sampleWidth) / mipDivisor;
+			const auto downsampleSizeY  = static_cast<float>(sampleHeight) / mipDivisor;
+			
+			static const uint32_t threadGroupWorkRegionDim = 8;
+			
+			uint32_t dispatch[3];
+			dispatch[0] = calcDispatchSize(downsampleSizeX, threadGroupWorkRegionDim);
+			dispatch[1] = calcDispatchSize(downsampleSizeY, threadGroupWorkRegionDim);
+			dispatch[2] = 1;
+			
+			// mip blur dispatch
+			m_core.recordComputeDispatchToCmdStream(
+					cmdStream,
+					m_downsamplePipeline,
+					dispatch,
+					{ DescriptorSetUsage(0, mipDescriptorSets[mipLevel]) },
+					PushConstants(0)
+			);
+			
+			// image barrier between mips
+			m_core.recordImageMemoryBarrier(cmdStream, sample);
+		}
+	}
+	
+	void BloomAndFlaresEffect::recordUpsampling(const CommandStreamHandle &cmdStream,
+												const ImageHandle &sample,
+												const std::vector<DescriptorSetHandle> &mipDescriptorSets) {
+		// upsample dispatch
+		m_core.prepareImageForStorage(cmdStream, sample);
+		
+		const uint32_t sampleWidth = m_core.getImageWidth(sample);
+		const uint32_t sampleHeight = m_core.getImageHeight(sample);
+		
+		// upsample dispatch for each mip map
+		for(uint32_t mipLevel = mipDescriptorSets.size(); mipLevel > 0; mipLevel--) {
+			// mip descriptor writes
+			DescriptorWrites mipUpsampleWrites;
+			mipUpsampleWrites.sampledImageWrites = {SampledImageDescriptorWrite(0, sample, mipLevel, true)};
+			mipUpsampleWrites.samplerWrites      = {SamplerDescriptorWrite(1, m_linearSampler)};
+			mipUpsampleWrites.storageImageWrites = {StorageImageDescriptorWrite(2, sample, mipLevel - 1) };
+			
+			m_core.writeDescriptorSet(mipDescriptorSets[mipLevel - 1], mipUpsampleWrites);
+			
+			float mipDivisor = 1.0f;
+			
+			for (uint32_t i = 0; i < mipLevel - 1; i++) {
+				mipDivisor *= 2.0f;
+			}
+			
+			const auto upsampleSizeX  = static_cast<float>(sampleWidth) / mipDivisor;
+			const auto upsampleSizeY  = static_cast<float>(sampleHeight) / mipDivisor;
+			
+			static const uint32_t threadGroupWorkRegionDim = 8;
+			
+			uint32_t dispatch[3];
+			dispatch[0] = calcDispatchSize(upsampleSizeX, threadGroupWorkRegionDim);
+			dispatch[1] = calcDispatchSize(upsampleSizeY, threadGroupWorkRegionDim);
+			dispatch[2] = 1;
+			
+			m_core.recordComputeDispatchToCmdStream(
+					cmdStream,
+					m_upsamplePipeline,
+					dispatch,
+					{ DescriptorSetUsage(0, mipDescriptorSets[mipLevel - 1]) },
+					PushConstants(0)
+			);
+			
+			// image barrier between mips
+			m_core.recordImageMemoryBarrier(cmdStream, sample);
+		}
+	}
+	
+	void BloomAndFlaresEffect::recordLensFlares(const CommandStreamHandle &cmdStream,
+												uint32_t mipLevel) {
+		// lens feature generation descriptor writes
+		m_core.prepareImageForSampling(cmdStream, m_blurImage);
+		m_core.prepareImageForStorage(cmdStream, m_flaresImage);
+		
+		const uint32_t flaresWidth = m_core.getImageWidth(m_flaresImage);
+		const uint32_t flaresHeight = m_core.getImageHeight(m_flaresImage);
+		
+		DescriptorWrites lensFlaresWrites;
+		lensFlaresWrites.sampledImageWrites = {SampledImageDescriptorWrite(0, m_blurImage, 0)};
+		lensFlaresWrites.samplerWrites = {SamplerDescriptorWrite(1, m_linearSampler)};
+		lensFlaresWrites.storageImageWrites = {StorageImageDescriptorWrite(2, m_flaresImage, mipLevel)};
+		
+		m_core.writeDescriptorSet(m_lensFlaresDescriptorSet, lensFlaresWrites);
+		
+		const auto sampleSizeX = static_cast<float>(flaresWidth);
+		const auto sampleSizeY = static_cast<float>(flaresHeight);
+		
+		float mipDivisor = 1.0f;
+		
+		for (uint32_t i = 0; i < mipLevel - 1; i++) {
+			mipDivisor *= 2.0f;
+		}
+		
+		static const uint32_t threadGroupWorkRegionDim = 8.0f;
+		
+		// lens feature generation dispatch
+		uint32_t dispatch[3];
+		dispatch[0] = calcDispatchSize(sampleSizeX / mipDivisor, threadGroupWorkRegionDim);
+		dispatch[1] = calcDispatchSize(sampleSizeY / mipDivisor, threadGroupWorkRegionDim);
+		dispatch[2] = 1;
+		
+		m_core.recordComputeDispatchToCmdStream(
+				cmdStream,
+				m_lensFlaresPipeline,
+				dispatch,
+				{ DescriptorSetUsage(0, m_lensFlaresDescriptorSet) },
+				PushConstants(0)
+		);
+	}
+	
+	void BloomAndFlaresEffect::recordComposition(const CommandStreamHandle &cmdStream,
+												 const ImageHandle &output) {
+		const uint32_t outputWidth = m_core.getImageWidth(output);
+		const uint32_t outputHeight = m_core.getImageHeight(output);
+		
+		m_core.prepareImageForSampling(cmdStream, m_blurImage);
+		m_core.prepareImageForSampling(cmdStream, m_flaresImage);
+		m_core.prepareImageForStorage(cmdStream, output);
+		
+		// bloom composite descriptor write
+		vkcv::DescriptorWrites compositeWrites;
+		
+		if (m_advanced) {
+			compositeWrites.sampledImageWrites = {
+					SampledImageDescriptorWrite(0, m_blurImage),
+					SampledImageDescriptorWrite(1, m_flaresImage),
+					SampledImageDescriptorWrite(4, m_radialLut),
+					SampledImageDescriptorWrite(6, m_lensDirt)
+			};
+			
+			compositeWrites.samplerWrites = {
+					SamplerDescriptorWrite(2, m_linearSampler),
+					SamplerDescriptorWrite(5, m_radialLutSampler)
+			};
+			
+			compositeWrites.storageImageWrites = {
+					StorageImageDescriptorWrite(3, output)
+			};
+		} else {
+			compositeWrites.sampledImageWrites = {
+					SampledImageDescriptorWrite(0, m_blurImage),
+					SampledImageDescriptorWrite(1, m_flaresImage)
+			};
+			
+			compositeWrites.samplerWrites = {
+					SamplerDescriptorWrite(2, m_linearSampler)
+			};
+			
+			compositeWrites.storageImageWrites = {
+					StorageImageDescriptorWrite(3, output)
+			};
+		}
+		
+		m_core.writeDescriptorSet(m_compositeDescriptorSet, compositeWrites);
+		
+		const auto sampleWidth = static_cast<float>(outputWidth);
+		const auto sampleHeight = static_cast<float>(outputHeight);
+		
+		static const uint32_t threadGroupWorkRegionDim = 8;
+		
+		uint32_t dispatch[3];
+		dispatch[0] = calcDispatchSize(sampleWidth, threadGroupWorkRegionDim);
+		dispatch[1] = calcDispatchSize(sampleHeight, threadGroupWorkRegionDim);
+		dispatch[2] = 1;
+		
+		PushConstants pushConstants (sizeof(m_cameraDirection));
+		pushConstants.appendDrawcall(m_cameraDirection);
+		
+		// bloom composite dispatch
+		m_core.recordComputeDispatchToCmdStream(
+				cmdStream,
+				m_compositePipeline,
+				dispatch,
+				{ DescriptorSetUsage(0, m_compositeDescriptorSet) },
+				m_advanced? pushConstants : PushConstants(0)
+		);
+	}
+	
+	void BloomAndFlaresEffect::recordEffect(const CommandStreamHandle &cmdStream,
+											const ImageHandle &input,
+											const ImageHandle &output) {
+		m_core.recordBeginDebugLabel(cmdStream, "vkcv::post_processing::BloomAndFlaresEffect", {
+				0.0f, 1.0f, 1.0f, 1.0f
+		});
+		
+		const uint32_t halfWidth = static_cast<uint32_t>(std::ceil(
+				static_cast<float>(m_core.getImageWidth(output)) * 0.5f
+		));
+		
+		const uint32_t halfHeight = static_cast<uint32_t>(std::ceil(
+				static_cast<float>(m_core.getImageHeight(output)) * 0.5f
+		));
+		
+		if ((!m_blurImage) ||
+			(halfWidth != m_core.getImageWidth(m_blurImage)) ||
+			(halfHeight != m_core.getImageHeight(m_blurImage))) {
+			m_blurImage = m_core.createImage(
+					m_core.getImageFormat(output),
+					halfWidth,
+					halfHeight,
+					1,
+					true,
+					true
+			).getHandle();
+			
+			m_downsampleDescriptorSets.clear();
+			m_upsampleDescriptorSets.clear();
+			
+			const uint32_t mipLevels = m_core.getImageMipLevels(m_blurImage);
+			
+			for (uint32_t i = 0; i < mipLevels; i++) {
+				m_downsampleDescriptorSets.push_back(m_core.createDescriptorSet(
+						m_downsampleDescriptorSetLayout
+				));
+			}
+			
+			for (uint32_t i = 0; i < std::min<uint32_t>(m_upsampleLimit, mipLevels); i++) {
+				m_upsampleDescriptorSets.push_back(m_core.createDescriptorSet(
+						m_upsampleDescriptorSetLayout
+				));
+			}
+		}
+		
+		if ((!m_flaresImage) ||
+			(halfWidth != m_core.getImageWidth(m_flaresImage)) ||
+			(halfHeight != m_core.getImageHeight(m_flaresImage))) {
+			m_flaresImage = m_core.createImage(
+					m_core.getImageFormat(output),
+					halfWidth,
+					halfHeight,
+					1,
+					true,
+					true
+			).getHandle();
+			
+			m_flaresDescriptorSets.clear();
+			
+			const uint32_t mipLevels = m_core.getImageMipLevels(m_flaresImage);
+			
+			for (uint32_t i = 0; i < std::min<uint32_t>(2, mipLevels); i++) {
+				m_flaresDescriptorSets.push_back(m_core.createDescriptorSet(
+						m_upsampleDescriptorSetLayout
+				));
+			}
+		}
+		
+		recordDownsampling(cmdStream, input, m_blurImage, m_downsampleDescriptorSets);
+		recordUpsampling(cmdStream, m_blurImage, m_upsampleDescriptorSets);
+		recordLensFlares(cmdStream, m_flaresDescriptorSets.size());
+		recordUpsampling(cmdStream, m_flaresImage, m_flaresDescriptorSets);
+		recordComposition(cmdStream, output);
+		
+		m_core.recordEndDebugLabel(cmdStream);
+	}
+	
+	void BloomAndFlaresEffect::updateCameraDirection(const camera::Camera &camera) {
+		m_cameraDirection = camera.getFront();
+	}
+	
+	void BloomAndFlaresEffect::setUpsamplingLimit(uint32_t limit) {
+		m_upsampleLimit = limit;
+	}
+	
+}
diff --git a/modules/effects/src/vkcv/effects/Effect.cpp b/modules/effects/src/vkcv/effects/Effect.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..3246db15144bcafcc253df34f358594c85868647
--- /dev/null
+++ b/modules/effects/src/vkcv/effects/Effect.cpp
@@ -0,0 +1,8 @@
+
+#include "vkcv/effects/Effect.hpp"
+
+namespace vkcv::effects {
+	
+	Effect::Effect(Core &core) : m_core(core) {}
+	
+}
diff --git a/projects/particle_simulation/CMakeLists.txt b/projects/particle_simulation/CMakeLists.txt
index 2ff6aaf62c6a9842c3d1b9669286becfd79d92ca..3adede6e25f88208824144b63a866ccabdfc5be9 100644
--- a/projects/particle_simulation/CMakeLists.txt
+++ b/projects/particle_simulation/CMakeLists.txt
@@ -14,14 +14,23 @@ add_executable(particle_simulation
 		src/ParticleSystem.hpp 
 		src/ParticleSystem.cpp
 		src/Particle.hpp 
-		src/Particle.cpp
-		src/BloomAndFlares.hpp
-		src/BloomAndFlares.cpp)
+		src/Particle.cpp)
 
 fix_project(particle_simulation)
 
 # including headers of dependencies and the VkCV framework
-target_include_directories(particle_simulation SYSTEM BEFORE PRIVATE ${vkcv_include} ${vkcv_includes} ${vkcv_testing_include} ${vkcv_camera_include} ${vkcv_shader_compiler_include})
+target_include_directories(particle_simulation SYSTEM BEFORE PRIVATE
+		${vkcv_include}
+		${vkcv_includes}
+		${vkcv_testing_include}
+		${vkcv_camera_include}
+		${vkcv_shader_compiler_include}
+		${vkcv_effects_include})
 
 # linking with libraries from all dependencies and the VkCV framework
-target_link_libraries(particle_simulation vkcv vkcv_testing vkcv_camera vkcv_shader_compiler)
+target_link_libraries(particle_simulation
+		vkcv
+		vkcv_testing
+		vkcv_camera
+		vkcv_shader_compiler
+		vkcv_effects)
diff --git a/projects/particle_simulation/shaders/bloom/composite.comp b/projects/particle_simulation/shaders/bloom/composite.comp
deleted file mode 100644
index 87b5ddb975106232d1cd3b6e5b8dc7e623dd0b59..0000000000000000000000000000000000000000
--- a/projects/particle_simulation/shaders/bloom/composite.comp
+++ /dev/null
@@ -1,38 +0,0 @@
-#version 450
-#extension GL_ARB_separate_shader_objects : enable
-
-layout(set=0, binding=0) uniform texture2D                          blurImage;
-layout(set=0, binding=1) uniform texture2D                          lensImage;
-layout(set=0, binding=2) uniform sampler                            linearSampler;
-layout(set=0, binding=3, r11f_g11f_b10f) uniform image2D            colorBuffer;
-
-layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
-
-
-void main()
-{
-    if(any(greaterThanEqual(gl_GlobalInvocationID.xy, imageSize(colorBuffer)))){
-        return;
-    }
-
-    ivec2 pixel_coord   = ivec2(gl_GlobalInvocationID.xy);
-    vec2  pixel_size    = vec2(1.0f) / textureSize(sampler2D(blurImage, linearSampler), 0);
-    vec2  UV            = pixel_coord.xy * pixel_size;
-
-    vec4 composite_color = vec4(0.0f);
-
-    vec3 blur_color   = texture(sampler2D(blurImage, linearSampler), UV).rgb;
-    vec3 lens_color   = texture(sampler2D(lensImage, linearSampler), UV).rgb;
-    vec3 main_color   = imageLoad(colorBuffer, pixel_coord).rgb;
-
-    // composite blur and lens features
-    float bloom_weight = 0.01f;
-    float lens_weight  = 0.f;
-    float main_weight = 1 - (bloom_weight + lens_weight);
-
-    composite_color.rgb = blur_color * bloom_weight +
-                          lens_color * lens_weight  +
-                          main_color * main_weight;
-
-    imageStore(colorBuffer, pixel_coord, composite_color);
-}
\ No newline at end of file
diff --git a/projects/particle_simulation/shaders/bloom/downsample.comp b/projects/particle_simulation/shaders/bloom/downsample.comp
deleted file mode 100644
index 2ab00c7c92798769153634f3479c5b7f3fb61d94..0000000000000000000000000000000000000000
--- a/projects/particle_simulation/shaders/bloom/downsample.comp
+++ /dev/null
@@ -1,76 +0,0 @@
-#version 450
-#extension GL_ARB_separate_shader_objects : enable
-
-layout(set=0, binding=0) uniform texture2D                          inBlurImage;
-layout(set=0, binding=1) uniform sampler                            inImageSampler;
-layout(set=0, binding=2, r11f_g11f_b10f) uniform writeonly image2D  outBlurImage;
-
-layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
-
-
-void main()
-{
-    if(any(greaterThanEqual(gl_GlobalInvocationID.xy, imageSize(outBlurImage)))){
-        return;
-    }
-
-    ivec2 pixel_coord   = ivec2(gl_GlobalInvocationID.xy);
-    vec2  pixel_size    = vec2(1.0f) / imageSize(outBlurImage);
-    vec2  UV            = pixel_coord.xy * pixel_size;
-    vec2  UV_offset     = UV + 0.5f * pixel_size;
-
-    vec2 color_fetches[13] = {
-        // center neighbourhood (RED)
-        vec2(-1,  1), // LT
-        vec2(-1, -1), // LB
-        vec2( 1, -1), // RB
-        vec2( 1,  1), // RT
-
-        vec2(-2, 2), // LT
-        vec2( 0, 2), // CT
-        vec2( 2, 2), // RT
-
-        vec2(0 ,-2), // LC
-        vec2(0 , 0), // CC
-        vec2(2,  0), // CR
-
-        vec2(-2, -2), // LB
-        vec2(0 , -2), // CB
-        vec2(2 , -2)  // RB
-    };
-
-    float color_weights[13] = {
-        // 0.5f
-        1.f/8.f,
-        1.f/8.f,
-        1.f/8.f,
-        1.f/8.f,
-
-        // 0.125f
-        1.f/32.f,
-        1.f/16.f,
-        1.f/32.f,
-
-        // 0.25f
-        1.f/16.f,
-        1.f/8.f,
-        1.f/16.f,
-
-        // 0.125f
-        1.f/32.f,
-        1.f/16.f,
-        1.f/32.f
-    };
-
-    vec3 sampled_color = vec3(0.0f);
-
-    for(uint i = 0; i < 13; i++)
-    {
-        vec2 color_fetch = UV_offset + color_fetches[i] * pixel_size;
-        vec3 color = texture(sampler2D(inBlurImage, inImageSampler), color_fetch).rgb;
-        color *= color_weights[i];
-        sampled_color += color;
-    }
-
-    imageStore(outBlurImage, pixel_coord, vec4(sampled_color, 1.f));
-}
\ No newline at end of file
diff --git a/projects/particle_simulation/shaders/bloom/lensFlares.comp b/projects/particle_simulation/shaders/bloom/lensFlares.comp
deleted file mode 100644
index ce27d8850b709f61332d467914ddc944dc63109f..0000000000000000000000000000000000000000
--- a/projects/particle_simulation/shaders/bloom/lensFlares.comp
+++ /dev/null
@@ -1,109 +0,0 @@
-#version 450
-#extension GL_ARB_separate_shader_objects : enable
-
-layout(set=0, binding=0) uniform texture2D                          blurBuffer;
-layout(set=0, binding=1) uniform sampler                            linearSampler;
-layout(set=0, binding=2, r11f_g11f_b10f) uniform image2D            lensBuffer;
-
-layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
-
-vec3 sampleColorChromaticAberration(vec2 _uv)
-{
-    vec2 toCenter = (vec2(0.5) - _uv);
-
-    vec3    colorScales     = vec3(-1, 0, 1);
-    float   aberrationScale = 0.1;
-    vec3 scaleFactors = colorScales * aberrationScale;
-
-    float r = texture(sampler2D(blurBuffer, linearSampler), _uv + toCenter * scaleFactors.r).r;
-    float g = texture(sampler2D(blurBuffer, linearSampler), _uv + toCenter * scaleFactors.g).g;
-    float b = texture(sampler2D(blurBuffer, linearSampler), _uv + toCenter * scaleFactors.b).b;
-    return vec3(r, g, b);
-}
-
-// _uv assumed to be flipped UV coordinates!
-vec3 ghost_vectors(vec2 _uv)
-{
-    vec2 ghost_vec = (vec2(0.5f) - _uv);
-
-    const uint c_ghost_count = 64;
-    const float c_ghost_spacing = length(ghost_vec) / c_ghost_count;
-
-    ghost_vec *= c_ghost_spacing;
-
-    vec3 ret_color = vec3(0.0f);
-
-    for (uint i = 0; i < c_ghost_count; ++i)
-    {
-        // sample scene color
-        vec2 s_uv = fract(_uv + ghost_vec * vec2(i));
-        vec3 s = sampleColorChromaticAberration(s_uv);
-
-        // tint/weight
-        float d = distance(s_uv, vec2(0.5));
-        float weight = 1.0f - smoothstep(0.0f, 0.75f, d);
-        s *= weight;
-
-        ret_color += s;
-    }
-
-    ret_color /= c_ghost_count;
-    return ret_color;
-}
-
-vec3 halo(vec2 _uv)
-{
-    const float c_aspect_ratio = float(imageSize(lensBuffer).x) / float(imageSize(lensBuffer).y);
-    const float c_radius = 0.6f;
-    const float c_halo_thickness = 0.1f;
-
-    vec2 halo_vec = vec2(0.5) - _uv;
-    //halo_vec.x /= c_aspect_ratio;
-    halo_vec = normalize(halo_vec);
-    //halo_vec.x *= c_aspect_ratio;
-
-
-    //vec2 w_uv = (_uv - vec2(0.5, 0.0)) * vec2(c_aspect_ratio, 1.0) + vec2(0.5, 0.0);
-    vec2 w_uv = _uv;
-    float d = distance(w_uv, vec2(0.5)); // distance to center
-
-    float distance_to_halo = abs(d - c_radius);
-
-    float halo_weight = 0.0f;
-    if(abs(d - c_radius) <= c_halo_thickness)
-    {
-        float distance_to_border = c_halo_thickness - distance_to_halo;
-        halo_weight = distance_to_border / c_halo_thickness;
-
-        //halo_weight = clamp((halo_weight / 0.4f), 0.0f, 1.0f);
-        halo_weight = pow(halo_weight, 2.0f);
-
-
-        //halo_weight = 1.0f;
-    }
-
-    return sampleColorChromaticAberration(_uv + halo_vec) * halo_weight;
-}
-
-
-
-void main()
-{
-    if(any(greaterThanEqual(gl_GlobalInvocationID.xy, imageSize(lensBuffer)))){
-        return;
-    }
-
-    ivec2 pixel_coord   = ivec2(gl_GlobalInvocationID.xy);
-    vec2  pixel_size    = vec2(1.0f) / imageSize(lensBuffer);
-    vec2  UV            = pixel_coord.xy * pixel_size;
-
-    vec2 flipped_UV = vec2(1.0f) - UV;
-
-    vec3 color = vec3(0.0f);
-
-    color += ghost_vectors(flipped_UV);
-    color += halo(UV);
-    color  *= 0.5f;
-
-    imageStore(lensBuffer, pixel_coord, vec4(color, 0.0f));
-}
\ No newline at end of file
diff --git a/projects/particle_simulation/shaders/bloom/upsample.comp b/projects/particle_simulation/shaders/bloom/upsample.comp
deleted file mode 100644
index 0ddeedb5b5af9e476dc19012fed6430544006c0e..0000000000000000000000000000000000000000
--- a/projects/particle_simulation/shaders/bloom/upsample.comp
+++ /dev/null
@@ -1,45 +0,0 @@
-#version 450
-#extension GL_ARB_separate_shader_objects : enable
-
-layout(set=0, binding=0) uniform texture2D                          inUpsampleImage;
-layout(set=0, binding=1) uniform sampler                            inImageSampler;
-layout(set=0, binding=2, r11f_g11f_b10f) uniform image2D  outUpsampleImage;
-
-layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
-
-void main()
-{
-    if(any(greaterThanEqual(gl_GlobalInvocationID.xy, imageSize(outUpsampleImage)))){
-        return;
-    }
-
-
-    ivec2 pixel_coord   = ivec2(gl_GlobalInvocationID.xy);
-    vec2  pixel_size    = vec2(1.0f) / imageSize(outUpsampleImage);
-    vec2  UV            = pixel_coord.xy * pixel_size;
-
-    const float gauss_kernel[3] = {1.f, 2.f, 1.f};
-    const float gauss_weight = 16.f;
-
-    vec3 sampled_color = vec3(0.f);
-
-    for(int i = -1; i <= 1; i++)
-    {
-        for(int j = -1; j <= 1; j++)
-        {
-            vec2 sample_location = UV + vec2(j, i) * pixel_size;
-            vec3 color = texture(sampler2D(inUpsampleImage, inImageSampler), sample_location).rgb;
-            color *= gauss_kernel[j+1];
-            color *= gauss_kernel[i+1];
-            color /= gauss_weight;
-
-            sampled_color += color;
-        }
-    }
-
-    //vec3 prev_color = imageLoad(outUpsampleImage, pixel_coord).rgb;
-    //float bloomRimStrength = 0.75f; // adjust this to change strength of bloom
-    //sampled_color = mix(prev_color, sampled_color, bloomRimStrength);
-
-    imageStore(outUpsampleImage, pixel_coord, vec4(sampled_color, 1.f));
-}
\ No newline at end of file
diff --git a/projects/particle_simulation/src/BloomAndFlares.cpp b/projects/particle_simulation/src/BloomAndFlares.cpp
deleted file mode 100644
index 6ab0a8deff3d5fe906567562cb86d75a1cc2c09b..0000000000000000000000000000000000000000
--- a/projects/particle_simulation/src/BloomAndFlares.cpp
+++ /dev/null
@@ -1,293 +0,0 @@
-#include "BloomAndFlares.hpp"
-#include <vkcv/shader/GLSLCompiler.hpp>
-
-BloomAndFlares::BloomAndFlares(
-        vkcv::Core *p_Core,
-        vk::Format colorBufferFormat,
-        uint32_t width,
-        uint32_t height) :
-
-        p_Core(p_Core),
-        m_ColorBufferFormat(colorBufferFormat),
-        m_Width(width),
-        m_Height(height),
-        m_LinearSampler(p_Core->createSampler(vkcv::SamplerFilterType::LINEAR,
-                                              vkcv::SamplerFilterType::LINEAR,
-                                              vkcv::SamplerMipmapMode::LINEAR,
-                                              vkcv::SamplerAddressMode::CLAMP_TO_EDGE)),
-        m_Blur(p_Core->createImage(colorBufferFormat, width, height, 1, true, true, false)),
-        m_LensFeatures(p_Core->createImage(colorBufferFormat, width, height, 1, false, true, false))
-{
-    vkcv::shader::GLSLCompiler compiler;
-
-    // DOWNSAMPLE
-    vkcv::ShaderProgram dsProg;
-    compiler.compile(vkcv::ShaderStage::COMPUTE,
-                     "shaders/bloom/downsample.comp",
-                     [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path)
-                     {
-                         dsProg.addShader(shaderStage, path);
-                     });
-    for(uint32_t mipLevel = 0; mipLevel < m_Blur.getMipCount(); mipLevel++)
-    {
-        m_DownsampleDescSetLayouts.push_back(
-                p_Core->createDescriptorSetLayout(dsProg.getReflectedDescriptors().at(0)));
-
-		m_DownsampleDescSets.push_back(
-		        p_Core->createDescriptorSet(m_DownsampleDescSetLayouts.back()));
-    }
-
-    m_DownsamplePipe = p_Core->createComputePipeline({
-        dsProg, m_DownsampleDescSetLayouts
-	});
-
-    // UPSAMPLE
-    vkcv::ShaderProgram usProg;
-    compiler.compile(vkcv::ShaderStage::COMPUTE,
-                     "shaders/bloom/upsample.comp",
-                     [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path)
-                     {
-                         usProg.addShader(shaderStage, path);
-                     });
-    for(uint32_t mipLevel = 0; mipLevel < m_Blur.getMipCount(); mipLevel++)
-    {
-        m_UpsampleDescSetLayouts.push_back(
-                p_Core->createDescriptorSetLayout(usProg.getReflectedDescriptors().at(0)));
-        m_UpsampleDescSets.push_back(
-                p_Core->createDescriptorSet(m_UpsampleDescSetLayouts.back()));
-    }
-
-    m_UpsamplePipe = p_Core->createComputePipeline({
-            usProg, m_UpsampleDescSetLayouts
-	});
-
-    // LENS FEATURES
-    vkcv::ShaderProgram lensProg;
-    compiler.compile(vkcv::ShaderStage::COMPUTE,
-                     "shaders/bloom/lensFlares.comp",
-                     [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path)
-                     {
-                         lensProg.addShader(shaderStage, path);
-                     });
-
-    m_LensFlareDescSetLayout = p_Core->createDescriptorSetLayout(lensProg.getReflectedDescriptors().at(0));
-    m_LensFlareDescSet = p_Core->createDescriptorSet(m_LensFlareDescSetLayout);
-    m_LensFlarePipe = p_Core->createComputePipeline({
-            lensProg, { m_LensFlareDescSetLayout }
-	});
-
-    // COMPOSITE
-    vkcv::ShaderProgram compProg;
-    compiler.compile(vkcv::ShaderStage::COMPUTE,
-                     "shaders/bloom/composite.comp",
-                     [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path)
-                     {
-                         compProg.addShader(shaderStage, path);
-                     });
-
-    m_CompositeDescSetLayout = p_Core->createDescriptorSetLayout(compProg.getReflectedDescriptors().at(0));
-    m_CompositeDescSet = p_Core->createDescriptorSet(m_CompositeDescSetLayout);
-    m_CompositePipe = p_Core->createComputePipeline({
-            compProg, { m_CompositeDescSetLayout }
-	});
-}
-
-void BloomAndFlares::execDownsamplePipe(const vkcv::CommandStreamHandle &cmdStream,
-                                        const vkcv::ImageHandle &colorAttachment)
-{
-    auto dispatchCountX  = static_cast<float>(m_Width)  / 8.0f;
-    auto dispatchCountY = static_cast<float>(m_Height) / 8.0f;
-    // blur dispatch
-    uint32_t initialDispatchCount[3] = {
-            static_cast<uint32_t>(glm::ceil(dispatchCountX)),
-            static_cast<uint32_t>(glm::ceil(dispatchCountY)),
-            1
-    };
-
-    // downsample dispatch of original color attachment
-    p_Core->prepareImageForSampling(cmdStream, colorAttachment);
-    p_Core->prepareImageForStorage(cmdStream, m_Blur.getHandle());
-
-    vkcv::DescriptorWrites initialDownsampleWrites;
-    initialDownsampleWrites.sampledImageWrites = {vkcv::SampledImageDescriptorWrite(0, colorAttachment)};
-    initialDownsampleWrites.samplerWrites      = {vkcv::SamplerDescriptorWrite(1, m_LinearSampler)};
-    initialDownsampleWrites.storageImageWrites = {vkcv::StorageImageDescriptorWrite(2, m_Blur.getHandle(), 0) };
-    p_Core->writeDescriptorSet(m_DownsampleDescSets[0], initialDownsampleWrites);
-
-    p_Core->recordComputeDispatchToCmdStream(
-            cmdStream,
-            m_DownsamplePipe,
-            initialDispatchCount,
-            {vkcv::DescriptorSetUsage(0, m_DownsampleDescSets[0])},
-            vkcv::PushConstants(0));
-
-    // downsample dispatches of blur buffer's mip maps
-    float mipDispatchCountX = dispatchCountX;
-    float mipDispatchCountY = dispatchCountY;
-    for(uint32_t mipLevel = 1; mipLevel < std::min((uint32_t)m_DownsampleDescSets.size(), m_Blur.getMipCount()); mipLevel++)
-    {
-        // mip descriptor writes
-        vkcv::DescriptorWrites mipDescriptorWrites;
-        mipDescriptorWrites.sampledImageWrites = {vkcv::SampledImageDescriptorWrite(0, m_Blur.getHandle(), mipLevel - 1, true)};
-        mipDescriptorWrites.samplerWrites      = {vkcv::SamplerDescriptorWrite(1, m_LinearSampler)};
-        mipDescriptorWrites.storageImageWrites = {vkcv::StorageImageDescriptorWrite(2, m_Blur.getHandle(), mipLevel) };
-        p_Core->writeDescriptorSet(m_DownsampleDescSets[mipLevel], mipDescriptorWrites);
-
-        // mip dispatch calculation
-        mipDispatchCountX  /= 2.0f;
-        mipDispatchCountY /= 2.0f;
-
-        uint32_t mipDispatchCount[3] = {
-                static_cast<uint32_t>(glm::ceil(mipDispatchCountX)),
-                static_cast<uint32_t>(glm::ceil(mipDispatchCountY)),
-                1
-        };
-
-        if(mipDispatchCount[0] == 0)
-            mipDispatchCount[0] = 1;
-        if(mipDispatchCount[1] == 0)
-            mipDispatchCount[1] = 1;
-
-        // mip blur dispatch
-        p_Core->recordComputeDispatchToCmdStream(
-                cmdStream,
-                m_DownsamplePipe,
-                mipDispatchCount,
-                {vkcv::DescriptorSetUsage(0, m_DownsampleDescSets[mipLevel])},
-                vkcv::PushConstants(0));
-
-        // image barrier between mips
-        p_Core->recordImageMemoryBarrier(cmdStream, m_Blur.getHandle());
-    }
-}
-
-void BloomAndFlares::execUpsamplePipe(const vkcv::CommandStreamHandle &cmdStream)
-{
-    // upsample dispatch
-    p_Core->prepareImageForStorage(cmdStream, m_Blur.getHandle());
-
-    const uint32_t upsampleMipLevels = std::min(
-    		static_cast<uint32_t>(m_UpsampleDescSets.size() - 1),
-    		static_cast<uint32_t>(3)
-	);
-
-    // upsample dispatch for each mip map
-    for(uint32_t mipLevel = upsampleMipLevels; mipLevel > 0; mipLevel--)
-    {
-        // mip descriptor writes
-        vkcv::DescriptorWrites mipUpsampleWrites;
-        mipUpsampleWrites.sampledImageWrites = {vkcv::SampledImageDescriptorWrite(0, m_Blur.getHandle(), mipLevel, true)};
-        mipUpsampleWrites.samplerWrites      = {vkcv::SamplerDescriptorWrite(1, m_LinearSampler)};
-        mipUpsampleWrites.storageImageWrites = {vkcv::StorageImageDescriptorWrite(2, m_Blur.getHandle(), mipLevel - 1) };
-        p_Core->writeDescriptorSet(m_UpsampleDescSets[mipLevel], mipUpsampleWrites);
-
-        auto mipDivisor = glm::pow(2.0f, static_cast<float>(mipLevel) - 1.0f);
-
-        auto upsampleDispatchX  = static_cast<float>(m_Width) / mipDivisor;
-        auto upsampleDispatchY = static_cast<float>(m_Height) / mipDivisor;
-        upsampleDispatchX /= 8.0f;
-        upsampleDispatchY /= 8.0f;
-
-        const uint32_t upsampleDispatchCount[3] = {
-                static_cast<uint32_t>(glm::ceil(upsampleDispatchX)),
-                static_cast<uint32_t>(glm::ceil(upsampleDispatchY)),
-                1
-        };
-
-        p_Core->recordComputeDispatchToCmdStream(
-                cmdStream,
-                m_UpsamplePipe,
-                upsampleDispatchCount,
-                {vkcv::DescriptorSetUsage(0, m_UpsampleDescSets[mipLevel])},
-                vkcv::PushConstants(0)
-        );
-        // image barrier between mips
-        p_Core->recordImageMemoryBarrier(cmdStream, m_Blur.getHandle());
-    }
-}
-
-void BloomAndFlares::execLensFeaturePipe(const vkcv::CommandStreamHandle &cmdStream)
-{
-    // lens feature generation descriptor writes
-    p_Core->prepareImageForSampling(cmdStream, m_Blur.getHandle());
-    p_Core->prepareImageForStorage(cmdStream, m_LensFeatures.getHandle());
-
-    vkcv::DescriptorWrites lensFeatureWrites;
-    lensFeatureWrites.sampledImageWrites = {vkcv::SampledImageDescriptorWrite(0, m_Blur.getHandle(), 0)};
-    lensFeatureWrites.samplerWrites = {vkcv::SamplerDescriptorWrite(1, m_LinearSampler)};
-    lensFeatureWrites.storageImageWrites = {vkcv::StorageImageDescriptorWrite(2, m_LensFeatures.getHandle(), 0)};
-    p_Core->writeDescriptorSet(m_LensFlareDescSet, lensFeatureWrites);
-
-    auto dispatchCountX  = static_cast<float>(m_Width)  / 8.0f;
-    auto dispatchCountY = static_cast<float>(m_Height) / 8.0f;
-    // lens feature generation dispatch
-    uint32_t lensFeatureDispatchCount[3] = {
-            static_cast<uint32_t>(glm::ceil(dispatchCountX)),
-            static_cast<uint32_t>(glm::ceil(dispatchCountY)),
-            1
-    };
-    p_Core->recordComputeDispatchToCmdStream(
-            cmdStream,
-            m_LensFlarePipe,
-            lensFeatureDispatchCount,
-            {vkcv::DescriptorSetUsage(0, m_LensFlareDescSet)},
-            vkcv::PushConstants(0));
-}
-
-void BloomAndFlares::execCompositePipe(const vkcv::CommandStreamHandle &cmdStream,
-                                       const vkcv::ImageHandle &colorAttachment)
-{
-    p_Core->prepareImageForSampling(cmdStream, m_Blur.getHandle());
-    p_Core->prepareImageForSampling(cmdStream, m_LensFeatures.getHandle());
-    p_Core->prepareImageForStorage(cmdStream, colorAttachment);
-
-    // bloom composite descriptor write
-    vkcv::DescriptorWrites compositeWrites;
-    compositeWrites.sampledImageWrites = {vkcv::SampledImageDescriptorWrite(0, m_Blur.getHandle()),
-                                          vkcv::SampledImageDescriptorWrite(1, m_LensFeatures.getHandle())};
-    compositeWrites.samplerWrites = {vkcv::SamplerDescriptorWrite(2, m_LinearSampler)};
-    compositeWrites.storageImageWrites = {vkcv::StorageImageDescriptorWrite(3, colorAttachment)};
-    p_Core->writeDescriptorSet(m_CompositeDescSet, compositeWrites);
-
-    float dispatchCountX = static_cast<float>(m_Width)  / 8.0f;
-    float dispatchCountY = static_cast<float>(m_Height) / 8.0f;
-
-    uint32_t compositeDispatchCount[3] = {
-            static_cast<uint32_t>(glm::ceil(dispatchCountX)),
-            static_cast<uint32_t>(glm::ceil(dispatchCountY)),
-            1
-    };
-
-    // bloom composite dispatch
-    p_Core->recordComputeDispatchToCmdStream(
-            cmdStream,
-            m_CompositePipe,
-            compositeDispatchCount,
-            {vkcv::DescriptorSetUsage(0, m_CompositeDescSet)},
-            vkcv::PushConstants(0));
-}
-
-void BloomAndFlares::execWholePipeline(const vkcv::CommandStreamHandle &cmdStream,
-                                       const vkcv::ImageHandle &colorAttachment)
-{
-    execDownsamplePipe(cmdStream, colorAttachment);
-    execUpsamplePipe(cmdStream);
-    execLensFeaturePipe(cmdStream);
-    execCompositePipe(cmdStream, colorAttachment);
-}
-
-void BloomAndFlares::updateImageDimensions(uint32_t width, uint32_t height)
-{
-    if ((width == m_Width) && (height == m_Height)) {
-        return;
-    }
-    
-    m_Width  = width;
-    m_Height = height;
-
-    p_Core->getContext().getDevice().waitIdle();
-    m_Blur = p_Core->createImage(m_ColorBufferFormat, m_Width, m_Height, 1, true, true, false);
-    m_LensFeatures = p_Core->createImage(m_ColorBufferFormat, m_Width, m_Height, 1, false, true, false);
-}
-
-
diff --git a/projects/particle_simulation/src/BloomAndFlares.hpp b/projects/particle_simulation/src/BloomAndFlares.hpp
deleted file mode 100644
index 2692034db51cf341e7ede2a26f5724c92dccbfed..0000000000000000000000000000000000000000
--- a/projects/particle_simulation/src/BloomAndFlares.hpp
+++ /dev/null
@@ -1,50 +0,0 @@
-#pragma once
-#include <vkcv/Core.hpp>
-#include <glm/glm.hpp>
-
-class BloomAndFlares{
-public:
-    BloomAndFlares(vkcv::Core *p_Core,
-                   vk::Format colorBufferFormat,
-                   uint32_t width,
-                   uint32_t height);
-
-    void execWholePipeline(const vkcv::CommandStreamHandle &cmdStream, const vkcv::ImageHandle &colorAttachment);
-
-    void updateImageDimensions(uint32_t width, uint32_t height);
-
-private:
-    vkcv::Core *p_Core;
-
-    vk::Format m_ColorBufferFormat;
-    uint32_t m_Width;
-    uint32_t m_Height;
-
-    vkcv::SamplerHandle m_LinearSampler;
-    vkcv::Image m_Blur;
-    vkcv::Image m_LensFeatures;
-
-    vkcv::ComputePipelineHandle                     m_DownsamplePipe;
-    std::vector<vkcv::DescriptorSetLayoutHandle>    m_DownsampleDescSetLayouts;
-    std::vector<vkcv::DescriptorSetHandle>          m_DownsampleDescSets; // per mip desc set
-
-    vkcv::ComputePipelineHandle                     m_UpsamplePipe;
-    std::vector<vkcv::DescriptorSetLayoutHandle>    m_UpsampleDescSetLayouts;
-    std::vector<vkcv::DescriptorSetHandle>          m_UpsampleDescSets;   // per mip desc set
-
-    vkcv::ComputePipelineHandle                     m_LensFlarePipe;
-    vkcv::DescriptorSetLayoutHandle                 m_LensFlareDescSetLayout;
-    vkcv::DescriptorSetHandle                       m_LensFlareDescSet;
-
-    vkcv::ComputePipelineHandle                     m_CompositePipe;
-    vkcv::DescriptorSetLayoutHandle                 m_CompositeDescSetLayout;
-    vkcv::DescriptorSetHandle                       m_CompositeDescSet;
-
-    void execDownsamplePipe(const vkcv::CommandStreamHandle &cmdStream, const vkcv::ImageHandle &colorAttachment);
-    void execUpsamplePipe(const vkcv::CommandStreamHandle &cmdStream);
-    void execLensFeaturePipe(const vkcv::CommandStreamHandle &cmdStream);
-    void execCompositePipe(const vkcv::CommandStreamHandle &cmdStream, const vkcv::ImageHandle &colorAttachment);
-};
-
-
-
diff --git a/projects/particle_simulation/src/main.cpp b/projects/particle_simulation/src/main.cpp
index 6637041e5ea9c8f1dd3dadf0303049d2a3b749e9..db17621eb6ee46a2a4e6e0564323c59045915c57 100644
--- a/projects/particle_simulation/src/main.cpp
+++ b/projects/particle_simulation/src/main.cpp
@@ -8,7 +8,7 @@
 #include <glm/gtc/matrix_access.hpp>
 #include <ctime>
 #include <vkcv/shader/GLSLCompiler.hpp>
-#include "BloomAndFlares.hpp"
+#include <vkcv/effects/BloomAndFlaresEffect.hpp>
 
 int main(int argc, const char **argv) {
     const char *applicationName = "Particlesystem";
@@ -220,7 +220,10 @@ int main(int argc, const char **argv) {
 			swapchainExtent.height,
 			1, false, true, true
 	).getHandle();
-    BloomAndFlares bloomAndFlares(&core, colorFormat, swapchainExtent.width, swapchainExtent.height);
+	
+	vkcv::effects::BloomAndFlaresEffect bloomAndFlares (core);
+	bloomAndFlares.setUpsamplingLimit(3);
+	
     window.e_resize.add([&](int width, int height) {
 		swapchainExtent = core.getSwapchain(windowHandle).getExtent();
         colorBuffer = core.createImage(
@@ -229,7 +232,6 @@ int main(int argc, const char **argv) {
 				swapchainExtent.height,
 				1, false, true, true
 		).getHandle();
-        bloomAndFlares.updateImageDimensions(width, height);
     });
 
     vkcv::ShaderProgram tonemappingShader;
@@ -299,8 +301,8 @@ int main(int argc, const char **argv) {
                 {drawcalls},
                 { colorBuffer },
                 windowHandle);
-
-        bloomAndFlares.execWholePipeline(cmdStream, colorBuffer);
+	
+		bloomAndFlares.recordEffect(cmdStream, colorBuffer, colorBuffer);
 
         core.prepareImageForStorage(cmdStream, colorBuffer);
         core.prepareImageForStorage(cmdStream, swapchainInput);
diff --git a/projects/sph/CMakeLists.txt b/projects/sph/CMakeLists.txt
index 8be89e0891d697bc22088e54dbeea5541d7f2065..985a33938b02202d05ac84a03dd505a35dd6b8a6 100644
--- a/projects/sph/CMakeLists.txt
+++ b/projects/sph/CMakeLists.txt
@@ -13,23 +13,24 @@ add_executable(sph
 		src/main.cpp
 		src/Particle.hpp 
 		src/Particle.cpp
-		src/BloomAndFlares.hpp
-		src/BloomAndFlares.cpp
 		src/PipelineInit.hpp
 		src/PipelineInit.cpp)
 
-# this should fix the execution path to load local files from the project (for MSVC)
-if(MSVC)
-	set_target_properties(sph PROPERTIES RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
-	set_target_properties(sph PROPERTIES RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
-
-	# in addition to setting the output directory, the working directory has to be set
-	# by default visual studio sets the working directory to the build directory, when using the debugger
-	set_target_properties(sph PROPERTIES VS_DEBUGGER_WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
-endif()
+fix_project(sph)
 
 # including headers of dependencies and the VkCV framework
-target_include_directories(sph SYSTEM BEFORE PRIVATE ${vkcv_include} ${vkcv_includes} ${vkcv_testing_include} ${vkcv_camera_include} ${vkcv_shader_compiler_include})
+target_include_directories(sph SYSTEM BEFORE PRIVATE
+		${vkcv_include}
+		${vkcv_includes}
+		${vkcv_testing_include}
+		${vkcv_camera_include}
+		${vkcv_shader_compiler_include}
+		${vkcv_effects_include})
 
 # linking with libraries from all dependencies and the VkCV framework
-target_link_libraries(sph vkcv vkcv_testing vkcv_camera vkcv_shader_compiler)
+target_link_libraries(sph
+		vkcv
+		vkcv_testing
+		vkcv_camera
+		vkcv_shader_compiler
+		vkcv_effects)
diff --git a/projects/sph/shaders/bloom/composite.comp b/projects/sph/shaders/bloom/composite.comp
deleted file mode 100644
index 87b5ddb975106232d1cd3b6e5b8dc7e623dd0b59..0000000000000000000000000000000000000000
--- a/projects/sph/shaders/bloom/composite.comp
+++ /dev/null
@@ -1,38 +0,0 @@
-#version 450
-#extension GL_ARB_separate_shader_objects : enable
-
-layout(set=0, binding=0) uniform texture2D                          blurImage;
-layout(set=0, binding=1) uniform texture2D                          lensImage;
-layout(set=0, binding=2) uniform sampler                            linearSampler;
-layout(set=0, binding=3, r11f_g11f_b10f) uniform image2D            colorBuffer;
-
-layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
-
-
-void main()
-{
-    if(any(greaterThanEqual(gl_GlobalInvocationID.xy, imageSize(colorBuffer)))){
-        return;
-    }
-
-    ivec2 pixel_coord   = ivec2(gl_GlobalInvocationID.xy);
-    vec2  pixel_size    = vec2(1.0f) / textureSize(sampler2D(blurImage, linearSampler), 0);
-    vec2  UV            = pixel_coord.xy * pixel_size;
-
-    vec4 composite_color = vec4(0.0f);
-
-    vec3 blur_color   = texture(sampler2D(blurImage, linearSampler), UV).rgb;
-    vec3 lens_color   = texture(sampler2D(lensImage, linearSampler), UV).rgb;
-    vec3 main_color   = imageLoad(colorBuffer, pixel_coord).rgb;
-
-    // composite blur and lens features
-    float bloom_weight = 0.01f;
-    float lens_weight  = 0.f;
-    float main_weight = 1 - (bloom_weight + lens_weight);
-
-    composite_color.rgb = blur_color * bloom_weight +
-                          lens_color * lens_weight  +
-                          main_color * main_weight;
-
-    imageStore(colorBuffer, pixel_coord, composite_color);
-}
\ No newline at end of file
diff --git a/projects/sph/shaders/bloom/downsample.comp b/projects/sph/shaders/bloom/downsample.comp
deleted file mode 100644
index 2ab00c7c92798769153634f3479c5b7f3fb61d94..0000000000000000000000000000000000000000
--- a/projects/sph/shaders/bloom/downsample.comp
+++ /dev/null
@@ -1,76 +0,0 @@
-#version 450
-#extension GL_ARB_separate_shader_objects : enable
-
-layout(set=0, binding=0) uniform texture2D                          inBlurImage;
-layout(set=0, binding=1) uniform sampler                            inImageSampler;
-layout(set=0, binding=2, r11f_g11f_b10f) uniform writeonly image2D  outBlurImage;
-
-layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
-
-
-void main()
-{
-    if(any(greaterThanEqual(gl_GlobalInvocationID.xy, imageSize(outBlurImage)))){
-        return;
-    }
-
-    ivec2 pixel_coord   = ivec2(gl_GlobalInvocationID.xy);
-    vec2  pixel_size    = vec2(1.0f) / imageSize(outBlurImage);
-    vec2  UV            = pixel_coord.xy * pixel_size;
-    vec2  UV_offset     = UV + 0.5f * pixel_size;
-
-    vec2 color_fetches[13] = {
-        // center neighbourhood (RED)
-        vec2(-1,  1), // LT
-        vec2(-1, -1), // LB
-        vec2( 1, -1), // RB
-        vec2( 1,  1), // RT
-
-        vec2(-2, 2), // LT
-        vec2( 0, 2), // CT
-        vec2( 2, 2), // RT
-
-        vec2(0 ,-2), // LC
-        vec2(0 , 0), // CC
-        vec2(2,  0), // CR
-
-        vec2(-2, -2), // LB
-        vec2(0 , -2), // CB
-        vec2(2 , -2)  // RB
-    };
-
-    float color_weights[13] = {
-        // 0.5f
-        1.f/8.f,
-        1.f/8.f,
-        1.f/8.f,
-        1.f/8.f,
-
-        // 0.125f
-        1.f/32.f,
-        1.f/16.f,
-        1.f/32.f,
-
-        // 0.25f
-        1.f/16.f,
-        1.f/8.f,
-        1.f/16.f,
-
-        // 0.125f
-        1.f/32.f,
-        1.f/16.f,
-        1.f/32.f
-    };
-
-    vec3 sampled_color = vec3(0.0f);
-
-    for(uint i = 0; i < 13; i++)
-    {
-        vec2 color_fetch = UV_offset + color_fetches[i] * pixel_size;
-        vec3 color = texture(sampler2D(inBlurImage, inImageSampler), color_fetch).rgb;
-        color *= color_weights[i];
-        sampled_color += color;
-    }
-
-    imageStore(outBlurImage, pixel_coord, vec4(sampled_color, 1.f));
-}
\ No newline at end of file
diff --git a/projects/sph/shaders/bloom/lensFlares.comp b/projects/sph/shaders/bloom/lensFlares.comp
deleted file mode 100644
index ce27d8850b709f61332d467914ddc944dc63109f..0000000000000000000000000000000000000000
--- a/projects/sph/shaders/bloom/lensFlares.comp
+++ /dev/null
@@ -1,109 +0,0 @@
-#version 450
-#extension GL_ARB_separate_shader_objects : enable
-
-layout(set=0, binding=0) uniform texture2D                          blurBuffer;
-layout(set=0, binding=1) uniform sampler                            linearSampler;
-layout(set=0, binding=2, r11f_g11f_b10f) uniform image2D            lensBuffer;
-
-layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
-
-vec3 sampleColorChromaticAberration(vec2 _uv)
-{
-    vec2 toCenter = (vec2(0.5) - _uv);
-
-    vec3    colorScales     = vec3(-1, 0, 1);
-    float   aberrationScale = 0.1;
-    vec3 scaleFactors = colorScales * aberrationScale;
-
-    float r = texture(sampler2D(blurBuffer, linearSampler), _uv + toCenter * scaleFactors.r).r;
-    float g = texture(sampler2D(blurBuffer, linearSampler), _uv + toCenter * scaleFactors.g).g;
-    float b = texture(sampler2D(blurBuffer, linearSampler), _uv + toCenter * scaleFactors.b).b;
-    return vec3(r, g, b);
-}
-
-// _uv assumed to be flipped UV coordinates!
-vec3 ghost_vectors(vec2 _uv)
-{
-    vec2 ghost_vec = (vec2(0.5f) - _uv);
-
-    const uint c_ghost_count = 64;
-    const float c_ghost_spacing = length(ghost_vec) / c_ghost_count;
-
-    ghost_vec *= c_ghost_spacing;
-
-    vec3 ret_color = vec3(0.0f);
-
-    for (uint i = 0; i < c_ghost_count; ++i)
-    {
-        // sample scene color
-        vec2 s_uv = fract(_uv + ghost_vec * vec2(i));
-        vec3 s = sampleColorChromaticAberration(s_uv);
-
-        // tint/weight
-        float d = distance(s_uv, vec2(0.5));
-        float weight = 1.0f - smoothstep(0.0f, 0.75f, d);
-        s *= weight;
-
-        ret_color += s;
-    }
-
-    ret_color /= c_ghost_count;
-    return ret_color;
-}
-
-vec3 halo(vec2 _uv)
-{
-    const float c_aspect_ratio = float(imageSize(lensBuffer).x) / float(imageSize(lensBuffer).y);
-    const float c_radius = 0.6f;
-    const float c_halo_thickness = 0.1f;
-
-    vec2 halo_vec = vec2(0.5) - _uv;
-    //halo_vec.x /= c_aspect_ratio;
-    halo_vec = normalize(halo_vec);
-    //halo_vec.x *= c_aspect_ratio;
-
-
-    //vec2 w_uv = (_uv - vec2(0.5, 0.0)) * vec2(c_aspect_ratio, 1.0) + vec2(0.5, 0.0);
-    vec2 w_uv = _uv;
-    float d = distance(w_uv, vec2(0.5)); // distance to center
-
-    float distance_to_halo = abs(d - c_radius);
-
-    float halo_weight = 0.0f;
-    if(abs(d - c_radius) <= c_halo_thickness)
-    {
-        float distance_to_border = c_halo_thickness - distance_to_halo;
-        halo_weight = distance_to_border / c_halo_thickness;
-
-        //halo_weight = clamp((halo_weight / 0.4f), 0.0f, 1.0f);
-        halo_weight = pow(halo_weight, 2.0f);
-
-
-        //halo_weight = 1.0f;
-    }
-
-    return sampleColorChromaticAberration(_uv + halo_vec) * halo_weight;
-}
-
-
-
-void main()
-{
-    if(any(greaterThanEqual(gl_GlobalInvocationID.xy, imageSize(lensBuffer)))){
-        return;
-    }
-
-    ivec2 pixel_coord   = ivec2(gl_GlobalInvocationID.xy);
-    vec2  pixel_size    = vec2(1.0f) / imageSize(lensBuffer);
-    vec2  UV            = pixel_coord.xy * pixel_size;
-
-    vec2 flipped_UV = vec2(1.0f) - UV;
-
-    vec3 color = vec3(0.0f);
-
-    color += ghost_vectors(flipped_UV);
-    color += halo(UV);
-    color  *= 0.5f;
-
-    imageStore(lensBuffer, pixel_coord, vec4(color, 0.0f));
-}
\ No newline at end of file
diff --git a/projects/sph/shaders/bloom/upsample.comp b/projects/sph/shaders/bloom/upsample.comp
deleted file mode 100644
index 0ddeedb5b5af9e476dc19012fed6430544006c0e..0000000000000000000000000000000000000000
--- a/projects/sph/shaders/bloom/upsample.comp
+++ /dev/null
@@ -1,45 +0,0 @@
-#version 450
-#extension GL_ARB_separate_shader_objects : enable
-
-layout(set=0, binding=0) uniform texture2D                          inUpsampleImage;
-layout(set=0, binding=1) uniform sampler                            inImageSampler;
-layout(set=0, binding=2, r11f_g11f_b10f) uniform image2D  outUpsampleImage;
-
-layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
-
-void main()
-{
-    if(any(greaterThanEqual(gl_GlobalInvocationID.xy, imageSize(outUpsampleImage)))){
-        return;
-    }
-
-
-    ivec2 pixel_coord   = ivec2(gl_GlobalInvocationID.xy);
-    vec2  pixel_size    = vec2(1.0f) / imageSize(outUpsampleImage);
-    vec2  UV            = pixel_coord.xy * pixel_size;
-
-    const float gauss_kernel[3] = {1.f, 2.f, 1.f};
-    const float gauss_weight = 16.f;
-
-    vec3 sampled_color = vec3(0.f);
-
-    for(int i = -1; i <= 1; i++)
-    {
-        for(int j = -1; j <= 1; j++)
-        {
-            vec2 sample_location = UV + vec2(j, i) * pixel_size;
-            vec3 color = texture(sampler2D(inUpsampleImage, inImageSampler), sample_location).rgb;
-            color *= gauss_kernel[j+1];
-            color *= gauss_kernel[i+1];
-            color /= gauss_weight;
-
-            sampled_color += color;
-        }
-    }
-
-    //vec3 prev_color = imageLoad(outUpsampleImage, pixel_coord).rgb;
-    //float bloomRimStrength = 0.75f; // adjust this to change strength of bloom
-    //sampled_color = mix(prev_color, sampled_color, bloomRimStrength);
-
-    imageStore(outUpsampleImage, pixel_coord, vec4(sampled_color, 1.f));
-}
\ No newline at end of file
diff --git a/projects/sph/src/BloomAndFlares.cpp b/projects/sph/src/BloomAndFlares.cpp
deleted file mode 100644
index 200c0dea16a0b1483a8b20786902b38a43b5f825..0000000000000000000000000000000000000000
--- a/projects/sph/src/BloomAndFlares.cpp
+++ /dev/null
@@ -1,289 +0,0 @@
-#include "BloomAndFlares.hpp"
-#include <vkcv/shader/GLSLCompiler.hpp>
-
-BloomAndFlares::BloomAndFlares(
-        vkcv::Core *p_Core,
-        vk::Format colorBufferFormat,
-        uint32_t width,
-        uint32_t height) :
-
-        p_Core(p_Core),
-        m_ColorBufferFormat(colorBufferFormat),
-        m_Width(width),
-        m_Height(height),
-        m_LinearSampler(p_Core->createSampler(vkcv::SamplerFilterType::LINEAR,
-                                              vkcv::SamplerFilterType::LINEAR,
-                                              vkcv::SamplerMipmapMode::LINEAR,
-                                              vkcv::SamplerAddressMode::CLAMP_TO_EDGE)),
-        m_Blur(p_Core->createImage(colorBufferFormat, width, height, 1, true, true, false)),
-        m_LensFeatures(p_Core->createImage(colorBufferFormat, width, height, 1, false, true, false))
-{
-    vkcv::shader::GLSLCompiler compiler;
-
-    // DOWNSAMPLE
-    vkcv::ShaderProgram dsProg;
-    compiler.compile(vkcv::ShaderStage::COMPUTE,
-                     "shaders/bloom/downsample.comp",
-                     [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path)
-                     {
-                         dsProg.addShader(shaderStage, path);
-                     });
-    for(uint32_t mipLevel = 0; mipLevel < m_Blur.getMipCount(); mipLevel++)
-    {
-        m_DownsampleDescSetLayouts.push_back(
-                p_Core->createDescriptorSetLayout(dsProg.getReflectedDescriptors().at(0)));
-
-		m_DownsampleDescSets.push_back(
-		        p_Core->createDescriptorSet(m_DownsampleDescSetLayouts.back()));
-    }
-    m_DownsamplePipe = p_Core->createComputePipeline({
-            dsProg, m_DownsampleDescSetLayouts
-	});
-
-    // UPSAMPLE
-    vkcv::ShaderProgram usProg;
-    compiler.compile(vkcv::ShaderStage::COMPUTE,
-                     "shaders/bloom/upsample.comp",
-                     [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path)
-                     {
-                         usProg.addShader(shaderStage, path);
-                     });
-    for(uint32_t mipLevel = 0; mipLevel < m_Blur.getMipCount(); mipLevel++)
-    {
-        m_UpsampleDescSetLayouts.push_back(
-                p_Core->createDescriptorSetLayout(usProg.getReflectedDescriptors().at(0)));
-        m_UpsampleDescSets.push_back(
-                p_Core->createDescriptorSet(m_UpsampleDescSetLayouts.back()));
-    }
-    m_UpsamplePipe = p_Core->createComputePipeline({
-            usProg, m_UpsampleDescSetLayouts
-	});
-
-    // LENS FEATURES
-    vkcv::ShaderProgram lensProg;
-    compiler.compile(vkcv::ShaderStage::COMPUTE,
-                     "shaders/bloom/lensFlares.comp",
-                     [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path)
-                     {
-                         lensProg.addShader(shaderStage, path);
-                     });
-    m_LensFlareDescSetLayout = p_Core->createDescriptorSetLayout(lensProg.getReflectedDescriptors().at(0));
-    m_LensFlareDescSet = p_Core->createDescriptorSet(m_LensFlareDescSetLayout);
-    m_LensFlarePipe = p_Core->createComputePipeline({
-            lensProg, { m_LensFlareDescSetLayout }
-	});
-
-    // COMPOSITE
-    vkcv::ShaderProgram compProg;
-    compiler.compile(vkcv::ShaderStage::COMPUTE,
-                     "shaders/bloom/composite.comp",
-                     [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path)
-                     {
-                         compProg.addShader(shaderStage, path);
-                     });
-    m_CompositeDescSetLayout = p_Core->createDescriptorSetLayout(compProg.getReflectedDescriptors().at(0));
-    m_CompositeDescSet = p_Core->createDescriptorSet(m_CompositeDescSetLayout);
-    m_CompositePipe = p_Core->createComputePipeline({
-            compProg, { m_CompositeDescSetLayout }
-	});
-}
-
-void BloomAndFlares::execDownsamplePipe(const vkcv::CommandStreamHandle &cmdStream,
-                                        const vkcv::ImageHandle &colorAttachment)
-{
-    auto dispatchCountX  = static_cast<float>(m_Width)  / 8.0f;
-    auto dispatchCountY = static_cast<float>(m_Height) / 8.0f;
-    // blur dispatch
-    uint32_t initialDispatchCount[3] = {
-            static_cast<uint32_t>(glm::ceil(dispatchCountX)),
-            static_cast<uint32_t>(glm::ceil(dispatchCountY)),
-            1
-    };
-
-    // downsample dispatch of original color attachment
-    p_Core->prepareImageForSampling(cmdStream, colorAttachment);
-    p_Core->prepareImageForStorage(cmdStream, m_Blur.getHandle());
-
-    vkcv::DescriptorWrites initialDownsampleWrites;
-    initialDownsampleWrites.sampledImageWrites = {vkcv::SampledImageDescriptorWrite(0, colorAttachment)};
-    initialDownsampleWrites.samplerWrites      = {vkcv::SamplerDescriptorWrite(1, m_LinearSampler)};
-    initialDownsampleWrites.storageImageWrites = {vkcv::StorageImageDescriptorWrite(2, m_Blur.getHandle(), 0) };
-    p_Core->writeDescriptorSet(m_DownsampleDescSets[0], initialDownsampleWrites);
-
-    p_Core->recordComputeDispatchToCmdStream(
-            cmdStream,
-            m_DownsamplePipe,
-            initialDispatchCount,
-            {vkcv::DescriptorSetUsage(0, m_DownsampleDescSets[0])},
-            vkcv::PushConstants(0));
-
-    // downsample dispatches of blur buffer's mip maps
-    float mipDispatchCountX = dispatchCountX;
-    float mipDispatchCountY = dispatchCountY;
-    for(uint32_t mipLevel = 1; mipLevel < std::min((uint32_t)m_DownsampleDescSets.size(), m_Blur.getMipCount()); mipLevel++)
-    {
-        // mip descriptor writes
-        vkcv::DescriptorWrites mipDescriptorWrites;
-        mipDescriptorWrites.sampledImageWrites = {vkcv::SampledImageDescriptorWrite(0, m_Blur.getHandle(), mipLevel - 1, true)};
-        mipDescriptorWrites.samplerWrites      = {vkcv::SamplerDescriptorWrite(1, m_LinearSampler)};
-        mipDescriptorWrites.storageImageWrites = {vkcv::StorageImageDescriptorWrite(2, m_Blur.getHandle(), mipLevel) };
-        p_Core->writeDescriptorSet(m_DownsampleDescSets[mipLevel], mipDescriptorWrites);
-
-        // mip dispatch calculation
-        mipDispatchCountX  /= 2.0f;
-        mipDispatchCountY /= 2.0f;
-
-        uint32_t mipDispatchCount[3] = {
-                static_cast<uint32_t>(glm::ceil(mipDispatchCountX)),
-                static_cast<uint32_t>(glm::ceil(mipDispatchCountY)),
-                1
-        };
-
-        if(mipDispatchCount[0] == 0)
-            mipDispatchCount[0] = 1;
-        if(mipDispatchCount[1] == 0)
-            mipDispatchCount[1] = 1;
-
-        // mip blur dispatch
-        p_Core->recordComputeDispatchToCmdStream(
-                cmdStream,
-                m_DownsamplePipe,
-                mipDispatchCount,
-                {vkcv::DescriptorSetUsage(0, m_DownsampleDescSets[mipLevel])},
-                vkcv::PushConstants(0));
-
-        // image barrier between mips
-        p_Core->recordImageMemoryBarrier(cmdStream, m_Blur.getHandle());
-    }
-}
-
-void BloomAndFlares::execUpsamplePipe(const vkcv::CommandStreamHandle &cmdStream)
-{
-    // upsample dispatch
-    p_Core->prepareImageForStorage(cmdStream, m_Blur.getHandle());
-
-    const uint32_t upsampleMipLevels = std::min(
-    		static_cast<uint32_t>(m_UpsampleDescSets.size() - 1),
-    		static_cast<uint32_t>(3)
-	);
-
-    // upsample dispatch for each mip map
-    for(uint32_t mipLevel = upsampleMipLevels; mipLevel > 0; mipLevel--)
-    {
-        // mip descriptor writes
-        vkcv::DescriptorWrites mipUpsampleWrites;
-        mipUpsampleWrites.sampledImageWrites = {vkcv::SampledImageDescriptorWrite(0, m_Blur.getHandle(), mipLevel, true)};
-        mipUpsampleWrites.samplerWrites      = {vkcv::SamplerDescriptorWrite(1, m_LinearSampler)};
-        mipUpsampleWrites.storageImageWrites = {vkcv::StorageImageDescriptorWrite(2, m_Blur.getHandle(), mipLevel - 1) };
-        p_Core->writeDescriptorSet(m_UpsampleDescSets[mipLevel], mipUpsampleWrites);
-
-        auto mipDivisor = glm::pow(2.0f, static_cast<float>(mipLevel) - 1.0f);
-
-        auto upsampleDispatchX  = static_cast<float>(m_Width) / mipDivisor;
-        auto upsampleDispatchY = static_cast<float>(m_Height) / mipDivisor;
-        upsampleDispatchX /= 8.0f;
-        upsampleDispatchY /= 8.0f;
-
-        const uint32_t upsampleDispatchCount[3] = {
-                static_cast<uint32_t>(glm::ceil(upsampleDispatchX)),
-                static_cast<uint32_t>(glm::ceil(upsampleDispatchY)),
-                1
-        };
-
-        p_Core->recordComputeDispatchToCmdStream(
-                cmdStream,
-                m_UpsamplePipe,
-                upsampleDispatchCount,
-                {vkcv::DescriptorSetUsage(0, m_UpsampleDescSets[mipLevel])},
-                vkcv::PushConstants(0)
-        );
-        // image barrier between mips
-        p_Core->recordImageMemoryBarrier(cmdStream, m_Blur.getHandle());
-    }
-}
-
-void BloomAndFlares::execLensFeaturePipe(const vkcv::CommandStreamHandle &cmdStream)
-{
-    // lens feature generation descriptor writes
-    p_Core->prepareImageForSampling(cmdStream, m_Blur.getHandle());
-    p_Core->prepareImageForStorage(cmdStream, m_LensFeatures.getHandle());
-
-    vkcv::DescriptorWrites lensFeatureWrites;
-    lensFeatureWrites.sampledImageWrites = {vkcv::SampledImageDescriptorWrite(0, m_Blur.getHandle(), 0)};
-    lensFeatureWrites.samplerWrites = {vkcv::SamplerDescriptorWrite(1, m_LinearSampler)};
-    lensFeatureWrites.storageImageWrites = {vkcv::StorageImageDescriptorWrite(2, m_LensFeatures.getHandle(), 0)};
-    p_Core->writeDescriptorSet(m_LensFlareDescSet, lensFeatureWrites);
-
-    auto dispatchCountX  = static_cast<float>(m_Width)  / 8.0f;
-    auto dispatchCountY = static_cast<float>(m_Height) / 8.0f;
-    // lens feature generation dispatch
-    uint32_t lensFeatureDispatchCount[3] = {
-            static_cast<uint32_t>(glm::ceil(dispatchCountX)),
-            static_cast<uint32_t>(glm::ceil(dispatchCountY)),
-            1
-    };
-    p_Core->recordComputeDispatchToCmdStream(
-            cmdStream,
-            m_LensFlarePipe,
-            lensFeatureDispatchCount,
-            {vkcv::DescriptorSetUsage(0, m_LensFlareDescSet)},
-            vkcv::PushConstants(0));
-}
-
-void BloomAndFlares::execCompositePipe(const vkcv::CommandStreamHandle &cmdStream,
-                                       const vkcv::ImageHandle &colorAttachment)
-{
-    p_Core->prepareImageForSampling(cmdStream, m_Blur.getHandle());
-    p_Core->prepareImageForSampling(cmdStream, m_LensFeatures.getHandle());
-    p_Core->prepareImageForStorage(cmdStream, colorAttachment);
-
-    // bloom composite descriptor write
-    vkcv::DescriptorWrites compositeWrites;
-    compositeWrites.sampledImageWrites = {vkcv::SampledImageDescriptorWrite(0, m_Blur.getHandle()),
-                                          vkcv::SampledImageDescriptorWrite(1, m_LensFeatures.getHandle())};
-    compositeWrites.samplerWrites = {vkcv::SamplerDescriptorWrite(2, m_LinearSampler)};
-    compositeWrites.storageImageWrites = {vkcv::StorageImageDescriptorWrite(3, colorAttachment)};
-    p_Core->writeDescriptorSet(m_CompositeDescSet, compositeWrites);
-
-    float dispatchCountX = static_cast<float>(m_Width)  / 8.0f;
-    float dispatchCountY = static_cast<float>(m_Height) / 8.0f;
-
-    uint32_t compositeDispatchCount[3] = {
-            static_cast<uint32_t>(glm::ceil(dispatchCountX)),
-            static_cast<uint32_t>(glm::ceil(dispatchCountY)),
-            1
-    };
-
-    // bloom composite dispatch
-    p_Core->recordComputeDispatchToCmdStream(
-            cmdStream,
-            m_CompositePipe,
-            compositeDispatchCount,
-            {vkcv::DescriptorSetUsage(0, m_CompositeDescSet)},
-            vkcv::PushConstants(0));
-}
-
-void BloomAndFlares::execWholePipeline(const vkcv::CommandStreamHandle &cmdStream,
-                                       const vkcv::ImageHandle &colorAttachment)
-{
-    execDownsamplePipe(cmdStream, colorAttachment);
-    execUpsamplePipe(cmdStream);
-    execLensFeaturePipe(cmdStream);
-    execCompositePipe(cmdStream, colorAttachment);
-}
-
-void BloomAndFlares::updateImageDimensions(uint32_t width, uint32_t height)
-{
-    if ((width == m_Width) && (height == m_Height)) {
-        return;
-    }
-    
-    m_Width  = width;
-    m_Height = height;
-
-    p_Core->getContext().getDevice().waitIdle();
-    m_Blur = p_Core->createImage(m_ColorBufferFormat, m_Width, m_Height, 1, true, true, false);
-    m_LensFeatures = p_Core->createImage(m_ColorBufferFormat, m_Width, m_Height, 1, false, true, false);
-}
-
-
diff --git a/projects/sph/src/BloomAndFlares.hpp b/projects/sph/src/BloomAndFlares.hpp
deleted file mode 100644
index 1644d38e9c98c7a0bf74d48b173f0e627214f1e1..0000000000000000000000000000000000000000
--- a/projects/sph/src/BloomAndFlares.hpp
+++ /dev/null
@@ -1,51 +0,0 @@
-#pragma once
-#include <vkcv/Core.hpp>
-#include <glm/glm.hpp>
-
-class BloomAndFlares{
-public:
-    BloomAndFlares(vkcv::Core *p_Core,
-                   vk::Format colorBufferFormat,
-                   uint32_t width,
-                   uint32_t height);
-
-    void execWholePipeline(const vkcv::CommandStreamHandle &cmdStream, const vkcv::ImageHandle &colorAttachment);
-
-    void updateImageDimensions(uint32_t width, uint32_t height);
-
-private:
-    vkcv::Core *p_Core;
-
-    vk::Format m_ColorBufferFormat;
-    uint32_t m_Width;
-    uint32_t m_Height;
-
-    vkcv::SamplerHandle m_LinearSampler;
-    vkcv::Image m_Blur;
-    vkcv::Image m_LensFeatures;
-
-
-    vkcv::ComputePipelineHandle                     m_DownsamplePipe;
-    std::vector<vkcv::DescriptorSetLayoutHandle>    m_DownsampleDescSetLayouts;
-    std::vector<vkcv::DescriptorSetHandle>          m_DownsampleDescSets; // per mip desc set
-
-    vkcv::ComputePipelineHandle                     m_UpsamplePipe;
-    std::vector<vkcv::DescriptorSetLayoutHandle>    m_UpsampleDescSetLayouts;
-    std::vector<vkcv::DescriptorSetHandle>          m_UpsampleDescSets;   // per mip desc set
-
-    vkcv::ComputePipelineHandle                     m_LensFlarePipe;
-    vkcv::DescriptorSetLayoutHandle                 m_LensFlareDescSetLayout;
-    vkcv::DescriptorSetHandle                       m_LensFlareDescSet;
-
-    vkcv::ComputePipelineHandle                     m_CompositePipe;
-    vkcv::DescriptorSetLayoutHandle                 m_CompositeDescSetLayout;
-    vkcv::DescriptorSetHandle                       m_CompositeDescSet;
-
-    void execDownsamplePipe(const vkcv::CommandStreamHandle &cmdStream, const vkcv::ImageHandle &colorAttachment);
-    void execUpsamplePipe(const vkcv::CommandStreamHandle &cmdStream);
-    void execLensFeaturePipe(const vkcv::CommandStreamHandle &cmdStream);
-    void execCompositePipe(const vkcv::CommandStreamHandle &cmdStream, const vkcv::ImageHandle &colorAttachment);
-};
-
-
-
diff --git a/projects/sph/src/main.cpp b/projects/sph/src/main.cpp
index 44d38850a134327e1bd4733d45e9e2aed295f8b3..4e98a3013484c75da7c39c711ff4312535a71ef6 100644
--- a/projects/sph/src/main.cpp
+++ b/projects/sph/src/main.cpp
@@ -9,7 +9,7 @@
 #include <glm/gtc/matrix_transform.hpp>
 #include <time.h>
 #include <vkcv/shader/GLSLCompiler.hpp>
-#include "BloomAndFlares.hpp"
+#include <vkcv/effects/BloomAndFlaresEffect.hpp>
 #include "PipelineInit.hpp"
 #include "Particle.hpp"
 
@@ -234,7 +234,9 @@ int main(int argc, const char **argv) {
 			swapchainExtent.height,
 			1, false, true, true
 	).getHandle();
-    BloomAndFlares bloomAndFlares(&core, colorFormat, swapchainExtent.width, swapchainExtent.height);
+	
+	vkcv::effects::BloomAndFlaresEffect bloomAndFlares (core);
+	bloomAndFlares.setUpsamplingLimit(3);
 
     //tone mapping shader & pipeline
     vkcv::ComputePipelineHandle tonemappingPipe;
@@ -375,8 +377,8 @@ int main(int argc, const char **argv) {
                 {drawcalls},
                 { colorBuffer },
                 windowHandle);
-
-        bloomAndFlares.execWholePipeline(cmdStream, colorBuffer);
+	
+		bloomAndFlares.recordEffect(cmdStream, colorBuffer, colorBuffer);
 
         core.prepareImageForStorage(cmdStream, colorBuffer);
         core.prepareImageForStorage(cmdStream, swapchainInput);
diff --git a/projects/voxelization/CMakeLists.txt b/projects/voxelization/CMakeLists.txt
index ba3c467766377d22925ac9c90acffee7fe324332..0f0585d9d5d9a26cb2daec486f3df8c417a01527 100644
--- a/projects/voxelization/CMakeLists.txt
+++ b/projects/voxelization/CMakeLists.txt
@@ -15,14 +15,29 @@ target_sources(voxelization PRIVATE
     src/Voxelization.hpp
     src/Voxelization.cpp
     src/ShadowMapping.hpp
-    src/ShadowMapping.cpp
-    src/BloomAndFlares.hpp
-    src/BloomAndFlares.cpp)
+    src/ShadowMapping.cpp)
 
 fix_project(voxelization)
 
 # including headers of dependencies and the VkCV framework
-target_include_directories(voxelization SYSTEM BEFORE PRIVATE ${vkcv_include} ${vkcv_includes} ${vkcv_asset_loader_include} ${vkcv_camera_include} ${vkcv_shader_compiler_include} ${vkcv_gui_include} ${vkcv_upscaling_include})
+target_include_directories(voxelization SYSTEM BEFORE PRIVATE
+		${vkcv_include}
+		${vkcv_includes}
+		${vkcv_asset_loader_include}
+		${vkcv_camera_include}
+		${vkcv_shader_compiler_include}
+		${vkcv_gui_include}
+		${vkcv_upscaling_include}
+		${vkcv_effects_include})
 
 # linking with libraries from all dependencies and the VkCV framework
-target_link_libraries(voxelization vkcv ${vkcv_libraries} vkcv_asset_loader ${vkcv_asset_loader_libraries} vkcv_camera vkcv_shader_compiler vkcv_gui vkcv_upscaling)
+target_link_libraries(voxelization
+		vkcv
+		${vkcv_libraries}
+		vkcv_asset_loader
+		${vkcv_asset_loader_libraries}
+		vkcv_camera
+		vkcv_shader_compiler
+		vkcv_gui
+		vkcv_upscaling
+		vkcv_effects)
diff --git a/projects/voxelization/src/BloomAndFlares.cpp b/projects/voxelization/src/BloomAndFlares.cpp
deleted file mode 100644
index ddb1326ae83c8bd596ce61dc1c47b81b5ddb17be..0000000000000000000000000000000000000000
--- a/projects/voxelization/src/BloomAndFlares.cpp
+++ /dev/null
@@ -1,374 +0,0 @@
-#include "BloomAndFlares.hpp"
-#include <vkcv/shader/GLSLCompiler.hpp>
-#include <vkcv/asset/asset_loader.hpp>
-
-vkcv::Image loadLenseDirtTexture(vkcv::Core* corePtr) {
-    const auto texture = vkcv::asset::loadTexture("assets/lensDirt.jpg");
-    vkcv::Image image = corePtr->createImage(vk::Format::eR8G8B8A8Unorm, texture.width, texture.height);
-    image.fill((void*)texture.data.data(), texture.data.size());
-    return image;
-}
-
-BloomAndFlares::BloomAndFlares(
-        vkcv::Core *p_Core,
-        vk::Format colorBufferFormat,
-        uint32_t width,
-        uint32_t height) :
-
-        p_Core(p_Core),
-        m_ColorBufferFormat(colorBufferFormat),
-        m_Width(width / 2),
-        m_Height(height / 2),
-        m_LinearSampler(p_Core->createSampler(vkcv::SamplerFilterType::LINEAR,
-                                              vkcv::SamplerFilterType::LINEAR,
-                                              vkcv::SamplerMipmapMode::LINEAR,
-                                              vkcv::SamplerAddressMode::CLAMP_TO_EDGE)),
-        m_RadialLutSampler(p_Core->createSampler(vkcv::SamplerFilterType::LINEAR,
-            vkcv::SamplerFilterType::LINEAR,
-            vkcv::SamplerMipmapMode::LINEAR,
-            vkcv::SamplerAddressMode::REPEAT)),
-        m_Blur(p_Core->createImage(colorBufferFormat, m_Width, m_Height, 1, true, true, false)),
-        m_LensFeatures(p_Core->createImage(colorBufferFormat, m_Width, m_Height, 1, true, true, false)),
-        m_radialLut(p_Core->createImage(vk::Format::eR8G8B8A8Unorm, 128, 10, 1)),
-        m_lensDirt(loadLenseDirtTexture(p_Core))
-{
-    vkcv::shader::GLSLCompiler compiler;
-
-    // DOWNSAMPLE
-    vkcv::ShaderProgram dsProg;
-    compiler.compile(vkcv::ShaderStage::COMPUTE,
-                     "assets/shaders/bloomDownsample.comp",
-                     [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path)
-                     {
-                         dsProg.addShader(shaderStage, path);
-                     });
-    for(uint32_t mipLevel = 0; mipLevel < m_Blur.getMipCount(); mipLevel++)
-    {
-        m_DownsampleDescSetLayouts.push_back(
-                p_Core->createDescriptorSetLayout(dsProg.getReflectedDescriptors().at(0))
-                );
-        m_DownsampleDescSets.push_back(p_Core->createDescriptorSet(m_DownsampleDescSetLayouts.back()));
-    }
-
-    m_DownsamplePipe = p_Core->createComputePipeline({
-        dsProg, m_DownsampleDescSetLayouts
-    });
-
-    // UPSAMPLE
-    vkcv::ShaderProgram usProg;
-    compiler.compile(vkcv::ShaderStage::COMPUTE,
-                     "assets/shaders/bloomUpsample.comp",
-                     [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path)
-                     {
-                         usProg.addShader(shaderStage, path);
-                     });
-    for(uint32_t mipLevel = 0; mipLevel < m_Blur.getMipCount(); mipLevel++)
-    {
-        m_UpsampleDescSetLayouts.push_back(
-                p_Core->createDescriptorSetLayout(usProg.getReflectedDescriptors().at(0)));
-        m_UpsampleDescSets.push_back(
-                p_Core->createDescriptorSet(m_UpsampleDescSetLayouts.back()));
-    }
-    for (uint32_t mipLevel = 0; mipLevel < m_LensFeatures.getMipCount(); mipLevel++) {
-        m_UpsampleLensFlareDescSetLayouts.push_back(
-                p_Core->createDescriptorSetLayout(usProg.getReflectedDescriptors().at(0)));
-        m_UpsampleLensFlareDescSets.push_back(
-                p_Core->createDescriptorSet(m_UpsampleLensFlareDescSetLayouts.back()));
-    }
-
-    m_UpsamplePipe = p_Core->createComputePipeline({
-        usProg, m_UpsampleDescSetLayouts
-    });
-
-    // LENS FEATURES
-    vkcv::ShaderProgram lensProg;
-    compiler.compile(vkcv::ShaderStage::COMPUTE,
-                     "assets/shaders/lensFlares.comp",
-                     [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path)
-                     {
-                         lensProg.addShader(shaderStage, path);
-                     });
-
-    m_LensFlareDescSetLayout = p_Core->createDescriptorSetLayout(lensProg.getReflectedDescriptors().at(0));
-    m_LensFlareDescSet = p_Core->createDescriptorSet(m_LensFlareDescSetLayout);
-    m_LensFlarePipe = p_Core->createComputePipeline(
-        { lensProg, { m_LensFlareDescSetLayout }
-	});
-
-
-    // COMPOSITE
-    vkcv::ShaderProgram compProg;
-    compiler.compile(vkcv::ShaderStage::COMPUTE,
-                     "assets/shaders/bloomFlaresComposite.comp",
-                     [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path)
-                     {
-                         compProg.addShader(shaderStage, path);
-                     });
-
-    m_CompositeDescSetLayout = p_Core->createDescriptorSetLayout(compProg.getReflectedDescriptors().at(0));
-    m_CompositeDescSet = p_Core->createDescriptorSet(m_CompositeDescSetLayout);
-    m_CompositePipe = p_Core->createComputePipeline(
-        { compProg, { m_CompositeDescSetLayout }
-	});
-
-    // radial LUT
-    const auto texture = vkcv::asset::loadTexture("assets/RadialLUT.png");
-
-    m_radialLut.fill((void*)texture.data.data(), texture.data.size());
-}
-
-void BloomAndFlares::execDownsamplePipe(const vkcv::CommandStreamHandle &cmdStream,
-                                        const vkcv::ImageHandle &colorAttachment)
-{
-    auto dispatchCountX  = static_cast<float>(m_Width)  / 8.0f;
-    auto dispatchCountY = static_cast<float>(m_Height) / 8.0f;
-    // blur dispatch
-    uint32_t initialDispatchCount[3] = {
-            static_cast<uint32_t>(glm::ceil(dispatchCountX)),
-            static_cast<uint32_t>(glm::ceil(dispatchCountY)),
-            1
-    };
-
-	p_Core->recordBeginDebugLabel(cmdStream, "Bloom downsample", { 1, 1, 1, 1 });
-
-    // downsample dispatch of original color attachment
-    p_Core->prepareImageForSampling(cmdStream, colorAttachment);
-    p_Core->prepareImageForStorage(cmdStream, m_Blur.getHandle());
-
-    vkcv::DescriptorWrites initialDownsampleWrites;
-    initialDownsampleWrites.sampledImageWrites = {vkcv::SampledImageDescriptorWrite(0, colorAttachment)};
-    initialDownsampleWrites.samplerWrites      = {vkcv::SamplerDescriptorWrite(1, m_LinearSampler)};
-    initialDownsampleWrites.storageImageWrites = {vkcv::StorageImageDescriptorWrite(2, m_Blur.getHandle(), 0) };
-    p_Core->writeDescriptorSet(m_DownsampleDescSets[0], initialDownsampleWrites);
-
-    p_Core->recordComputeDispatchToCmdStream(
-            cmdStream,
-            m_DownsamplePipe,
-            initialDispatchCount,
-            {vkcv::DescriptorSetUsage(0, m_DownsampleDescSets[0])},
-            vkcv::PushConstants(0));
-
-    // downsample dispatches of blur buffer's mip maps
-    float mipDispatchCountX = dispatchCountX;
-    float mipDispatchCountY = dispatchCountY;
-    for(uint32_t mipLevel = 1; mipLevel < std::min((uint32_t)m_DownsampleDescSets.size(), m_Blur.getMipCount()); mipLevel++)
-    {
-        // mip descriptor writes
-        vkcv::DescriptorWrites mipDescriptorWrites;
-        mipDescriptorWrites.sampledImageWrites = {vkcv::SampledImageDescriptorWrite(0, m_Blur.getHandle(), mipLevel - 1, true)};
-        mipDescriptorWrites.samplerWrites      = {vkcv::SamplerDescriptorWrite(1, m_LinearSampler)};
-        mipDescriptorWrites.storageImageWrites = {vkcv::StorageImageDescriptorWrite(2, m_Blur.getHandle(), mipLevel) };
-        p_Core->writeDescriptorSet(m_DownsampleDescSets[mipLevel], mipDescriptorWrites);
-
-        // mip dispatch calculation
-        mipDispatchCountX  /= 2.0f;
-        mipDispatchCountY /= 2.0f;
-
-        uint32_t mipDispatchCount[3] = {
-                static_cast<uint32_t>(glm::ceil(mipDispatchCountX)),
-                static_cast<uint32_t>(glm::ceil(mipDispatchCountY)),
-                1
-        };
-
-        if(mipDispatchCount[0] == 0)
-            mipDispatchCount[0] = 1;
-        if(mipDispatchCount[1] == 0)
-            mipDispatchCount[1] = 1;
-
-        // mip blur dispatch
-        p_Core->recordComputeDispatchToCmdStream(
-                cmdStream,
-                m_DownsamplePipe,
-                mipDispatchCount,
-                {vkcv::DescriptorSetUsage(0, m_DownsampleDescSets[mipLevel])},
-                vkcv::PushConstants(0));
-
-        // image barrier between mips
-        p_Core->recordImageMemoryBarrier(cmdStream, m_Blur.getHandle());
-    }
-
-    p_Core->recordEndDebugLabel(cmdStream);
-}
-
-void BloomAndFlares::execUpsamplePipe(const vkcv::CommandStreamHandle &cmdStream)
-{
-    p_Core->recordBeginDebugLabel(cmdStream, "Bloom upsample", { 1, 1, 1, 1 });
-
-    // upsample dispatch
-    p_Core->prepareImageForStorage(cmdStream, m_Blur.getHandle());
-
-    const uint32_t upsampleMipLevels = std::min(
-    		static_cast<uint32_t>(m_UpsampleDescSets.size() - 1),
-    		static_cast<uint32_t>(5)
-	);
-
-    // upsample dispatch for each mip map
-    for(uint32_t mipLevel = upsampleMipLevels; mipLevel > 0; mipLevel--)
-    {
-        // mip descriptor writes
-        vkcv::DescriptorWrites mipUpsampleWrites;
-        mipUpsampleWrites.sampledImageWrites = {vkcv::SampledImageDescriptorWrite(0, m_Blur.getHandle(), mipLevel, true)};
-        mipUpsampleWrites.samplerWrites      = {vkcv::SamplerDescriptorWrite(1, m_LinearSampler)};
-        mipUpsampleWrites.storageImageWrites = {vkcv::StorageImageDescriptorWrite(2, m_Blur.getHandle(), mipLevel - 1) };
-        p_Core->writeDescriptorSet(m_UpsampleDescSets[mipLevel], mipUpsampleWrites);
-
-        auto mipDivisor = glm::pow(2.0f, static_cast<float>(mipLevel) - 1.0f);
-
-        auto upsampleDispatchX  = static_cast<float>(m_Width) / mipDivisor;
-        auto upsampleDispatchY = static_cast<float>(m_Height) / mipDivisor;
-        upsampleDispatchX /= 8.0f;
-        upsampleDispatchY /= 8.0f;
-
-        const uint32_t upsampleDispatchCount[3] = {
-                static_cast<uint32_t>(glm::ceil(upsampleDispatchX)),
-                static_cast<uint32_t>(glm::ceil(upsampleDispatchY)),
-                1
-        };
-
-        p_Core->recordComputeDispatchToCmdStream(
-                cmdStream,
-                m_UpsamplePipe,
-                upsampleDispatchCount,
-                {vkcv::DescriptorSetUsage(0, m_UpsampleDescSets[mipLevel])},
-                vkcv::PushConstants(0)
-        );
-        // image barrier between mips
-        p_Core->recordImageMemoryBarrier(cmdStream, m_Blur.getHandle());
-    }
-
-    p_Core->recordEndDebugLabel(cmdStream);
-}
-
-void BloomAndFlares::execLensFeaturePipe(const vkcv::CommandStreamHandle &cmdStream)
-{
-    p_Core->recordBeginDebugLabel(cmdStream, "Lense flare generation", { 1, 1, 1, 1 });
-
-    // lens feature generation descriptor writes
-    p_Core->prepareImageForSampling(cmdStream, m_Blur.getHandle());
-    p_Core->prepareImageForStorage(cmdStream, m_LensFeatures.getHandle());
-
-    const uint32_t targetMip = 2;
-    const uint32_t mipLevel = std::min(targetMip, m_LensFeatures.getMipCount());
-
-    vkcv::DescriptorWrites lensFeatureWrites;
-    lensFeatureWrites.sampledImageWrites = {vkcv::SampledImageDescriptorWrite(0, m_Blur.getHandle(), 0)};
-    lensFeatureWrites.samplerWrites = {vkcv::SamplerDescriptorWrite(1, m_LinearSampler)};
-    lensFeatureWrites.storageImageWrites = {vkcv::StorageImageDescriptorWrite(2, m_LensFeatures.getHandle(), mipLevel)};
-    p_Core->writeDescriptorSet(m_LensFlareDescSet, lensFeatureWrites);
-
-    auto dispatchCountX  = static_cast<float>(m_Width)  / 8.0f;
-    auto dispatchCountY = static_cast<float>(m_Height) / 8.0f;
-    // lens feature generation dispatch
-    uint32_t lensFeatureDispatchCount[3] = {
-            static_cast<uint32_t>(glm::ceil(dispatchCountX / std::exp2(mipLevel))),
-            static_cast<uint32_t>(glm::ceil(dispatchCountY / std::exp2(mipLevel))),
-            1
-    };
-    p_Core->recordComputeDispatchToCmdStream(
-            cmdStream,
-            m_LensFlarePipe,
-            lensFeatureDispatchCount,
-            {vkcv::DescriptorSetUsage(0, m_LensFlareDescSet)},
-            vkcv::PushConstants(0));
-
-    // upsample dispatch
-    p_Core->prepareImageForStorage(cmdStream, m_LensFeatures.getHandle());
-
-    // upsample dispatch for each mip map
-    for (uint32_t i = mipLevel; i > 0; i--)
-    {
-        // mip descriptor writes
-        vkcv::DescriptorWrites mipUpsampleWrites;
-        mipUpsampleWrites.sampledImageWrites = { vkcv::SampledImageDescriptorWrite(0, m_LensFeatures.getHandle(), i, true) };
-        mipUpsampleWrites.samplerWrites = { vkcv::SamplerDescriptorWrite(1, m_LinearSampler) };
-        mipUpsampleWrites.storageImageWrites = { vkcv::StorageImageDescriptorWrite(2, m_LensFeatures.getHandle(), i - 1) };
-        p_Core->writeDescriptorSet(m_UpsampleLensFlareDescSets[i], mipUpsampleWrites);
-
-        auto mipDivisor = glm::pow(2.0f, static_cast<float>(i) - 1.0f);
-
-        auto upsampleDispatchX = static_cast<float>(m_Width) / mipDivisor;
-        auto upsampleDispatchY = static_cast<float>(m_Height) / mipDivisor;
-        upsampleDispatchX /= 8.0f;
-        upsampleDispatchY /= 8.0f;
-
-        const uint32_t upsampleDispatchCount[3] = {
-                static_cast<uint32_t>(glm::ceil(upsampleDispatchX)),
-                static_cast<uint32_t>(glm::ceil(upsampleDispatchY)),
-                1
-        };
-
-        p_Core->recordComputeDispatchToCmdStream(
-            cmdStream,
-            m_UpsamplePipe,
-            upsampleDispatchCount,
-            { vkcv::DescriptorSetUsage(0, m_UpsampleLensFlareDescSets[i]) },
-            vkcv::PushConstants(0)
-        );
-        // image barrier between mips
-        p_Core->recordImageMemoryBarrier(cmdStream, m_LensFeatures.getHandle());
-    }
-
-    p_Core->recordEndDebugLabel(cmdStream);
-}
-
-void BloomAndFlares::execCompositePipe(const vkcv::CommandStreamHandle &cmdStream, const vkcv::ImageHandle& colorAttachment,
-    const uint32_t attachmentWidth, const uint32_t attachmentHeight, const glm::vec3& cameraForward)
-{
-    p_Core->recordBeginDebugLabel(cmdStream, "Bloom/lense flare composition", { 1, 1, 1, 1 });
-
-    p_Core->prepareImageForSampling(cmdStream, m_Blur.getHandle());
-    p_Core->prepareImageForSampling(cmdStream, m_LensFeatures.getHandle());
-    p_Core->prepareImageForStorage(cmdStream, colorAttachment);
-
-    // bloom composite descriptor write
-    vkcv::DescriptorWrites compositeWrites;
-    compositeWrites.sampledImageWrites = {vkcv::SampledImageDescriptorWrite(0, m_Blur.getHandle()),
-                                          vkcv::SampledImageDescriptorWrite(1, m_LensFeatures.getHandle()),
-                                          vkcv::SampledImageDescriptorWrite(4, m_radialLut.getHandle()),
-                                          vkcv::SampledImageDescriptorWrite(6, m_lensDirt.getHandle()) };
-    compositeWrites.samplerWrites = {vkcv::SamplerDescriptorWrite(2, m_LinearSampler),
-                                     vkcv::SamplerDescriptorWrite(5, m_RadialLutSampler) };
-    compositeWrites.storageImageWrites = {vkcv::StorageImageDescriptorWrite(3, colorAttachment)};
-    p_Core->writeDescriptorSet(m_CompositeDescSet, compositeWrites);
-
-    float dispatchCountX = static_cast<float>(attachmentWidth)  / 8.0f;
-    float dispatchCountY = static_cast<float>(attachmentHeight) / 8.0f;
-
-    uint32_t compositeDispatchCount[3] = {
-            static_cast<uint32_t>(glm::ceil(dispatchCountX)),
-            static_cast<uint32_t>(glm::ceil(dispatchCountY)),
-            1
-    };
-
-    vkcv::PushConstants pushConstants(sizeof(cameraForward));
-    pushConstants.appendDrawcall(cameraForward);
-
-    // bloom composite dispatch
-    p_Core->recordComputeDispatchToCmdStream(
-        cmdStream,
-        m_CompositePipe,
-        compositeDispatchCount,
-        {vkcv::DescriptorSetUsage(0, m_CompositeDescSet)},
-        pushConstants);
-
-    p_Core->recordEndDebugLabel(cmdStream);
-}
-
-void BloomAndFlares::execWholePipeline(const vkcv::CommandStreamHandle &cmdStream, const vkcv::ImageHandle &colorAttachment, 
-    const uint32_t attachmentWidth, const uint32_t attachmentHeight, const glm::vec3& cameraForward)
-{
-    execDownsamplePipe(cmdStream, colorAttachment);
-    execUpsamplePipe(cmdStream);
-    execLensFeaturePipe(cmdStream);
-    execCompositePipe(cmdStream, colorAttachment, attachmentWidth, attachmentHeight, cameraForward);
-}
-
-void BloomAndFlares::updateImageDimensions(uint32_t width, uint32_t height)
-{
-    m_Width  = width / 2;
-    m_Height = height / 2;
-
-    p_Core->getContext().getDevice().waitIdle();
-    m_Blur = p_Core->createImage(m_ColorBufferFormat, m_Width, m_Height, 1, true, true, false);
-    m_LensFeatures = p_Core->createImage(m_ColorBufferFormat, m_Width, m_Height, 1, true, true, false);
-}
\ No newline at end of file
diff --git a/projects/voxelization/src/BloomAndFlares.hpp b/projects/voxelization/src/BloomAndFlares.hpp
deleted file mode 100644
index 3d63d9f37b4733eaea170a3e4561774c0d53208b..0000000000000000000000000000000000000000
--- a/projects/voxelization/src/BloomAndFlares.hpp
+++ /dev/null
@@ -1,59 +0,0 @@
-#pragma once
-#include <vkcv/Core.hpp>
-#include <glm/glm.hpp>
-
-class BloomAndFlares{
-public:
-    BloomAndFlares(vkcv::Core *p_Core,
-                   vk::Format colorBufferFormat,
-                   uint32_t width,
-                   uint32_t height);
-
-    void execWholePipeline(const vkcv::CommandStreamHandle &cmdStream, const vkcv::ImageHandle &colorAttachment,
-        const uint32_t attachmentWidth, const uint32_t attachmentHeight, const glm::vec3& cameraForward);
-
-    void updateImageDimensions(uint32_t width, uint32_t height);
-
-private:
-    vkcv::Core *p_Core;
-
-    vk::Format m_ColorBufferFormat;
-    uint32_t m_Width;
-    uint32_t m_Height;
-
-    vkcv::SamplerHandle m_LinearSampler;
-    vkcv::SamplerHandle m_RadialLutSampler;
-    vkcv::Image m_Blur;
-    vkcv::Image m_LensFeatures;
-
-    vkcv::Image m_radialLut;
-    vkcv::Image m_lensDirt;
-
-    vkcv::ComputePipelineHandle                     m_DownsamplePipe;
-    std::vector<vkcv::DescriptorSetLayoutHandle>    m_DownsampleDescSetLayouts;
-    std::vector<vkcv::DescriptorSetHandle>          m_DownsampleDescSets; // per mip desc set
-
-    std::vector<vkcv::DescriptorSetLayoutHandle>    m_UpsampleLensFlareDescSetLayouts;
-    std::vector<vkcv::DescriptorSetHandle>          m_UpsampleLensFlareDescSets; // per mip desc set
-
-    vkcv::ComputePipelineHandle                     m_UpsamplePipe;
-    std::vector<vkcv::DescriptorSetLayoutHandle>    m_UpsampleDescSetLayouts;
-    std::vector<vkcv::DescriptorSetHandle>          m_UpsampleDescSets;   // per mip desc set
-
-    vkcv::ComputePipelineHandle                            m_LensFlarePipe;
-    vkcv::DescriptorSetLayoutHandle                 m_LensFlareDescSetLayout;
-    vkcv::DescriptorSetHandle                       m_LensFlareDescSet;
-
-    vkcv::ComputePipelineHandle                     m_CompositePipe;
-    vkcv::DescriptorSetLayoutHandle                 m_CompositeDescSetLayout;
-    vkcv::DescriptorSetHandle                       m_CompositeDescSet;
-
-    void execDownsamplePipe(const vkcv::CommandStreamHandle &cmdStream, const vkcv::ImageHandle &colorAttachment);
-    void execUpsamplePipe(const vkcv::CommandStreamHandle &cmdStream);
-    void execLensFeaturePipe(const vkcv::CommandStreamHandle &cmdStream);
-    void execCompositePipe(const vkcv::CommandStreamHandle &cmdStream, const vkcv::ImageHandle &colorAttachment, 
-        const uint32_t attachmentWidth, const uint32_t attachmentHeight, const glm::vec3& cameraForward);
-};
-
-
-
diff --git a/projects/voxelization/src/main.cpp b/projects/voxelization/src/main.cpp
index 2245419f87d196e913ba27e8e78ffff73aab04b6..dd4e0374e7e18477f69fde79e090e3d4c5689aa5 100644
--- a/projects/voxelization/src/main.cpp
+++ b/projects/voxelization/src/main.cpp
@@ -8,9 +8,9 @@
 #include "Voxelization.hpp"
 #include "vkcv/gui/GUI.hpp"
 #include "ShadowMapping.hpp"
-#include "BloomAndFlares.hpp"
 #include <vkcv/upscaling/FSRUpscaling.hpp>
 #include <vkcv/upscaling/BilinearUpscaling.hpp>
+#include <vkcv/effects/BloomAndFlaresEffect.hpp>
 
 int main(int argc, const char** argv) {
 	const char* applicationName = "Voxelization";
@@ -556,14 +556,7 @@ int main(int argc, const char** argv) {
 		voxelSampler,
 		msaa);
 
-	BloomAndFlares bloomFlares(&core, colorBufferFormat, swapchainExtent.width, swapchainExtent.height);
-
-	window.e_key.add([&](int key, int scancode, int action, int mods) {
-		if (key == GLFW_KEY_R && action == GLFW_PRESS) {
-			bloomFlares = BloomAndFlares(&core, colorBufferFormat, swapchainExtent.width, swapchainExtent.height);
-		}
-	});
-
+	vkcv::effects::BloomAndFlaresEffect bloomFlares (core, true);
 	vkcv::Buffer<glm::vec3> cameraPosBuffer = core.createBuffer<glm::vec3>(vkcv::BufferType::UNIFORM, 1);
 
 	struct VolumetricSettings {
@@ -701,8 +694,6 @@ int main(int argc, const char** argv) {
 					swapchainWidth, swapchainHeight, 1,
 					false, true
 			).getHandle();
-			
-			bloomFlares.updateImageDimensions(swapchainWidth, swapchainHeight);
 		}
 
 		auto end = std::chrono::system_clock::now();
@@ -869,10 +860,9 @@ int main(int argc, const char** argv) {
 			}
 			core.recordEndDebugLabel(cmdStream);
 		}
-
-		bloomFlares.execWholePipeline(cmdStream, resolvedColorBuffer, fsrWidth, fsrHeight,
-			glm::normalize(cameraManager.getActiveCamera().getFront())
-		);
+		
+		bloomFlares.updateCameraDirection(cameraManager.getActiveCamera());
+		bloomFlares.recordEffect(cmdStream, resolvedColorBuffer, resolvedColorBuffer);
 
 		core.prepareImageForStorage(cmdStream, swapBuffer);
 		core.prepareImageForSampling(cmdStream, resolvedColorBuffer);
diff --git a/src/vkcv/Core.cpp b/src/vkcv/Core.cpp
index ba69cab5fcd2a1ce78e5a3491e24ab7f273e0474..a44ee97821e4fbf6241e74b9f010fcf46970e91f 100644
--- a/src/vkcv/Core.cpp
+++ b/src/vkcv/Core.cpp
@@ -875,6 +875,10 @@ namespace vkcv
 	vk::Format Core::getImageFormat(const ImageHandle& image) {
 		return m_ImageManager->getImageFormat(image);
 	}
+	
+	uint32_t Core::getImageMipLevels(const ImageHandle &image) {
+		return m_ImageManager->getImageMipCount(image);
+	}
 
 	Swapchain& Core::getSwapchainOfCurrentWindow() {
 		return m_SwapchainManager->getSwapchain(Window::getFocusedWindow().getSwapchainHandle());