/** * OpenColorIO FileTransform Iop. */ #include "OCIOFileTransform.h" namespace OCIO = OCIO_NAMESPACE; #include #include #include #include #include #include #include #include #include OCIOFileTransform::OCIOFileTransform(Node *n) :DD::Image::OCIOWorkingSpaceBase(n) { m_file = nullptr; m_interpindex = 1; m_reload_version = 1; } OCIOFileTransform::~OCIOFileTransform() { } const char* OCIOFileTransform::interp[] = { "nearest", "linear", "tetrahedral", "best", nullptr }; void OCIOFileTransform::knobs(DD::Image::Knob_Callback f) { File_knob(f, &m_file, "file", "file"); DD::Image::Tooltip(f, "Specify the file, on disk, to use for this transform. See the node help for the list of supported formats."); // Reload button, and hidden "version" knob to invalidate cache on reload Button(f, "reload", "reload"); DD::Image::Tooltip(f, "Reloads specified files"); Int_knob(f, &m_reload_version, "version"); DD::Image::SetFlags(f, DD::Image::Knob::HIDDEN); String_knob(f, &m_cccid, "cccid"); const char * srchelp2 = "If the source file is an ASC CDL CCC (color correction collection), " "this specifys the id to lookup. OpenColorIO::Contexts (envvars) are obeyed."; DD::Image::Tooltip(f, srchelp2); DD::Image::PyScript_knob(f, "import ocionuke.cdl; ocionuke.cdl.select_cccid_for_filetransform(fileknob='file', cccidknob = 'cccid')", "select_cccid", "select cccid"); Obsolete_knob(f, "direction", "if {$value==\"inverse\"} { knob invert true }"); DD::Image::Newline(f); DD::Image::Bool_knob(f, &m_invert, "invert", "invert direction"); DD::Image::Tooltip(f, "Specify the transform direction."); Enumeration_knob(f, &m_interpindex, interp, "interpolation", "interpolation"); DD::Image::Tooltip(f, "Specify the interpolation method. For files that are not LUTs (mtx, etc) this is ignored."); OCIOWorkingSpaceBase::knobs(f); } OCIO::FileTransformRcPtr OCIOFileTransform::createFileTransform() { OCIO::FileTransformRcPtr transform = OCIO::FileTransform::Create(); transform->setSrc(m_file); transform->setCCCId(m_cccid.c_str()); transform->setDirection(m_invert ? OCIO::TransformDirection::TRANSFORM_DIR_INVERSE : OCIO::TransformDirection::TRANSFORM_DIR_FORWARD); if (m_interpindex == 0) transform->setInterpolation(OCIO::INTERP_NEAREST); else if (m_interpindex == 1) transform->setInterpolation(OCIO::INTERP_LINEAR); else if (m_interpindex == 2) transform->setInterpolation(OCIO::INTERP_TETRAHEDRAL); else if (m_interpindex == 3) transform->setInterpolation(OCIO::INTERP_BEST); else { // Should never happen error("Interpolation value out of bounds"); return nullptr; } return transform; } OCIO::ColorSpaceTransformRcPtr OCIOFileTransform::createColorSpaceTransform(const std::string& refName, const std::string& inputName, const OCIO::TransformDirection direction) { auto colorTransform = OCIO::ColorSpaceTransform::Create(); colorTransform->setSrc(refName.c_str()); colorTransform->setDst(inputName.c_str()); colorTransform->setDirection(direction); return colorTransform; } void OCIOFileTransform::updateOCIOProcessors() { OCIO::ConstConfigRcPtr config = getConfigForProject(); OCIO::GroupTransformRcPtr group = OCIO::GroupTransform::Create(); const std::string colorManagement = rootOp()->knob("colorManagement")->enumerationKnob()->getSelectedItemString(); const std::string refName = (colorManagement == "OCIO") ? rootOp()->knob("workingSpaceLUT")->enumerationKnob()->getSelectedItemString() : OCIO::ROLE_SCENE_LINEAR; const std::string inputName = knob(OCIOWorkingSpaceBase::m_workingSpaceKnobName)->enumerationKnob()->getSelectedItemString(); auto colorTransformIn = createColorSpaceTransform(refName, inputName, OCIO::TRANSFORM_DIR_FORWARD); auto colorTransformOut = createColorSpaceTransform(refName, inputName, OCIO::TRANSFORM_DIR_INVERSE); auto transform = createFileTransform(); group->appendTransform(colorTransformIn); group->appendTransform(transform); group->appendTransform(colorTransformOut); if (refName != inputName) { m_processor = config->getProcessor(group, OCIO::TRANSFORM_DIR_FORWARD); } else { // input and output colorspaces are the same - just do the file transform only m_processor = config->getProcessor(transform, OCIO::TRANSFORM_DIR_FORWARD); } m_cpuProcessor = m_processor->getDefaultCPUProcessor(); } void OCIOFileTransform::_validate(bool for_real) { // Call through to the base implementation OCIOWorkingSpaceBase::_validate(for_real); if ( this->inErrorState() ) return; if (!m_file) { error("The source file must be specified."); return; } try { updateOCIOProcessors(); m_cpuProcessor = m_processor->getDefaultCPUProcessor(); setupGPUShaders(m_processor); } catch (OCIO::Exception &e) { error(e.what()); 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); } } // Note that this is copied by others (OCIODisplay) void OCIOFileTransform::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; } void OCIOFileTransform::append(DD::Image::Hash& nodehash) { // There is a bug where in Nuke <6.3 the String_knob (used for // cccid) is not included in the node's hash. Include it manually // so the node correctly redraws. Appears fixed in in 6.3 nodehash.append(m_cccid.c_str()); // Incremented to force reloading after rereading the LUT file nodehash.append(m_reload_version); OCIOWorkingSpaceBase::append(nodehash); } int OCIOFileTransform::knob_changed(DD::Image::Knob* k) { // Only show the cccid knob when loading a .cc/.ccc file. Set // hidden state when the src is changed, or the node properties // are shown if (k->is("file") | k->is("showPanel")) { // Convoluted equiv to pysting::endswith(m_file, ".ccc") // TODO: Could this be queried from the processor? const std::string srcstring = m_file; const std::string cccext = "ccc"; const std::string ccext = "cc"; const bool hasName = !srcstring.empty(); // avoid stl debug asserts on windows. bool isCCorCCC = false; if (hasName) { isCCorCCC = std::equal(cccext.rbegin(), cccext.rend(), srcstring.rbegin()) || std::equal(ccext.rbegin(), ccext.rend(), srcstring.rbegin()); } if (isCCorCCC) { knob("cccid")->show(); knob("select_cccid")->show(); } else { knob("cccid")->hide(); knob("select_cccid")->hide(); } // Ensure this callback is always triggered (for src knob) return 1; } if (k->is("reload")) { knob("version")->set_value(m_reload_version+1); OCIO::ClearAllCaches(); return 1; // ensure callback is triggered again } // Return zero to avoid callbacks for other knobs return 0; } DD::Image::Hash OCIOFileTransform::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* workingSpaceKnob = knob(OCIOWorkingSpaceBase::m_workingSpaceKnobName)) { hash.append(workingSpaceKnob->get_value_at(time)); } if (DD::Image::Knob* fileKnob = knob("file")) { hash.append(fileKnob->get_value_at(time)); } if (DD::Image::Knob* cccidKnob = knob("cccid")) { // Included in append(), added here for clarity and robustness. hash.append(cccidKnob->get_value_at(time)); } if (DD::Image::Knob* invertKnob = knob("invert")) { hash.append(invertKnob->get_value_at(time)); } if (DD::Image::Knob* interpolationKnob = knob("interpolation")) { hash.append(interpolationKnob->get_value_at(time)); } return hash; } // See Saturation::pixel_engine for a well-commented example. // Note that this is copied by others (OCIODisplay) void OCIOFileTransform::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 (OCIO::Exception &e) { error(e.what()); } } } const DD::Image::Op::Description OCIOFileTransform::description("OCIOFileTransform", build); const char* OCIOFileTransform::Class() const { return description.name; } const char* OCIOFileTransform::displayName() const { return description.name; } const char* OCIOFileTransform::node_help() const { if (m_nodehelp.empty()) { const char * helptext = "Use OpenColorIO to apply a transform loaded from the given " "file.\n\n" "This is usually a 1D or 3D LUT file, but can be other file-based " "transform, for example an ASC ColorCorrection XML file.\n\n" "Note that the file's transform is applied with no special " "input/output colorspace handling - so if the file expects " "log-encoded pixels, but you apply the node to a linear " "image, you will get incorrect results.\n\n"; std::ostringstream os; os << helptext; os << "Supported formats:\n"; for (int i=0; ichannels(DD::Image::Mask_RGB); return op; }