Compare commits

...

2 Commits

Author SHA1 Message Date
indigo 46d83d300d Add ADR for custom viewport color correction
Document the decision to replace HdxColorCorrectionTask with a custom GL
post-process (linear render + sRGB/OCIO correction via the OCIO GPU API),
including rationale, pipeline, consequences, and alternatives considered.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 17:13:46 +08:00
indigo fb1ae9d3a6 Add custom viewport color correction (sRGB/OCIO), bypassing HdxColorCorrectionTask
Hydra's HdxColorCorrectionTask rendered prims black in OCIO mode and could
corrupt the GlfDrawTarget bind stack on failure (skipping Unbind), blacking
out every later frame including sRGB. Replace it with our own GL post-process:
the scene renders linear (RGBA16F) and is corrected by a fullscreen shader --
linear->sRGB encode, or OCIO via the OCIO 2.1 GPU API (GpuShaderDesc plus
uploaded 1D/3D LUT textures). OCIO build failures fall back to sRGB (never
black) and USD diagnostics are routed to the app log.

- core: ViewportColorCorrector + ApplyViewportColorCorrection in UsdSceneRenderer
- utils: OcioConfigParser enumerates displays/views/colorspaces/looks from $OCIO
- ui: gear-menu OCIO controls (ViewportTile) + per-viewport persistence (ViewportPanel)
- Application: point $OCIO at the bundled ACES 1.2 config
- CMake: link/copy OpenColorIO, download ACES 1.2 config; plus hdCycles build
  config (disable OpenVDB/Embree, fix TBB/OpenSubdiv/Imath dirs, exclude CRT DLLs)
- main: pre-flight plugin DLL load check to skip plugins with missing deps

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 17:10:44 +08:00
13 changed files with 1055 additions and 23 deletions
+3
View File
@@ -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/
+120 -10
View File
@@ -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 <BINARY_DIR> --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"
"$<TARGET_FILE_DIR:UsdLayerManager>"
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 "$<TARGET_FILE_DIR:UsdLayerManager>"
@@ -220,10 +267,10 @@ if(WIN32)
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${OpenUSD_BIN_DIR}"
"$<TARGET_FILE_DIR:UsdLayerManager>"
# OpenUSD bin/ — tbb.dll, MaterialX, OpenEXR, OpenImageIO, etc.
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${OpenUSD_ROOT_DIR}/bin"
"$<TARGET_FILE_DIR:UsdLayerManager>"
# OpenUSD bin/ — tbb.dll, MaterialX, OpenEXR, OpenImageIO, etc. (CRT DLLs excluded)
COMMAND ${CMAKE_COMMAND}
"-DDEST_DIR=$<TARGET_FILE_DIR:UsdLayerManager>"
-P "${CMAKE_BINARY_DIR}/copy_usd_bin_dlls.cmake"
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${OpenUSD_ROOT_DIR}/lib/usd"
"$<TARGET_FILE_DIR:UsdLayerManager>/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=$<TARGET_FILE_DIR:UsdLayerManager>/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
+1 -1
View File
@@ -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",
+183
View File
@@ -0,0 +1,183 @@
# ADR 0001 — Custom Viewport Color Correction (sRGB / OCIO)
- **Status:** Accepted
- **Date:** 2026-06-27
- **Component:** `src/core/UsdSceneRenderer.cpp` (`ViewportColorCorrector`, `ApplyViewportColorCorrection`)
- **Commit:** `fb1ae9d`
## Context
The viewport renders a USD stage through `UsdImagingGLEngine` (Hydra / Storm)
into an offscreen `GlfDrawTarget`, whose color texture ImGui samples for
display. Color management (linear → display) was delegated to Hydra's
`HdxColorCorrectionTask` via `UsdImagingGLEngine::SetColorCorrectionSettings()`,
configured by the per-viewport `ColorCorrectionMode` (Disabled / sRGB /
OpenColorIO) plus OCIO display/view/colorspace/look.
Two problems made the OCIO path unusable:
1. **OCIO output was black.** sRGB correction rendered correctly through the
same color AOV, proving the correction *input* was fine — the hdx OCIO
shader/LUT path itself emitted black. The exact cause stayed opaque because
USD reports it via `TF_WARN`, which this GUI app discarded (no console).
2. **The failure was sticky.** OCIO resources are built lazily *inside*
`Render()` (`HdxColorCorrectionTask::Sync`), not in `SetColorCorrectionSettings()`.
A throw there escaped `UsdSceneRenderer::Render()` and skipped
`m_drawTarget->Unbind()`, permanently unbalancing the `GlfDrawTarget`
bind/restore stack — so every later frame, **including after switching back
to sRGB**, stayed black until an app restart.
We do not control the bundled USD build, so fixing `HdxColorCorrectionTask`
itself was not an option. App and USD both link OpenColorIO 2.1, and a usable
ACES 1.2 config is bundled and pointed to via `$OCIO`.
## Goals / Non-Goals
**Goals:**
- Correct, non-black OCIO display transforms in the viewport (ACES 1.2).
- sRGB output visually identical to the previous hdx sRGB path.
- A color-management failure can never permanently corrupt the viewport.
- Overlays (grid, axis, bbox, camera/light gizmos) remain display-referred,
drawn on top of the corrected image — unchanged from the prior behavior.
**Non-Goals:**
- Replacing Hydra's *rendering* — only its color-correction stage is replaced.
- Exposure/gamma dynamic-property UI, or interactive grading.
- Applying an explicit OCIO look override beyond the view's own looks.
- Color-managing the ImGui UI chrome outside the viewport image.
## Decision
### D1 — Replace `HdxColorCorrectionTask` with our own GL post-process
**Decision:** Hydra always renders **linear** (`colorCorrectionMode = "disabled"`).
A custom fullscreen pass (`ViewportColorCorrector`) applies the correction
afterwards, fully under our control.
**Rationale:** The hdx OCIO path is a black box we cannot patch in the bundled
USD. Owning the post-process gives deterministic, debuggable behavior and
removes the lazy-throw-inside-Render hazard entirely. sRGB and OCIO now share
one code path with consistent ordering.
---
### D2 — Render linear into an `RGBA16F` color attachment
**Decision:** The draw-target color attachment was changed from `GL_RGBA`
(8-bit unorm) to `GL_RGBA16F`.
**Rationale:** We now store the *linear* scene as the intermediate. Display
transforms (especially ACES) need scene values outside `[0,1]`; an 8-bit unorm
buffer would clip highlights and band the shadows before correction even runs.
16F is ample headroom at negligible cost. ImGui and `CaptureFrame` sample/read
it unchanged.
---
### D3 — OCIO via the OCIO GPU API (`GpuShaderDesc`), LUTs uploaded by us
**Decision:** For OCIO mode, build the transform directly with OCIO 2.1:
`config->getProcessor(srcColorSpace, display, view, FORWARD)`
`getDefaultGPUProcessor()``extractGpuShaderInfo(GpuShaderDesc)` with
`GPU_LANGUAGE_GLSL_1_3` and function name `OCIODisplay`. We compile the
generated GLSL into our fragment shader and upload/bind every 1D/2D/3D LUT it
requests as GL textures. The source colorspace defaults to the `scene_linear`
role when unset.
**Rationale:** This is exactly what `HdxColorCorrectionTask` does internally,
but visible and ours to debug. `GLSL_1_3` matches the project's existing
`#version 130` shaders and emits modern `texture()` calls. The program and LUTs
are cached and rebuilt only when display/view/colorspace/look change.
---
### D4 — Graceful degradation to sRGB; never black
**Decision:** Any OCIO failure — processor build, shader compile/link, or LUT
upload — logs the reason and falls back to the sRGB encode shader for that
frame. The failing key is remembered so we do not retry (and re-spam logs)
until the parameters change.
**Rationale:** A misconfigured display/view must degrade to a usable image, not
a black viewport. sRGB is the correct neutral fallback and matches the default.
---
### D5 — Implemented inline in `UsdSceneRenderer.cpp` (pimpl), not new TU/files
**Decision:** `ViewportColorCorrector` is a class defined in
`UsdSceneRenderer.cpp`, held via `std::unique_ptr` behind a forward declaration
in the header.
**Rationale:** `CORE_SOURCES` is a `file(GLOB_RECURSE ...)`; adding a new `.cpp`
forces a CMake reconfigure, which risks re-triggering the heavy Cycles
`ExternalProject` build. Keeping it in an existing TU avoids that while the
pimpl keeps OCIO/GL includes out of the public header. The corrector follows
the file's existing `Init*/Destroy*` GL-resource lifecycle conventions.
---
### D6 — Route USD diagnostics to the app log
**Decision:** Install a process-wide `TfDiagnosticMgr::Delegate`
(`InstallUsdDiagnosticLogger`) that forwards USD `TF_ERROR` / `TF_WARN` /
`TF_STATUS` to the app logger with a `[USD]` prefix.
**Rationale:** USD's own diagnostics were invisible in this GUI app. This made
the original OCIO failure undiagnosable and is generally useful for any Hydra
issue.
## Pipeline
Per frame, after Hydra renders linear into the draw target:
1. `m_drawTarget->Resolve()` — resolves MSAA into the color attachment (no-op
when MSAA is off), giving a single-sample linear image.
2. Blit that attachment into `m_ccLinearTex` — a separate copy is required
because sampling and writing the same texture in one pass is illegal.
3. Bind the draw-target render FBO (the MSAA FBO when multisampling) and draw
the corrected fullscreen triangle sampling `m_ccLinearTex`.
4. Overlays draw on top (uncorrected), then the final `Resolve()` in
`GetColorTextureID()` produces the texture ImGui samples.
This keeps overlays display-referred exactly as before, and the correction is
MSAA-correct (a flat fullscreen quad resolves to the same value per sample).
## Consequences
**Positive:**
- OCIO display transforms work; OCIO failures degrade to sRGB, never black.
- A color-correction failure can no longer corrupt the bind stack — the throw
hazard is gone because Hydra no longer does OCIO.
- The full transform is inspectable/loggable in our own code.
- HDR-linear intermediate enables correct tone-mapped output.
**Negative / trade-offs:**
- We reimplement and now maintain OCIO GPU plumbing (LUT upload, sampler
binding) that USD would otherwise own; OCIO major-version API changes could
require updates.
- One extra fullscreen pass, one blit, and an `RGBA16F` buffer per viewport.
- OCIO dynamic properties (exposure/gamma) and explicit look overrides are not
wired up.
- Logic lives in `UsdSceneRenderer.cpp` rather than its own module, by D5.
## Alternatives Considered
- **Keep `HdxColorCorrectionTask`, fix configuration.** Rejected: the failure
was inside the bundled USD's GPU/LUT integration, not our parameters; we
cannot patch it, and sRGB-correct/OCIO-black pointed at the hdx shader path.
- **Custom sRGB only, keep hdx for OCIO.** Rejected: OCIO is the actual
requirement; a custom sRGB-only path wouldn't address it.
- **Bake the OCIO transform to a single 3D LUT on the CPU.** Rejected: loses
HDR shaper handling and precision that `GpuShaderDesc` manages correctly.
- **New `ViewportColorCorrection.{h,cpp}` module.** Rejected for now (D5) to
avoid a CMake reconfigure / Cycles rebuild; revisit if the file grows.
## Verification
Built clean (Release) and confirmed in-app by the maintainer: OCIO produces the
expected corrected (non-black) image and sRGB is unchanged. Headless automated
verification is blocked by the stale test-target CMake config (missing USD
include dirs); restoring those targets would let an OCIO-vs-sRGB center-pixel
test assert this automatically.
+400 -3
View File
@@ -23,11 +23,21 @@
#include <pxr/base/gf/rect2i.h>
#include <pxr/base/gf/frustum.h>
#include <pxr/base/gf/rotation.h>
#include <pxr/base/tf/diagnosticMgr.h>
#include <pxr/base/tf/error.h>
#include <pxr/base/tf/warning.h>
#include <pxr/base/tf/status.h>
#include <OpenColorIO/OpenColorIO.h>
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <exception>
#include <unordered_set>
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<LutTex>* 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<LutTex> 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<ViewportColorCorrector>();
// (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<uint32_t>(att->GetGlTextureName()) : 0;
+43 -1
View File
@@ -21,9 +21,14 @@
#include <vector>
#include <string>
#include <memory>
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<ViewportColorCorrector> 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
+52 -3
View File
@@ -1,6 +1,7 @@
#include "ui/Application.h"
#include "utils/Logger.h"
#include <pxr/base/plug/registry.h>
#include <cstdio>
#include <exception>
#include <Windows.h>
#include <string>
@@ -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<std::string> checkedPaths;
for (const auto& infoPath : pluginPaths) {
// Derive the DLL path from the plugInfo.json location:
// <plugin>/resources/plugInfo.json → ../<plugin>.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<DWORD>(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<unsigned>(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);
+12
View File
@@ -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;
+12 -2
View File
@@ -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;
}
}
+131 -1
View File
@@ -1,5 +1,6 @@
#include "ViewportTile.h"
#include "../utils/Logger.h"
#include "../utils/OcioConfigParser.h"
#include <pxr/usd/usdGeom/camera.h>
#include <pxr/usd/usdGeom/xformCommonAPI.h>
@@ -99,7 +100,12 @@ ViewportTileSettings ViewportTile::GetSettings() const
s.bboxMode = static_cast<int>(m_renderer.GetBBoxMode());
s.ambientLightOnly = m_renderer.GetAmbientLightOnly();
s.domeLightEnabled = m_renderer.GetDomeLightEnabled();
s.shadingMode = static_cast<int>(m_renderer.GetShadingMode());
s.shadingMode = static_cast<int>(m_renderer.GetShadingMode());
s.colorCorrectionMode = static_cast<int>(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<ShadingMode>(s.shadingMode));
m_renderer.SetColorCorrectionMode(static_cast<ColorCorrectionMode>(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<std::string> 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();
+14 -2
View File
@@ -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;
+62
View File
@@ -0,0 +1,62 @@
#include "OcioConfigParser.h"
#include <OpenColorIO/OpenColorIO.h>
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
+22
View File
@@ -0,0 +1,22 @@
#pragma once
#include <string>
#include <vector>
#include <unordered_map>
namespace UsdLayerManager {
struct OcioConfig {
std::vector<std::string> displays;
std::unordered_map<std::string, std::vector<std::string>> views; // display → views
std::vector<std::string> colorSpaces;
std::vector<std::string> 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