/** * OpenColorIO ColorSpace Iop. */ #include "OCIOLookTransform.h" namespace OCIO = OCIO_NAMESPACE; #include #include #include #include #include #include #include #include #include #include #include #include // Should we use cascasing ColorSpace menus #if defined kDDImageVersionInteger && (kDDImageVersionInteger>=62300) #define OCIO_CASCADE #endif OCIOLookTransform::OCIOLookTransform(Node *n) : DD::Image::OCIOPluginBase(n) { m_inputColorSpaceIndex = 0; m_outputColorSpaceIndex = 0; m_ignoreErrors = false; m_reload_version = 1; setExtendedOpProperties(std::make_shared(kNumOCIOContextVariables)); // Query the colorspace names from the current config configChanged(); } OCIOLookTransform::~OCIOLookTransform() { } void OCIOLookTransform::configChanged() { try { _config = getConfigForProject(); if(_config->getNumLooks()>0) { m_look = _config->getLookNameByIndex(0); } m_lookhelp = constructLookHelpString(); } catch (const OCIO::Exception& e) { std::cerr << "OCIOLookTransform: " << e.what() << std::endl; } catch (...) { std::cerr << "OCIOLookTransform: Unknown exception during OCIO setup." << std::endl; } } std::string OCIOLookTransform::constructLookHelpString() const { std::ostringstream os; os << "Specify the look(s) to apply, as predefined in the OpenColorIO "; os << "configuration. This may be the name of a single look, or a "; os << "combination of looks using the 'look syntax' (outlined below)\n\n"; std::string firstlook = "a"; std::string secondlook = "b"; if (_config->getNumLooks()>0) { os << "Looks: "; for (int i = 0; i < _config->getNumLooks(); ++i) { if (i!=0) os << ", "; const char * lookname = _config->getLookNameByIndex(i); os << lookname; if (i==0) firstlook = lookname; if (i==1) secondlook = lookname; } os << "\n\n"; } else { os << "NO LOOKS DEFINED -- "; os << "This node cannot be used until looks are added to the OCIO Configuration. "; os << "See opencolorio.org for examples.\n\n"; } os << "Look Syntax:\n"; os << "Multiple looks are combined with commas: '"; os << firstlook << ", " << secondlook << "'\n"; os << "Direction is specified with +/- prefixes: '"; os << "+" << firstlook << ", -" << secondlook << "'\n"; os << "Missing look 'fallbacks' specified with |: '"; os << firstlook << ", -" << secondlook << " | -" << secondlook << "'"; return os.str(); } DD::Image::Hash OCIOLookTransform::gpuEngine_shader_hash_at_impl(double time) { DD::Image::Hash hash; // Using append() takes into account the current config. // TODO: No doubt there's some way to animate the config selection but this doesn't // support that. In practice, I can't believe anyone would do this. append(hash); // We also need to hash the knobs that determine which processor gets used, // evaluated at the specified time. if (DD::Image::Knob* inKnob = knob("in_colorspace")) { hash.append(inKnob->get_value_at(time)); } if (DD::Image::Knob* outKnob = knob("out_colorspace")) { hash.append(outKnob->get_value_at(time)); } if (DD::Image::Knob* lookKnob = knob("look")) { hash.append(lookKnob->get_value_at(time)); } if (DD::Image::Knob* invertKnob = knob("invert")) { hash.append(invertKnob->get_value_at(time)); } return hash; } void OCIOLookTransform::knobs(DD::Image::Knob_Callback f) { updateConfigBeforeCreatingKnobs(f); DD::Image::Colorspace_knob(f, &m_inputColorSpaceIndex, OCIO::ROLE_SCENE_LINEAR, "in_colorspace", "input"); DD::Image::Tooltip(f, "Input data is taken to be in this color transform." "\n\n" "Warning" "\n" "The label for this knob has been changed - however the knob name is the same as in 13.0" " - in a future major release the knob name will also be changed."); DD::Image::String_knob(f, &m_look, "look"); DD::Image::Tooltip(f, m_lookhelp.c_str()); DD::Image::SetFlags(f, DD::Image::Knob::ALWAYS_SAVE); Obsolete_knob(f, "direction", "if {$value==\"inverse\"} { knob invert true }"); // Reload button, and hidden "version" knob to invalidate cache on reload DD::Image::Spacer(f, 8); Button(f, "reload", "reload"); DD::Image::Tooltip(f, "Reload all files used in the underlying Look(s)."); Int_knob(f, &m_reload_version, "version"); DD::Image::SetFlags(f, DD::Image::Knob::HIDDEN); DD::Image::Bool_knob(f, &m_invert, "invert", "invert direction"); DD::Image::Tooltip(f, "Specify the look transform direction. input/output color transform handling is not affected."); DD::Image::Colorspace_knob(f, &m_outputColorSpaceIndex, OCIO::ROLE_SCENE_LINEAR, "out_colorspace", "output"); DD::Image::Tooltip(f, "Image data is converted to this color transform for output." "\n\n" "Warning" "\n" "The label for this knob has been changed - however the knob name is the same as in 13.0" " - in a future major release the knob name will also be changed."); DD::Image::Bool_knob(f, &m_ignoreErrors, "ignore_errors", "ignore errors"); DD::Image::Tooltip(f, "If enabled, looks that cannot find the specified correction" " are treated as a normal ColorSpace conversion instead of triggering a render error."); DD::Image::SetFlags(f, DD::Image::Knob::STARTLINE ); if (!f.makeKnobs()) { validateConfig(); } } void OCIOLookTransform::append(DD::Image::Hash& nodehash) { // Incremented to force reloading after rereading the LUT file nodehash.append(m_reload_version); // TODO: Hang onto the context, what if getting it // (and querying getCacheID) is expensive? try { OCIO::ConstConfigRcPtr config = getConfigForProject(); OCIO::ConstContextRcPtr context = getLocalContext(); std::string configCacheID = config->getCacheID(context); nodehash.append(configCacheID); } catch (const OCIO::Exception &e) { error(e.what()); } catch (...) { error("OCIOLookTransform: Unknown exception during hash generation."); } } int OCIOLookTransform::knob_changed(DD::Image::Knob* k) { if (k->is("reload")) { knob("version")->set_value(m_reload_version+1); OCIO::ClearAllCaches(); return true; // ensure callback is triggered again } // Return zero to avoid callbacks for other knobs return false; } void OCIOLookTransform::updateOCIOProcessors() { OCIO::ConstConfigRcPtr config = getConfigForProject(); OCIO::ConstContextRcPtr context = getLocalContext(); const std::string inputName = knob("in_colorspace")->enumerationKnob()->getSelectedItemString(); const std::string outputName = knob("out_colorspace")->enumerationKnob()->getSelectedItemString(); try { OCIO::LookTransformRcPtr transform = OCIO::LookTransform::Create(); transform->setLooks(m_look.c_str()); OCIO::TransformDirection direction = OCIO::TRANSFORM_DIR_FORWARD; // Forward if (!m_invert) { transform->setSrc(inputName.c_str()); transform->setDst(outputName.c_str()); direction = OCIO::TRANSFORM_DIR_FORWARD; } else { // The TRANSFORM_DIR_INVERSE applies an inverse for the end-to-end transform, // which would otherwise do dst->inv look -> src. // This is an unintuitive result for the artist (who would expect in, out to // remain unchanged), so we account for that here by flipping src/dst transform->setSrc(outputName.c_str()); transform->setDst(inputName.c_str()); direction = OCIO::TRANSFORM_DIR_INVERSE; } m_processor = config->getProcessor(context, transform, direction); m_cpuProcessor = m_processor->getDefaultCPUProcessor(); } // We only catch the exceptions for missing files, and try to succeed // in this case. All other errors represent more serious problems and // should fail through. catch (const OCIO::ExceptionMissingFile &) { if (!m_ignoreErrors) { throw; } m_processor = config->getProcessor(context, inputName.c_str(), outputName.c_str()); m_cpuProcessor = m_processor->getDefaultCPUProcessor(); } } void OCIOLookTransform::_validate(bool for_real) { validateConfig(); try { updateOCIOProcessors(); setupGPUShaders(m_processor); } catch (const OCIO::Exception &e) { error(e.what()); return; } catch (...) { error("OCIOLookTransform: Unknown exception during _validate."); return; } if (m_processor->isNoOp()) { set_out_channels(DD::Image::Mask_None); // prevents engine() from being called } else { set_out_channels(DD::Image::Mask_All); } DD::Image::PixelIop::_validate(for_real); } // Note that this is copied by others (OCIODisplay) void OCIOLookTransform::in_channels(int /* n unused */, DD::Image::ChannelSet& mask) const { DD::Image::ChannelSet done; foreach (c, mask) { if (DD::Image::colourIndex(c) < 3 && !(done & c)) { done.addBrothers(c, 3); } } mask += done; } // See Saturation::pixel_engine for a well-commented example. // Note that this is copied by others (OCIODisplay) void OCIOLookTransform::pixel_engine( const DD::Image::Row& in, int /* rowY unused */, int rowX, int rowXBound, DD::Image::ChannelMask outputChannels, DD::Image::Row& out) { int rowWidth = rowXBound - rowX; DD::Image::ChannelSet done; foreach (requestedChannel, outputChannels) { // Skip channels which had their trios processed already, if (done & requestedChannel) { continue; } // Pass through channels which are not selected for processing // and non-rgb channels. if (colourIndex(requestedChannel) >= 3) { out.copy(in, requestedChannel, rowX, rowXBound); continue; } DD::Image::Channel rChannel = DD::Image::brother(requestedChannel, 0); DD::Image::Channel gChannel = DD::Image::brother(requestedChannel, 1); DD::Image::Channel bChannel = DD::Image::brother(requestedChannel, 2); done += rChannel; done += gChannel; done += bChannel; const float *rIn = in[rChannel] + rowX; const float *gIn = in[gChannel] + rowX; const float *bIn = in[bChannel] + rowX; float *rOut = out.writable(rChannel) + rowX; float *gOut = out.writable(gChannel) + rowX; float *bOut = out.writable(bChannel) + rowX; // OCIO modifies in-place // Note: xOut can equal xIn in some circumstances, such as when the // 'Black' (throwaway) scanline is uses. We thus must guard memcpy, // which does not allow for overlapping regions. if (rOut != rIn) memcpy(rOut, rIn, sizeof(float)*rowWidth); if (gOut != gIn) memcpy(gOut, gIn, sizeof(float)*rowWidth); if (bOut != bIn) memcpy(bOut, bIn, sizeof(float)*rowWidth); try { OCIO::PlanarImageDesc img(rOut, gOut, bOut, nullptr, rowWidth, /*height*/ 1); applyImageCPU(m_cpuProcessor, img); } catch (const OCIO::Exception &e) { error(e.what()); } catch (...) { error("OCIOLookTransform: Unknown exception during pixel_engine."); } } } const DD::Image::Op::Description OCIOLookTransform::description("OCIOLookTransform", build); const char* OCIOLookTransform::Class() const { return description.name; } const char* OCIOLookTransform::displayName() const { return description.name; } const char* OCIOLookTransform::node_help() const { static const char * help = "OpenColorIO LookTransform\n\n" "A 'look' is a named color transform, intended to modify the look of an " "image in a 'creative' manner (as opposed to a colorspace definion which " "tends to be technically/mathematically defined).\n\n" "Examples of looks may be a neutral grade, to be applied to film scans " "prior to VFX work, or a per-shot DI grade decided on by the director, " "to be applied just before the viewing transform.\n\n" "OCIOLooks must be predefined in the OpenColorIO configuration before usage, " "and often reference per-shot/sequence LUTs/CCs.\n\n" "See the look knob for further syntax details.\n\n" "See opencolorio.org for look configuration customization examples."; return help; } // This class is necessary in order to call knobsAtTheEnd(). Otherwise, the NukeWrapper knobs // will be added to the Context tab instead of the primary tab. class OCIOLookTransformNukeWrapper : public DD::Image::NukeWrapper { public: OCIOLookTransformNukeWrapper(DD::Image::PixelIop* op) : DD::Image::NukeWrapper(op) { } void attach() override { wrapped_iop()->attach(); } void detach() override { wrapped_iop()->detach(); } void knobs(DD::Image::Knob_Callback f) override { OCIOLookTransform* lookIop = dynamic_cast(wrapped_iop()); if (!lookIop) return; DD::Image::NukeWrapper::knobs(f); lookIop->getExtendedOpProperties()->extendedPropertiesKnobs(f, "Context", nullptr, DD::Image::OCIOPluginBase::kContextTooltip); } }; static DD::Image::Op* build(Node *node) { DD::Image::NukeWrapper *op = (new OCIOLookTransformNukeWrapper(new OCIOLookTransform(node))); op->channels(DD::Image::Mask_RGB); return op; }