diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..cc5e99a --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,24 @@ +## Description + +Explain what, why and the though process leading to the changes + +link to issue if relevant. + +### code changes +- point form description of changes + +## Type of change + +is this a major or a minor or patch update ? + +## How Has This Been Tested? + +Describe operating system and Nuke versions + +## Checklist: + +- [ ] My code follows the style guidelines of this project +- [ ] I have performed a self-review of my own code +- [ ] I have commented my code, particularly in hard-to-understand areas +- [ ] I have made corresponding changes to the documentation +- [ ] My changes generate no new warnings diff --git a/documentation/CMakeLists.txt b/documentation/CMakeLists.txt index dead310..f30a41a 100644 --- a/documentation/CMakeLists.txt +++ b/documentation/CMakeLists.txt @@ -13,8 +13,8 @@ add_custom_target(documentation ALL add_custom_target(documentation_package DEPENDS documentation COMMENT "creating documentation package for ${PROJECT_PACKAGE_NAME}" - COMMAND ${CMAKE_COMMAND} -E tar "cfvz" "${PROJECT_NAME}-${LAYER_ALCHEMY_VERSION_STRING}_documentation.tgz" "${CMAKE_CURRENT_BINARY_DIR}/documentation" - COMMAND ${CMAKE_COMMAND} -E copy "${PROJECT_NAME}-${LAYER_ALCHEMY_VERSION_STRING}_documentation.tgz" "../" + COMMAND ${CMAKE_COMMAND} -E tar "cfvz" "${PROJECT_NAME}-${LAYER_ALCHEMY_VERSION_STRING}_documentation.tar.gz" "${CMAKE_CURRENT_BINARY_DIR}/documentation" + COMMAND ${CMAKE_COMMAND} -E copy "${PROJECT_NAME}-${LAYER_ALCHEMY_VERSION_STRING}_documentation.tar.gz" "../" ) install( diff --git a/documentation/docs/GradeBeauty.md b/documentation/docs/GradeBeauty.md index 424cd79..d099695 100644 --- a/documentation/docs/GradeBeauty.md +++ b/documentation/docs/GradeBeauty.md @@ -22,8 +22,8 @@ Lighters can use it with the light_group [LayerSet](core.md#layersets) to : -* Tweak light group values and animations, (flicker matching) -* Use two nodes, one in _multiply_ mode, and one in _stops_ mode to adhere to the exposure/gain decoupling +* Tweak light group values or animations, (flicker matching) +* Use two nodes, one in _multiply_ mode, and one in _stops_ mode to mimic to the exposure/gain decoupling * Send the values back to their cg application ! Compositors: @@ -42,6 +42,7 @@ Compositors: | target_layer | enumeration | selects which layer to pre-subtract layers from (if enabled) and add the modified layers to | | math_type | enumeration | selects the color knob preference | subtract | bool | controls pre-subtracting the [LayerSet](core.md#layersets) from the target layer | +| black_clamp | bool | clamp negative values from all output layers | | reset values | button | resets all color knobs to their defaults | ## [PixelIop](https://learn.foundry.com/nuke/developers/11.3/ndkdevguide/2d/pixeliops.html) Knobs @@ -61,7 +62,7 @@ Compositors: ### subtract The purpose of the subtract knob is to make sure that the output will always match the beauty render, even if -some layers are missing, yet included in the beauty render. +some aov layers are missing in the beauty render. This can happen, so it is enabled by default. @@ -70,9 +71,24 @@ This can happen, so it is enabled by default. | enabled | the additive sum of the chosen [LayerSet](core.md#layersets) is subtracted from the target layer before recombining with this node's modifications |

this means any difference between the target layer and the render layers is kept in the final output

| disabled | bypasses aov/target layer pre-subtraction | _when this is disabled, it replaces the target layer with the result_ +!!! info "" + + If enabled and you completely remove layers, it's possible that you get negative values when + completely removing aovs AND subtracting. In this scenario, enable black_clamp + +### black_clamp +The purpose of the black_clamp knob is to make sure that the output pixels always are alaways positive. + +| value | what it does | notes | +| ----- | ------------ | ----- | +| enabled | no negative values are passed to the output |

in the odd case that you need to stop negative values this will do the job

+| disabled | bypasses aov/target layer negative clamping | + ## Arnold 5 LayerSets +LayerAlchemy includes arnold configurations by default + Arnold can separate the beauty render components in various ways. The following type are natively supported with the [default](https://docs.arnoldrenderer.com/display/A5AFMUG/AOVs#AOVs-AOVs) aov naming : diff --git a/documentation/docs/GradeBeautyLayer.md b/documentation/docs/GradeBeautyLayer.md new file mode 100644 index 0000000..ec9dc02 --- /dev/null +++ b/documentation/docs/GradeBeautyLayer.md @@ -0,0 +1,22 @@ +# GradeBeautyLayer + +!!! info "" + + GradeBeautyLayer provides a simple way to specifically grade a cg layer and replace it in the beauty + +#### Order of operations : +- input layer subtracted from the target layer +- layer is modified +- modified source layer is added to the target layer + + +![GradeBeautyLayer](media/parameters/GradeBeautyLayer.png) + +## Knob reference + +| knob name | type | what it does | +| --------- | ---- | ------------ +| source_layer | enumeration | layer to to grade | +| target_layer | enumeration | layer to subtract and add the modied source_layer to | +| reset values | button | resets all color knobs to their defaults | + diff --git a/documentation/docs/GradeBeautyLayerSet.md b/documentation/docs/GradeBeautyLayerSet.md index 9bd3ed1..0a9f7ba 100644 --- a/documentation/docs/GradeBeautyLayerSet.md +++ b/documentation/docs/GradeBeautyLayerSet.md @@ -5,11 +5,6 @@ GradeBeautyLayerSet provides a simple way to specifically grade multiple cg layers using a [LayerSet](core .md#layersets) - it's a cross between : - - - [GradeLayerSet](GradeLayerSet.md) - - [GradeBeauty](GradeBeauty.md) - - [FlattenLayerSet](FlattenLayerSet.md) Image processing math is exactly like the Nuke Grade node except that, you can grade multiple layers at the @@ -30,5 +25,5 @@ same time | mode name | what it does | | --------- | ------------ | -| copy | outputs only the addition of modified layers to the target layer | +| copy | outputs only the modified layers to the target layer (added together)| | add | this first subtracts all layers from the target layer, then adds each of them back diff --git a/documentation/docs/about.md b/documentation/docs/about.md index 7a8057c..b9057dc 100644 --- a/documentation/docs/about.md +++ b/documentation/docs/about.md @@ -4,10 +4,13 @@ | ---- | ---- | ----- | -------- | | Sébastien Jacob | author, designer | [email](mailto:sebjacobvfx@gmail.com) | [LinkedIn](https://www.linkedin.com/in/s%C3%A9bastien-jacob-3b05112/) -!!! info "special thanks 👍, for their help in kick starting the very first builds goes to :" +# special thanks 👍 - - Jean-Christophe Morin - - Gregory Starck +- Charles Fleche +- Christian Morin +- Mathieu Dupuis +- Jean-Christophe Morin +- Gregory Starck # Contributing diff --git a/documentation/docs/core.md b/documentation/docs/core.md index 6b99afd..35c9988 100644 --- a/documentation/docs/core.md +++ b/documentation/docs/core.md @@ -22,9 +22,8 @@ as they will will be referenced throughout the documentation - limited mostly by the callback system to trigger behaviors. - cannot be cloned in Nuke. - - internally, the algorithm is spread out across multiple nodes that must be managed. - - lots of knob setting python code, and workarounds are required. - - no direct image processing possible. + - internally, the algorithm is spread out across multiple Nuke nodes that must be managed. + - lots python and expression code, workarounds are required. - some knobs are c++ only. - In general, the fewer the nodes, the faster the comp. diff --git a/documentation/docs/media/parameters/GradeBeauty.png b/documentation/docs/media/parameters/GradeBeauty.png index 4d57eb6..975b69f 100644 Binary files a/documentation/docs/media/parameters/GradeBeauty.png and b/documentation/docs/media/parameters/GradeBeauty.png differ diff --git a/documentation/docs/media/parameters/GradeBeautyLayer.png b/documentation/docs/media/parameters/GradeBeautyLayer.png new file mode 100644 index 0000000..738c4a1 Binary files /dev/null and b/documentation/docs/media/parameters/GradeBeautyLayer.png differ diff --git a/documentation/docs/media/parameters/GradeBeauty_uncollapsed.png b/documentation/docs/media/parameters/GradeBeauty_uncollapsed.png index 0071fc3..7b7ef50 100644 Binary files a/documentation/docs/media/parameters/GradeBeauty_uncollapsed.png and b/documentation/docs/media/parameters/GradeBeauty_uncollapsed.png differ diff --git a/documentation/docs/tools.md b/documentation/docs/tools.md index 3bf7d2e..4947562 100644 --- a/documentation/docs/tools.md +++ b/documentation/docs/tools.md @@ -11,13 +11,23 @@ This also runs at Nuke startup to make sure the config files are ok ConfigTester 😷 -Simple executable to test if a yaml file can be loaded and a LayerMap object can be constructed +Simple executable to validate yaml files + +LayerAlchemy 0.9.0 +https://github.com/sebjacob/LayerAlchemy -Example usage: ConfigTester /path/to/config.yaml +Example usage: + +ConfigTester --config /path/to/config.yaml +ConfigTester --config /path/to/config1.yaml /path/to/config2.yaml +Usage: ./ConfigTester [options] +Options: + --config List of layer names to test (Required) + --quiet disable terminal output, return code only ``` ```bash -./ConfigTester $LAYER_ALCHEMY_LAYER_CONFIG +./ConfigTester --config $LAYER_ALCHEMY_LAYER_CONFIG ✅ LayerAlchemy : valid configuration file /path/to/layers.yaml ``` diff --git a/documentation/mkdocs.yml b/documentation/mkdocs.yml index 545bbd3..1965c3c 100644 --- a/documentation/mkdocs.yml +++ b/documentation/mkdocs.yml @@ -17,6 +17,7 @@ nav : - Nuke plugins : - GradeBeauty.md - GradeBeautyLayerSet.md + - GradeBeautyLayer.md - GradeLayerSet.md - FlattenLayerSet.md - RemoveLayerSet.md diff --git a/icons/GradeBeautyLayer.png b/icons/GradeBeautyLayer.png new file mode 100644 index 0000000..2fd3dc0 Binary files /dev/null and b/icons/GradeBeautyLayer.png differ diff --git a/include/nuke/LayerSet.h b/include/nuke/LayerSet.h index 88179de..c1c8c04 100644 --- a/include/nuke/LayerSet.h +++ b/include/nuke/LayerSet.h @@ -30,8 +30,12 @@ namespace LayerSet { namespace Utilities { void hard_copy(const DD::Image::Row& fromRow, int x, int r, DD::Image::ChannelSet channels, DD::Image::Row& toRow); float* hard_copy(const DD::Image::Row& fromRow, int x, int r, DD::Image::Channel channel, DD::Image::Row& toRow); + // centralized pixel engine code for Grade type plugins + void gradeChannelPixelEngine(const DD::Image::Row& in, int y, int x, int r, DD::Image::ChannelSet& channels, DD::Image::Row& aRow, float* A, float* B, float* G, bool reverse, bool clampBlack, bool clampWhite); // use to validate if a target layer the user selects is within the required color ranges void validateTargetLayerColorIndex(DD::Image::Op* t_op, const DD::Image::ChannelSet& targetLayer, unsigned minIndex, unsigned maxIndex); + // test float value for pow functions, NDK states that linux behaves badly for very large or very small exponent values. + float validateGammaValue(const float& gammaValue); } // End namespace Utilities namespace Knobs { diff --git a/include/version.h b/include/version.h index c2bd15b..0585cf9 100644 --- a/include/version.h +++ b/include/version.h @@ -1,6 +1,6 @@ #pragma once #define LAYER_ALCHEMY_VERSION_MAJOR 0 -#define LAYER_ALCHEMY_VERSION_MINOR 8 +#define LAYER_ALCHEMY_VERSION_MINOR 9 #define LAYER_ALCHEMY_VERSION_PATCH 0 #include diff --git a/python/nuke/init.py b/python/nuke/init.py index cc49929..c216cee 100644 --- a/python/nuke/init.py +++ b/python/nuke/init.py @@ -4,5 +4,6 @@ import layer_alchemy.callbacks if layer_alchemy.utilities.nukeVersionCompatible(): + layer_alchemy.utilities.validateConfigFileEnvironmentVariables() layer_alchemy.utilities.pluginAddPaths() layer_alchemy.callbacks.setupCallbacks() diff --git a/python/nuke/layer_alchemy/callbacks.py b/python/nuke/layer_alchemy/callbacks.py index c61b91e..3e36743 100644 --- a/python/nuke/layer_alchemy/callbacks.py +++ b/python/nuke/layer_alchemy/callbacks.py @@ -1,61 +1,41 @@ """LayerAlchemy callback module""" -import os import nuke -import nukescripts -import utilities import constants def setupCallbacks(): """ Utility function to add all callbacks for plugins in this suite. - Adds knobChanged and autolabel callbacks """ - for pluginName in constants.LAYER_ALCHEMY_PLUGINS.keys(): - nuke.addKnobChanged( - lambda: knobChangedCommon(nuke.thisNode(), nuke.thisKnob()), - nodeClass=pluginName - ) - nuke.addAutolabel(autolabel, nodeClass=pluginName) + for pluginName in constants.LAYER_ALCHEMY_PLUGIN_NAMES: + nuke.addAutolabel(_autolabel, nodeClass=pluginName) + pass -def knobChangedCommon(node, knob): - """ - Common knobChanged function for plugins in this suite - :param node: the Nuke node object - :type node: :class:`nuke.Node` - :param knob: the Nuke knob object - :type knob: :class:`nuke.Knob` - """ - if knob.name() == 'docButton': - documentationIndex = utilities.getDocumentationIndexPath() - if not documentationIndex: - message = 'Local documentation is unavailable, please visit :\n\n{website}'.format( - website=constants.LAYER_ALCHEMY_URL) - nuke.message(message) - return - pluginDocFileName = '{0}.{1}'.format(node.Class(), 'html') - htmlFile = os.path.join(os.path.dirname(documentationIndex), pluginDocFileName) - outputPath = htmlFile if os.path.isfile(htmlFile) else documentationIndex - nukescripts.start(outputPath) - - -def autolabel(): +def _autolabel(): """ Common autolabel function for plugins in this suite + :return: the formatted text to use as a label + :rtype: str """ node = nuke.thisNode() nodeName = node.name() - layerSetKnob = node.knob('layer_set') - index = int(layerSetKnob.getValue()) - layerSetName = layerSetKnob.enumName(index) + text = [] + for knobName in ('layer_set', 'channels', 'maskChannelInput', 'unpremult'): + knob = node.knob(knobName) + if knob: + index = int(knob.getValue()) + value = knob.enumName(index) if isinstance(knob, nuke.Enumeration_Knob) else knob.value() + if value and value != 'none': + text.append(value) + node.knob('indicators').setValue(_getIndicatorValue(node)) - if layerSetName: - return '{name}\n({layerSet})'.format(name=nodeName, layerSet=layerSetName) + if text: + return '{name}\n({layers})'.format(name=nodeName, layers=' / '.join(text)) else: return nodeName @@ -71,8 +51,11 @@ def _getIndicatorValue(node): indicators = 0 knobs = node.allKnobs() mixKnob = node.knob('mix') + maskKnob = node.knob('maskChannelInput') if mixKnob and mixKnob.value() != 1: indicators += 16 + if maskKnob and maskKnob.getValue() != 0: + indicators += 4 if node.clones(): indicators += 8 if any(knob.isAnimated() for knob in knobs): diff --git a/python/nuke/layer_alchemy/constants.py b/python/nuke/layer_alchemy/constants.py index 013ad79..ff47f8f 100644 --- a/python/nuke/layer_alchemy/constants.py +++ b/python/nuke/layer_alchemy/constants.py @@ -2,22 +2,23 @@ import os -LAYER_ALCHEMY_PLUGINS = { # The name of the plugin and its icon name - 'GradeBeauty': 'GradeBeauty.png', - 'GradeBeautyLayerSet': 'GradeBeautyLayerSet.png', - 'FlattenLayerSet': 'FlattenLayerSet.png', - 'RemoveLayerSet': 'RemoveLayerSet.png', - 'MultiplyLayerSet': 'MultiplyLayerSet.png', - 'GradeLayerSet': 'GradeLayerSet.png', -} +LAYER_ALCHEMY_URL = 'https://github.com/sebjacob/LayerAlchemy' + +LAYER_ALCHEMY_PLUGIN_NAMES = [ + 'GradeBeauty', + 'GradeBeautyLayerSet', + 'GradeBeautyLayer', + 'GradeLayerSet', + 'MultiplyLayerSet', + 'RemoveLayerSet', + 'FlattenLayerSet' +] LAYER_ALCHEMY_CONFIGS_DICT = { 'LAYER_ALCHEMY_LAYER_CONFIG': 'layers.yaml', 'LAYER_ALCHEMY_CHANNEL_CONFIG': 'channels.yaml' } -LAYER_ALCHEMY_URL = 'https://github.com/sebjacob/LayerAlchemy' - _thisDir = os.path.dirname(os.path.realpath(__file__)) _layerAlchemyNukeDir = os.path.abspath(os.path.join(_thisDir, '..')) diff --git a/python/nuke/layer_alchemy/documentation.py b/python/nuke/layer_alchemy/documentation.py new file mode 100644 index 0000000..b026bbb --- /dev/null +++ b/python/nuke/layer_alchemy/documentation.py @@ -0,0 +1,79 @@ +"""shared documentation/help module for LayerAlchemy""" + +import os + +import nuke + +import constants +import utilities + +if nuke.GUI: + if nuke.NUKE_VERSION_MAJOR > 10: + from PySide2.QtWebEngineWidgets import QWebEngineView as qWebview + from PySide2.QtCore import QUrl + from PySide2.QtWidgets import QApplication + else: + from PySide.QtWebKit import QWebView as qWebview + from PySide.QtCore import QUrl + from PySide.QtGui import QApplication + + +def documentationPath(node=None): + """ + find an absolute path to the project documentation, or the html file for the chosen node + :param node: the Nuke node object + :type node: :class:`nuke.Node` + :return: absolute html file path + :rtype: str + :raises ValueError if absolutely no html file can be found + """ + documentationIndexPath = utilities.getDocumentationIndexPath() + htmlFile = documentationIndexPath + if not documentationIndexPath or not os.path.exists(documentationIndexPath): + message = 'Local documentation is unavailable, please visit :\n\n{website}'.format( + website=constants.LAYER_ALCHEMY_URL) + if nuke.GUI: + nuke.message(message) + raise ValueError(message) + if node: + pluginDocFileName = '{0}.html'.format(node.Class()) + htmlFile = os.path.join(os.path.dirname(documentationIndexPath), pluginDocFileName) + if not os.path.isfile(htmlFile): + htmlFile = documentationIndexPath + return htmlFile + + +def documentationWebViewWidget(documentationPath): + """ + Creates a PySide web view Widget + :param str documentationPath: the absolute path to the html file to display + :return: a PySide web view widget set up with the local documentation + :rtype: PySide2.QtWebEngineWidgets.QWebEngineView + """ + webView = qWebview() + webView.setWindowTitle('LayerAlchemy Documentation') + qUrl = QUrl.fromLocalFile(documentationPath) + webView.load(qUrl) + screenGeo = QApplication.desktop().geometry() + webView.setMinimumHeight(screenGeo.height() / 2) + center = screenGeo.center() + webView.move(center - webView.rect().center()) + return webView + + +def displayDocumentation(node=None): + """ + Wrapper function to display documentation in Nuke as a PySide2.QtWebEngineWidgets.QWebEngineView + Due to a bug in Nuke 11, the documentation will be displayed using the web browser + https://support.foundry.com/hc/en-us/articles/360000148684-Q100379-Importing-PySide2-QtWebEngine-into-Nuke-11 + :param node: the Nuke node object + :type node: :class:`nuke.Node` + :return: a PySide web view widget set up with the local documentation + :rtype: PySide2.QtWebEngineWidgets.QWebEngineView + """ + htmlFile = documentationPath(node) + if nuke.env['NukeVersionMajor'] == 11: + import nukescripts + nukescripts.start(htmlFile) + else: + return documentationWebViewWidget(htmlFile) diff --git a/python/nuke/layer_alchemy/utilities.py b/python/nuke/layer_alchemy/utilities.py index b6a0556..264446b 100644 --- a/python/nuke/layer_alchemy/utilities.py +++ b/python/nuke/layer_alchemy/utilities.py @@ -10,10 +10,8 @@ def pluginAddPaths(): """ - This validates that configuration files are present and valid - and adds the icon and plugin directories to Nuke's pluginPath + adds the icon and plugin directories to Nuke's pluginPath """ - validateConfigFileEnvironmentVariables() nuke.pluginAddPath(constants.LAYER_ALCHEMY_ICON_DIR) nuke.pluginAddPath(_getPluginDirForCurrentNukeVersion()) @@ -67,9 +65,9 @@ def _validateConfigFile(configFilePath): """ if not os.path.isfile(configFilePath): raise ValueError('missing configuration file') - error = subprocess.call([constants.LAYER_ALCHEMY_CONFIGTESTER_BIN, configFilePath]) - if error: - raise ValueError('invalid configuration file') + return subprocess.call( + [constants.LAYER_ALCHEMY_CONFIGTESTER_BIN, '--config', configFilePath, '--quiet'] + ) def validateConfigFileEnvironmentVariables(): @@ -83,4 +81,6 @@ def validateConfigFileEnvironmentVariables(): if not configFile: configFile = os.path.join(constants.LAYER_ALCHEMY_CONFIGS_DIR, baseName) os.environ[envVarName] = configFile - _validateConfigFile(configFile) + error = _validateConfigFile(configFile) + if error: + raise ValueError('invalid configuration file {}'.format(configFile)) diff --git a/python/nuke/menu.py b/python/nuke/menu.py index c2b0bba..e69978e 100644 --- a/python/nuke/menu.py +++ b/python/nuke/menu.py @@ -2,22 +2,50 @@ import nuke -import layer_alchemy.constants import layer_alchemy.utilities if layer_alchemy.utilities.nukeVersionCompatible(): toolbar = nuke.menu("Nodes") menu = toolbar.addMenu("LayerAlchemy", icon="layer_alchemy.png", index=-1) - for pluginName, icon in sorted(layer_alchemy.constants.LAYER_ALCHEMY_PLUGINS.items()): - menu.addCommand(name=pluginName, - command="nuke.createNode('{}')".format(pluginName), - icon=icon) + menu.addCommand(name='GradeBeauty', + command="nuke.createNode('GradeBeauty')", + icon="GradeBeauty.png" + ) + menu.addCommand(name='GradeBeautyLayerSet', + command="nuke.createNode('GradeBeautyLayerSet')", + icon="GradeBeautyLayerSet.png" + ) + menu.addCommand(name='GradeBeautyLayer', + command="nuke.createNode('GradeBeautyLayer')", + icon="GradeBeautyLayer.png" + ) + menu.addSeparator() + menu.addCommand(name='GradeLayerSet', + command="nuke.createNode('GradeLayerSet')", + icon="GradeLayerSet.png" + ) + menu.addCommand(name='MultiplyLayerSet', + command="nuke.createNode('MultiplyLayerSet')", + icon="MultiplyLayerSet.png" + ) + menu.addSeparator() + menu.addCommand(name='FlattenLayerSet', + command="nuke.createNode('FlattenLayerSet')", + icon="FlattenLayerSet.png" + ) + menu.addCommand(name='RemoveLayerSet', + command="nuke.createNode('RemoveLayerSet')", + icon="RemoveLayerSet.png" + ) menu.addSeparator() menu.addCommand(name="documentation", - command=("import layer_alchemy;import nuke;import nukescripts;" - "nukescripts.start(layer_alchemy.utilities.getDocumentationIndexPath())"), + command=( + "import layer_alchemy.documentation\n" + "webview = layer_alchemy.documentation.displayDocumentation(node=None)\n" + "if webview:\n" + " webview.show()"), icon="documentation.png" ) diff --git a/src/ConfigTester.cpp b/src/ConfigTester.cpp index 0fcdd63..56dca75 100644 --- a/src/ConfigTester.cpp +++ b/src/ConfigTester.cpp @@ -3,11 +3,13 @@ * usage example: ConfigTester /path/to/config.yaml */ -#include #include #include +#include "argparse.h" + #include "LayerSetCore.h" +#include "version.h" static const string greenText = "\x1B[92m"; static const string redText = "\x1B[31m"; @@ -16,39 +18,76 @@ static const string emojiOk = "\xE2\x9C\x85 "; static const string emojiMedical = "\xF0\x9F\x98\xB7 "; static const string emojiError = "\xE2\x9D\x97 "; -int main(int argc, char** argv) { - if (argc <= 1) { - string usage = - "\nConfigTester " + emojiMedical + "\n\n" - "Simple executable to test if a yaml file can be loaded " - "and a LayerMap object can be constructed\n\n" - "Example usage: ConfigTester /path/to/config.yaml\n"; - std::cout << usage << std::endl; - return 1; - } - if (argc >= 3) { - std::cerr << emojiError << redText << "One config at a time!" << std::endl << endColor; - return 1; +static const std::string DESCRIPTION = "Simple executable to validate yaml files"; +static const string LAYER_ALCHEMY_PROJECT_URL = "https://github.com/sebjacob/LayerAlchemy"; + +static const string HEADER = + "\nConfigTester " + emojiMedical + "\n\n" + DESCRIPTION + "\n\n" + "LayerAlchemy " + LAYER_ALCHEMY_VERSION_STRING + "\n" + + LAYER_ALCHEMY_PROJECT_URL + "\n\n" + "Example usage: \n\nConfigTester --config /path/to/config.yaml\n" + "ConfigTester --config /path/to/config1.yaml /path/to/config2.yaml"; + + +void logException(const char* filePath, const std::exception& e) +{ + std::cerr << emojiError << redText << "[ERROR] LayerAlchemy : " << filePath << e.what() << std::endl << endColor; +} + +int main(int argc, const char* argv[]) +{ + ArgumentParser parser(DESCRIPTION); + parser.add_argument("--config", "List of layer names to test", true); + parser.add_argument("--quiet", "disable terminal output, return code only", false); + + try + { + parser.parse(argc, argv); } - string strFilePath = reinterpret_cast (argv[1]); - std::ifstream inputFile(strFilePath); - if (!inputFile || opendir(strFilePath.c_str())) { - std::cerr << emojiError << redText << - "LayerAlchemy ERROR : not a file : " << strFilePath - << std::endl << endColor; - return 1; + catch (const ArgumentParser::ArgumentNotFound &ex) + { + std::cout << HEADER << std::endl; + parser.print_help(); + + std::cout << ex.what() << std::endl; + return 0; } - try { - LayerMap testLayerMap = LayerMap(loadConfigToMap(strFilePath)); + if (parser.is_help()) + return 0; + + auto configs = parser.getv("config"); + bool quiet = parser.get("quiet"); + + for(auto it = configs.begin(); it != configs.end(); ++it) + { + auto configFilePath = it->c_str(); + std::ifstream inputFile(configFilePath); - } catch (const std::exception& e) { - std::cerr << emojiError << redText << - "LayerAlchemy ERROR : configuration file " << strFilePath << " " << e.what() + if (!inputFile || opendir(configFilePath)) + { + if (!quiet) + { + logException(configFilePath, std::invalid_argument(" is not a file")); + } + return 1; + } + try + { + LayerMap testLayerMap = LayerMap(loadConfigToMap(configFilePath)); + if (!quiet) + { + std::cout << greenText << emojiOk << "LayerAlchemy : valid configuration file " << configFilePath << std::endl << endColor; - return 1; + } + } + catch (const std::exception& e) + { + if (!quiet) + { + logException(configFilePath, e); + } + return 1; + } } - std::cout << greenText << emojiOk << - "LayerAlchemy : valid configuration file " << strFilePath - << std::endl << endColor; return 0; } diff --git a/src/nuke/CMakeLists.txt b/src/nuke/CMakeLists.txt index a457119..70016b6 100644 --- a/src/nuke/CMakeLists.txt +++ b/src/nuke/CMakeLists.txt @@ -72,6 +72,9 @@ list(APPEND NUKE_PLUGINS GradeBeauty) add_library(GradeBeautyLayerSet SHARED ${PROJECT_NUKE_SRC_DIR}/GradeBeautyLayerSet.cpp) list(APPEND NUKE_PLUGINS GradeBeautyLayerSet) +add_library(GradeBeautyLayer SHARED ${PROJECT_NUKE_SRC_DIR}/GradeBeautyLayer.cpp) +list(APPEND NUKE_PLUGINS GradeBeautyLayer) + foreach(PLUGIN ${NUKE_PLUGINS}) set_target_properties(${PLUGIN} PROPERTIES PREFIX "") target_link_libraries(${PLUGIN} ${LAYERSET_LIBS} ${NUKE_LAYERSET_LIBS} ${LIB_DDIMAGE}) diff --git a/src/nuke/FlattenLayerSet.cpp b/src/nuke/FlattenLayerSet.cpp index cdf90ba..9b31d22 100644 --- a/src/nuke/FlattenLayerSet.cpp +++ b/src/nuke/FlattenLayerSet.cpp @@ -102,6 +102,7 @@ void FlattenLayerSet::_validate(bool for_real) updateLayerSetKnob(this, m_lsKnobData, LayerAlchemy::layerCollection, inChannels, excludeLayerFilter); } set_out_channels(activeChannelSet()); + info_.turn_on(m_targetLayer); } void FlattenLayerSet::pixel_engine(const Row& in, int y, int x, int r, ChannelMask channels, Row& out) diff --git a/src/nuke/GradeBeauty.cpp b/src/nuke/GradeBeauty.cpp index 53d9f46..7eba485 100644 --- a/src/nuke/GradeBeauty.cpp +++ b/src/nuke/GradeBeauty.cpp @@ -1,4 +1,5 @@ -#include "math.h" +#include +#include #include #include @@ -149,8 +150,8 @@ class GradeBeautyValueMap { return ptrValueMap.find(knobName)->second[0]; // the first index if where the layer pointer is. } - // computes, for a given layer name, the total value to multiply the pixels with at a specific color index for any given - // math mode + // computes, for a given layer name, the total value to multiply the pixels with + // at a specific color index for any given math mode float getLayerMultiplier(const string& layerName, const int& colorIndex, const int& mode) const { vector values = layerValuePointersForIndex(layerName, colorIndex); @@ -188,8 +189,10 @@ class GradeBeautyValueMap { class GradeBeauty : public DD::Image::PixelIop { private: + bool m_firstRun {true}; LayerAlchemy::LayerSetKnob::LayerSetKnobData m_lsKnobData; int m_mathMode {GRADE_BEAUTY_MATH_MODE::STOPS}; + bool m_clampBlack {true}; bool m_beautyDiff {true}; ChannelSet m_targetLayer {Mask_RGB}; GradeBeautyValueMap m_valueMap; @@ -226,6 +229,7 @@ class GradeBeauty : public DD::Image::PixelIop { void channelPixelEngine(const Row&, int, int, int, ChannelMask, Row&); // pixel engine function when the target layer is requested to render void beautyPixelEngine(const Row&, int y, int x, int r, ChannelSet&, Row&); + GradeBeauty* firstGradeBeauty(); }; @@ -244,6 +248,11 @@ const Iop::Description GradeBeauty::description( build ); +GradeBeauty* GradeBeauty::firstGradeBeauty() +{ + return static_cast( this->firstOp() ); +} + void GradeBeauty::in_channels(int input_number, ChannelSet& mask) const { mask += ChannelMask(activeChannelSet()); @@ -262,7 +271,6 @@ ChannelSet GradeBeauty::activeChannelSet() const return outChans; } - void GradeBeauty::_validate(bool for_real) { copy_info(); // this copies the input info to the output @@ -280,29 +288,35 @@ void GradeBeauty::_validate(bool for_real) calculateLayerValues(m_lsKnobData.m_selectedChannels, m_valueMap); set_out_channels(activeChannelSet()); + info_.turn_on(m_targetLayer); } void GradeBeauty::channelPixelEngine(const Row& in, int y, int x, int r, ChannelMask channels, Row& aRow) { - map aovPtrIdxMap; + map aovFloatPtrChannelMap; foreach(channel, channels) { LayerAlchemy::Utilities::hard_copy(in, x, r, channel, aRow); - aovPtrIdxMap[channel] = aRow.writable(channel); + aovFloatPtrChannelMap[channel] = aRow.writable(channel); } foreach(channel, channels) { unsigned chanIdx = colourIndex(channel); string layerName = getLayerName(channel); - float* outAovValue = aovPtrIdxMap[channel]; + float* outAovValue = aovFloatPtrChannelMap[channel]; const float* inAovValue = in[channel]; float multValue = m_valueMap.multipliers[channel]; for (unsigned X = x; X < r; X++) { float origValue = inAovValue[X]; - outAovValue[X] = origValue * multValue; + if (m_clampBlack) + { + outAovValue[X] = std::max(0.0f, (origValue * multValue)); + } else { + outAovValue[X] = origValue * multValue; + } } } } @@ -313,8 +327,8 @@ void GradeBeauty::beautyPixelEngine(const Row& in, int y, int x, int r, ChannelS ChannelSet aovs = m_lsKnobData.m_selectedChannels.intersection(channels); map btyPtrIdxMap; - map aovPtrIdxMap; - map aovInPtrIdxMap; + map aovFloatPtrChannelMap; + map aovConstFloatPtrChannelMap; foreach(channel, bty) { unsigned chanIdx = colourIndex(channel); @@ -331,8 +345,8 @@ void GradeBeauty::beautyPixelEngine(const Row& in, int y, int x, int r, ChannelS } foreach(channel, aovs) { - aovPtrIdxMap[channel] = aRow.writable(channel); - aovInPtrIdxMap[channel] = in[channel]; + aovFloatPtrChannelMap[channel] = aRow.writable(channel); + aovConstFloatPtrChannelMap[channel] = in[channel]; } for (const auto& kvp : btyPtrIdxMap) @@ -348,8 +362,8 @@ void GradeBeauty::beautyPixelEngine(const Row& in, int y, int x, int r, ChannelS continue; } float* aRowBty = btyPtrIdxMap[aovChanIdx]; - float* aRowAov = aovPtrIdxMap[aov]; - const float* inAov = aovInPtrIdxMap[aov]; + float* aRowAov = aovFloatPtrChannelMap[aov]; + const float* inAov = aovConstFloatPtrChannelMap[aov]; for (int X = x; X < r; X++) { @@ -360,7 +374,8 @@ void GradeBeauty::beautyPixelEngine(const Row& in, int y, int x, int r, ChannelS float aovInPixel = inAov[X]; btyPixel -= aovInPixel; } - aRowBty[X] = btyPixel + aovPixel; + float result = btyPixel + aovPixel; + aRowBty[X] = m_clampBlack ? std::max(0.0f, result) : result; } } } @@ -375,9 +390,9 @@ void GradeBeauty::pixel_engine(const Row& in, int y, int x, int r, ChannelMask c bool isTargetLayer = m_targetLayer.intersection(inChannels).size() == m_targetLayer.size(); if (isTargetLayer) { - channelPixelEngine(in, y, x, r, activeChannels, aRow); + channelPixelEngine(in, y, x, r, m_lsKnobData.m_selectedChannels, aRow); beautyPixelEngine(in, y, x, r, activeChannels, aRow); - } + } else { channelPixelEngine(in, y, x, r, inChannels, aRow); @@ -417,30 +432,40 @@ void GradeBeauty::knobs(Knob_Callback f) "

(this means any difference between the target layer " "and the render layers is kept in the final output)

"); + Bool_knob(f, &m_clampBlack, "black_clamp", "black clamp"); + Tooltip(f, + "

enabled : clamp negative values from all output layers

" + "

disabled : negative values permitted

"); + Divider(f, 0); // separates master from the rest Knob* masterKnob = createColorKnob(f, m_valueMap.getLayerFloatPointer(MASTER_KNOB_NAME), MASTER_KNOB_NAME, true); Tooltip(f, "this knob contributes to each layer"); - if (!colorKnobVectorComplete) { + if (!colorKnobVectorComplete) + { m_valueMap.addColorKnob(masterKnob); } Divider(f, 0); // separates master from the rest - for (auto iterLayerName = layers::nonShading.begin(); iterLayerName != layers::nonShading.end(); iterLayerName++) { + for (auto iterLayerName = layers::nonShading.begin(); iterLayerName != layers::nonShading.end(); iterLayerName++) + { Knob* aovKnob = createColorKnob(f, m_valueMap.getLayerFloatPointer(*iterLayerName), *iterLayerName, false); Tooltip(f, "applies to this layer or to layers in this layer set"); - if (!colorKnobVectorComplete) { + if (!colorKnobVectorComplete) + { m_valueMap.addColorKnob(aovKnob); } } BeginClosedGroup(f, "shading_group", "beauty shading layers"); SetFlags(f, Knob::HIDDEN); - for (auto iterLayerName = layers::shading.begin(); iterLayerName != layers::shading.end(); iterLayerName++) { + for (auto iterLayerName = layers::shading.begin(); iterLayerName != layers::shading.end(); iterLayerName++) + { Knob* aovKnob = createColorKnob(f, m_valueMap.getLayerFloatPointer(*iterLayerName), *iterLayerName, false); Tooltip(f, "apply to this layer only"); - if (!colorKnobVectorComplete) { + if (!colorKnobVectorComplete) + { m_valueMap.addColorKnob(aovKnob); } } @@ -455,23 +480,27 @@ void GradeBeauty::knobs(Knob_Callback f) EndToolbar(f); } -int GradeBeauty::knob_changed(Knob* k) { - if (k->is("math_type")) { +int GradeBeauty::knob_changed(Knob* k) +{ + if (k->is("math_type")) + { setKnobRanges(m_mathMode, true); setKnobDefaultValue(this); - return 1; } - if (k == &DD::Image::Knob::inputChange) { + if (k == &DD::Image::Knob::inputChange) + { _validate(true); } return 1; } -bool GradeBeauty::colorKnobsPopulated() const { +bool GradeBeauty::colorKnobsPopulated() const +{ return (m_valueMap.m_colorKnobs.size() >= (categories::all.size() + 1)); } -Knob* GradeBeauty::createColorKnob(Knob_Callback f, float* valueStore, const string& name, const bool& visible) { +Knob* GradeBeauty::createColorKnob(Knob_Callback f, float* valueStore, const string& name, const bool& visible) +{ const char* knobName = name.c_str(); Knob* colorKnob = Color_knob(f, valueStore, IRange(COLOR_KNOB_RANGES[this->m_mathMode][0], COLOR_KNOB_RANGES[m_mathMode][1]), knobName, knobName); SetFlags(f, Knob::LOG_SLIDER); @@ -482,9 +511,11 @@ Knob* GradeBeauty::createColorKnob(Knob_Callback f, float* valueStore, const str return colorKnob; } -void GradeBeauty::setKnobRanges(const int& modeValue, const bool& reset) { +void GradeBeauty::setKnobRanges(const int& modeValue, const bool& reset) +{ const char* script = (modeValue == 0 ? "{0}" : "{1}"); - for (auto iterKnob = m_valueMap.m_colorKnobs.begin(); iterKnob != m_valueMap.m_colorKnobs.end(); iterKnob++) { + for (auto iterKnob = m_valueMap.m_colorKnobs.begin(); iterKnob != m_valueMap.m_colorKnobs.end(); iterKnob++) + { if (reset) { (*iterKnob)->from_script(script); } @@ -492,49 +523,54 @@ void GradeBeauty::setKnobRanges(const int& modeValue, const bool& reset) { } } -void GradeBeauty::setKnobVisibility() { +void GradeBeauty::setKnobVisibility() +{ bool isBeautyShading = LayerAlchemy::LayerSetKnob::getLayerSetKnobEnumString(this) == categories::shading; auto layerNames = LayerAlchemy::LayerSet::getLayerNames(m_lsKnobData.m_selectedChannels); LayerMap categorized = LayerAlchemy::layerCollection.categorizeLayers(layerNames, categorizeType::pub); - for (vector::const_iterator iterKnob = m_valueMap.m_colorKnobs.begin(); iterKnob != m_valueMap.m_colorKnobs.end(); iterKnob++) { + for (vector::const_iterator iterKnob = m_valueMap.m_colorKnobs.begin(); iterKnob != m_valueMap.m_colorKnobs.end(); iterKnob++) + { Knob* colorKnob = *iterKnob; string knobName = colorKnob->name(); bool isGlobalLayer = categorized.contains(knobName); bool contains = categorized.isMember("all", knobName); - if (knobName == MASTER_KNOB_NAME) { contains = true; - } + } else if (isBeautyShading && isGlobalLayer && categorized.contains(knobName)) { contains = true; } - if (contains) { + if (contains) + { colorKnob->clear_flag(Knob::DO_NOT_WRITE); colorKnob->set_flag(Knob::ALWAYS_SAVE); - // since hidden knobs can be revealed, make sure the values are correct - if (m_valueMap.isDefault(colorKnob->name(), 1.0f) && m_mathMode == GRADE_BEAUTY_MATH_MODE::MULTIPLY && !colorKnob->is_animated()) { - colorKnob->from_script("{1}"); - } - } else { + } else // this is run on knobs that are not visible to the user + { colorKnob->clear_flag(Knob::ALWAYS_SAVE); colorKnob->set_flag(Knob::DO_NOT_WRITE); + if (firstGradeBeauty()->m_firstRun) + { + colorKnob->set_value(m_mathMode); // make sure the unsaved knobs are the correct default + } } - //printf("setKnobVisibility: knob name is %s and visible is %d is global %d \n", knobName.c_str(), contains, isGlobalLayer); colorKnob->visible(contains); } knob("shading_group")->visible(isBeautyShading); + firstGradeBeauty()->m_firstRun = false; } -void GradeBeauty::setKnobDefaultValue(DD::Image::Op* nukeOpPtr) { +void GradeBeauty::setKnobDefaultValue(DD::Image::Op* nukeOpPtr) +{ nukeOpPtr->script_command(DEFAULT_VALUE_PYSCRIPT, true, false); } -void GradeBeauty::calculateLayerValues(const DD::Image::ChannelSet& channels, GradeBeautyValueMap& valueMap) { +void GradeBeauty::calculateLayerValues(const DD::Image::ChannelSet& channels, GradeBeautyValueMap& valueMap) +{ foreach(channel, channels) { int chanIdx = colourIndex(channel); string layerName = getLayerName(channel); diff --git a/src/nuke/GradeBeautyLayer.cpp b/src/nuke/GradeBeautyLayer.cpp new file mode 100644 index 0000000..07d2a56 --- /dev/null +++ b/src/nuke/GradeBeautyLayer.cpp @@ -0,0 +1,276 @@ +#include "math.h" + +#include +#include + +#include "LayerSet.h" +#include "LayerSetKnob.h" +#include "GradeBeautyLayerSet.cpp" + + +namespace GradeBeautyLayer { + +const char* const HELP = + "

Grade node for cg multichannel beauty aovs

" + "order of operations is : \n\n" + "1 - subtract source layer from the target layer\n" + "2 - perform grade modification to the source layer\n" + "3 - add the modified source layer to the target layer\n"; + +static const char* const layerNames[] = { + " ", 0 +}; + +using namespace DD::Image; + +static const StrVecType all = { + "beauty_direct_indirect", "beauty_shading_global", "light_group", "beauty_shading" +}; +static const CategorizeFilter CategorizeFilterAllBeauty(all, CategorizeFilter::modes::INCLUDE); + +class GradeBeautyLayer : public PixelIop { + +private: + float blackpoint[3]{0.0f, 0.0f, 0.0f}; + float whitepoint[3]{1.0f, 1.0f, 1.0f}; + float lift[3]{0.0f, 0.0f, 0.0f}; + float gain[3]{1.0f, 1.0f, 1.0f}; + float offset[3]{0.0f, 0.0f, 0.0f}; + float multiply[3]{1.0f, 1.0f, 1.0f}; + float gamma[3]{1.0f, 1.0f, 1.0f}; + bool reverse{false}; + bool clampBlack{true}; + bool clampWhite{false}; + ChannelSet m_targetLayer{Mask_RGB}; + ChannelSet m_sourceLayer{Mask_None}; + ChannelSet m_selectedLayers; + + // intermediate grade algorithm storage + float A[3]{0.0f, 0.0f, 0.0f}; + float B[3]{0.0f, 0.0f, 0.0f}; + float G[3]{0.0f, 0.0f, 0.0f}; + +public: + void knobs(Knob_Callback); + void _validate(bool for_real); + bool pass_transform() const {return true;} + void in_channels(int, ChannelSet& channels) const; + void pixel_engine(const Row&, int, int, int, ChannelMask, Row&); + int knob_changed(Knob*); + const char* Class() const {return description.name;} + const char* node_help() const {return HELP;} + static const Iop::Description description; + // channel set that contains all channels that are modified by the node + ChannelSet activeChannelSet() const; + // pixel engine function when anything but the target layer is requested to render + void channelPixelEngine(const Row&, int, int, int, ChannelSet&, Row&); + // pixel engine functon when the target layer is requested to render + void beautyPixelEngine(const Row&, int y, int x, int r, ChannelSet&, Row&); + // This function calculates and stores the grade algorithm's intermediate calculations + bool precomputeValues(); + GradeBeautyLayer(Node* node); + ~GradeBeautyLayer(); +}; + +GradeBeautyLayer::GradeBeautyLayer(Node* node) : PixelIop(node) +{ + precomputeValues(); +} + +static Op* build(Node* node) +{ + return (new NukeWrapper(new GradeBeautyLayer(node)))->noChannels()->mixLuminance(); +} + +GradeBeautyLayer::~GradeBeautyLayer() {} + +const Iop::Description GradeBeautyLayer::description( + "GradeBeautyLayer", + "LayerAlchemy/GradeBeautyLayer", + build +); + +ChannelSet GradeBeautyLayer::activeChannelSet() const +{ + ChannelSet outChans = ChannelSet(m_targetLayer + m_sourceLayer); + return outChans; +} + +void GradeBeautyLayer::in_channels(int input_number, ChannelSet& mask) const { + mask += activeChannelSet(); +} + +bool GradeBeautyLayer::precomputeValues() { + bool changeZero = false; + for (unsigned int chanIdx = 0; chanIdx < 3; chanIdx++) { + float a = whitepoint[chanIdx] - blackpoint[chanIdx]; + a = a ? (gain[chanIdx] - lift[chanIdx]) / a : 10000.0f; + a *= multiply[chanIdx]; + float b = offset[chanIdx] + lift[chanIdx] - blackpoint[chanIdx] * a; + float g = LayerAlchemy::Utilities::validateGammaValue(gamma[chanIdx]); + A[chanIdx] = a; + B[chanIdx] = b; + G[chanIdx] = g; + if (a != 1.0f || b != 0.0f || g != 1.0f) + { + if (b) + { + changeZero = true; + } + } + } + return changeZero; +} + +void GradeBeautyLayer::_validate(bool for_real) { + copy_info(); // this copies the input info to the output + bool changeZero = precomputeValues(); + info_.black_outside(!changeZero); + ChannelSet inChannels = info_.channels(); + LayerAlchemy::Utilities::validateTargetLayerColorIndex(this, m_targetLayer, 0, 2); + m_selectedLayers = activeChannelSet(); + set_out_channels(m_selectedLayers); + info_.turn_on(m_targetLayer); +} +void GradeBeautyLayer::channelPixelEngine(const Row& in, int y, int x, int r, ChannelSet& channels, Row& aRow) +{ + LayerAlchemy::Utilities::gradeChannelPixelEngine(in, y, x, r, channels, aRow, A, B, G, reverse, clampBlack, clampWhite); +} +void GradeBeautyLayer::beautyPixelEngine(const Row& in, int y, int x, int r, ChannelSet& channels, Row& aRow) +{ + ChannelSet bty = m_targetLayer.intersection(channels); + ChannelSet aovs = m_sourceLayer.intersection(channels); + + map btyPtrIdxMap; + map aovPtrIdxMap; + map aovInPtrIdxMap; + + foreach(channel, bty) { + unsigned chanIdx = colourIndex(channel); + float* rowBtyChan; + LayerAlchemy::Utilities::hard_copy(in, x, r, channel, aRow); + rowBtyChan = aRow.writable(channel); + btyPtrIdxMap[chanIdx] = rowBtyChan; + + } + foreach(channel, aovs) { + aovPtrIdxMap[channel] = aRow.writable(channel); + aovInPtrIdxMap[channel] = in[channel]; + } + + for (const auto& kvp : btyPtrIdxMap) + { + unsigned btyChanIdx = kvp.first; + float* aRowBty = kvp.second; + + foreach(aov, aovs) + { + unsigned aovChanIdx = colourIndex(aov); + if (btyChanIdx != aovChanIdx) + { + continue; + } + + float* aRowBty = btyPtrIdxMap[aovChanIdx]; + float* aRowAov = aovPtrIdxMap[aov]; + const float* inAov = aovInPtrIdxMap[aov]; + for (int X = x; X < r; X++) + { + float aovPixel = aRowAov[X]; + float btyPixel = aRowBty[X]; + float aovInPixel = inAov[X]; + btyPixel -= aovInPixel; + float resultPixel = btyPixel + aovPixel; + aRowBty[X] = btyPixel + aovPixel; + } + } + // clamp + if (clampWhite || clampBlack) { + for (int X = x; X < r; X++) + { + float btyPixel = aRowBty[X]; + + if (btyPixel < 0.0f && clampBlack) + { + btyPixel = 0.0f; + } + else if (btyPixel > 1.0f && clampWhite) + { + btyPixel = 1.0f; + } + aRowBty[X] = btyPixel; + } + } + } +} + +void GradeBeautyLayer::pixel_engine(const Row& in, int y, int x, int r, ChannelMask channels, Row& out) { + ChannelSet inChannels = ChannelSet(channels); + ChannelSet activeChannels = m_selectedLayers; + Row aRow(x, r); + bool isTargetLayer = m_targetLayer.intersection(inChannels).size() == m_targetLayer.size(); + + if (isTargetLayer) + { + LayerAlchemy::Utilities::gradeChannelPixelEngine(in, y, x, r, m_sourceLayer, aRow, A, B, G, reverse, clampBlack, clampWhite); + beautyPixelEngine(in, y, x, r, activeChannels, aRow); + } + else + { + LayerAlchemy::Utilities::gradeChannelPixelEngine(in, y, x, r, inChannels, aRow, A, B, G, reverse, clampBlack, clampWhite); + } + LayerAlchemy::Utilities::hard_copy(aRow, x, r, inChannels, out); +} + +void GradeBeautyLayer::knobs(Knob_Callback f) { + ChannelSet_knob(f, &m_sourceLayer, "channels", "source layer"); + Tooltip(f, + "

Order of operations :

" + "

1 - source_layer subtracted from target_layer

" + "

2 - source_layer is graded

" + "

2 - graded source_layer is added to target_layer

" + ); + SetFlags(f, Knob::NO_ALPHA_PULLDOWN); + SetFlags(f, Knob::NO_CHECKMARKS); + + LayerAlchemy::Knobs::createDocumentationButton(f); + LayerAlchemy::Knobs::createColorKnobResetButton(f); + LayerAlchemy::Knobs::createVersionTextKnob(f); + Divider(f, 0); // separates layer set knobs from the rest + + Input_ChannelMask_knob(f, &m_targetLayer, 0, "target layer"); + SetFlags(f, Knob::NO_ALPHA_PULLDOWN); + Tooltip(f, "

Selects which layer to pre-subtract layers from (if enabled) and offset the modified layers to

"); + SetFlags(f, Knob::EXPAND_TO_CONTENTS); + + Divider(f, 0); // separates layer set knobs from the rest + + Color_knob(f, blackpoint, IRange(-1, 1), "blackpoint"); + Tooltip(f, "This color is turned into black"); + Color_knob(f, whitepoint, IRange(0, 4), "whitepoint"); + Tooltip(f, "This color is turned into white"); + Color_knob(f, lift, IRange(-1, 1), "lift", "lift"); + Tooltip(f, "Black is turned into this color"); + Color_knob(f, gain, IRange(0, 4), "gain", "gain"); + Tooltip(f, "White is turned into this color"); + Color_knob(f, multiply, IRange(0, 4), "multiply"); + Tooltip(f, "Constant to multiply result by"); + Color_knob(f, offset, IRange(-1, 1), "offset", "offset"); + Tooltip(f, "Constant to offset to result (raises both black & white, unlike lift)"); + Color_knob(f, gamma, IRange(.2, 5), "gamma"); + Tooltip(f, "Gamma correction applied to final result"); + Newline(f, " "); + Bool_knob(f, &reverse, "reverse"); + Tooltip(f, "Invert the math to undo the correction"); + Bool_knob(f, &clampBlack, "clampBlack", "black clamp"); + Tooltip(f, "Output that is less than zero is changed to zero"); + Bool_knob(f, &clampWhite, "clampWhite", "white clamp"); + Tooltip(f, "Output that is greater than 1 is changed to 1"); + + Divider(f, 0); // separates NukeWrapper knobs created after this +} + +int GradeBeautyLayer::knob_changed(Knob* k) { + return 1; +} +} // End namespace GradeBeautyLayer diff --git a/src/nuke/GradeBeautyLayerSet.cpp b/src/nuke/GradeBeautyLayerSet.cpp index 3402d4e..03f8a90 100644 --- a/src/nuke/GradeBeautyLayerSet.cpp +++ b/src/nuke/GradeBeautyLayerSet.cpp @@ -23,24 +23,6 @@ static const char* const HELP = " * more info with the documentation button" ; -// patch for linux alphas because the pow function behaves badly -// for very large or very small exponent values. -static bool LINUX = false; -#ifdef __alpha -LINUX = true; -#endif - -float validateGammaValue(const float& gammaValue) { - if (LINUX) { - if (gammaValue < 0.008f) { - return 0.0f; - } else if (gammaValue > 125.0f) { - return 125.0f; - } - } - return gammaValue; -} - enum operationModes { ADD = 0, COPY }; @@ -90,7 +72,7 @@ class GradeBeautyLayerSet : public PixelIop { static const Iop::Description description; // This function calculates and stores the grade algorithm's intermediate calculations - void precomputeValues(); + bool precomputeValues(); GradeBeautyLayerSet(Node* node); ~GradeBeautyLayerSet(); // channel set that contains all channels that are modified by the node @@ -144,31 +126,32 @@ void GradeBeautyLayerSet::in_channels(int input_number, ChannelSet& mask) const mask += activeChannelSet(); } -void GradeBeautyLayerSet::precomputeValues() { - for (int chanIdx = 0; chanIdx < 3; chanIdx++) { +bool GradeBeautyLayerSet::precomputeValues() { + bool changeZero = false; + for (unsigned int chanIdx = 0; chanIdx < 3; chanIdx++) { float a = whitepoint[chanIdx] - blackpoint[chanIdx]; a = a ? (gain[chanIdx] - lift[chanIdx]) / a : 10000.0f; a *= multiply[chanIdx]; float b = offset[chanIdx] + lift[chanIdx] - blackpoint[chanIdx] * a; - float g = validateGammaValue(gamma[chanIdx]); + float g = LayerAlchemy::Utilities::validateGammaValue(gamma[chanIdx]); A[chanIdx] = a; B[chanIdx] = b; G[chanIdx] = g; - } -} - -void GradeBeautyLayerSet::_validate(bool for_real) { - bool changeZero = false; - precomputeValues(); - for (int chanIdx = 0; chanIdx <= 3; chanIdx++) { - if (A[chanIdx] != 1 || B[chanIdx] || gamma[chanIdx] != 1.0f) { - if (B[chanIdx]) { + if (a != 1.0f || b != 0.0f || g != 1.0f) + { + if (b) + { changeZero = true; } } } - info_.black_outside(!changeZero); + return changeZero; +} + +void GradeBeautyLayerSet::_validate(bool for_real) { copy_info(); // this copies the input info to the output + bool changeZero = precomputeValues(); + info_.black_outside(!changeZero); ChannelSet inChannels = info_.channels(); LayerAlchemy::Utilities::validateTargetLayerColorIndex(this, m_targetLayer, 0, 2); @@ -176,99 +159,11 @@ void GradeBeautyLayerSet::_validate(bool for_real) { updateLayerSetKnob(this, m_lsKnobData, LayerAlchemy::layerCollection, inChannels, CategorizeFilterAllBeauty); } set_out_channels(activeChannelSet()); + info_.turn_on(m_targetLayer); } void GradeBeautyLayerSet::channelPixelEngine(const Row& in, int y, int x, int r, ChannelSet& channels, Row& aRow) { - map aovPtrIdxMap; - foreach(channel, channels) - { - LayerAlchemy::Utilities::hard_copy(in, x, r, channel, aRow); - aovPtrIdxMap[channel] = aRow.writable(channel); - } - - foreach(channel, channels) { - unsigned chanIdx = colourIndex(channel); - const float* inAovValue = in[channel]; - float* outAovValue = aovPtrIdxMap[channel]; - - float _A = A[chanIdx]; - float _B = B[chanIdx]; - float _G = G[chanIdx]; - - for (int X = x; X < r; X++) - { - float outPixel = inAovValue[X]; - - if (!reverse) { - if (_A != 1.0f || _B) { - outPixel *= _A; - outPixel += _B; - } - if (clampWhite || clampBlack) { - if (outPixel < 0.0f && clampBlack) { // clamp black - outPixel = 0.0f; - } - if (outPixel > 1.0f && clampWhite) { // clamp white - outPixel = 1.0f; - } - } - if (_G <= 0) { - if (outPixel < 1.0f) { - outPixel = 0.0f; - } else if (outPixel > 1.0f) { - outPixel = INFINITY; - } - } else if (_G != 1.0f) { - float power = 1.0f / _G; - if (LINUX & (outPixel <= 1e-6f && power > 1.0f)) { - outPixel = 0.0f; - } else if (outPixel < 1) { - outPixel = powf(outPixel, power); - } else { - outPixel = (1.0f + outPixel - 1.0f) * power; - } - } - } - if (reverse) { // Reverse gamma: - if (_G <= 0) { - outPixel = outPixel > 0.0f ? 1.0f : 0.0f; - } - if (_G != 1.0f) { - if (LINUX & (outPixel <= 1e-6f && _G > 1.0f)) { - outPixel = 0.0f; - } else if (outPixel < 1.0f) { - outPixel = powf(outPixel, _G); - } else { - outPixel = 1.0f + (outPixel - 1.0f) * _G; - } - } - // Reverse the linear part: - if (_A != 1.0f || _B) { - float b = _B; - float a = _A; - if (a) { - a = 1 / a; - } else { - a = 1.0f; - } - b = -b * a; - outPixel = (outPixel * a) + b; - } - } - // clamp - if (clampWhite || clampBlack) { - if (outPixel < 0.0f && clampBlack) - { - outPixel = 0.0f; - } - else if (outPixel > 1.0f && clampWhite) - { - outPixel = 1.0f; - } - } - outAovValue[X] = outPixel; - } - } + LayerAlchemy::Utilities::gradeChannelPixelEngine(in, y, x, r, channels, aRow, A, B, G, reverse, clampBlack, clampWhite); } void GradeBeautyLayerSet::beautyPixelEngine(const Row& in, int y, int x, int r, ChannelSet& channels, Row& aRow) { @@ -356,7 +251,7 @@ void GradeBeautyLayerSet::pixel_engine(const Row& in, int y, int x, int r, Chann if (isTargetLayer) { - channelPixelEngine(in, y, x, r, activeChannels, aRow); + channelPixelEngine(in, y, x, r, m_lsKnobData.m_selectedChannels, aRow); beautyPixelEngine(in, y, x, r, activeChannels, aRow); } else diff --git a/src/nuke/GradeLayerSet.cpp b/src/nuke/GradeLayerSet.cpp index 9d9ec51..4e16a97 100644 --- a/src/nuke/GradeLayerSet.cpp +++ b/src/nuke/GradeLayerSet.cpp @@ -20,25 +20,6 @@ const char* const HELP = "invert the grade. This will do the opposite gamma correction followed by the " "opposite linear ramp."; -// patch for linux alphas because the pow function behaves badly -// for very large or very small exponent values. -static bool LINUX = false; -#ifdef __alpha -LINUX = true; -#endif - -float validateGammaValue(const float& gammaValue) -{ - if (LINUX) { - if (gammaValue < 0.008f) { - return 0.0f; - } else if (gammaValue > 125.0f) { - return 125.0f; - } - } - return gammaValue; -} - class GradeLayerSet : public PixelIop { private: @@ -70,7 +51,7 @@ class GradeLayerSet : public PixelIop { GradeLayerSet(Node* node); ~GradeLayerSet(); // This function calculates and stores the grade algorithm's intermediate calculations - void precomputeValues(); + bool precomputeValues(); // channel set that contains all channels that are modified by the node ChannelSet activeChannelSet() const {return ChannelSet(m_lsKnobData.m_selectedChannels);} }; @@ -93,12 +74,9 @@ void GradeLayerSet::in_channels(int input, ChannelSet& mask) const {} void GradeLayerSet::_validate(bool for_real) { - bool change_zero = false; - precomputeValues(); - if (change_zero) { - info_.black_outside(false); - } copy_info(); // this copies the input info to the output + bool changeZero = precomputeValues(); + info_.black_outside(!changeZero); ChannelSet inChannels = info_.channels(); if (validateLayerSetKnobUpdate(this, m_lsKnobData, LayerAlchemy::layerCollection, inChannels)) { @@ -106,114 +84,34 @@ void GradeLayerSet::_validate(bool for_real) } set_out_channels(activeChannelSet()); } -void GradeLayerSet::precomputeValues() -{ - for (int chanIdx = 0; chanIdx < 3; chanIdx++) { + +bool GradeLayerSet::precomputeValues() { + bool changeZero = false; + for (unsigned int chanIdx = 0; chanIdx < 4; chanIdx++) { float a = whitepoint[chanIdx] - blackpoint[chanIdx]; a = a ? (gain[chanIdx] - lift[chanIdx]) / a : 10000.0f; a *= multiply[chanIdx]; float b = offset[chanIdx] + lift[chanIdx] - blackpoint[chanIdx] * a; - float g = validateGammaValue(gamma[chanIdx]); + float g = LayerAlchemy::Utilities::validateGammaValue(gamma[chanIdx]); A[chanIdx] = a; B[chanIdx] = b; G[chanIdx] = g; + if (a != 1.0f || b != 0.0f || g != 1.0f) + { + if (b) + { + changeZero = true; + } + } } + return changeZero; } void GradeLayerSet::pixel_engine(const Row& in, int y, int x, int r, ChannelMask inChannels, Row& out) { Row aRow(x, r); - - map aovPtrIdxMap; - foreach(channel, inChannels) - { - LayerAlchemy::Utilities::hard_copy(in, x, r, channel, aRow); - aovPtrIdxMap[channel] = aRow.writable(channel); - } - - foreach(channel, inChannels) { - unsigned chanIdx = colourIndex(channel); - const float* inAovValue = in[channel]; - float* outAovValue = aovPtrIdxMap[channel]; - - float _A = A[chanIdx]; - float _B = B[chanIdx]; - float _G = G[chanIdx]; - - for (int X = x; X < r; X++) - { - float outPixel = inAovValue[X]; - - if (!reverse) { - if (_A != 1.0f || _B) { - outPixel *= _A; - outPixel += _B; - } - if (clampWhite || clampBlack) { - if (outPixel < 0.0f && clampBlack) { // clamp black - outPixel = 0.0f; - } - if (outPixel > 1.0f && clampWhite) { // clamp white - outPixel = 1.0f; - } - } - if (_G <= 0) { - if (outPixel < 1.0f) { - outPixel = 0.0f; - } else if (outPixel > 1.0f) { - outPixel = INFINITY; - } - } else if (_G != 1.0f) { - float power = 1.0f / _G; - if (LINUX & (outPixel <= 1e-6f && power > 1.0f)) { - outPixel = 0.0f; - } else if (outPixel < 1) { - outPixel = powf(outPixel, power); - } else { - outPixel = (1.0f + outPixel - 1.0f) * power; - } - } - } - if (reverse) { // Reverse gamma: - if (_G <= 0) { - outPixel = outPixel > 0.0f ? 1.0f : 0.0f; - } - if (_G != 1.0f) { - if (LINUX & (outPixel <= 1e-6f && _G > 1.0f)) { - outPixel = 0.0f; - } else if (outPixel < 1.0f) { - outPixel = powf(outPixel, _G); - } else { - outPixel = 1.0f + (outPixel - 1.0f) * _G; - } - } - // Reverse the linear part: - if (_A != 1.0f || _B) { - float b = _B; - float a = _A; - if (a) { - a = 1 / a; - } else { - a = 1.0f; - } - b = -b * a; - outPixel = (outPixel * a) + b; - } - } - // clamp - if (clampWhite || clampBlack) { - if (outPixel < 0.0f && clampBlack) - { - outPixel = 0.0f; - } - else if (outPixel > 1.0f && clampWhite) - { - outPixel = 1.0f; - } - } - outAovValue[X] = outPixel; - } - } + ChannelSet channels = ChannelSet(inChannels); + LayerAlchemy::Utilities::gradeChannelPixelEngine(in, y, x, r, channels, aRow, A, B, G, reverse, clampBlack, clampWhite); LayerAlchemy::Utilities::hard_copy(aRow, x, r, inChannels, out); } diff --git a/src/nuke/LayerSet.cpp b/src/nuke/LayerSet.cpp index b7f5d7a..315b435 100644 --- a/src/nuke/LayerSet.cpp +++ b/src/nuke/LayerSet.cpp @@ -72,8 +72,14 @@ namespace Knobs { DD::Image::Knob* createDocumentationButton(DD::Image::Knob_Callback& f) { - DD::Image::Knob* docButton = Button(f, "docButton", "documentation"); - Tooltip(f, "

This will launch the default browser and load the included plugin documentation

"); + const char* docButtonScript = + "import layer_alchemy.documentation\n" + "qtWidget = layer_alchemy.documentation.displayDocumentation(node=nuke.thisNode())\n" + "if qtWidget:\n" + " qtWidget.show()"; + + DD::Image::Knob* docButton = PyScript_knob(f, docButtonScript, "documentation"); + Tooltip(f, "

This will display the included plugin documentation

"); return docButton; } @@ -136,6 +142,128 @@ void hard_copy(const DD::Image::Row& fromRow, int x, int r, DD::Image::ChannelSe hard_copy(fromRow, x, r, channel, toRow); } } + +void gradeChannelPixelEngine(const DD::Image::Row& in, int y, int x, int r, DD::Image::ChannelSet& channels, DD::Image::Row& aRow, float* A, float* B, float* G, bool reverse, bool clampBlack, bool clampWhite) +{ + // patch for linux alphas because the pow function behaves badly + // for very large or very small exponent values. + static bool LINUX = false; + #ifdef __alpha + LINUX = true; + #endif + + + map aovPtrIdxMap; + foreach(channel, channels) + { + LayerAlchemy::Utilities::hard_copy(in, x, r, channel, aRow); + aovPtrIdxMap[channel] = aRow.writable(channel); + } + + foreach(channel, channels) { + unsigned chanIdx = colourIndex(channel); + const float* inAovValue = in[channel]; + float* outAovValue = aovPtrIdxMap[channel]; + + float _A = A[chanIdx]; + float _B = B[chanIdx]; + float _G = G[chanIdx]; + + for (int X = x; X < r; X++) + { + float outPixel = inAovValue[X]; + + if (!reverse) { + if (_A != 1.0f || _B) { + outPixel *= _A; + outPixel += _B; + } + if (clampWhite || clampBlack) { + if (outPixel < 0.0f && clampBlack) { // clamp black + outPixel = 0.0f; + } + if (outPixel > 1.0f && clampWhite) { // clamp white + outPixel = 1.0f; + } + } + if (_G <= 0) { + if (outPixel < 1.0f) { + outPixel = 0.0f; + } else if (outPixel > 1.0f) { + outPixel = INFINITY; + } + } else if (_G != 1.0f) { + float power = 1.0f / _G; + if (LINUX & (outPixel <= 1e-6f && power > 1.0f)) { + outPixel = 0.0f; + } else if (outPixel < 1) { + outPixel = powf(outPixel, power); + } else { + outPixel = (1.0f + outPixel - 1.0f) * power; + } + } + } + if (reverse) { // Reverse gamma: + if (_G <= 0) { + outPixel = outPixel > 0.0f ? 1.0f : 0.0f; + } + if (_G != 1.0f) { + if (LINUX & (outPixel <= 1e-6f && _G > 1.0f)) { + outPixel = 0.0f; + } else if (outPixel < 1.0f) { + outPixel = powf(outPixel, _G); + } else { + outPixel = 1.0f + (outPixel - 1.0f) * _G; + } + } + // Reverse the linear part: + if (_A != 1.0f || _B) { + float b = _B; + float a = _A; + if (a) { + a = 1 / a; + } else { + a = 1.0f; + } + b = -b * a; + outPixel = (outPixel * a) + b; + } + } + // clamp + if (clampWhite || clampBlack) { + if (outPixel < 0.0f && clampBlack) + { + outPixel = 0.0f; + } + else if (outPixel > 1.0f && clampWhite) + { + outPixel = 1.0f; + } + } + outAovValue[X] = outPixel; + } + } +} + +float validateGammaValue(const float& gammaValue) +{ + static bool LINUX = false; + #ifdef __alpha + LINUX = true; + #endif + if (LINUX) + { + if (gammaValue < 0.008f) + { + return 0.0f; + } + else if (gammaValue > 125.0f) + { + return 125.0f; + } + } + return gammaValue; +} } // End namespace Utilities } // End namespace LayerAlchemy diff --git a/src/nuke/LayerSetKnob.cpp b/src/nuke/LayerSetKnob.cpp index 7a9fc3b..28cb474 100644 --- a/src/nuke/LayerSetKnob.cpp +++ b/src/nuke/LayerSetKnob.cpp @@ -16,7 +16,7 @@ LayerSetKnobData::~LayerSetKnobData() DD::Image::Knob* LayerSetKnob(DD::Image::Knob_Callback& f, LayerSetKnobData& pLayerSetKnobData) { DD::Image::Knob* layerSetKnob = DD::Image::Enumeration_knob( - f, &pLayerSetKnobData.selectedLayerSetIndex, pLayerSetKnobData.items, "layer_set", "layer set"); + f, &pLayerSetKnobData.selectedLayerSetIndex, pLayerSetKnobData.items, LAYER_SET_KNOB_NAME, "layer set"); Tooltip(f, "This selects a specific layer set for processing in this node"); SetFlags(f, DD::Image::Knob::SAVE_MENU); SetFlags(f, DD::Image::Knob::ALWAYS_SAVE);