diff --git a/.gitignore b/.gitignore index e4dd654..cb5ac4b 100644 --- a/.gitignore +++ b/.gitignore @@ -75,3 +75,6 @@ desktop.ini # ImGui layout (saved to %APPDATA%\UsdLayerManager at runtime) imgui.ini + +# OCIO config LUT files — downloaded automatically at CMake configure time +resources/OpenColorIO-Configs/ diff --git a/CMakeLists.txt b/CMakeLists.txt index b158328..a634633 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,6 +16,12 @@ find_package(Imgui REQUIRED) find_package(Glad REQUIRED) find_package(FFmpeg REQUIRED) +# OpenColorIO — provided by the OpenUSD distribution (USD built with OCIO support) +set(OpenColorIO_DIR + "${OpenUSD_ROOT_DIR}/lib/cmake/OpenColorIO" + CACHE PATH "OpenColorIO CMake config dir" FORCE) +find_package(OpenColorIO CONFIG REQUIRED) + # USD build that includes hdEmbree (built with PXR_ENABLE_EMBREE_PLUGIN=ON). # Can be a separate install from OpenUSD_ROOT_DIR; leave empty to search there instead. set(HDEMBREE_USD_ROOT "" CACHE PATH "USD build containing hdEmbree plugin (separate from OpenUSD_ROOT_DIR)") @@ -123,6 +129,13 @@ if(WITH_CYCLES) -DWITH_CYCLES_DEVICE_HIP=OFF -DWITH_CYCLES_DEVICE_ONEAPI=OFF -DWITH_CYCLES_LOGGING=OFF + # USD ships OpenVDB v9; Cycles prebuilt is v13 — ABI mismatch, both named + # openvdb.dll. Disable openvdb in Cycles to avoid the version conflict. + -DWITH_CYCLES_OPENVDB=OFF + # Embree4 links tbb12.dll; USD already loads tbb.dll at process startup. + # oneTBB detects the older tbb.dll and aborts initialization (DLL_INIT_FAILED). + # Disable Embree so Cycles uses its own BVH traversal instead. + -DWITH_CYCLES_EMBREE=OFF # ---- pxrConfig.cmake dependency overrides ---- # pxrConfig.cmake runs find_dependency() for these before Cycles' # precompiled lib paths are on CMAKE_PREFIX_PATH, so we must supply @@ -131,12 +144,17 @@ if(WITH_CYCLES) -DPython3_EXECUTABLE=$ENV{LOCALAPPDATA}/Programs/Python/Python312/python.exe -DPython3_LIBRARY=$ENV{LOCALAPPDATA}/Programs/Python/Python312/libs/python312.lib -DPython3_INCLUDE_DIR=$ENV{LOCALAPPDATA}/Programs/Python/Python312/include - -DTBB_DIR=${OpenUSD_ROOT_DIR}/lib/cmake/TBB + -DTBB_DIR=${CMAKE_SOURCE_DIR}/third_party/cycles/lib/windows_x64/tbb/lib/cmake/TBB -DMaterialX_DIR=${OpenUSD_ROOT_DIR}/lib/cmake/MaterialX - # Our USD install has no standalone OpenSubdiv/Imath cmake packages - # (they're linked into the USD DLLs); skip those find_dependency calls. - -DPXR_FIND_OPENSUBDIV_IN_CONFIG=OFF - -DPXR_FIND_IMATH_IN_CONFIG=OFF + # OpenSubdiv and Imath cmake configs are bundled in the USD install. + # We must supply their dirs so pxrTargets.cmake can resolve the imported targets. + -DOpenSubdiv_DIR=${OpenUSD_ROOT_DIR}/lib/cmake/OpenSubdiv + -DImath_DIR=${OpenUSD_ROOT_DIR}/lib/cmake/Imath + # Force Cycles to use its own prebuilt OCIO 2.5, not the OCIO 2.1 + # bundled in the new USD include dir. Without this, the new USD's + # include/OpenColorIO/ (OCIO 2.1) shadows Cycles' prebuilt OCIO 2.5 + # headers and the colorspace.cpp API calls fail to compile. + -DOpenColorIO_DIR=${CMAKE_SOURCE_DIR}/third_party/cycles/lib/windows_x64/opencolorio/lib/cmake/OpenColorIO BUILD_COMMAND ${CMAKE_COMMAND} --build --config Release INSTALL_COMMAND @@ -179,6 +197,7 @@ target_link_libraries(UsdLayerManager PRIVATE Imgui::Imgui Glad::Glad FFmpeg::FFmpeg + OpenColorIO::OpenColorIO ole32 shell32 ) @@ -202,6 +221,14 @@ if(WIN32) _CRT_SECURE_NO_WARNINGS ) + # OpenColorIO DLL — copy from USD bin/ (which already ships OpenColorIO_2_1.dll) + add_custom_command(TARGET UsdLayerManager POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${OpenUSD_ROOT_DIR}/bin/OpenColorIO_2_1.dll" + "$" + COMMENT "Copying OpenColorIO runtime DLL..." + ) + # FFmpeg DLLs — copy all .dll files from third_party/ffmpeg/bin file(GLOB FFMPEG_RUNTIME_DLLS "${CMAKE_SOURCE_DIR}/third_party/ffmpeg/bin/*.dll") if(FFMPEG_RUNTIME_DLLS) @@ -213,6 +240,26 @@ if(WIN32) ) endif() + # cmake -P script to copy USD bin/ DLLs excluding CRT DLLs (msvcp, vcruntime, etc.). + # The USD distribution bundles older CRT DLLs (14.34) that conflict with Cycles + # binaries built against the newer system CRT (14.42+). The system CRT is + # always present on Windows 10+ — no need to bundle our own copy. + file(WRITE "${CMAKE_BINARY_DIR}/copy_usd_bin_dlls.cmake" + "file(GLOB _all_dlls \"${OpenUSD_ROOT_DIR}/bin/*.dll\")\n" + "set(_dlls)\n" + "foreach(_dll \${_all_dlls})\n" + " get_filename_component(_name \"\${_dll}\" NAME)\n" + " string(TOLOWER \"\${_name}\" _lname)\n" + " if(_lname MATCHES \"^(msvcp|vcruntime|ucrtbase|concrt|api-ms-win-)\")\n" + " continue()\n" + " endif()\n" + " list(APPEND _dlls \"\${_dll}\")\n" + "endforeach()\n" + "if(_dlls)\n" + " file(COPY \${_dlls} DESTINATION \"\${DEST_DIR}\")\n" + "endif()\n" + ) + # Copy runtime DLLs to output directory for each configuration add_custom_command(TARGET UsdLayerManager POST_BUILD COMMAND ${CMAKE_COMMAND} -E make_directory "$" @@ -220,10 +267,10 @@ if(WIN32) COMMAND ${CMAKE_COMMAND} -E copy_directory "${OpenUSD_BIN_DIR}" "$" - # OpenUSD bin/ — tbb.dll, MaterialX, OpenEXR, OpenImageIO, etc. - COMMAND ${CMAKE_COMMAND} -E copy_directory - "${OpenUSD_ROOT_DIR}/bin" - "$" + # OpenUSD bin/ — tbb.dll, MaterialX, OpenEXR, OpenImageIO, etc. (CRT DLLs excluded) + COMMAND ${CMAKE_COMMAND} + "-DDEST_DIR=$" + -P "${CMAKE_BINARY_DIR}/copy_usd_bin_dlls.cmake" COMMAND ${CMAKE_COMMAND} -E copy_directory "${OpenUSD_ROOT_DIR}/lib/usd" "$/usd" @@ -307,9 +354,16 @@ if(WIN32) "foreach(_dll \${_all_dlls})\n" " get_filename_component(_name \"\${_dll}\" NAME)\n" " string(TOLOWER \"\${_name}\" _lname)\n" + # Exclude CRT DLLs (system must supply these; Cycles bundles an older version). " if(_lname MATCHES \"^(msvcp|vcruntime|ucrtbase|concrt|api-ms-win-)\")\n" " continue()\n" " endif()\n" + # Exclude debug-variant DLLs (not needed in release, just wasted space). + # Pattern: OpenColorIO_d_2_5.dll → contains "_d_" + # openjph.0.25d.dll → ends with "d.dll" + " if(_lname MATCHES \"_d_\" OR _lname MATCHES \"d[.]dll$\")\n" + " continue()\n" + " endif()\n" " list(APPEND _dlls \"\${_dll}\")\n" "endforeach()\n" "if(_dlls)\n" @@ -355,10 +409,17 @@ install(DIRECTORY ${OpenUSD_BIN_DIR}/ ) # Install OpenUSD bin/ DLLs — tbb.dll, tbbmalloc.dll, MaterialX, OpenEXR, -# OpenImageIO, zlib, etc. These are separate from lib/ and also required at runtime. +# OpenImageIO, zlib, etc. Exclude CRT DLLs: the system VC++ Redistributable supplies +# these at runtime; bundling an older copy (14.34) causes crashes on machines with +# Cycles or other DLLs compiled against the newer CRT (14.42+). install(DIRECTORY ${OpenUSD_ROOT_DIR}/bin/ DESTINATION bin FILES_MATCHING PATTERN "*.dll" + PATTERN "msvcp*" EXCLUDE + PATTERN "vcruntime*" EXCLUDE + PATTERN "ucrtbase*" EXCLUDE + PATTERN "concrt*" EXCLUDE + PATTERN "api-ms-win-*" EXCLUDE ) install(DIRECTORY ${OpenUSD_ROOT_DIR}/lib/usd @@ -435,6 +496,8 @@ if(OPENUSD_HAS_CYCLES) PATTERN "VCRUNTIME140*.dll" EXCLUDE PATTERN "ucrtbase*.dll" EXCLUDE PATTERN "api-ms-win-*.dll" EXCLUDE + PATTERN "OpenColorIO_d_*.dll" EXCLUDE # debug OCIO build + PATTERN "openjph.*d.dll" EXCLUDE # debug OpenJPH ) # tbb12.dll (used by openvdb/embree4) lives in the precompiled lib tree, not install output install(FILES "${CYCLES_TBB12_DLL}" @@ -485,6 +548,35 @@ if(PYTHON311_DLL) ) endif() +# --------------------------------------------------------------------------- +# OCIO: download ACES 1.2 config at configure time if not present +# --------------------------------------------------------------------------- +set(_ocio_config_dir "${CMAKE_SOURCE_DIR}/resources/OpenColorIO-Configs/aces_1.2") +if(NOT EXISTS "${_ocio_config_dir}/config.ocio") + message(STATUS "Downloading ACES 1.2 OCIO config (~124 MB) ...") + set(_ocio_zip "${CMAKE_BINARY_DIR}/aces_1.2.zip") + file(DOWNLOAD + "https://github.com/colour-science/OpenColorIO-Configs/releases/download/v1.2/OpenColorIO-Config-ACES-1.2.zip" + "${_ocio_zip}" + SHOW_PROGRESS + STATUS _ocio_dl_status + ) + list(GET _ocio_dl_status 0 _ocio_dl_code) + if(_ocio_dl_code EQUAL 0) + execute_process( + COMMAND ${CMAKE_COMMAND} -E tar xf "${_ocio_zip}" + WORKING_DIRECTORY "${CMAKE_BINARY_DIR}" + ) + file(COPY "${CMAKE_BINARY_DIR}/OpenColorIO-Config-ACES-1.2/aces_1.2/" + DESTINATION "${_ocio_config_dir}") + file(REMOVE_RECURSE "${CMAKE_BINARY_DIR}/OpenColorIO-Config-ACES-1.2") + file(REMOVE "${_ocio_zip}") + message(STATUS "ACES 1.2 OCIO config installed to ${_ocio_config_dir}") + else() + message(WARNING "ACES 1.2 OCIO download failed (${_ocio_dl_status}) — OCIO will not be bundled.") + endif() +endif() + # Copy resources to build directory file(COPY ${CMAKE_SOURCE_DIR}/resources DESTINATION ${CMAKE_BINARY_DIR} @@ -503,6 +595,24 @@ add_custom_command(TARGET UsdLayerManager POST_BUILD COMMENT "Copying fonts and SVG icons to build output" ) +# Copy ACES OCIO config to build output — only on first build (skipped if already present). +if(EXISTS "${_ocio_config_dir}/config.ocio") + add_custom_command(TARGET UsdLayerManager POST_BUILD + COMMAND ${CMAKE_COMMAND} + "-DSRC=${_ocio_config_dir}" + "-DDST=$/resources/OpenColorIO-Configs/aces_1.2" + -P "${CMAKE_SOURCE_DIR}/cmake/CopyDirIfMissing.cmake" + COMMENT "Copying ACES OCIO config to build output (first time only)" + ) +endif() + +# Install OpenColorIO DLL (from the OpenUSD distribution's bin/) +install(FILES + "${OpenUSD_ROOT_DIR}/bin/OpenColorIO_2_1.dll" + DESTINATION bin + OPTIONAL +) + # Install resources (icons, fonts placeholder) next to the exe so the # exe-relative path "resources/..." resolves correctly from install/bin/. install(DIRECTORY ${CMAKE_SOURCE_DIR}/resources diff --git a/CMakePresets.json b/CMakePresets.json index 93d8303..5a37e0b 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -9,7 +9,7 @@ "binaryDir": "${sourceDir}/build", "cacheVariables": { "CMAKE_INSTALL_PREFIX": "${sourceDir}/install", - "CMAKE_PREFIX_PATH": "${sourceDir}/third_party/OpenUSD_v25.05", + "CMAKE_PREFIX_PATH": "${sourceDir}/third_party/OpenUSD-v25.05", "IMGUI_DIR": "${sourceDir}/third_party/imgui-1.92.7", "CMAKE_CXX_STANDARD": "17", "CMAKE_CXX_STANDARD_REQUIRED": "ON", diff --git a/src/core/UsdSceneRenderer.cpp b/src/core/UsdSceneRenderer.cpp index 056b941..1933c0c 100644 --- a/src/core/UsdSceneRenderer.cpp +++ b/src/core/UsdSceneRenderer.cpp @@ -23,11 +23,21 @@ #include #include #include +#include +#include +#include +#include + +#include #include #include +#include +#include #include +namespace OCIO = OCIO_NAMESPACE; + namespace UsdLayerManager { // --------------------------------------------------------------------------- @@ -39,6 +49,37 @@ static std::string Vec3fStr(const pxr::GfVec3f& v) { + std::to_string(v[2]) + ")"; } +// Forwards USD's TfError / TF_WARN / TF_STATUS messages into the app log. +// Without this, Hydra diagnostics (e.g. the reason HdxColorCorrectionTask +// fails to build an OCIO processor) only go to a stderr console the GUI +// never shows. Installed once, process-wide, from InitRenderer(). +namespace { +class UsdDiagnosticLogger : public pxr::TfDiagnosticMgr::Delegate { +public: + void IssueError(pxr::TfError const& err) override { + LOG_ERROR("[USD] " + err.GetCommentary()); + } + void IssueFatalError(pxr::TfCallContext const&, + std::string const& msg) override { + LOG_ERROR("[USD fatal] " + msg); + } + void IssueStatus(pxr::TfStatus const& status) override { + LOG_INFO("[USD] " + status.GetCommentary()); + } + void IssueWarning(pxr::TfWarning const& warning) override { + LOG_WARNING("[USD] " + warning.GetCommentary()); + } +}; + +void InstallUsdDiagnosticLogger() { + static UsdDiagnosticLogger s_logger; // process-lifetime; never removed + static bool s_installed = false; + if (s_installed) return; + s_installed = true; + pxr::TfDiagnosticMgr::GetInstance().AddDelegate(&s_logger); +} +} // namespace + /// Port of stageView._ComputeCameraFraming(): /// Converts a Y-up integer viewport rect into a CameraUtilFraming whose /// display/data windows are expressed in the Y-down coordinate system that @@ -102,6 +143,271 @@ static GLuint LinkProgram(GLuint vs, GLuint fs) { return prog; } +// =========================================================================== +// ViewportColorCorrector — custom GL color correction (sRGB / OCIO) +// =========================================================================== +// +// Replaces HdxColorCorrectionTask. The scene is rendered linear; Apply() +// samples that linear texture and draws a corrected fullscreen triangle into +// the currently-bound FBO. For OCIO it uses the OCIO GPU shader API directly +// (GpuShaderDesc) so the exact transform — including 1D/3D LUTs — is built and +// bound under our control, independent of Hydra. +class ViewportColorCorrector { +public: + enum class Mode { sRGB, OpenColorIO }; + + ~ViewportColorCorrector() { DestroyGLResources(); } + + /// Draw a corrected fullscreen quad into the bound FBO sampling srcTex + /// (single-sample, linear RGBA). Caller has set the GL viewport. + /// Returns true if a pass was drawn. On OCIO build failure it falls back + /// to the sRGB encode so the viewport degrades gracefully (never black). + bool Apply(GLuint srcTex, Mode mode, + const std::string& disp, const std::string& view, + const std::string& cs, const std::string& look) + { + if (!EnsureCommon()) return false; + + GLuint prog = m_srgbProgram; + std::vector* luts = nullptr; + if (mode == Mode::OpenColorIO && EnsureOcioProgram(disp, view, cs, look)) { + prog = m_ocioProgram; + luts = &m_ocioLuts; + } + if (!prog) return false; + + // Save the GL state we touch. + GLboolean depthTest = glIsEnabled(GL_DEPTH_TEST); + GLboolean blend = glIsEnabled(GL_BLEND); + GLboolean depthMask = GL_TRUE; + glGetBooleanv(GL_DEPTH_WRITEMASK, &depthMask); + glDisable(GL_DEPTH_TEST); + glDisable(GL_BLEND); + glDepthMask(GL_FALSE); + + glUseProgram(prog); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, srcTex); + glUniform1i(glGetUniformLocation(prog, "uTex"), 0); + + if (luts) { + int unit = 1; + for (const auto& l : *luts) { + glActiveTexture(GL_TEXTURE0 + unit); + glBindTexture(l.target, l.id); + GLint loc = glGetUniformLocation(prog, l.sampler.c_str()); + if (loc >= 0) glUniform1i(loc, unit); + ++unit; + } + glActiveTexture(GL_TEXTURE0); + } + + glBindVertexArray(m_vao); + glDrawArrays(GL_TRIANGLES, 0, 3); + glBindVertexArray(0); + glUseProgram(0); + + // Restore state. + if (depthTest) glEnable(GL_DEPTH_TEST); + if (blend) glEnable(GL_BLEND); + glDepthMask(depthMask); + return true; + } + + void DestroyGLResources() { + DestroyOcioResources(); + if (m_srgbProgram) { glDeleteProgram(m_srgbProgram); m_srgbProgram = 0; } + if (m_vao) { glDeleteVertexArrays(1, &m_vao); m_vao = 0; } + } + +private: + struct LutTex { GLuint id; GLenum target; std::string sampler; }; + + bool EnsureCommon() { + if (m_vao == 0) glGenVertexArrays(1, &m_vao); + if (m_srgbProgram == 0) { + GLuint vs = CompileShader(kFullscreenVS, GL_VERTEX_SHADER); + GLuint fs = CompileShader(kSrgbFS, GL_FRAGMENT_SHADER); + if (vs && fs) m_srgbProgram = LinkProgram(vs, fs); + } + return m_vao != 0 && m_srgbProgram != 0; + } + + bool EnsureOcioProgram(const std::string& disp, const std::string& view, + const std::string& cs, const std::string& look) + { + const std::string key = disp + "|" + view + "|" + cs + "|" + look; + if (m_ocioProgram && key == m_ocioKey) return true; + if (key == m_ocioFailedKey) return false; + + DestroyOcioResources(); + m_ocioKey.clear(); + + std::string fragText; + OCIO::GpuShaderDescRcPtr desc; + try { + OCIO::ConstConfigRcPtr config = OCIO::GetCurrentConfig(); + if (!config) throw std::runtime_error("no current OCIO config"); + const char* srcCS = cs.empty() ? OCIO::ROLE_SCENE_LINEAR : cs.c_str(); + + OCIO::ConstProcessorRcPtr proc = config->getProcessor( + srcCS, disp.c_str(), view.c_str(), OCIO::TRANSFORM_DIR_FORWARD); + OCIO::ConstGPUProcessorRcPtr gpu = proc->getDefaultGPUProcessor(); + + desc = OCIO::GpuShaderDesc::CreateShaderDesc(); + desc->setLanguage(OCIO::GPU_LANGUAGE_GLSL_1_3); + desc->setFunctionName("OCIODisplay"); + desc->setResourcePrefix("ocio_"); + gpu->extractGpuShaderInfo(desc); + fragText = desc->getShaderText(); + } catch (const std::exception& e) { + LOG_WARNING("Custom OCIO build failed (" + key + + "), using sRGB: " + std::string(e.what())); + m_ocioFailedKey = key; + return false; + } + + // Assemble the fragment shader: OCIO declares its samplers + the + // OCIODisplay(vec4) function; our main() samples the linear input and + // runs it through. + std::string fs = "#version 130\n" + "uniform sampler2D uTex;\n" + "in vec2 vUv;\n" + "out vec4 outColor;\n" + + fragText + + "\nvoid main(){ outColor = OCIODisplay(texture(uTex, vUv)); }\n"; + + GLuint vsh = CompileShader(kFullscreenVS, GL_VERTEX_SHADER); + GLuint fsh = CompileShader(fs.c_str(), GL_FRAGMENT_SHADER); + GLuint prog = (vsh && fsh) ? LinkProgram(vsh, fsh) : 0; + if (!prog) { + LOG_WARNING("Custom OCIO shader compile/link failed for " + key + + " — using sRGB"); + m_ocioFailedKey = key; + return false; + } + + // Upload the LUT textures OCIO requested. + if (!CreateOcioTextures(desc)) { + glDeleteProgram(prog); + DestroyOcioResources(); + m_ocioFailedKey = key; + return false; + } + + m_ocioProgram = prog; + m_ocioKey = key; + LOG_INFO("Custom OCIO program built: " + key + + " (" + std::to_string(m_ocioLuts.size()) + " LUTs)"); + return true; + } + + bool CreateOcioTextures(const OCIO::GpuShaderDescRcPtr& desc) { + // 3D LUTs (RGB). + for (unsigned i = 0; i < desc->getNum3DTextures(); ++i) { + const char* texName = nullptr; const char* samplerName = nullptr; + unsigned edgelen = 0; OCIO::Interpolation interp = OCIO::INTERP_LINEAR; + desc->get3DTexture(i, texName, samplerName, edgelen, interp); + const float* values = nullptr; + desc->get3DTextureValues(i, values); + if (!values || edgelen == 0 || !samplerName) return false; + + GLuint id = 0; + glGenTextures(1, &id); + glBindTexture(GL_TEXTURE_3D, id); + glTexImage3D(GL_TEXTURE_3D, 0, GL_RGB32F, edgelen, edgelen, edgelen, + 0, GL_RGB, GL_FLOAT, values); + GLint filt = (interp == OCIO::INTERP_NEAREST) ? GL_NEAREST : GL_LINEAR; + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, filt); + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, filt); + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); + m_ocioLuts.push_back({ id, GL_TEXTURE_3D, samplerName }); + } + + // 1D / 2D LUTs. + for (unsigned i = 0; i < desc->getNumTextures(); ++i) { + const char* texName = nullptr; const char* samplerName = nullptr; + unsigned width = 0, height = 0; + OCIO::GpuShaderDesc::TextureType channel = OCIO::GpuShaderDesc::TEXTURE_RGB_CHANNEL; + OCIO::Interpolation interp = OCIO::INTERP_LINEAR; + desc->getTexture(i, texName, samplerName, width, height, channel, interp); + const float* values = nullptr; + desc->getTextureValues(i, values); + if (!values || width == 0 || !samplerName) return false; + + const bool isRed = (channel == OCIO::GpuShaderDesc::TEXTURE_RED_CHANNEL); + const GLint internal = isRed ? GL_R32F : GL_RGB32F; + const GLenum format = isRed ? GL_RED : GL_RGB; + const GLint filt = (interp == OCIO::INTERP_NEAREST) ? GL_NEAREST : GL_LINEAR; + const GLenum target = (height > 1) ? GL_TEXTURE_2D : GL_TEXTURE_1D; + + GLuint id = 0; + glGenTextures(1, &id); + glBindTexture(target, id); + if (target == GL_TEXTURE_2D) { + glTexImage2D(GL_TEXTURE_2D, 0, internal, width, height, 0, + format, GL_FLOAT, values); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + } else { + glTexImage1D(GL_TEXTURE_1D, 0, internal, width, 0, + format, GL_FLOAT, values); + } + glTexParameteri(target, GL_TEXTURE_MIN_FILTER, filt); + glTexParameteri(target, GL_TEXTURE_MAG_FILTER, filt); + glTexParameteri(target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + m_ocioLuts.push_back({ id, target, samplerName }); + } + return true; + } + + void DestroyOcioResources() { + for (auto& l : m_ocioLuts) glDeleteTextures(1, &l.id); + m_ocioLuts.clear(); + if (m_ocioProgram) { glDeleteProgram(m_ocioProgram); m_ocioProgram = 0; } + m_ocioKey.clear(); + m_ocioFailedKey.clear(); + } + + static const char* kFullscreenVS; + static const char* kSrgbFS; + + GLuint m_vao = 0; + GLuint m_srgbProgram = 0; + GLuint m_ocioProgram = 0; + std::string m_ocioKey; + std::string m_ocioFailedKey; + std::vector m_ocioLuts; +}; + +// Attribute-less fullscreen triangle; UV in [0,2] covers the [0,1] screen. +const char* ViewportColorCorrector::kFullscreenVS = R"(#version 130 +out vec2 vUv; +void main() { + vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2)); + vUv = p; + gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0); +} +)"; + +// Linear → sRGB encode (matches HdxColorCorrectionTask's sRGB path). +const char* ViewportColorCorrector::kSrgbFS = R"(#version 130 +uniform sampler2D uTex; +in vec2 vUv; +out vec4 outColor; +vec3 lin2srgb(vec3 c) { + vec3 lo = c * 12.92; + vec3 hi = 1.055 * pow(max(c, vec3(0.0)), vec3(1.0/2.4)) - 0.055; + bvec3 cut = lessThanEqual(c, vec3(0.0031308)); + return mix(hi, lo, vec3(cut)); +} +void main() { + vec4 c = texture(uTex, vUv); + outColor = vec4(lin2srgb(c.rgb), c.a); +} +)"; + // --------------------------------------------------------------------------- // GLSL sources // --------------------------------------------------------------------------- @@ -132,6 +438,7 @@ UsdSceneRenderer::UsdSceneRenderer() , m_aaEnabled(false) , m_backgroundColor(0.15f, 0.15f, 0.15f) , m_shadingMode(ShadingMode::SmoothShaded) + , m_colorCorrectionMode(ColorCorrectionMode::sRGB) , m_ambientLightOnly(true) , m_domeLightEnabled(false) , m_stageIsZup(false) @@ -171,6 +478,9 @@ UsdSceneRenderer::~UsdSceneRenderer() { DestroyBBoxResources(); DestroyCamWireResources(); DestroyLightWireResources(); + m_colorCorrector.reset(); // deletes its GL program / LUT textures + if (m_ccLinearFBO) glDeleteFramebuffers(1, &m_ccLinearFBO); + if (m_ccLinearTex) glDeleteTextures(1, &m_ccLinearTex); } // =========================================================================== @@ -211,6 +521,9 @@ void UsdSceneRenderer::InitRenderer() { LOG_INFO("UsdSceneRenderer::InitRenderer - initializing..."); + // Route USD/Hydra diagnostics (incl. OCIO failures) to the app log. + InstallUsdDiagnosticLogger(); + pxr::GlfContextCaps::InitInstance(); pxr::UsdImagingGLEngine::Parameters params; @@ -225,10 +538,14 @@ void UsdSceneRenderer::InitRenderer() { // Plugin selection: prefer HdStorm / GL-based renderers auto plugins = pxr::UsdImagingGLEngine::GetRendererPlugins(); LOG_INFO("Available renderer plugins: " + std::to_string(plugins.size())); + bool hasCycles = false; for (const auto& p : plugins) { LOG_INFO(" " + std::string(p.GetText()) + " -> " + pxr::UsdImagingGLEngine::GetRendererDisplayName(p)); + if (std::string(p.GetText()) == "HdCyclesPlugin") hasCycles = true; } + if (!hasCycles) + LOG_WARNING("HdCyclesPlugin not available (see startup log for DLL load errors)."); pxr::TfToken currentPlugin = m_renderer->GetCurrentRendererId(); if (currentPlugin.IsEmpty() && !plugins.empty()) { @@ -518,7 +835,10 @@ void UsdSceneRenderer::Render(int width, int height) m_drawTarget = pxr::GlfDrawTarget::New(pxr::GfVec2i(width, height), wantMSAA); if (!m_drawTarget) { LOG_ERROR("Failed to create GlfDrawTarget"); return; } m_drawTarget->Bind(); - m_drawTarget->AddAttachment("color", GL_RGBA, GL_FLOAT, GL_RGBA); + // RGBA16F so the scene is stored linear with HDR headroom: we render + // linear (Hydra correction disabled) and apply our own color correction + // afterwards, which needs values outside [0,1] for OCIO/ACES. + m_drawTarget->AddAttachment("color", GL_RGBA, GL_FLOAT, GL_RGBA16F); m_drawTarget->AddAttachment("depth", GL_DEPTH_COMPONENT, GL_FLOAT, GL_DEPTH_COMPONENT32F); m_drawTarget->Unbind(); @@ -649,9 +969,27 @@ void UsdSceneRenderer::Render(int width, int height) m_renderParams.forceRefresh = m_forceRefresh; m_renderParams.clipPlanes = m_clipPlanes; - m_renderer->Render(m_stage->GetPseudoRoot(), m_renderParams); + // Color correction is done by our own GL post-process (ApplyViewport- + // ColorCorrection below), not HdxColorCorrectionTask — so Hydra always + // renders linear ("disabled"). This bypasses the hdx OCIO path entirely. + m_renderParams.colorCorrectionMode = pxr::TfToken("disabled"); + m_renderer->SetColorCorrectionSettings(pxr::TfToken("disabled")); + + // Guard the Hydra render: if it throws, the m_drawTarget->Unbind() below + // would be skipped, permanently unbalancing the GlfDrawTarget bind stack + // and blacking out every later frame. + try { + m_renderer->Render(m_stage->GetPseudoRoot(), m_renderParams); + } catch (const std::exception& e) { + LOG_ERROR("Hydra render failed: " + std::string(e.what())); + } m_forceRefresh = false; + // --- Custom color-correction post-process (linear → sRGB / OCIO) --- + if (m_colorCorrectionMode != ColorCorrectionMode::Disabled) { + ApplyViewportColorCorrection(width, height); + } + // --- Optional grid overlay --- if (m_showGrid) { // When MSAA is active, render the grid into the MSAA FBO so it is @@ -680,6 +1018,66 @@ void UsdSceneRenderer::Render(int width, int height) m_drawTarget->Unbind(); } +// =========================================================================== +// Custom color-correction post-process +// =========================================================================== + +void UsdSceneRenderer::ApplyViewportColorCorrection(int width, int height) +{ + if (!m_drawTarget) return; + + if (!m_colorCorrector) + m_colorCorrector = std::make_unique(); + + // (Re)create the single-sample linear copy texture + its FBO on size change. + if (m_ccLinearTex == 0 || m_ccLinearW != width || m_ccLinearH != height) { + if (m_ccLinearTex == 0) glGenTextures(1, &m_ccLinearTex); + glBindTexture(GL_TEXTURE_2D, m_ccLinearTex); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, width, height, 0, + GL_RGBA, GL_FLOAT, nullptr); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glBindTexture(GL_TEXTURE_2D, 0); + + if (m_ccLinearFBO == 0) glGenFramebuffers(1, &m_ccLinearFBO); + glBindFramebuffer(GL_FRAMEBUFFER, m_ccLinearFBO); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, + GL_TEXTURE_2D, m_ccLinearTex, 0); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + m_ccLinearW = width; + m_ccLinearH = height; + } + + // Resolve MSAA (no-op otherwise) so the color attachment holds the linear + // single-sample image, then copy it into m_ccLinearTex — sampling and + // writing the same texture in one pass is illegal, hence the copy. + m_drawTarget->Resolve(); + glBindFramebuffer(GL_READ_FRAMEBUFFER, m_drawTarget->GetFramebufferId()); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_ccLinearFBO); + glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, + GL_COLOR_BUFFER_BIT, GL_NEAREST); + + // Draw the corrected result back into the draw-target render FBO (the MSAA + // FBO when multisampling, so it resolves together with the overlays drawn + // on top of it afterwards). + GLuint dstFbo = m_drawTarget->HasMSAA() + ? m_drawTarget->GetFramebufferMSId() + : m_drawTarget->GetFramebufferId(); + glBindFramebuffer(GL_FRAMEBUFFER, dstFbo); + glViewport(0, 0, width, height); + + ViewportColorCorrector::Mode mode = + (m_colorCorrectionMode == ColorCorrectionMode::OpenColorIO) + ? ViewportColorCorrector::Mode::OpenColorIO + : ViewportColorCorrector::Mode::sRGB; + + m_colorCorrector->Apply(m_ccLinearTex, mode, + m_ocioDisplay, m_ocioView, + m_ocioColorSpace, m_ocioLook); +} + // =========================================================================== // Output // =========================================================================== @@ -690,7 +1088,6 @@ uint32_t UsdSceneRenderer::GetColorTextureID() // Resolve MSAA → regular texture before the caller samples it. // Called after all overlay draws (axis, bboxes, camera wireframes) so // every layer of MSAA-rendered content is included in the resolve. - // No-op when MSAA is not enabled. m_drawTarget->Resolve(); auto att = m_drawTarget->GetAttachment("color"); return att ? static_cast(att->GetGlTextureName()) : 0; diff --git a/src/core/UsdSceneRenderer.h b/src/core/UsdSceneRenderer.h index 6f9ea2f..9f3fec9 100644 --- a/src/core/UsdSceneRenderer.h +++ b/src/core/UsdSceneRenderer.h @@ -21,9 +21,14 @@ #include #include +#include namespace UsdLayerManager { +/// Custom viewport color-correction post-process (sRGB / OCIO) implemented in +/// our own GL shader instead of HdxColorCorrectionTask. Defined in the .cpp. +class ViewportColorCorrector; + /// Bounding box display mode for selected prims. enum class BBoxMode { None, ///< No bounding boxes drawn @@ -40,6 +45,13 @@ enum class ShadingMode { Unlit, ///< Smooth geometry, lighting disabled }; +/// Viewport color correction mode — maps to UsdImagingGLRenderParams::colorCorrectionMode. +enum class ColorCorrectionMode { + Disabled, ///< No correction (raw linear output) + sRGB, ///< Linear → sRGB gamma (default) + OpenColorIO, ///< Full OCIO pipeline via bundled ACES 1.2 config +}; + class UsdSceneRenderer { public: UsdSceneRenderer(); @@ -211,6 +223,18 @@ public: ShadingMode GetShadingMode() const { return m_shadingMode; } void SetShadingMode(ShadingMode m) { m_shadingMode = m; m_forceRefresh = true; } + ColorCorrectionMode GetColorCorrectionMode() const { return m_colorCorrectionMode; } + void SetColorCorrectionMode(ColorCorrectionMode m) { m_colorCorrectionMode = m; m_forceRefresh = true; } + + const std::string& GetOcioDisplay() const { return m_ocioDisplay; } + void SetOcioDisplay(std::string v) { m_ocioDisplay = std::move(v); m_forceRefresh = true; } + const std::string& GetOcioView() const { return m_ocioView; } + void SetOcioView(std::string v) { m_ocioView = std::move(v); m_forceRefresh = true; } + const std::string& GetOcioColorSpace() const { return m_ocioColorSpace; } + void SetOcioColorSpace(std::string v){ m_ocioColorSpace = std::move(v); m_forceRefresh = true; } + const std::string& GetOcioLook() const { return m_ocioLook; } + void SetOcioLook(std::string v) { m_ocioLook = std::move(v); m_forceRefresh = true; } + /// Default material ambient (kA, default 0.2 — matches viewSettingsDataModel.py). float GetDefaultMaterialAmbient() const { return m_defaultMaterialAmbient; } void SetDefaultMaterialAmbient(float v) { m_defaultMaterialAmbient = v; } @@ -239,6 +263,7 @@ private: int m_lastRenderWidth = 0; int m_lastRenderHeight = 0; void InitRenderer(); + void InitGridResources(); void RebuildGridVBO(); ///< (Re)build grid line geometry after up-axis or size change. void DestroyGridResources(); @@ -310,7 +335,12 @@ private: pxr::GfVec3f m_backgroundColor; // Lighting settings (mirrors stageView.py viewSettings) - ShadingMode m_shadingMode; // draw mode + lighting flags + ShadingMode m_shadingMode; // draw mode + lighting flags + ColorCorrectionMode m_colorCorrectionMode; // viewport color correction + std::string m_ocioDisplay; + std::string m_ocioView; + std::string m_ocioColorSpace; + std::string m_ocioLook; bool m_ambientLightOnly; // camera headlight bool m_domeLightEnabled; // dome/IBL light bool m_stageIsZup; // used for dome light rotation @@ -361,6 +391,18 @@ private: // --- Camera wireframe cache --- pxr::SdfPathVector m_cachedCameraPaths; bool m_cameraCacheDirty = true; + + // --- Custom color-correction post-process --------------------------------- + // The scene is rendered linear (Hydra correction "disabled") and corrected + // by our own GL shader, bypassing HdxColorCorrectionTask. ApplyViewport- + // ColorCorrection() resolves the linear result into m_ccLinearTex, then + // draws a corrected fullscreen quad back into the draw-target FBO. + void ApplyViewportColorCorrection(int width, int height); + std::unique_ptr m_colorCorrector; + GLuint m_ccLinearFBO = 0; ///< FBO wrapping m_ccLinearTex (correction input) + GLuint m_ccLinearTex = 0; ///< single-sample linear copy of the Hydra output + int m_ccLinearW = 0; + int m_ccLinearH = 0; }; } // namespace UsdLayerManager diff --git a/src/main.cpp b/src/main.cpp index 2b1b541..189d471 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,6 +1,7 @@ #include "ui/Application.h" #include "utils/Logger.h" #include +#include #include #include #include @@ -37,16 +38,64 @@ static void SetUsdPluginPath() { FindClose(hFind); } - if (!pluginPaths.empty()) { + // Pre-flight: attempt to load each plugin DLL before registering it. + // This catches missing dependencies (e.g. sycl8.dll → Intel GPU drivers) + // gracefully and shows our own warning rather than a cryptic TF_ERROR. + std::vector checkedPaths; + for (const auto& infoPath : pluginPaths) { + // Derive the DLL path from the plugInfo.json location: + // /resources/plugInfo.json → ../.dll + // e.g. usd/hdCycles/resources/plugInfo.json → usd/hdCycles.dll + size_t resPos = infoPath.rfind("\\resources\\plugInfo.json"); + if (resPos != std::string::npos) { + std::string pluginDir = infoPath.substr(0, resPos); + size_t dirSlash = pluginDir.rfind('\\'); + std::string pluginName = (dirSlash != std::string::npos) + ? pluginDir.substr(dirSlash + 1) : pluginDir; + std::string dllPath = pluginPath + "\\" + pluginName + ".dll"; + DWORD dllAttrs = GetFileAttributesA(dllPath.c_str()); + if (dllAttrs != INVALID_FILE_ATTRIBUTES) { + // DLL file exists — verify it can actually be loaded. + // No LOAD_WITH_ALTERED_SEARCH_PATH: keep the application + // directory (exe dir, where embree4.dll etc. live) in the + // DLL search path so transitive deps resolve correctly. + HMODULE h = LoadLibraryA(dllPath.c_str()); + if (!h) { + DWORD err = GetLastError(); + char msg[256] = {}; + FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + nullptr, err, 0, msg, static_cast(sizeof(msg) - 1), nullptr); + std::string errMsg(msg); + while (!errMsg.empty() && (errMsg.back() == '\r' || errMsg.back() == '\n')) + errMsg.pop_back(); + char hexErr[12] = {}; + sprintf_s(hexErr, "0x%08X", static_cast(err)); + LOG_WARNING("Skipping plugin " + pluginName + + " (DLL load failed " + hexErr + "): " + errMsg); + continue; // Skip registering this plugin + } + // Intentionally do NOT FreeLibrary here. Releasing the handle + // drops the refcount to 0 and unloads the DLL; when USD's plug + // system later loads it again DllMain re-fires, re-registering + // any TfEnvSettings and causing "duplicate definition" warnings. + // Keeping the handle loaded means USD's LoadLibrary is a no-op + // (refcount bump only, no DllMain call), preventing duplicates. + // The process holds these handles for its entire lifetime anyway. + } + } + checkedPaths.push_back(infoPath); + } + + if (!checkedPaths.empty()) { auto& registry = pxr::PlugRegistry::GetInstance(); - auto registered = registry.RegisterPlugins(pluginPaths); + auto registered = registry.RegisterPlugins(checkedPaths); LOG_INFO("Registered " + std::to_string(registered.size()) + " USD plugins from " + pluginPath); } else { LOG_WARNING("No USD plugins found at " + pluginPath); } } -int main(int argc, char* argv[]) { +int main(int /*argc*/, char* /*argv*/[]) { try { UsdLayerManager::Logger::Instance().SetLogLevel(UsdLayerManager::LogLevel::Info); diff --git a/src/ui/Application.cpp b/src/ui/Application.cpp index 18635ff..622b169 100644 --- a/src/ui/Application.cpp +++ b/src/ui/Application.cpp @@ -138,6 +138,18 @@ bool Application::Initialize(const std::string& windowTitle, int width, int heig RefreshManagers(); } + // Set OCIO to bundled ACES 1.2 config if not already configured externally. + // Must happen before the first Render() call so Hydra picks up the env var. + if (!getenv("OCIO")) { + namespace fs = std::filesystem; + std::string ocioConfig = ResourcePath( + "resources/OpenColorIO-Configs/aces_1.2/config.ocio"); + if (fs::exists(ocioConfig)) { + _putenv_s("OCIO", ocioConfig.c_str()); + LOG_INFO("OCIO config: " + ocioConfig); + } + } + // Load per-viewport settings (render delegate, grid, AA, etc.) from AppData. if (const char* appData = getenv("APPDATA")) { namespace fs = std::filesystem; diff --git a/src/ui/ViewportPanel.cpp b/src/ui/ViewportPanel.cpp index aca4c84..4cedb74 100644 --- a/src/ui/ViewportPanel.cpp +++ b/src/ui/ViewportPanel.cpp @@ -635,7 +635,12 @@ void ViewportPanel::SaveSettings(const std::string& path) const f << "BBoxMode=" << s.bboxMode << "\n"; f << "AmbientLightOnly=" << (s.ambientLightOnly ? 1 : 0) << "\n"; f << "DomeLightEnabled=" << (s.domeLightEnabled ? 1 : 0) << "\n"; - f << "ShadingMode=" << s.shadingMode << "\n"; + f << "ShadingMode=" << s.shadingMode << "\n"; + f << "ColorCorrectionMode=" << s.colorCorrectionMode << "\n"; + f << "OcioDisplay=" << s.ocioDisplay << "\n"; + f << "OcioView=" << s.ocioView << "\n"; + f << "OcioColorSpace=" << s.ocioColorSpace << "\n"; + f << "OcioLook=" << s.ocioLook << "\n"; } } @@ -701,7 +706,12 @@ void ViewportPanel::LoadSettings(const std::string& path) else if (key == "BBoxMode") ts.bboxMode = toInt(val, ts.bboxMode); else if (key == "AmbientLightOnly") ts.ambientLightOnly = (val == "1"); else if (key == "DomeLightEnabled") ts.domeLightEnabled = (val == "1"); - else if (key == "ShadingMode") ts.shadingMode = toInt(val, ts.shadingMode); + else if (key == "ShadingMode") ts.shadingMode = toInt(val, ts.shadingMode); + else if (key == "ColorCorrectionMode") ts.colorCorrectionMode = toInt(val, ts.colorCorrectionMode); + else if (key == "OcioDisplay") ts.ocioDisplay = val; + else if (key == "OcioView") ts.ocioView = val; + else if (key == "OcioColorSpace") ts.ocioColorSpace = val; + else if (key == "OcioLook") ts.ocioLook = val; } } diff --git a/src/ui/ViewportTile.cpp b/src/ui/ViewportTile.cpp index 4362e6b..5ee020f 100644 --- a/src/ui/ViewportTile.cpp +++ b/src/ui/ViewportTile.cpp @@ -1,5 +1,6 @@ #include "ViewportTile.h" #include "../utils/Logger.h" +#include "../utils/OcioConfigParser.h" #include #include @@ -99,7 +100,12 @@ ViewportTileSettings ViewportTile::GetSettings() const s.bboxMode = static_cast(m_renderer.GetBBoxMode()); s.ambientLightOnly = m_renderer.GetAmbientLightOnly(); s.domeLightEnabled = m_renderer.GetDomeLightEnabled(); - s.shadingMode = static_cast(m_renderer.GetShadingMode()); + s.shadingMode = static_cast(m_renderer.GetShadingMode()); + s.colorCorrectionMode = static_cast(m_renderer.GetColorCorrectionMode()); + s.ocioDisplay = m_renderer.GetOcioDisplay(); + s.ocioView = m_renderer.GetOcioView(); + s.ocioColorSpace = m_renderer.GetOcioColorSpace(); + s.ocioLook = m_renderer.GetOcioLook(); return s; } @@ -114,6 +120,12 @@ void ViewportTile::ApplySettings(const ViewportTileSettings& s) m_renderer.SetAmbientLightOnly(s.ambientLightOnly); m_renderer.SetDomeLightEnabled(s.domeLightEnabled); m_renderer.SetShadingMode(static_cast(s.shadingMode)); + m_renderer.SetColorCorrectionMode(static_cast(s.colorCorrectionMode)); + m_renderer.SetOcioDisplay(s.ocioDisplay); + m_renderer.SetOcioView(s.ocioView); + m_renderer.SetOcioColorSpace(s.ocioColorSpace); + m_renderer.SetOcioLook(s.ocioLook); + m_ocioFieldsSynced = false; // re-seed OCIO edit buffers from restored values } // --------------------------------------------------------------------------- @@ -985,6 +997,124 @@ void ViewportTile::RenderCompactToolbar(int tileIndex) if (ImGui::BeginPopupContextItem(gearPopupId.c_str(), ImGuiPopupFlags_MouseButtonLeft)) { + // -- Color correction -------------------------------------------- + if (ImGui::BeginMenu("Color Correction")) { + ColorCorrectionMode ccm = m_renderer.GetColorCorrectionMode(); + auto ccItem = [&](const char* label, ColorCorrectionMode mode) { + if (ImGui::MenuItem(label, nullptr, ccm == mode)) { + m_renderer.SetColorCorrectionMode(mode); + m_ocioFieldsSynced = false; + } + }; + ccItem("Disabled", ColorCorrectionMode::Disabled); + ccItem("sRGB", ColorCorrectionMode::sRGB); + ccItem("OpenColorIO", ColorCorrectionMode::OpenColorIO); + + if (ccm == ColorCorrectionMode::OpenColorIO) { + ImGui::Separator(); + const OcioConfig& ocfg = GetCurrentOcioConfig(); + + // Seed buffers from renderer, falling back to OCIO config defaults + if (!m_ocioFieldsSynced) { + std::string disp = m_renderer.GetOcioDisplay(); + std::string view = m_renderer.GetOcioView(); + if (disp.empty()) disp = ocfg.defaultDisplay; + if (view.empty()) view = ocfg.defaultView; + strncpy(m_ocioDisplayBuf, disp.c_str(), 127); + strncpy(m_ocioViewBuf, view.c_str(), 127); + strncpy(m_ocioColorSpaceBuf, m_renderer.GetOcioColorSpace().c_str(), 127); + strncpy(m_ocioLookBuf, m_renderer.GetOcioLook().c_str(), 127); + if (m_renderer.GetOcioDisplay().empty() && !disp.empty()) + m_renderer.SetOcioDisplay(disp); + if (m_renderer.GetOcioView().empty() && !view.empty()) + m_renderer.SetOcioView(view); + m_ocioFieldsSynced = true; + } + + ImGui::PushItemWidth(200.f); + + // Display combo + if (ImGui::BeginCombo("Display##ocio", m_ocioDisplayBuf)) { + for (const auto& d : ocfg.displays) { + bool sel = (d == m_ocioDisplayBuf); + if (ImGui::Selectable(d.c_str(), sel)) { + strncpy(m_ocioDisplayBuf, d.c_str(), 127); + m_renderer.SetOcioDisplay(d); + // Auto-select default view for new display + auto vit = ocfg.views.find(d); + if (vit != ocfg.views.end() && !vit->second.empty()) { + strncpy(m_ocioViewBuf, vit->second[0].c_str(), 127); + m_renderer.SetOcioView(vit->second[0]); + } + } + if (sel) ImGui::SetItemDefaultFocus(); + } + ImGui::EndCombo(); + } + + // View combo — filtered by current display + { + static const std::vector kEmpty; + auto vit = ocfg.views.find(std::string(m_ocioDisplayBuf)); + const auto& views = (vit != ocfg.views.end()) ? vit->second : kEmpty; + if (ImGui::BeginCombo("View##ocio", m_ocioViewBuf)) { + for (const auto& v : views) { + bool sel = (v == m_ocioViewBuf); + if (ImGui::Selectable(v.c_str(), sel)) { + strncpy(m_ocioViewBuf, v.c_str(), 127); + m_renderer.SetOcioView(v); + } + if (sel) ImGui::SetItemDefaultFocus(); + } + ImGui::EndCombo(); + } + } + + // Color Space combo + if (ImGui::BeginCombo("Color Space##ocio", + m_ocioColorSpaceBuf[0] ? m_ocioColorSpaceBuf : "(default)")) { + if (ImGui::Selectable("(default)", m_ocioColorSpaceBuf[0] == '\0')) { + m_ocioColorSpaceBuf[0] = '\0'; + m_renderer.SetOcioColorSpace(""); + } + if (m_ocioColorSpaceBuf[0] == '\0') ImGui::SetItemDefaultFocus(); + for (const auto& cs : ocfg.colorSpaces) { + bool sel = (cs == m_ocioColorSpaceBuf); + if (ImGui::Selectable(cs.c_str(), sel)) { + strncpy(m_ocioColorSpaceBuf, cs.c_str(), 127); + m_renderer.SetOcioColorSpace(cs); + } + if (sel) ImGui::SetItemDefaultFocus(); + } + ImGui::EndCombo(); + } + + // Look combo + if (ImGui::BeginCombo("Look##ocio", + m_ocioLookBuf[0] ? m_ocioLookBuf : "(none)")) { + if (ImGui::Selectable("(none)", m_ocioLookBuf[0] == '\0')) { + m_ocioLookBuf[0] = '\0'; + m_renderer.SetOcioLook(""); + } + if (m_ocioLookBuf[0] == '\0') ImGui::SetItemDefaultFocus(); + for (const auto& look : ocfg.looks) { + bool sel = (look == m_ocioLookBuf); + if (ImGui::Selectable(look.c_str(), sel)) { + strncpy(m_ocioLookBuf, look.c_str(), 127); + m_renderer.SetOcioLook(look); + } + if (sel) ImGui::SetItemDefaultFocus(); + } + ImGui::EndCombo(); + } + + ImGui::PopItemWidth(); + } + ImGui::EndMenu(); + } + + ImGui::Separator(); + // -- Shading mode ------------------------------------------------ if (ImGui::BeginMenu("Shading")) { ShadingMode cur = m_renderer.GetShadingMode(); diff --git a/src/ui/ViewportTile.h b/src/ui/ViewportTile.h index 1403930..2d900c6 100644 --- a/src/ui/ViewportTile.h +++ b/src/ui/ViewportTile.h @@ -26,9 +26,14 @@ struct ViewportTileSettings { float bgColorG = 0.15f; float bgColorB = 0.15f; int bboxMode = 0; ///< BBoxMode cast to int - bool ambientLightOnly = false; + bool ambientLightOnly = true; bool domeLightEnabled = false; - int shadingMode = 0; ///< ShadingMode cast to int (0 = SmoothShaded) + int shadingMode = 0; ///< ShadingMode cast to int (0 = SmoothShaded) + int colorCorrectionMode = 1; ///< ColorCorrectionMode cast to int (1 = sRGB) + std::string ocioDisplay; + std::string ocioView; + std::string ocioColorSpace; + std::string ocioLook; }; /// Named orthographic view directions. @@ -178,6 +183,13 @@ private: // ── Orthographic view ──────────────────────────────────────────────────── OrthoView m_orthoView = OrthoView::None; + // ── OCIO InputText edit buffers (per-tile, seeded on mode activation) ──── + char m_ocioDisplayBuf[128] = {}; + char m_ocioViewBuf[128] = {}; + char m_ocioColorSpaceBuf[128] = {}; + char m_ocioLookBuf[128] = {}; + bool m_ocioFieldsSynced = false; + // ── Per-frame interaction flags ────────────────────────────────────────── bool m_wasClickedThisFrame = false; bool m_wasHoveredThisFrame = false; diff --git a/src/utils/OcioConfigParser.cpp b/src/utils/OcioConfigParser.cpp new file mode 100644 index 0000000..bf1fe15 --- /dev/null +++ b/src/utils/OcioConfigParser.cpp @@ -0,0 +1,62 @@ +#include "OcioConfigParser.h" + +#include +namespace OCIO = OCIO_NAMESPACE; + +namespace UsdLayerManager { + +const OcioConfig& GetCurrentOcioConfig() { + static OcioConfig s_config; + static bool s_loaded = false; + if (s_loaded) return s_config; + s_loaded = true; + + try { + OCIO::ConstConfigRcPtr cfg = OCIO::GetCurrentConfig(); + if (!cfg) return s_config; + + // Displays + views per display + const char* defDisp = cfg->getDefaultDisplay(); + if (defDisp) s_config.defaultDisplay = defDisp; + + int nD = cfg->getNumDisplays(); + for (int i = 0; i < nD; ++i) { + const char* disp = cfg->getDisplay(i); + if (!disp) continue; + s_config.displays.emplace_back(disp); + auto& viewVec = s_config.views[disp]; + int nV = cfg->getNumViews(disp); + for (int j = 0; j < nV; ++j) { + const char* v = cfg->getView(disp, j); + if (v) viewVec.emplace_back(v); + } + } + + if (!s_config.defaultDisplay.empty()) { + const char* dv = cfg->getDefaultView(s_config.defaultDisplay.c_str()); + if (dv) s_config.defaultView = dv; + } + + // Color spaces + int nCS = cfg->getNumColorSpaces(); + for (int i = 0; i < nCS; ++i) { + const char* cs = cfg->getColorSpaceNameByIndex(i); + if (cs) s_config.colorSpaces.emplace_back(cs); + } + + // Looks + int nL = cfg->getNumLooks(); + for (int i = 0; i < nL; ++i) { + const char* look = cfg->getLookNameByIndex(i); + if (look) s_config.looks.emplace_back(look); + } + + s_config.valid = true; + } catch (const std::exception&) { + // OCIO not configured — s_config stays empty with valid=false + } + + return s_config; +} + +} // namespace UsdLayerManager diff --git a/src/utils/OcioConfigParser.h b/src/utils/OcioConfigParser.h new file mode 100644 index 0000000..c067e05 --- /dev/null +++ b/src/utils/OcioConfigParser.h @@ -0,0 +1,22 @@ +#pragma once +#include +#include +#include + +namespace UsdLayerManager { + +struct OcioConfig { + std::vector displays; + std::unordered_map> views; // display → views + std::vector colorSpaces; + std::vector looks; + std::string defaultDisplay; + std::string defaultView; // default view for defaultDisplay + bool valid = false; // false if $OCIO unavailable +}; + +/// Returns the OCIO config loaded from the $OCIO env var via the OCIO C++ API. +/// Result is cached — parsed once per process lifetime. +const OcioConfig& GetCurrentOcioConfig(); + +} // namespace UsdLayerManager