From 58058a87ad36b103d725890d82e499cdef3950ac Mon Sep 17 00:00:00 2001 From: Paul Molodowitch Date: Thu, 15 Aug 2024 13:58:50 -0700 Subject: [PATCH 01/19] [hdEmbree][build_usd] add to build_usd.py status message --- build_scripts/build_usd.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build_scripts/build_usd.py b/build_scripts/build_usd.py index 6d4e8feb05..ba53958f85 100644 --- a/build_scripts/build_usd.py +++ b/build_scripts/build_usd.py @@ -2610,6 +2610,7 @@ def _JoinVersion(v): OpenVDB support: {enableOpenVDB} OpenImageIO support: {buildOIIO} OpenColorIO support: {buildOCIO} + Embree support: {buildEmbree} PRMan support: {buildPrman} UsdImaging {buildUsdImaging} usdview: {buildUsdview} @@ -2673,6 +2674,7 @@ def FormatBuildArguments(buildArgs): enableOpenVDB=("On" if context.enableOpenVDB else "Off"), buildOIIO=("On" if context.buildOIIO else "Off"), buildOCIO=("On" if context.buildOCIO else "Off"), + buildEmbree=("On" if context.buildEmbree else "Off"), buildPrman=("On" if context.buildPrman else "Off"), buildUsdImaging=("On" if context.buildUsdImaging else "Off"), buildUsdview=("On" if context.buildUsdview else "Off"), From 7341bf03299cf664d3131f066e94c652a8444e3f Mon Sep 17 00:00:00 2001 From: Paul Molodowitch Date: Tue, 22 Oct 2024 10:41:20 -0700 Subject: [PATCH 02/19] [work] fix docs for Work_NormalizeThreadCount --- pxr/base/work/threadLimits.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pxr/base/work/threadLimits.cpp b/pxr/base/work/threadLimits.cpp index a1fafdd64d..60abc34f37 100644 --- a/pxr/base/work/threadLimits.cpp +++ b/pxr/base/work/threadLimits.cpp @@ -71,14 +71,14 @@ WorkGetPhysicalConcurrencyLimit() #endif } -// This function always returns an actual thread count >= 1. +// This function always returns either 0 (meaning "no change") or >= 1 static unsigned Work_NormalizeThreadCount(const int n) { // Zero means "no change", and n >= 1 means exactly n threads, so simply // pass those values through unchanged. // For negative integers, subtract the absolute value from the total number - // of available cores (denoting all but n cores). If n == number of cores, + // of available cores (denoting all but n cores). If |n| >= number of cores, // clamp to 1 to set single-threaded mode. return n >= 0 ? n : std::max(1, n + WorkGetPhysicalConcurrencyLimit()); } From 89e974f9373c158dbdcc3ec1b89e28ba2ea709d4 Mon Sep 17 00:00:00 2001 From: Paul Molodowitch Date: Wed, 10 Jul 2024 09:41:04 -0700 Subject: [PATCH 03/19] [hdEmbree] ensure we respect PXR_WORK_THREAD_LIMIT --- pxr/imaging/plugin/hdEmbree/renderer.cpp | 84 ++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/pxr/imaging/plugin/hdEmbree/renderer.cpp b/pxr/imaging/plugin/hdEmbree/renderer.cpp index 88d5e79093..26df6d3171 100644 --- a/pxr/imaging/plugin/hdEmbree/renderer.cpp +++ b/pxr/imaging/plugin/hdEmbree/renderer.cpp @@ -22,6 +22,87 @@ #include #include +// ------------------------------------------------------------------------- +// Old TBB workaround - can remove once OneTBB is mandatory +// ------------------------------------------------------------------------- +#include + +#if TBB_INTERFACE_VERSION_MAJOR < 12 + +#include + +#include + +PXR_NAMESPACE_OPEN_SCOPE + +// PXR_WORK_THREAD_LIMIT isn't exported as part of it's api, and we're not +// part of the work library, so we can't use: +// extern TfEnvSetting PXR_WORK_THREAD_LIMIT; +extern std::variant const * +Tf_GetEnvSettingByName(std::string const&); + +PXR_NAMESPACE_CLOSE_SCOPE + +namespace { + +PXR_NAMESPACE_USING_DIRECTIVE + +// This function always returns either 0 (meaning "no change") or >= 1 +// +// Duplication of code from threadLimits.cpp - copied here to avoid having to +// change API of work lib. Will go away once we don't need to support tbb < 12 +// (ie, pre-OneTBB) +static unsigned +HdEmbree_NormalizeThreadCount(const int n) +{ + // Zero means "no change", and n >= 1 means exactly n threads, so simply + // pass those values through unchanged. + // For negative integers, subtract the absolute value from the total number + // of available cores (denoting all but n cores). If |n| >= number of cores, + // clamp to 1 to set single-threaded mode. + return n >= 0 ? n : std::max(1, n + WorkGetPhysicalConcurrencyLimit()); +} + + +// Returns the normalized thread limit value from the environment setting. Note +// that 0 means "no change", i.e. the environment setting does not apply. +// +// Duplication of code from threadLimits.cpp - copied here to avoid having to +// change API of work lib. Will go away once we don't need to support tbb < 12 +// (ie, pre-OneTBB) +static unsigned +HdEmbree_GetConcurrencyLimitSetting() +{ + std::variant const * + variantValue = Tf_GetEnvSettingByName("PXR_WORK_THREAD_LIMIT"); + int threadLimit = 0; + if (int const *value = std::get_if(variantValue)) { + threadLimit = *value; + } + return HdEmbree_NormalizeThreadCount(threadLimit); +} + + +// Make the calling context respect PXR_WORK_THREAD_LIMIT, if run from a thread +// other than the main thread (ie, the renderThread) +class _ScopedThreadScheduler { +public: + _ScopedThreadScheduler() { + auto limit = HdEmbree_GetConcurrencyLimitSetting(); + if (limit != 0) { + _tbbTaskSchedInit = + std::make_unique(limit); + } + } + + std::unique_ptr _tbbTaskSchedInit; +}; + +} // anonymous namespace + +#endif // TBB_INTERFACE_VERSION_MAJOR < 12 + + namespace { PXR_NAMESPACE_USING_DIRECTIVE @@ -453,6 +534,9 @@ HdEmbreeRenderer::Render(HdRenderThread *renderThread) // Render by scheduling square tiles of the sample buffer in a parallel // for loop. +#if TBB_INTERFACE_VERSION_MAJOR < 12 + _ScopedThreadScheduler scheduler; +#endif // Always pass the renderThread to _RenderTiles to allow the first frame // to be interrupted. WorkParallelForN(numTilesX*numTilesY, From 76c213598de0536521bcf355288b086f1b7972ce Mon Sep 17 00:00:00 2001 From: Anders Langlands Date: Fri, 1 Sep 2023 07:34:44 +1200 Subject: [PATCH 04/19] [hdEmbree] Initial UsdLux reference implementation This modifies the hdEmbree example plugin to do direct lighting so as to provide a reference implementation of the expected behaviour for UsdLux. If no lights are present in the stage, the old ambient occlusion path will be used. --- pxr/imaging/plugin/hdEmbree/CMakeLists.txt | 1 + pxr/imaging/plugin/hdEmbree/light.cpp | 171 +++++ pxr/imaging/plugin/hdEmbree/light.h | 109 +++ pxr/imaging/plugin/hdEmbree/renderBuffer.h | 2 +- .../plugin/hdEmbree/renderDelegate.cpp | 21 +- pxr/imaging/plugin/hdEmbree/renderParam.h | 8 +- pxr/imaging/plugin/hdEmbree/renderer.cpp | 694 ++++++++++++++++-- pxr/imaging/plugin/hdEmbree/renderer.h | 49 +- 8 files changed, 987 insertions(+), 68 deletions(-) create mode 100644 pxr/imaging/plugin/hdEmbree/light.cpp create mode 100644 pxr/imaging/plugin/hdEmbree/light.h diff --git a/pxr/imaging/plugin/hdEmbree/CMakeLists.txt b/pxr/imaging/plugin/hdEmbree/CMakeLists.txt index 43aa0826e5..2efef685eb 100644 --- a/pxr/imaging/plugin/hdEmbree/CMakeLists.txt +++ b/pxr/imaging/plugin/hdEmbree/CMakeLists.txt @@ -31,6 +31,7 @@ pxr_plugin(hdEmbree PUBLIC_CLASSES config instancer + light mesh meshSamplers renderBuffer diff --git a/pxr/imaging/plugin/hdEmbree/light.cpp b/pxr/imaging/plugin/hdEmbree/light.cpp new file mode 100644 index 0000000000..082e4605d2 --- /dev/null +++ b/pxr/imaging/plugin/hdEmbree/light.cpp @@ -0,0 +1,171 @@ +// +// Copyright 2024 Pixar +// +// Licensed under the terms set forth in the LICENSE.txt file available at +// https://openusd.org/license. +// +#include "pxr/imaging/plugin/hdEmbree/light.h" + +#include "light.h" +#include "pxr/imaging/plugin/hdEmbree/debugCodes.h" +#include "pxr/imaging/plugin/hdEmbree/renderParam.h" +#include "pxr/imaging/plugin/hdEmbree/renderer.h" + +#include "pxr/imaging/hd/sceneDelegate.h" +#include "pxr/imaging/hio/image.h" + +#include +#include + +#include +#include +#include + +PXR_NAMESPACE_OPEN_SCOPE + +HdEmbree_Light::HdEmbree_Light(SdfPath const& id, TfToken const& lightType) + : HdLight(id) { + if (id.IsEmpty()) { + return; + } + + // Set the variant to the right type - Sync will fill rest of data + if (lightType == HdSprimTypeTokens->cylinderLight) { + _lightData.lightVariant = HdEmbree_Cylinder(); + } else if (lightType == HdSprimTypeTokens->diskLight) { + _lightData.lightVariant = HdEmbree_Disk(); + } else if (lightType == HdSprimTypeTokens->rectLight) { + // Get shape parameters + _lightData.lightVariant = HdEmbree_Rect(); + } else if (lightType == HdSprimTypeTokens->sphereLight) { + _lightData.lightVariant = HdEmbree_Sphere(); + } else { + TF_WARN("HdEmbree - Unrecognized light type: %s", lightType.GetText()); + _lightData.lightVariant = HdEmbree_UnknownLight(); + } +} + +HdEmbree_Light::~HdEmbree_Light() = default; + +void +HdEmbree_Light::Sync(HdSceneDelegate *sceneDelegate, + HdRenderParam *renderParam, HdDirtyBits *dirtyBits) +{ + HD_TRACE_FUNCTION(); + HF_MALLOC_TAG_FUNCTION(); + + HdEmbreeRenderParam *embreeRenderParam = + static_cast(renderParam); + + // calling this bumps the scene version and causes a re-render + embreeRenderParam->AcquireSceneForEdit(); + + SdfPath const& id = GetId(); + + // Get _lightData's transform. We'll only consider the first time sample for now + HdTimeSampleArray xformSamples; + sceneDelegate->SampleTransform(id, &xformSamples); + _lightData.xformLightToWorld = GfMatrix4f(xformSamples.values[0]); + _lightData.xformWorldToLight = _lightData.xformLightToWorld.GetInverse(); + _lightData.normalXformLightToWorld = + _lightData.xformWorldToLight.ExtractRotationMatrix().GetTranspose(); + + // Store luminance parameters + _lightData.intensity = sceneDelegate->GetLightParamValue( + id, HdLightTokens->intensity).GetWithDefault(1.0f); + _lightData.exposure = sceneDelegate->GetLightParamValue( + id, HdLightTokens->exposure).GetWithDefault(0.0f); + _lightData.color = sceneDelegate->GetLightParamValue( + id, HdLightTokens->color).GetWithDefault(GfVec3f{1.0f, 1.0f, 1.0f}); + _lightData.normalize = sceneDelegate->GetLightParamValue( + id, HdLightTokens->normalize).GetWithDefault(false); + _lightData.colorTemperature = sceneDelegate->GetLightParamValue( + id, HdLightTokens->colorTemperature).GetWithDefault(6500.0f); + _lightData.enableColorTemperature = sceneDelegate->GetLightParamValue( + id, HdLightTokens->enableColorTemperature).GetWithDefault(false); + + // Get visibility + _lightData.visible = sceneDelegate->GetVisible(id); + + // Switch on the _lightData type and pull the relevant attributes from the scene + // delegate + std::visit([this, &id, &sceneDelegate](auto& typedLight) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + // Do nothing + } else if constexpr (std::is_same_v) { + typedLight = HdEmbree_Cylinder{ + sceneDelegate->GetLightParamValue(id, HdLightTokens->radius) + .GetWithDefault(0.5f), + sceneDelegate->GetLightParamValue(id, HdLightTokens->length) + .GetWithDefault(1.0f), + }; + } else if constexpr (std::is_same_v) { + typedLight = HdEmbree_Disk{ + sceneDelegate->GetLightParamValue(id, HdLightTokens->radius) + .GetWithDefault(0.5f), + }; + } else if constexpr (std::is_same_v) { + typedLight = HdEmbree_Rect{ + sceneDelegate->GetLightParamValue(id, HdLightTokens->width) + .Get(), + sceneDelegate->GetLightParamValue(id, HdLightTokens->height) + .Get(), + }; + } else if constexpr (std::is_same_v) { + typedLight = HdEmbree_Sphere{ + sceneDelegate->GetLightParamValue(id, HdLightTokens->radius) + .GetWithDefault(0.5f), + }; + } else { + static_assert(false, "non-exhaustive _LightVariant visitor"); + } + }, _lightData.lightVariant); + + if (const auto value = sceneDelegate->GetLightParamValue( + id, HdLightTokens->shapingFocus); + value.IsHolding()) { + _lightData.shaping.focus = value.UncheckedGet(); + } + + if (const auto value = sceneDelegate->GetLightParamValue( + id, HdLightTokens->shapingFocusTint); + value.IsHolding()) { + _lightData.shaping.focusTint = value.UncheckedGet(); + } + + if (const auto value = sceneDelegate->GetLightParamValue( + id, HdLightTokens->shapingConeAngle); + value.IsHolding()) { + _lightData.shaping.coneAngle = value.UncheckedGet(); + } + + if (const auto value = sceneDelegate->GetLightParamValue( + id, HdLightTokens->shapingConeSoftness); + value.IsHolding()) { + _lightData.shaping.coneSoftness = value.UncheckedGet(); + } + + HdEmbreeRenderer *renderer = embreeRenderParam->GetRenderer(); + renderer->AddLight(id, this); + + *dirtyBits &= ~HdLight::AllDirty; +} + +HdDirtyBits +HdEmbree_Light::GetInitialDirtyBitsMask() const +{ + return HdLight::AllDirty; +} + +void +HdEmbree_Light::Finalize(HdRenderParam *renderParam) +{ + auto* embreeParam = static_cast(renderParam); + + // Remove from renderer's light map + HdEmbreeRenderer *renderer = embreeParam->GetRenderer(); + renderer->RemoveLight(GetId(), this); +} + +PXR_NAMESPACE_CLOSE_SCOPE diff --git a/pxr/imaging/plugin/hdEmbree/light.h b/pxr/imaging/plugin/hdEmbree/light.h new file mode 100644 index 0000000000..906ba185bc --- /dev/null +++ b/pxr/imaging/plugin/hdEmbree/light.h @@ -0,0 +1,109 @@ +// +// Copyright 2024 Pixar +// +// Licensed under the terms set forth in the LICENSE.txt file available at +// https://openusd.org/license. +// +#ifndef PXR_IMAGING_PLUGIN_HD_EMBREE_LIGHT_H +#define PXR_IMAGING_PLUGIN_HD_EMBREE_LIGHT_H + +#include "pxr/base/gf/vec3f.h" +#include "pxr/base/gf/matrix3f.h" +#include "pxr/base/gf/matrix4f.h" +#include "pxr/imaging/hd/light.h" + +#include +#include + +#include +#include + +PXR_NAMESPACE_OPEN_SCOPE + +class HdEmbreeRenderer; + +struct HdEmbree_UnknownLight +{}; +struct HdEmbree_Cylinder +{ + float radius; + float length; +}; + +struct HdEmbree_Disk +{ + float radius; +}; + +struct HdEmbree_Rect +{ + float width; + float height; +}; + +struct HdEmbree_Sphere +{ + float radius; +}; + +using HdEmbree_LightVariant = std::variant< + HdEmbree_UnknownLight, + HdEmbree_Cylinder, + HdEmbree_Disk, + HdEmbree_Rect, + HdEmbree_Sphere>; + +struct HdEmbree_Shaping +{ + GfVec3f focusTint; + float focus = 0.0f; + float coneAngle = 180.0f; + float coneSoftness = 0.0f; +}; + +struct HdEmbree_LightData +{ + GfMatrix4f xformLightToWorld; + GfMatrix3f normalXformLightToWorld; + GfMatrix4f xformWorldToLight; + GfVec3f color; + float intensity = 1.0f; + float exposure = 0.0f; + float colorTemperature = 6500.0f; + bool enableColorTemperature = false; + HdEmbree_LightVariant lightVariant; + bool normalize = false; + bool visible = true; + HdEmbree_Shaping shaping; +}; + +class HdEmbree_Light final : public HdLight +{ +public: + HdEmbree_Light(SdfPath const& id, TfToken const& lightType); + ~HdEmbree_Light(); + + /// Synchronizes state from the delegate to this object. + void Sync(HdSceneDelegate* sceneDelegate, + HdRenderParam* renderParam, + HdDirtyBits* dirtyBits) override; + + /// Returns the minimal set of dirty bits to place in the + /// change tracker for use in the first sync of this prim. + /// Typically this would be all dirty bits. + HdDirtyBits GetInitialDirtyBitsMask() const override; + + void Finalize(HdRenderParam *renderParam) override; + + HdEmbree_LightData const& LightData() const { + return _lightData; + } + +private: + HdEmbree_LightData _lightData; +}; + + +PXR_NAMESPACE_CLOSE_SCOPE + +#endif \ No newline at end of file diff --git a/pxr/imaging/plugin/hdEmbree/renderBuffer.h b/pxr/imaging/plugin/hdEmbree/renderBuffer.h index 10c9813c8d..b3f1a38ca7 100644 --- a/pxr/imaging/plugin/hdEmbree/renderBuffer.h +++ b/pxr/imaging/plugin/hdEmbree/renderBuffer.h @@ -166,7 +166,7 @@ class HdEmbreeRenderBuffer : public HdRenderBuffer // For multisampled buffers: the input write buffer. std::vector _sampleBuffer; // For multisampled buffers: the sample count buffer. - std::vector _sampleCount; + std::vector _sampleCount; // The number of callers mapping this buffer. std::atomic _mappers; diff --git a/pxr/imaging/plugin/hdEmbree/renderDelegate.cpp b/pxr/imaging/plugin/hdEmbree/renderDelegate.cpp index 32ef99dbbb..100e9c133a 100644 --- a/pxr/imaging/plugin/hdEmbree/renderDelegate.cpp +++ b/pxr/imaging/plugin/hdEmbree/renderDelegate.cpp @@ -8,6 +8,7 @@ #include "pxr/imaging/plugin/hdEmbree/config.h" #include "pxr/imaging/plugin/hdEmbree/instancer.h" +#include "pxr/imaging/plugin/hdEmbree/light.h" #include "pxr/imaging/plugin/hdEmbree/renderParam.h" #include "pxr/imaging/plugin/hdEmbree/renderPass.h" @@ -35,6 +36,10 @@ const TfTokenVector HdEmbreeRenderDelegate::SUPPORTED_SPRIM_TYPES = { HdPrimTypeTokens->camera, HdPrimTypeTokens->extComputation, + HdPrimTypeTokens->cylinderLight, + HdPrimTypeTokens->diskLight, + HdPrimTypeTokens->rectLight, + HdPrimTypeTokens->sphereLight, }; const TfTokenVector HdEmbreeRenderDelegate::SUPPORTED_BPRIM_TYPES = @@ -147,7 +152,7 @@ HdEmbreeRenderDelegate::_Initialize() // Store top-level embree objects inside a render param that can be // passed to prims during Sync(). Also pass a handle to the render thread. _renderParam = std::make_shared( - _rtcDevice, _rtcScene, &_renderThread, &_sceneVersion); + _rtcDevice, _rtcScene, &_renderThread, &_renderer, &_sceneVersion); // Pass the scene handle to the renderer. _renderer.SetScene(_rtcScene); @@ -230,7 +235,7 @@ HdAovDescriptor HdEmbreeRenderDelegate::GetDefaultAovDescriptor(TfToken const& name) const { if (name == HdAovTokens->color) { - return HdAovDescriptor(HdFormatUNorm8Vec4, true, + return HdAovDescriptor(HdFormatFloat32Vec4, true, VtValue(GfVec4f(0.0f))); } else if (name == HdAovTokens->normal || name == HdAovTokens->Neye) { return HdAovDescriptor(HdFormatFloat32Vec3, false, @@ -331,6 +336,12 @@ HdEmbreeRenderDelegate::CreateSprim(TfToken const& typeId, return new HdCamera(sprimId); } else if (typeId == HdPrimTypeTokens->extComputation) { return new HdExtComputation(sprimId); + } else if (typeId == HdPrimTypeTokens->light || + typeId == HdPrimTypeTokens->diskLight || + typeId == HdPrimTypeTokens->rectLight || + typeId == HdPrimTypeTokens->sphereLight || + typeId == HdPrimTypeTokens->cylinderLight) { + return new HdEmbree_Light(sprimId, typeId); } else { TF_CODING_ERROR("Unknown Sprim Type %s", typeId.GetText()); } @@ -347,6 +358,12 @@ HdEmbreeRenderDelegate::CreateFallbackSprim(TfToken const& typeId) return new HdCamera(SdfPath::EmptyPath()); } else if (typeId == HdPrimTypeTokens->extComputation) { return new HdExtComputation(SdfPath::EmptyPath()); + } else if (typeId == HdPrimTypeTokens->light || + typeId == HdPrimTypeTokens->diskLight || + typeId == HdPrimTypeTokens->rectLight || + typeId == HdPrimTypeTokens->sphereLight || + typeId == HdPrimTypeTokens->cylinderLight) { + return new HdEmbree_Light(SdfPath::EmptyPath(), typeId); } else { TF_CODING_ERROR("Unknown Sprim Type %s", typeId.GetText()); } diff --git a/pxr/imaging/plugin/hdEmbree/renderParam.h b/pxr/imaging/plugin/hdEmbree/renderParam.h index 206a7458bc..e333b2dc4b 100644 --- a/pxr/imaging/plugin/hdEmbree/renderParam.h +++ b/pxr/imaging/plugin/hdEmbree/renderParam.h @@ -15,6 +15,8 @@ PXR_NAMESPACE_OPEN_SCOPE +class HdEmbreeRenderer; + /// /// \class HdEmbreeRenderParam /// @@ -27,9 +29,10 @@ class HdEmbreeRenderParam final : public HdRenderParam public: HdEmbreeRenderParam(RTCDevice device, RTCScene scene, HdRenderThread *renderThread, + HdEmbreeRenderer *renderer, std::atomic *sceneVersion) : _scene(scene), _device(device) - , _renderThread(renderThread), _sceneVersion(sceneVersion) + , _renderThread(renderThread), _renderer(renderer), _sceneVersion(sceneVersion) {} /// Accessor for the top-level embree scene. @@ -41,6 +44,8 @@ class HdEmbreeRenderParam final : public HdRenderParam /// Accessor for the top-level embree device (library handle). RTCDevice GetEmbreeDevice() { return _device; } + HdEmbreeRenderer* GetRenderer() { return _renderer; } + private: /// A handle to the top-level embree scene. RTCScene _scene; @@ -48,6 +53,7 @@ class HdEmbreeRenderParam final : public HdRenderParam RTCDevice _device; /// A handle to the global render thread. HdRenderThread *_renderThread; + HdEmbreeRenderer* _renderer; /// A version counter for edits to _scene. std::atomic *_sceneVersion; }; diff --git a/pxr/imaging/plugin/hdEmbree/renderer.cpp b/pxr/imaging/plugin/hdEmbree/renderer.cpp index 26df6d3171..47f93cf498 100644 --- a/pxr/imaging/plugin/hdEmbree/renderer.cpp +++ b/pxr/imaging/plugin/hdEmbree/renderer.cpp @@ -6,20 +6,31 @@ // #include "pxr/imaging/plugin/hdEmbree/renderer.h" -#include "pxr/imaging/plugin/hdEmbree/renderBuffer.h" #include "pxr/imaging/plugin/hdEmbree/config.h" -#include "pxr/imaging/plugin/hdEmbree/context.h" +#include "pxr/imaging/plugin/hdEmbree/light.h" #include "pxr/imaging/plugin/hdEmbree/mesh.h" +#include "pxr/imaging/plugin/hdEmbree/renderBuffer.h" #include "pxr/imaging/hd/perfLog.h" +#include "pxr/base/gf/color.h" +#include "pxr/base/gf/colorSpace.h" #include "pxr/base/gf/matrix3f.h" +#include "pxr/base/gf/range1f.h" #include "pxr/base/gf/vec2f.h" +#include "pxr/base/gf/vec3f.h" #include "pxr/base/work/loops.h" #include "pxr/base/tf/hash.h" +#include +#include +#include + +#include #include +#include +#include #include // ------------------------------------------------------------------------- @@ -63,7 +74,6 @@ HdEmbree_NormalizeThreadCount(const int n) return n >= 0 ? n : std::max(1, n + WorkGetPhysicalConcurrencyLimit()); } - // Returns the normalized thread limit value from the environment setting. Note // that 0 means "no change", i.e. the environment setting does not apply. // @@ -82,7 +92,6 @@ HdEmbree_GetConcurrencyLimitSetting() return HdEmbree_NormalizeThreadCount(threadLimit); } - // Make the calling context respect PXR_WORK_THREAD_LIMIT, if run from a thread // other than the main thread (ie, the renderThread) class _ScopedThreadScheduler { @@ -102,11 +111,118 @@ class _ScopedThreadScheduler { #endif // TBB_INTERFACE_VERSION_MAJOR < 12 - namespace { PXR_NAMESPACE_USING_DIRECTIVE +// ------------------------------------------------------------------------- +// Constants +// ------------------------------------------------------------------------- + +template +constexpr T _pi = static_cast(M_PI); + +constexpr float _rayHitContinueBias = 0.001f; + +constexpr float _minLuminanceCutoff = 1e-9f; + +constexpr GfVec3f _invalidColor = GfVec3f(-std::numeric_limits::infinity()); + +// ------------------------------------------------------------------------- +// General Math Utilities +// ------------------------------------------------------------------------- + +inline float +_Sqr(float x) +{ + return x*x; +} + +// The latitudinal polar coordinate of v, in the range [0, pi] +inline float +_Theta(GfVec3f const& v) +{ + return acosf(GfClamp(v[2], -1.0f, 1.0f)); +} + +// The longitudinal polar coordinate of v, in the range [0, 2*pi) +inline float +_Phi(GfVec3f const& v) +{ + float p = atan2f(v[1], v[0]); + return p < 0.0f ? (p + 2.0f * _pi) : p; +} + +// Dot product, but set to 0 if less than 0 - ie, 0 for backward-facing rays +inline float +_DotZeroClip(GfVec3f const& a, GfVec3f const& b) +{ + return std::max(0.0f, GfDot(a, b)); +} + +float +_Smoothstep(float t, GfRange1f range) +{ + const float length = range.GetSize(); + if (length == 0) { + if (t <= range.GetMin()) { + // Note that in the case of t == range.GetMin(), we have a + // degenerate case where there's no clear answer what the "right" + // thing to do is. + + // I arbitrarily chose 0.0 to return in this case, so at least we + // have consistent / well defined behavior; could have also done 1.0 + // or 0.5... + return 0.0; + } + return 1.0; + } + t = GfClamp((t - range.GetMin())/length, 0.0f, 1.0f); + return t * t * (3.0f - 2.0f * t); +} + +float +_AreaRect(GfMatrix4f const& xf, float width, float height) +{ + const GfVec3f U = xf.TransformDir(GfVec3f{width, 0.0f, 0.0f}); + const GfVec3f V = xf.TransformDir(GfVec3f{0.0f, height, 0.0f}); + return GfCross(U, V).GetLength(); +} + +float +_AreaSphere(GfMatrix4f const& xf, float radius) +{ + // Area of the ellipsoid + const float a = xf.TransformDir(GfVec3f{radius, 0.0f, 0.0f}).GetLength(); + const float b = xf.TransformDir(GfVec3f{0.0f, radius, 0.0f}).GetLength(); + const float c = xf.TransformDir(GfVec3f{0.0f, 0.0f, radius}).GetLength(); + const float ab = powf(a*b, 1.6f); + const float ac = powf(a*c, 1.6f); + const float bc = powf(b*c, 1.6f); + return powf((ab + ac + bc) / 3.0f, 1.0f / 1.6f) * 4.0f * _pi; +} + +float +_AreaDisk(GfMatrix4f const& xf, float radius) +{ + // Calculate surface area of the ellipse + const float a = xf.TransformDir(GfVec3f{radius, 0.0f, 0.0f}).GetLength(); + const float b = xf.TransformDir(GfVec3f{0.0f, radius, 0.0f}).GetLength(); + return _pi * a * b; +} + +float +_AreaCylinder(GfMatrix4f const& xf, float radius, float length) +{ + const float c = xf.TransformDir(GfVec3f{length, 0.0f, 0.0f}).GetLength(); + const float a = xf.TransformDir(GfVec3f{0.0f, radius, 0.0f}).GetLength(); + const float b = xf.TransformDir(GfVec3f{0.0f, 0.0f, radius}).GetLength(); + // Ramanujan's approximation to perimeter of ellipse + const float e = + _pi * (3.0f * (a + b) - sqrtf((3.0f * a + b) * (a + 3.0f * b))); + return e * c; +} + // ------------------------------------------------------------------------- // General Ray Utilities // ------------------------------------------------------------------------- @@ -119,6 +235,328 @@ _CalculateHitPosition(RTCRayHit const& rayHit) rayHit.ray.org_z + rayHit.ray.tfar * rayHit.ray.dir_z); } +// ------------------------------------------------------------------------- +// Color utilities +// ------------------------------------------------------------------------- + +const GfColorSpace _linRec709(GfColorSpaceNames->LinearRec709); +const GfColorSpace _xyzColorSpace(GfColorSpaceNames->CIEXYZ); + +// Ideally, we could could move this to GfColor::GetLuminance() +inline float +_GetLuminance(GfColor const& color) +{ + GfColor xyzColor(color, _xyzColorSpace); + // The "Y" component in XYZ space is luminance + return xyzColor.GetRGB()[1]; +} + +const GfVec3f _rec709LuminanceComponents( + _GetLuminance(GfColor(GfVec3f::XAxis(), _linRec709)), + _GetLuminance(GfColor(GfVec3f::YAxis(), _linRec709)), + _GetLuminance(GfColor(GfVec3f::ZAxis(), _linRec709))); + + +// Recreates UsdLuxBlackbodyTemperatureAsRgb in "pxr/usd/usdLux/blackbody.h"... +/// But uses new GfColor functionality, since we shouldn't import usd into +// imaging + +// Perhaps UsdLuxBlackbodyTemperatureAsRgb should be deprecated, and this made +// a new utility function somewhere, for use by other HdRenderDelegates? +// (Maybe in gf/color.h?) +inline GfVec3f +_BlackbodyTemperatureAsRgb(float kelvinColorTemp) +{ + auto tempColor = GfColor(_linRec709); + // Get color in Rec709 with luminance 1.0 + tempColor.SetFromPlanckianLocus(kelvinColorTemp, 1.0f); + // We normalize to the luminance of (1,1,1) in Rec709 + GfVec3f tempColorRGB = tempColor.GetRGB(); + float rec709Luminance = GfDot(tempColorRGB, _rec709LuminanceComponents); + return tempColorRGB / rec709Luminance; +} + +// ------------------------------------------------------------------------- +// Light sampling structures / utilities +// ------------------------------------------------------------------------- + +struct _ShapeSample { + GfVec3f pWorld; + GfVec3f nWorld; + GfVec2f uv; + float invPdfA; +}; + +struct _LightSample { + GfVec3f Li; + GfVec3f wI; + float dist; + float invPdfW; +}; + +_ShapeSample +_SampleRect(GfMatrix4f const& xf, GfMatrix3f const& normalXform, float width, + float height, float u1, float u2) +{ + // Sample rectangle in object space + const GfVec3f pLight( + (u1 - 0.5f) * width, + (u2 - 0.5f) * height, + 0.0f + ); + const GfVec3f nLight(0.0f, 0.0f, -1.0f); + const GfVec2f uv(u1, u2); + + // Transform to world space + const GfVec3f pWorld = xf.Transform(pLight); + const GfVec3f nWorld = (nLight * normalXform).GetNormalized(); + + const float area = _AreaRect(xf, width, height); + + return _ShapeSample { + pWorld, + nWorld, + uv, + area + }; +} + +_ShapeSample +_SampleSphere(GfMatrix4f const& xf, GfMatrix3f const& normalXform, float radius, + float u1, float u2) +{ + // Sample sphere in light space + const float z = 1.0 - 2.0 * u1; + const float r = sqrtf(std::max(0.0f, 1.0f - z*z)); + const float phi = 2.0f * _pi * u2; + GfVec3f pLight{r * std::cos(phi), r * std::sin(phi), z}; + const GfVec3f nLight = pLight; + pLight *= radius; + const GfVec2f uv(u2, z); + + // Transform to world space + const GfVec3f pWorld = xf.Transform(pLight); + const GfVec3f nWorld = (nLight * normalXform).GetNormalized(); + + const float area = _AreaSphere(xf, radius); + + return _ShapeSample { + pWorld, + nWorld, + uv, + area + }; +} + +GfVec3f +_SampleDiskPolar(float u1, float u2) +{ + const float r = sqrtf(u1); + const float theta = 2.0f * _pi * u2; + return GfVec3f(r * cosf(theta), r * sinf(theta), 0.0f); +} + +_ShapeSample +_SampleDisk(GfMatrix4f const& xf, GfMatrix3f const& normalXform, float radius, + float u1, float u2) +{ + // Sample disk in light space + GfVec3f pLight = _SampleDiskPolar(u1, u2); + const GfVec3f nLight(0.0f, 0.0f, -1.0f); + const GfVec2f uv(pLight[0], pLight[1]); + pLight *= radius; + + // Transform to world space + const GfVec3f pWorld = xf.Transform(pLight); + const GfVec3f nWorld = (nLight * normalXform).GetNormalized(); + + const float area = _AreaDisk(xf, radius); + + return _ShapeSample { + pWorld, + nWorld, + uv, + area + }; +} + +_ShapeSample +_SampleCylinder(GfMatrix4f const& xf, GfMatrix3f const& normalXform, + float radius,float length, float u1, float u2) { + float z = GfLerp(u1, -length/2.0f, length/2.0f); + float phi = u2 * 2.0f * _pi; + // Compute cylinder sample position _pi_ and normal _n_ from $z$ and $\phi$ + GfVec3f pLight = GfVec3f(z, radius * cosf(phi), radius * sinf(phi)); + // Reproject _pObj_ to cylinder surface and compute _pObjError_ + float hitRad = sqrtf(_Sqr(pLight[1]) + _Sqr(pLight[2])); + pLight[1] *= radius / hitRad; + pLight[2] *= radius / hitRad; + + GfVec3f nLight(0.0f, pLight[1], pLight[2]); + nLight.Normalize(); + + // Transform to world space + const GfVec3f pWorld = xf.Transform(pLight); + const GfVec3f nWorld = (nLight * normalXform).GetNormalized(); + + const float area = _AreaCylinder(xf, radius, length); + + return _ShapeSample { + pWorld, + nWorld, + GfVec2f(u2, u1), + area + }; +} + +GfVec3f +_EvalLightBasic(HdEmbree_LightData const& light) +{ + GfVec3f Le = light.color * light.intensity * powf(2.0f, light.exposure); + if (light.enableColorTemperature) { + Le = GfCompMult(Le, + _BlackbodyTemperatureAsRgb(light.colorTemperature)); + } + return Le; +} + +_LightSample +_EvalAreaLight(HdEmbree_LightData const& light, _ShapeSample const& ss, + GfVec3f const& position) +{ + // Transform PDF from area measure to solid angle measure. We use the + // inverse PDF here to avoid division by zero when the surface point is + // behind the light + GfVec3f wI = ss.pWorld - position; + const float dist = wI.GetLength(); + wI /= dist; + const float cosThetaOffNormal = _DotZeroClip(-wI, ss.nWorld); + float invPdfW = cosThetaOffNormal / _Sqr(dist) * ss.invPdfA; + GfVec3f lightNegZ = -light.xformLightToWorld.GetRow3(2).GetNormalized(); + const float cosThetaOffZ = GfDot(-wI, lightNegZ); + + // Combine the brightness parameters to get initial emission luminance + // (nits) + GfVec3f Le = cosThetaOffNormal > 0.0f ? + _EvalLightBasic(light) + : GfVec3f(0.0f); + + // If normalize is enabled, we need to divide the luminance by the surface + // area of the light, which for an area light is equivalent to multiplying + // by the area pdf, which is itself the reciprocal of the surface area + if (light.normalize && ss.invPdfA != 0) { + Le /= ss.invPdfA; + } + + // Apply focus shaping + if (light.shaping.focus > 0.0f) { + const float ff = powf(GfAbs(cosThetaOffZ), light.shaping.focus); + const GfVec3f focusTint = GfLerp(ff, light.shaping.focusTint, + GfVec3f(1.0f)); + Le = GfCompMult(Le, focusTint); + } + + // Apply cone shaping + const float thetaCone = GfDegreesToRadians(light.shaping.coneAngle); + const float thetaSoft = GfLerp(light.shaping.coneSoftness, thetaCone, 0.0f); + const float thetaOffZ = acosf(cosThetaOffZ); + Le *= 1.0f - _Smoothstep(thetaOffZ, GfRange1f(thetaSoft, thetaCone)); + + return _LightSample { + Le, + wI, + dist, + invPdfW + }; +} + +class _LightSampler { +public: + static _LightSample GetLightSample(HdEmbree_LightData const& lightData, + GfVec3f const& hitPosition, + GfVec3f const& normal, + float u1, + float u2) + { + _LightSampler lightSampler(lightData, hitPosition, normal, u1, u2); + return std::visit(lightSampler, lightData.lightVariant); + } + + // callables to be used with std::visit + _LightSample operator()(HdEmbree_UnknownLight const& rect) { + // Could warn, but we should have already warned when lightVariant + // first created / set to HdEmbree_UnknownLight... and warning here + // could result in a LOT of spam + return _LightSample { + GfVec3f(0.0f), + GfVec3f(0.0f), + 0.0f, + 0.0f, + }; + } + + _LightSample operator()(HdEmbree_Rect const& rect) { + _ShapeSample shapeSample = _SampleRect( + _lightData.xformLightToWorld, + _lightData.normalXformLightToWorld, + rect.width, + rect.height, + _u1, + _u2); + return _EvalAreaLight(_lightData, shapeSample, _hitPosition); + } + + _LightSample operator()(HdEmbree_Sphere const& sphere) { + _ShapeSample shapeSample = _SampleSphere( + _lightData.xformLightToWorld, + _lightData.normalXformLightToWorld, + sphere.radius, + _u1, + _u2); + return _EvalAreaLight(_lightData, shapeSample, _hitPosition); + } + + _LightSample operator()(HdEmbree_Disk const& disk) { + _ShapeSample shapeSample = _SampleDisk( + _lightData.xformLightToWorld, + _lightData.normalXformLightToWorld, + disk.radius, + _u1, + _u2); + return _EvalAreaLight(_lightData, shapeSample, _hitPosition); + } + + _LightSample operator()(HdEmbree_Cylinder const& cylinder) { + _ShapeSample shapeSample = _SampleCylinder( + _lightData.xformLightToWorld, + _lightData.normalXformLightToWorld, + cylinder.radius, + cylinder.length, + _u1, + _u2); + return _EvalAreaLight(_lightData, shapeSample, _hitPosition); + } + +private: + _LightSampler(HdEmbree_LightData const& lightData, + GfVec3f const& hitPosition, + GfVec3f const& normal, + float u1, + float u2) : + _lightData(lightData), + _hitPosition(hitPosition), + _normal(normal), + _u1(u1), + _u2(u2) + {} + + HdEmbree_LightData const& _lightData; + GfVec3f const& _hitPosition; + GfVec3f const& _normal; + float _u1; + float _u2; +}; + } // anonymous namespace PXR_NAMESPACE_OPEN_SCOPE @@ -211,6 +649,22 @@ HdEmbreeRenderer::SetAovBindings( _aovBindingsNeedValidation = true; } + +void +HdEmbreeRenderer::AddLight(SdfPath const& lightPath, + HdEmbree_Light* light) +{ + ScopedLock lightsWriteLock(_lightsWriteMutex); + _lightMap[lightPath] = light; +} + +void +HdEmbreeRenderer::RemoveLight(SdfPath const& lightPath, HdEmbree_Light* light) +{ + ScopedLock lightsWriteLock(_lightsWriteMutex); + _lightMap.erase(lightPath); +} + bool HdEmbreeRenderer::_ValidateAovBindings() { @@ -452,7 +906,7 @@ _IsContained(const GfRect2i &rect, int width, int height) } void -HdEmbreeRenderer::Render(HdRenderThread *renderThread) +HdEmbreeRenderer::_PreRenderSetup() { _completedSamples.store(0); @@ -503,9 +957,14 @@ HdEmbreeRenderer::Render(HdRenderThread *renderThread) if (!_IsContained(_dataWindow, _width, _height)) { TF_CODING_ERROR( "dataWindow is larger than render buffer"); - } } +} + +void +HdEmbreeRenderer::Render(HdRenderThread *renderThread) +{ + _PreRenderSetup(); // Render the image. Each pass through the loop adds a sample per pixel // (with jittered ray direction); the longer the loop runs, the less noisy @@ -617,7 +1076,9 @@ HdEmbreeRenderer::_RenderTiles(HdRenderThread *renderThread, int sampleNum, // Create a uniform distribution for jitter calculations. std::uniform_real_distribution uniform_dist(0.0f, 1.0f); - auto uniform_float = [&random, &uniform_dist]() { return uniform_dist(random); }; + auto uniform_float = [&random, &uniform_dist]() { + return uniform_dist(random); + }; // _RenderTiles gets a range of tiles; iterate through them. for (unsigned int tile = tileStart; tile < tileEnd; ++tile) { @@ -641,7 +1102,6 @@ HdEmbreeRenderer::_RenderTiles(HdRenderThread *renderThread, int sampleNum, // Loop over pixels casting rays. for (unsigned int y = y0; y < y1; ++y) { for (unsigned int x = x0; x < x1; ++x) { - // Jitter the camera ray direction. GfVec2f jitter(0.0f, 0.0f); if (HdEmbreeConfig::GetInstance().jitterCamera) { @@ -655,25 +1115,25 @@ HdEmbreeRenderer::_RenderTiles(HdRenderThread *renderThread, int sampleNum, const float h(_dataWindow.GetHeight()); const GfVec3f ndc( - 2 * ((x + jitter[0] - minX) / w) - 1, - 2 * ((y + jitter[1] - minY) / h) - 1, - -1); + 2.0f * ((x + jitter[0] - minX) / w) - 1.0f, + 2.0f * ((y + jitter[1] - minY) / h) - 1.0f, + -1.0f); const GfVec3f nearPlaneTrace(_inverseProjMatrix.Transform(ndc)); GfVec3f origin; GfVec3f dir; - const bool isOrthographic = round(_projMatrix[3][3]) == 1; + const bool isOrthographic = round(_projMatrix[3][3]) == 1.0; if (isOrthographic) { // During orthographic projection: trace parallel rays // from the near plane trace. origin = nearPlaneTrace; - dir = GfVec3f(0,0,-1); + dir = GfVec3f(0.0f, 0.0f, -1.0f); } else { // Otherwise, assume this is a perspective projection; // project from the camera origin through the // near plane trace. - origin = GfVec3f(0,0,0); + origin = GfVec3f(0.0f, 0.0f, 0.0f); dir = nearPlaneTrace; } // Transform camera rays to world space. @@ -691,7 +1151,9 @@ HdEmbreeRenderer::_RenderTiles(HdRenderThread *renderThread, int sampleNum, /// Fill in an RTCRay structure from the given parameters. static void _PopulateRay(RTCRay *ray, GfVec3f const& origin, - GfVec3f const& dir, float nearest) + GfVec3f const& dir, float nearest, + float furthest = std::numeric_limits::infinity(), + HdEmbree_RayMask mask = HdEmbree_RayMask::All) { ray->org_x = origin[0]; ray->org_y = origin[1]; @@ -703,25 +1165,26 @@ _PopulateRay(RTCRay *ray, GfVec3f const& origin, ray->dir_z = dir[2]; ray->time = 0.0f; - ray->tfar = std::numeric_limits::infinity(); - ray->mask = -1; + ray->tfar = furthest; + ray->mask = static_cast(mask); } /// Fill in an RTCRayHit structure from the given parameters. // note this containts a Ray and a RayHit static void _PopulateRayHit(RTCRayHit* rayHit, GfVec3f const& origin, - GfVec3f const& dir, float nearest) + GfVec3f const& dir, float nearest, + float furthest = std::numeric_limits::infinity(), + HdEmbree_RayMask mask = HdEmbree_RayMask::All) { // Fill in defaults for the ray - _PopulateRay(&rayHit->ray, origin, dir, nearest); + _PopulateRay(&rayHit->ray, origin, dir, nearest, furthest, mask); // Fill in defaults for the hit rayHit->hit.primID = RTC_INVALID_GEOMETRY_ID; rayHit->hit.geomID = RTC_INVALID_GEOMETRY_ID; } - /// Generate a random cosine-weighted direction ray (in the hemisphere /// around <0,0,1>). The input is a pair of uniformly distributed random /// numbers in the range [0,1]. @@ -732,7 +1195,7 @@ static GfVec3f _CosineWeightedDirection(GfVec2f const& uniform_float) { GfVec3f dir; - float theta = 2.0f * M_PI * uniform_float[0]; + float theta = 2.0f * _pi * uniform_float[0]; float eta = uniform_float[1]; float sqrteta = sqrtf(eta); dir[0] = cosf(theta) * sqrteta; @@ -749,7 +1212,9 @@ HdEmbreeRenderer::_TraceRay(unsigned int x, unsigned int y, // Intersect the camera ray. RTCRayHit rayHit; // EMBREE_FIXME: use RTCRay for occlusion rays rayHit.ray.flags = 0; - _PopulateRayHit(&rayHit, origin, dir, 0.0f); + _PopulateRayHit(&rayHit, origin, dir, 0.0f, + std::numeric_limits::max(), + HdEmbree_RayMask::Camera); { RTCIntersectContext context; rtcInitIntersectContext(&context); @@ -830,11 +1295,13 @@ HdEmbreeRenderer::_ComputeId(RTCRayHit const& rayHit, TfToken const& idType, // flatten everything in hydra. So instID[0] should always be correct. const HdEmbreeInstanceContext *instanceContext = static_cast( - rtcGetGeometryUserData(rtcGetGeometry(_scene, rayHit.hit.instID[0]))); + rtcGetGeometryUserData(rtcGetGeometry(_scene, + rayHit.hit.instID[0]))); const HdEmbreePrototypeContext *prototypeContext = static_cast( - rtcGetGeometryUserData(rtcGetGeometry(instanceContext->rootScene,rayHit.hit.geomID))); + rtcGetGeometryUserData(rtcGetGeometry(instanceContext->rootScene, + rayHit.hit.geomID))); if (idType == HdAovTokens->primId) { *id = prototypeContext->rprim->GetPrimId(); @@ -890,11 +1357,14 @@ HdEmbreeRenderer::_ComputeNormal(RTCRayHit const& rayHit, // flatten everything in hydra. So instID[0] should always be correct. const HdEmbreeInstanceContext *instanceContext = static_cast( - rtcGetGeometryUserData(rtcGetGeometry(_scene,rayHit.hit.instID[0]))); + rtcGetGeometryUserData(rtcGetGeometry(_scene, + rayHit.hit.instID[0]))); const HdEmbreePrototypeContext *prototypeContext = static_cast( - rtcGetGeometryUserData(rtcGetGeometry(instanceContext->rootScene,rayHit.hit.geomID))); + rtcGetGeometryUserData( + rtcGetGeometry(instanceContext->rootScene, + rayHit.hit.geomID))); GfVec3f n = -GfVec3f(rayHit.hit.Ng_x, rayHit.hit.Ng_y, rayHit.hit.Ng_z); auto it = prototypeContext->primvarMap.find(HdTokens->normals); @@ -925,27 +1395,33 @@ HdEmbreeRenderer::_ComputePrimvar(RTCRayHit const& rayHit, // flatten everything in hydra. So instID[0] should always be correct. const HdEmbreeInstanceContext *instanceContext = static_cast( - rtcGetGeometryUserData(rtcGetGeometry(_scene,rayHit.hit.instID[0]))); + rtcGetGeometryUserData(rtcGetGeometry(_scene, + rayHit.hit.instID[0]))); const HdEmbreePrototypeContext *prototypeContext = static_cast( - rtcGetGeometryUserData(rtcGetGeometry(instanceContext->rootScene,rayHit.hit.geomID))); + rtcGetGeometryUserData( + rtcGetGeometry(instanceContext->rootScene, + rayHit.hit.geomID))); // XXX: This is a little clunky, although sample will early out if the // types don't match. auto it = prototypeContext->primvarMap.find(primvar); if (it != prototypeContext->primvarMap.end()) { const HdEmbreePrimvarSampler *sampler = it->second; - if (sampler->Sample(rayHit.hit.primID, rayHit.hit.u, rayHit.hit.v, value)) { + if (sampler->Sample(rayHit.hit.primID, rayHit.hit.u, rayHit.hit.v, + value)) { return true; } GfVec2f v2; - if (sampler->Sample(rayHit.hit.primID, rayHit.hit.u, rayHit.hit.v, &v2)) { + if (sampler->Sample(rayHit.hit.primID, rayHit.hit.u, rayHit.hit.v, + &v2)) { value->Set(v2[0], v2[1], 0.0f); return true; } float v1; - if (sampler->Sample(rayHit.hit.primID, rayHit.hit.u, rayHit.hit.v, &v1)) { + if (sampler->Sample(rayHit.hit.primID, rayHit.hit.u, rayHit.hit.v, + &v1)) { value->Set(v1, 0.0f, 0.0f); return true; } @@ -953,32 +1429,52 @@ HdEmbreeRenderer::_ComputePrimvar(RTCRayHit const& rayHit, return false; } +float +HdEmbreeRenderer::_Visibility( + GfVec3f const& position, GfVec3f const& direction, float dist) const +{ + RTCRay shadow; + shadow.flags = 0; + _PopulateRay(&shadow, position, direction, 0.001f, dist, + HdEmbree_RayMask::Shadow); + { + RTCIntersectContext context; + rtcInitIntersectContext(&context); + rtcOccluded1(_scene,&context,&shadow); + } + // XXX: what do we do about shadow visibility (continuation) here? + // probably need to use rtcIntersect instead of rtcOccluded + + // occluded sets tfar < 0 if the ray hit anything + return shadow.tfar > 0.0f; +} + GfVec4f HdEmbreeRenderer::_ComputeColor(RTCRayHit const& rayHit, std::default_random_engine &random, GfVec4f const& clearColor) { - if (rayHit.hit.geomID == RTC_INVALID_GEOMETRY_ID) { - return clearColor; - } - // Get the instance and prototype context structures for the hit prim. // We don't use embree's multi-level instancing; we // flatten everything in hydra. So instID[0] should always be correct. const HdEmbreeInstanceContext *instanceContext = static_cast( - rtcGetGeometryUserData(rtcGetGeometry(_scene,rayHit.hit.instID[0]))); + rtcGetGeometryUserData(rtcGetGeometry(_scene, + rayHit.hit.instID[0]))); const HdEmbreePrototypeContext *prototypeContext = static_cast( - rtcGetGeometryUserData(rtcGetGeometry(instanceContext->rootScene,rayHit.hit.geomID))); + rtcGetGeometryUserData( + rtcGetGeometry(instanceContext->rootScene, + rayHit.hit.geomID))); // Compute the worldspace location of the rayHit hit. GfVec3f hitPos = _CalculateHitPosition(rayHit); // If a normal primvar is present (e.g. from smooth shading), use that // for shading; otherwise use the flat face normal. - GfVec3f normal = -GfVec3f(rayHit.hit.Ng_x, rayHit.hit.Ng_y, rayHit.hit.Ng_z); + GfVec3f normal = -GfVec3f(rayHit.hit.Ng_x, rayHit.hit.Ng_y, + rayHit.hit.Ng_z); auto it = prototypeContext->primvarMap.find(HdTokens->normals); if (it != prototypeContext->primvarMap.end()) { it->second->Sample( @@ -987,12 +1483,12 @@ HdEmbreeRenderer::_ComputeColor(RTCRayHit const& rayHit, // If a color primvar is present, use that as diffuse color; otherwise, // use flat grey. - GfVec3f color = GfVec3f(0.5f, 0.5f, 0.5f); + GfVec3f materialColor = _invalidColor; if (_enableSceneColors) { auto it = prototypeContext->primvarMap.find(HdTokens->displayColor); if (it != prototypeContext->primvarMap.end()) { it->second->Sample( - rayHit.hit.primID, rayHit.hit.u, rayHit.hit.v, &color); + rayHit.hit.primID, rayHit.hit.u, rayHit.hit.v, &materialColor); } } @@ -1002,38 +1498,63 @@ HdEmbreeRenderer::_ComputeColor(RTCRayHit const& rayHit, // Make sure the normal is unit-length. normal.Normalize(); - // Lighting model: (camera dot normal), i.e. diffuse-only point light - // centered on the camera. - GfVec3f dir = GfVec3f(rayHit.ray.dir_x, rayHit.ray.dir_y, rayHit.ray.dir_z); - float diffuseLight = fabs(GfDot(-dir, normal)) * - HdEmbreeConfig::GetInstance().cameraLightIntensity; + GfVec3f lightingColor(0.0f); - // Lighting gets modulated by an ambient occlusion term. - float aoLightIntensity = - _ComputeAmbientOcclusion(hitPos, normal, random); + // If there are no lights, then keep the existing camera light + AO path to + // be able to inspect the scene + if (_lightMap.empty()) + { + // For ambient occlusion, default material is flat 50% gray + if (materialColor == _invalidColor) { + materialColor = GfVec3f(.5f); + } + + // Lighting model: (camera dot normal), i.e. diffuse-only point light + // centered on the camera. + GfVec3f dir = GfVec3f(rayHit.ray.dir_x, rayHit.ray.dir_y, + rayHit.ray.dir_z); + float diffuseLight = fabs(GfDot(-dir, normal)) * + HdEmbreeConfig::GetInstance().cameraLightIntensity; - // XXX: We should support opacity here... + // Lighting gets modulated by an ambient occlusion term. + float aoLightIntensity = + _ComputeAmbientOcclusion(hitPos, normal, random); - // Return color * diffuseLight * aoLightIntensity. - GfVec3f finalColor = color * diffuseLight * aoLightIntensity; + // XXX: We should support opacity here... - // Clamp colors to [0,1]. + lightingColor = GfVec3f(diffuseLight * aoLightIntensity); + } + else + { + // For lighting, default material is 100% white + if (materialColor == _invalidColor) { + materialColor = GfVec3f(1.0f); + } + + lightingColor = _ComputeLighting( + hitPos, normal,random, prototypeContext); + } + const GfVec3f finalColor = GfCompMult(materialColor, lightingColor); + + // Clamp colors to > 0 GfVec4f output; - output[0] = std::max(0.0f, std::min(1.0f, finalColor[0])); - output[1] = std::max(0.0f, std::min(1.0f, finalColor[1])); - output[2] = std::max(0.0f, std::min(1.0f, finalColor[2])); + output[0] = std::max(0.0f, finalColor[0]); + output[1] = std::max(0.0f, finalColor[1]); + output[2] = std::max(0.0f, finalColor[2]); output[3] = 1.0f; return output; } float HdEmbreeRenderer::_ComputeAmbientOcclusion(GfVec3f const& position, - GfVec3f const& normal, - std::default_random_engine &random) + GfVec3f const& normal, + std::default_random_engine &random) { // Create a uniform random distribution for AO calculations. std::uniform_real_distribution uniform_dist(0.0f, 1.0f); - auto uniform_float = [&random, &uniform_dist]() { return uniform_dist(random); }; + auto uniform_float = [&random, &uniform_dist]() { + return uniform_dist(random); + }; // 0 ambient occlusion samples means disable the ambient occlusion term. if (_ambientOcclusionSamples < 1) { @@ -1046,12 +1567,12 @@ HdEmbreeRenderer::_ComputeAmbientOcclusion(GfVec3f const& position, // point. For the purposes of _CosineWeightedDirection, the normal needs // to map to (0,0,1), but since the distribution is radially symmetric // we don't care about the other axes. - GfMatrix3f basis(1); + GfMatrix3f basis(1.0f); GfVec3f xAxis; - if (fabsf(GfDot(normal, GfVec3f(0,0,1))) < 0.9f) { - xAxis = GfCross(normal, GfVec3f(0,0,1)); + if (fabsf(GfDot(normal, GfVec3f(0.0f,0.0f,1.0f))) < 0.9f) { + xAxis = GfCross(normal, GfVec3f(0.0f,0.0f,1.0f)); } else { - xAxis = GfCross(normal, GfVec3f(0,1,0)); + xAxis = GfCross(normal, GfVec3f(0.0f,1.0f,0.0f)); } GfVec3f yAxis = GfCross(normal, xAxis); basis.SetColumn(0, xAxis.GetNormalized()); @@ -1108,4 +1629,51 @@ HdEmbreeRenderer::_ComputeAmbientOcclusion(GfVec3f const& position, return occlusionFactor; } +GfVec3f +HdEmbreeRenderer::_ComputeLighting( + GfVec3f const& position, + GfVec3f const& normal, + std::default_random_engine &random, + HdEmbreePrototypeContext const* prototypeContext) const +{ + std::uniform_real_distribution uniform_dist(0.0f, 1.0f); + auto uniform_float = [&random, &uniform_dist]() { + return uniform_dist(random); + }; + + GfVec3f finalColor(0.0f); + // For now just a 100% reflective diffuse BRDF + float brdf = 1.0f / _pi; + + // For now just iterate over all lights + /// XXX: simple uniform sampling may be better here + for (auto const& it : _lightMap) + { + auto const& light = it.second->LightData(); + // Skip light if it's hidden + if (!light.visible) + { + continue; + } + + // Sample the light + _LightSample ls = _LightSampler::GetLightSample( + light, position, normal, uniform_float(), uniform_float()); + if (GfIsClose(ls.Li, GfVec3f(0.0f), _minLuminanceCutoff)) { + continue; + } + + // Trace shadow + float vis = _Visibility(position, ls.wI, ls.dist * 0.99f); + + // Add exitant luminance + finalColor += ls.Li + * _DotZeroClip(ls.wI, normal) + * brdf + * vis + * ls.invPdfW; + } + return finalColor; +} + PXR_NAMESPACE_CLOSE_SCOPE diff --git a/pxr/imaging/plugin/hdEmbree/renderer.h b/pxr/imaging/plugin/hdEmbree/renderer.h index 2da9880848..85a44ad4b2 100644 --- a/pxr/imaging/plugin/hdEmbree/renderer.h +++ b/pxr/imaging/plugin/hdEmbree/renderer.h @@ -9,20 +9,35 @@ #include "pxr/pxr.h" +#include "pxr/imaging/plugin/hdEmbree/context.h" +#include "pxr/imaging/plugin/hdEmbree/light.h" + +#include "pxr/imaging/hd/aov.h" #include "pxr/imaging/hd/renderThread.h" -#include "pxr/imaging/hd/renderPassState.h" #include "pxr/base/gf/matrix4d.h" #include "pxr/base/gf/rect2i.h" #include +#include #include #include #include +#include +#include PXR_NAMESPACE_OPEN_SCOPE +enum HdEmbree_RayMask: uint32_t { + None = 0, + + Camera = 1 << 0, + Shadow = 1 << 1, + + All = UINT_MAX, +}; + /// \class HdEmbreeRenderer /// /// HdEmbreeRenderer implements a renderer on top of Embree's raycasting @@ -37,6 +52,9 @@ PXR_NAMESPACE_OPEN_SCOPE class HdEmbreeRenderer final { public: + using WriteMutex = std::mutex; + using ScopedLock = std::scoped_lock; + /// Renderer constructor. HdEmbreeRenderer(); @@ -60,6 +78,12 @@ class HdEmbreeRenderer final /// \param aovBindings A list of aov bindings. void SetAovBindings(HdRenderPassAovBindingVector const &aovBindings); + /// Add a light + void AddLight(SdfPath const& lightPath, HdEmbree_Light* light); + + /// Remove a light + void RemoveLight(SdfPath const& lightPath, HdEmbree_Light* light); + /// Get the aov bindings being used for rendering. /// \return the current aov bindings. HdRenderPassAovBindingVector const& GetAovBindings() const { @@ -104,6 +128,9 @@ class HdEmbreeRenderer final int GetCompletedSamples() const; private: + // Perform validation and setup immediately before starting a render + void _PreRenderSetup(); + // Validate the internal consistency of aov bindings provided to // SetAovBindings. If the aov bindings are invalid, this will issue // appropriate warnings. If the function returns false, Render() will fail @@ -154,6 +181,22 @@ class HdEmbreeRenderer final GfVec3f const& normal, std::default_random_engine &random); + ///If the scene has lights, sample them to return the color at a given + ///position + GfVec3f _ComputeLighting( + GfVec3f const& position, + GfVec3f const& normal, + std::default_random_engine &random, + HdEmbreePrototypeContext const* prototypeContext) const; + + // Return the visibility from `position` along `direction` + float _Visibility(GfVec3f const& position, + GfVec3f const& direction, + float offset = 1.0e-3f) const; + + // Should the ray continue based on the possibly intersected prim's visibility settings? + bool _RayShouldContinue(RTCRayHit const& rayHit) const; + // The bound aovs for this renderer. HdRenderPassAovBindingVector _aovBindings; // Parsed AOV name tokens. @@ -195,6 +238,10 @@ class HdEmbreeRenderer final // How many samples have been completed. std::atomic _completedSamples; + + // Lights + mutable WriteMutex _lightsWriteMutex; // protects the 2 below + std::map _lightMap; }; PXR_NAMESPACE_CLOSE_SCOPE From 6e232d7aaa1e99d96905105f03a72ddc2fc35538 Mon Sep 17 00:00:00 2001 From: Paul Molodowitch Date: Mon, 29 Jul 2024 01:08:53 -0700 Subject: [PATCH 05/19] [hdEmbree] add HDEMBREE_LIGHT_CREATE debug code --- pxr/imaging/plugin/hdEmbree/CMakeLists.txt | 1 + pxr/imaging/plugin/hdEmbree/debugCodes.cpp | 20 ++++++++++++++++++++ pxr/imaging/plugin/hdEmbree/debugCodes.h | 21 +++++++++++++++++++++ pxr/imaging/plugin/hdEmbree/light.cpp | 2 ++ 4 files changed, 44 insertions(+) create mode 100644 pxr/imaging/plugin/hdEmbree/debugCodes.cpp create mode 100644 pxr/imaging/plugin/hdEmbree/debugCodes.h diff --git a/pxr/imaging/plugin/hdEmbree/CMakeLists.txt b/pxr/imaging/plugin/hdEmbree/CMakeLists.txt index 2efef685eb..c3c92aa552 100644 --- a/pxr/imaging/plugin/hdEmbree/CMakeLists.txt +++ b/pxr/imaging/plugin/hdEmbree/CMakeLists.txt @@ -46,6 +46,7 @@ pxr_plugin(hdEmbree renderParam.h PRIVATE_CLASSES + debugCodes implicitSurfaceSceneIndexPlugin RESOURCE_FILES diff --git a/pxr/imaging/plugin/hdEmbree/debugCodes.cpp b/pxr/imaging/plugin/hdEmbree/debugCodes.cpp new file mode 100644 index 0000000000..e38f776489 --- /dev/null +++ b/pxr/imaging/plugin/hdEmbree/debugCodes.cpp @@ -0,0 +1,20 @@ +// +// Copyright 2024 Pixar +// +// Licensed under the terms set forth in the LICENSE.txt file available at +// https://openusd.org/license. +// +#include "pxr/pxr.h" +#include "pxr/imaging/plugin/hdEmbree/debugCodes.h" + +#include "pxr/base/tf/debug.h" +#include "pxr/base/tf/registryManager.h" + +PXR_NAMESPACE_OPEN_SCOPE + +TF_REGISTRY_FUNCTION(TfDebug) +{ + TF_DEBUG_ENVIRONMENT_SYMBOL(HDEMBREE_LIGHT_CREATE, "Creation of HdEmbree lights"); +} + +PXR_NAMESPACE_CLOSE_SCOPE diff --git a/pxr/imaging/plugin/hdEmbree/debugCodes.h b/pxr/imaging/plugin/hdEmbree/debugCodes.h new file mode 100644 index 0000000000..c65002452b --- /dev/null +++ b/pxr/imaging/plugin/hdEmbree/debugCodes.h @@ -0,0 +1,21 @@ +// +// Copyright 2024 Pixar +// +// Licensed under the terms set forth in the LICENSE.txt file available at +// https://openusd.org/license. +// +#ifndef PXR_IMAGING_PLUGIN_HD_EMBREE_DEBUG_CODES_H +#define PXR_IMAGING_PLUGIN_HD_EMBREE_DEBUG_CODES_H + +#include "pxr/pxr.h" +#include "pxr/base/tf/debug.h" + +PXR_NAMESPACE_OPEN_SCOPE + +TF_DEBUG_CODES( + HDEMBREE_LIGHT_CREATE +); + +PXR_NAMESPACE_CLOSE_SCOPE + +#endif // PXR_IMAGING_PLUGIN_HD_EMBREE_DEBUG_CODES_H diff --git a/pxr/imaging/plugin/hdEmbree/light.cpp b/pxr/imaging/plugin/hdEmbree/light.cpp index 082e4605d2..b1555a5ba2 100644 --- a/pxr/imaging/plugin/hdEmbree/light.cpp +++ b/pxr/imaging/plugin/hdEmbree/light.cpp @@ -29,6 +29,8 @@ HdEmbree_Light::HdEmbree_Light(SdfPath const& id, TfToken const& lightType) return; } + TF_DEBUG(HDEMBREE_LIGHT_CREATE).Msg("Creating light %s: %s\n", id.GetText(), lightType.GetText()); + // Set the variant to the right type - Sync will fill rest of data if (lightType == HdSprimTypeTokens->cylinderLight) { _lightData.lightVariant = HdEmbree_Cylinder(); From 035a0d1cbad721c18f571fb02f80f078284b3f0f Mon Sep 17 00:00:00 2001 From: Paul Molodowitch Date: Mon, 22 Jul 2024 05:16:48 -0700 Subject: [PATCH 06/19] [hdEmbree] add support for lighting double-sided meshes --- pxr/imaging/plugin/hdEmbree/mesh.h | 5 +++++ pxr/imaging/plugin/hdEmbree/renderer.cpp | 17 ++++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/pxr/imaging/plugin/hdEmbree/mesh.h b/pxr/imaging/plugin/hdEmbree/mesh.h index bbb006302f..2d1ff9a257 100644 --- a/pxr/imaging/plugin/hdEmbree/mesh.h +++ b/pxr/imaging/plugin/hdEmbree/mesh.h @@ -99,6 +99,11 @@ class HdEmbreeMesh final : public HdMesh { /// embree state. virtual void Finalize(HdRenderParam *renderParam) override; + bool EmbreeMeshIsDoubleSided() const + { + return _doubleSided; + } + protected: // Initialize the given representation of this Rprim. // This is called prior to syncing the prim, the first time the repr diff --git a/pxr/imaging/plugin/hdEmbree/renderer.cpp b/pxr/imaging/plugin/hdEmbree/renderer.cpp index 47f93cf498..88603a7059 100644 --- a/pxr/imaging/plugin/hdEmbree/renderer.cpp +++ b/pxr/imaging/plugin/hdEmbree/renderer.cpp @@ -1667,8 +1667,23 @@ HdEmbreeRenderer::_ComputeLighting( float vis = _Visibility(position, ls.wI, ls.dist * 0.99f); // Add exitant luminance + float cosOffNormal = GfDot(ls.wI, normal); + if (cosOffNormal < 0.0f) { + bool doubleSided = false; + HdEmbreeMesh *mesh = + dynamic_cast(prototypeContext->rprim); + if (mesh) { + doubleSided = mesh->EmbreeMeshIsDoubleSided(); + } + + if (doubleSided) { + cosOffNormal *= -1.0f; + } else { + cosOffNormal = 0.0f; + } + } finalColor += ls.Li - * _DotZeroClip(ls.wI, normal) + * cosOffNormal * brdf * vis * ls.invPdfW; From 988665cc28c38caaa2bf3785e7216d5a60b57a7a Mon Sep 17 00:00:00 2001 From: Paul Molodowitch Date: Tue, 30 Jul 2024 22:10:02 -0700 Subject: [PATCH 07/19] [hdEmbree] add support for inputs:diffuse --- pxr/imaging/plugin/hdEmbree/light.cpp | 2 ++ pxr/imaging/plugin/hdEmbree/light.h | 1 + pxr/imaging/plugin/hdEmbree/renderer.cpp | 4 +++- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/pxr/imaging/plugin/hdEmbree/light.cpp b/pxr/imaging/plugin/hdEmbree/light.cpp index b1555a5ba2..ee03567894 100644 --- a/pxr/imaging/plugin/hdEmbree/light.cpp +++ b/pxr/imaging/plugin/hdEmbree/light.cpp @@ -75,6 +75,8 @@ HdEmbree_Light::Sync(HdSceneDelegate *sceneDelegate, // Store luminance parameters _lightData.intensity = sceneDelegate->GetLightParamValue( id, HdLightTokens->intensity).GetWithDefault(1.0f); + _lightData.diffuse = sceneDelegate->GetLightParamValue( + id, HdLightTokens->diffuse).GetWithDefault(1.0f); _lightData.exposure = sceneDelegate->GetLightParamValue( id, HdLightTokens->exposure).GetWithDefault(0.0f); _lightData.color = sceneDelegate->GetLightParamValue( diff --git a/pxr/imaging/plugin/hdEmbree/light.h b/pxr/imaging/plugin/hdEmbree/light.h index 906ba185bc..2dd44d0898 100644 --- a/pxr/imaging/plugin/hdEmbree/light.h +++ b/pxr/imaging/plugin/hdEmbree/light.h @@ -68,6 +68,7 @@ struct HdEmbree_LightData GfMatrix4f xformWorldToLight; GfVec3f color; float intensity = 1.0f; + float diffuse = 1.0f; float exposure = 0.0f; float colorTemperature = 6500.0f; bool enableColorTemperature = false; diff --git a/pxr/imaging/plugin/hdEmbree/renderer.cpp b/pxr/imaging/plugin/hdEmbree/renderer.cpp index 88603a7059..fa2d8074e9 100644 --- a/pxr/imaging/plugin/hdEmbree/renderer.cpp +++ b/pxr/imaging/plugin/hdEmbree/renderer.cpp @@ -412,7 +412,9 @@ _SampleCylinder(GfMatrix4f const& xf, GfMatrix3f const& normalXform, GfVec3f _EvalLightBasic(HdEmbree_LightData const& light) { - GfVec3f Le = light.color * light.intensity * powf(2.0f, light.exposure); + // Our current material model is always 100% diffuse, so diffuse parameter + // is a stright multiplier + GfVec3f Le = light.color * light.intensity * light.diffuse * powf(2.0f, light.exposure); if (light.enableColorTemperature) { Le = GfCompMult(Le, _BlackbodyTemperatureAsRgb(light.colorTemperature)); From ebb46db1defd6bbb849506572ad9061a249dea8a Mon Sep 17 00:00:00 2001 From: Paul Molodowitch Date: Tue, 30 Jul 2024 00:27:19 -0700 Subject: [PATCH 08/19] [hdEmbree] add light texture support --- pxr/imaging/plugin/hdEmbree/light.cpp | 56 ++++++++++++++++++++++++ pxr/imaging/plugin/hdEmbree/light.h | 8 ++++ pxr/imaging/plugin/hdEmbree/renderer.cpp | 19 ++++++++ 3 files changed, 83 insertions(+) diff --git a/pxr/imaging/plugin/hdEmbree/light.cpp b/pxr/imaging/plugin/hdEmbree/light.cpp index ee03567894..e8bcbd88eb 100644 --- a/pxr/imaging/plugin/hdEmbree/light.cpp +++ b/pxr/imaging/plugin/hdEmbree/light.cpp @@ -21,6 +21,61 @@ #include #include +namespace { + +PXR_NAMESPACE_USING_DIRECTIVE + +HdEmbree_LightTexture +_LoadLightTexture(std::string const& path) +{ + if (path.empty()) { + return HdEmbree_LightTexture(); + } + + HioImageSharedPtr img = HioImage::OpenForReading(path); + if (!img) { + return HdEmbree_LightTexture(); + } + + int width = img->GetWidth(); + int height = img->GetHeight(); + + std::vector pixels(width * height * 3.0f); + + HioImage::StorageSpec storage; + storage.width = width; + storage.height = height; + storage.depth = 1; + storage.format = HioFormatFloat32Vec3; + storage.data = &pixels.front(); + + if (img->Read(storage)) { + return {std::move(pixels), width, height}; + } + TF_WARN("Could not read image %s", path.c_str()); + return { std::vector(), 0, 0 }; +} + + +void +_SyncLightTexture(const SdfPath& id, HdEmbree_LightData& light, HdSceneDelegate *sceneDelegate) +{ + std::string path; + if (VtValue textureValue = sceneDelegate->GetLightParamValue( + id, HdLightTokens->textureFile); + textureValue.IsHolding()) { + SdfAssetPath texturePath = + textureValue.UncheckedGet(); + path = texturePath.GetResolvedPath(); + if (path.empty()) { + path = texturePath.GetAssetPath(); + } + } + light.texture = _LoadLightTexture(path); +} + + +} // anonymous namespace PXR_NAMESPACE_OPEN_SCOPE HdEmbree_Light::HdEmbree_Light(SdfPath const& id, TfToken const& lightType) @@ -116,6 +171,7 @@ HdEmbree_Light::Sync(HdSceneDelegate *sceneDelegate, sceneDelegate->GetLightParamValue(id, HdLightTokens->height) .Get(), }; + _SyncLightTexture(id, _lightData, sceneDelegate); } else if constexpr (std::is_same_v) { typedLight = HdEmbree_Sphere{ sceneDelegate->GetLightParamValue(id, HdLightTokens->radius) diff --git a/pxr/imaging/plugin/hdEmbree/light.h b/pxr/imaging/plugin/hdEmbree/light.h index 2dd44d0898..fece3056d8 100644 --- a/pxr/imaging/plugin/hdEmbree/light.h +++ b/pxr/imaging/plugin/hdEmbree/light.h @@ -53,6 +53,13 @@ using HdEmbree_LightVariant = std::variant< HdEmbree_Rect, HdEmbree_Sphere>; +struct HdEmbree_LightTexture +{ + std::vector pixels; + int width = 0; + int height = 0; +}; + struct HdEmbree_Shaping { GfVec3f focusTint; @@ -67,6 +74,7 @@ struct HdEmbree_LightData GfMatrix3f normalXformLightToWorld; GfMatrix4f xformWorldToLight; GfVec3f color; + HdEmbree_LightTexture texture; float intensity = 1.0f; float diffuse = 1.0f; float exposure = 0.0f; diff --git a/pxr/imaging/plugin/hdEmbree/renderer.cpp b/pxr/imaging/plugin/hdEmbree/renderer.cpp index fa2d8074e9..281d091e8c 100644 --- a/pxr/imaging/plugin/hdEmbree/renderer.cpp +++ b/pxr/imaging/plugin/hdEmbree/renderer.cpp @@ -294,6 +294,19 @@ struct _LightSample { float invPdfW; }; +GfVec3f +_SampleLightTexture(HdEmbree_LightTexture const& texture, float s, float t) +{ + if (texture.pixels.empty()) { + return GfVec3f(0.0f); + } + + int x = float(texture.width) * s; + int y = float(texture.height) * t; + + return texture.pixels.at(y*texture.width + x); +} + _ShapeSample _SampleRect(GfMatrix4f const& xf, GfMatrix3f const& normalXform, float width, float height, float u1, float u2) @@ -443,6 +456,12 @@ _EvalAreaLight(HdEmbree_LightData const& light, _ShapeSample const& ss, _EvalLightBasic(light) : GfVec3f(0.0f); + // Multiply by the texture, if there is one + if (!light.texture.pixels.empty()) { + Le = GfCompMult(Le, _SampleLightTexture(light.texture, ss.uv[0], + 1.0f - ss.uv[1])); + } + // If normalize is enabled, we need to divide the luminance by the surface // area of the light, which for an area light is equivalent to multiplying // by the area pdf, which is itself the reciprocal of the surface area From 4cb4d2f118ac97a194d5b80b39550851137249c1 Mon Sep 17 00:00:00 2001 From: Paul Molodowitch Date: Tue, 30 Jul 2024 00:27:48 -0700 Subject: [PATCH 09/19] [hdEmbree] add dome light suppport --- pxr/imaging/plugin/hdEmbree/light.cpp | 5 ++ pxr/imaging/plugin/hdEmbree/light.h | 9 +++ .../plugin/hdEmbree/renderDelegate.cpp | 3 + pxr/imaging/plugin/hdEmbree/renderer.cpp | 73 +++++++++++++++++++ pxr/imaging/plugin/hdEmbree/renderer.h | 1 + 5 files changed, 91 insertions(+) diff --git a/pxr/imaging/plugin/hdEmbree/light.cpp b/pxr/imaging/plugin/hdEmbree/light.cpp index e8bcbd88eb..a556d385d3 100644 --- a/pxr/imaging/plugin/hdEmbree/light.cpp +++ b/pxr/imaging/plugin/hdEmbree/light.cpp @@ -91,6 +91,8 @@ HdEmbree_Light::HdEmbree_Light(SdfPath const& id, TfToken const& lightType) _lightData.lightVariant = HdEmbree_Cylinder(); } else if (lightType == HdSprimTypeTokens->diskLight) { _lightData.lightVariant = HdEmbree_Disk(); + } else if (lightType == HdSprimTypeTokens->domeLight) { + _lightData.lightVariant = HdEmbree_Dome(); } else if (lightType == HdSprimTypeTokens->rectLight) { // Get shape parameters _lightData.lightVariant = HdEmbree_Rect(); @@ -164,6 +166,9 @@ HdEmbree_Light::Sync(HdSceneDelegate *sceneDelegate, sceneDelegate->GetLightParamValue(id, HdLightTokens->radius) .GetWithDefault(0.5f), }; + } else if constexpr (std::is_same_v) { + typedLight = HdEmbree_Dome{}; + _SyncLightTexture(id, _lightData, sceneDelegate); } else if constexpr (std::is_same_v) { typedLight = HdEmbree_Rect{ sceneDelegate->GetLightParamValue(id, HdLightTokens->width) diff --git a/pxr/imaging/plugin/hdEmbree/light.h b/pxr/imaging/plugin/hdEmbree/light.h index fece3056d8..05ab262cb2 100644 --- a/pxr/imaging/plugin/hdEmbree/light.h +++ b/pxr/imaging/plugin/hdEmbree/light.h @@ -35,6 +35,10 @@ struct HdEmbree_Disk float radius; }; +// Needed for HdEmbree_LightVariant +struct HdEmbree_Dome +{}; + struct HdEmbree_Rect { float width; @@ -50,6 +54,7 @@ using HdEmbree_LightVariant = std::variant< HdEmbree_UnknownLight, HdEmbree_Cylinder, HdEmbree_Disk, + HdEmbree_Dome, HdEmbree_Rect, HdEmbree_Sphere>; @@ -108,6 +113,10 @@ class HdEmbree_Light final : public HdLight return _lightData; } + bool IsDome() const { + return std::holds_alternative(_lightData.lightVariant); + } + private: HdEmbree_LightData _lightData; }; diff --git a/pxr/imaging/plugin/hdEmbree/renderDelegate.cpp b/pxr/imaging/plugin/hdEmbree/renderDelegate.cpp index 100e9c133a..27cad002da 100644 --- a/pxr/imaging/plugin/hdEmbree/renderDelegate.cpp +++ b/pxr/imaging/plugin/hdEmbree/renderDelegate.cpp @@ -38,6 +38,7 @@ const TfTokenVector HdEmbreeRenderDelegate::SUPPORTED_SPRIM_TYPES = HdPrimTypeTokens->extComputation, HdPrimTypeTokens->cylinderLight, HdPrimTypeTokens->diskLight, + HdPrimTypeTokens->domeLight, HdPrimTypeTokens->rectLight, HdPrimTypeTokens->sphereLight, }; @@ -338,6 +339,7 @@ HdEmbreeRenderDelegate::CreateSprim(TfToken const& typeId, return new HdExtComputation(sprimId); } else if (typeId == HdPrimTypeTokens->light || typeId == HdPrimTypeTokens->diskLight || + typeId == HdPrimTypeTokens->domeLight || typeId == HdPrimTypeTokens->rectLight || typeId == HdPrimTypeTokens->sphereLight || typeId == HdPrimTypeTokens->cylinderLight) { @@ -360,6 +362,7 @@ HdEmbreeRenderDelegate::CreateFallbackSprim(TfToken const& typeId) return new HdExtComputation(SdfPath::EmptyPath()); } else if (typeId == HdPrimTypeTokens->light || typeId == HdPrimTypeTokens->diskLight || + typeId == HdPrimTypeTokens->domeLight || typeId == HdPrimTypeTokens->rectLight || typeId == HdPrimTypeTokens->sphereLight || typeId == HdPrimTypeTokens->cylinderLight) { diff --git a/pxr/imaging/plugin/hdEmbree/renderer.cpp b/pxr/imaging/plugin/hdEmbree/renderer.cpp index 281d091e8c..23e4c72e4b 100644 --- a/pxr/imaging/plugin/hdEmbree/renderer.cpp +++ b/pxr/imaging/plugin/hdEmbree/renderer.cpp @@ -491,6 +491,45 @@ _EvalAreaLight(HdEmbree_LightData const& light, _ShapeSample const& ss, }; } +_LightSample +_SampleDomeLight(HdEmbree_LightData const& light, GfVec3f const& direction) +{ + float t = acosf(direction[1]) / _pi; + float s = atan2f(direction[0], direction[2]) / (2.0f * _pi); + s = 1.0f - fmodf(s+0.5f, 1.0f); + + GfVec3f Li = light.texture.pixels.empty() ? + GfVec3f(1.0f) + : _SampleLightTexture(light.texture, s, t); + + return _LightSample { + Li, + direction, + std::numeric_limits::max(), + 4.0f * _pi + }; +} + +_LightSample +_EvalDomeLight(HdEmbree_LightData const& light, GfVec3f const& W, + float u1, float u2) +{ + GfVec3f U, V; + GfBuildOrthonormalFrame(W, &U, &V); + + float z = u1; + float r = sqrtf(std::max(0.0f, 1.0f - _Sqr(z))); + float phi = 2.0f * _pi * u2; + + const GfVec3f wI = + (W * z + r * cosf(phi) * U + r * sinf(phi) * V).GetNormalized(); + + _LightSample ls = _SampleDomeLight(light, wI); + ls.invPdfW = 2.0f * _pi; // We only picked from the hemisphere + + return ls; +} + class _LightSampler { public: static _LightSample GetLightSample(HdEmbree_LightData const& lightData, @@ -558,6 +597,10 @@ class _LightSampler { return _EvalAreaLight(_lightData, shapeSample, _hitPosition); } + _LightSample operator()(HdEmbree_Dome const& dome) { + return _EvalDomeLight(_lightData, _normal, _u1, _u2); + } + private: _LightSampler(HdEmbree_LightData const& lightData, GfVec3f const& hitPosition, @@ -677,6 +720,10 @@ HdEmbreeRenderer::AddLight(SdfPath const& lightPath, { ScopedLock lightsWriteLock(_lightsWriteMutex); _lightMap[lightPath] = light; + + if (light->IsDome()) { + _domes.push_back(light); + } } void @@ -684,6 +731,9 @@ HdEmbreeRenderer::RemoveLight(SdfPath const& lightPath, HdEmbree_Light* light) { ScopedLock lightsWriteLock(_lightsWriteMutex); _lightMap.erase(lightPath); + _domes.erase(std::remove_if(_domes.begin(), _domes.end(), + [&light](auto& l){ return l == light; }), + _domes.end()); } bool @@ -1475,6 +1525,29 @@ HdEmbreeRenderer::_ComputeColor(RTCRayHit const& rayHit, std::default_random_engine &random, GfVec4f const& clearColor) { + if (rayHit.hit.geomID == RTC_INVALID_GEOMETRY_ID) { + if (_domes.empty()) { + return clearColor; + } + + // if we missed all geometry in the scene, evaluate the infinite lights + // directly + GfVec4f domeColor(0.0f, 0.0f, 0.0f, 1.0f); + + for (auto* dome : _domes) { + _LightSample ls = _SampleDomeLight( + dome->LightData(), + GfVec3f(rayHit.ray.dir_x, + rayHit.ray.dir_y, + rayHit.ray.dir_z) + ); + domeColor[0] += ls.Li[0]; + domeColor[1] += ls.Li[1]; + domeColor[2] += ls.Li[2]; + } + return domeColor; + } + // Get the instance and prototype context structures for the hit prim. // We don't use embree's multi-level instancing; we // flatten everything in hydra. So instID[0] should always be correct. diff --git a/pxr/imaging/plugin/hdEmbree/renderer.h b/pxr/imaging/plugin/hdEmbree/renderer.h index 85a44ad4b2..e5a8426136 100644 --- a/pxr/imaging/plugin/hdEmbree/renderer.h +++ b/pxr/imaging/plugin/hdEmbree/renderer.h @@ -242,6 +242,7 @@ class HdEmbreeRenderer final // Lights mutable WriteMutex _lightsWriteMutex; // protects the 2 below std::map _lightMap; + std::vector _domes; }; PXR_NAMESPACE_CLOSE_SCOPE From bfd9507edb874bd05bf29d4b2ffc4580d1c03784 Mon Sep 17 00:00:00 2001 From: Paul Molodowitch Date: Tue, 30 Jul 2024 00:28:30 -0700 Subject: [PATCH 10/19] [hdEmbree] add direct camera visibility support for rect lights --- pxr/imaging/plugin/hdEmbree/context.h | 4 + pxr/imaging/plugin/hdEmbree/light.cpp | 89 +++++++++++++++++++- pxr/imaging/plugin/hdEmbree/light.h | 6 ++ pxr/imaging/plugin/hdEmbree/mesh.cpp | 1 + pxr/imaging/plugin/hdEmbree/renderer.cpp | 102 +++++++++++++++++++++++ 5 files changed, 200 insertions(+), 2 deletions(-) diff --git a/pxr/imaging/plugin/hdEmbree/context.h b/pxr/imaging/plugin/hdEmbree/context.h index 4165adb1e6..d0d2a96b6a 100644 --- a/pxr/imaging/plugin/hdEmbree/context.h +++ b/pxr/imaging/plugin/hdEmbree/context.h @@ -13,12 +13,14 @@ #include "pxr/base/gf/matrix4f.h" #include "pxr/base/vt/array.h" +#include "pxr/base/vt/types.h" #include PXR_NAMESPACE_OPEN_SCOPE class HdRprim; +class HdEmbree_Light; /// \class HdEmbreePrototypeContext /// @@ -51,6 +53,8 @@ struct HdEmbreeInstanceContext RTCScene rootScene; /// The instance id of this instance. int32_t instanceId; + /// The light (if this is a light) + HdEmbree_Light *light = nullptr; }; diff --git a/pxr/imaging/plugin/hdEmbree/light.cpp b/pxr/imaging/plugin/hdEmbree/light.cpp index a556d385d3..d83d07b59f 100644 --- a/pxr/imaging/plugin/hdEmbree/light.cpp +++ b/pxr/imaging/plugin/hdEmbree/light.cpp @@ -15,6 +15,7 @@ #include "pxr/imaging/hio/image.h" #include +#include #include #include @@ -78,6 +79,11 @@ _SyncLightTexture(const SdfPath& id, HdEmbree_LightData& light, HdSceneDelegate } // anonymous namespace PXR_NAMESPACE_OPEN_SCOPE +TF_DEFINE_PRIVATE_TOKENS(_tokens, + ((inputsVisibilityCamera, "inputs:visibility:camera")) + ((inputsVisibilityShadow, "inputs:visibility:shadow")) +); + HdEmbree_Light::HdEmbree_Light(SdfPath const& id, TfToken const& lightType) : HdLight(id) { if (id.IsEmpty()) { @@ -117,7 +123,8 @@ HdEmbree_Light::Sync(HdSceneDelegate *sceneDelegate, static_cast(renderParam); // calling this bumps the scene version and causes a re-render - embreeRenderParam->AcquireSceneForEdit(); + RTCScene scene = embreeRenderParam->AcquireSceneForEdit(); + RTCDevice device = embreeRenderParam->GetEmbreeDevice(); SdfPath const& id = GetId(); @@ -147,6 +154,13 @@ HdEmbree_Light::Sync(HdSceneDelegate *sceneDelegate, // Get visibility _lightData.visible = sceneDelegate->GetVisible(id); + _lightData.visible_camera = sceneDelegate->GetLightParamValue( + id, _tokens->inputsVisibilityCamera).GetWithDefault(false); + // XXX: Don't think we can get this to work in Embree unless it's built with + // masking only solution would be to use rtcIntersect instead of rtcOccluded + // for shadow rays, which maybe isn't the worst for a reference renderer + _lightData.visible_shadow = sceneDelegate->GetLightParamValue( + id, _tokens->inputsVisibilityShadow).GetWithDefault(false); // Switch on the _lightData type and pull the relevant attributes from the scene // delegate @@ -211,12 +225,71 @@ HdEmbree_Light::Sync(HdSceneDelegate *sceneDelegate, _lightData.shaping.coneSoftness = value.UncheckedGet(); } + _PopulateRtcLight(device, scene); + HdEmbreeRenderer *renderer = embreeRenderParam->GetRenderer(); renderer->AddLight(id, this); *dirtyBits &= ~HdLight::AllDirty; } +void +HdEmbree_Light::_PopulateRtcLight(RTCDevice device, RTCScene scene) +{ + _lightData.rtcMeshId = RTC_INVALID_GEOMETRY_ID; + + // create the light geometry, if required + if (_lightData.visible) { + if (auto* rect = std::get_if(&_lightData.lightVariant)) + { + // create _lightData mesh + GfVec3f v0(-rect->width/2.0f, -rect->height/2.0f, 0.0f); + GfVec3f v1( rect->width/2.0f, -rect->height/2.0f, 0.0f); + GfVec3f v2( rect->width/2.0f, rect->height/2.0f, 0.0f); + GfVec3f v3(-rect->width/2.0f, rect->height/2.0f, 0.0f); + + v0 = _lightData.xformLightToWorld.Transform(v0); + v1 = _lightData.xformLightToWorld.Transform(v1); + v2 = _lightData.xformLightToWorld.Transform(v2); + v3 = _lightData.xformLightToWorld.Transform(v3); + + _lightData.rtcGeometry = rtcNewGeometry(device, + RTC_GEOMETRY_TYPE_QUAD); + GfVec3f* vertices = static_cast( + rtcSetNewGeometryBuffer(_lightData.rtcGeometry, + RTC_BUFFER_TYPE_VERTEX, + 0, + RTC_FORMAT_FLOAT3, + sizeof(GfVec3f), + 4)); + vertices[0] = v0; + vertices[1] = v1; + vertices[2] = v2; + vertices[3] = v3; + + unsigned* index = static_cast( + rtcSetNewGeometryBuffer(_lightData.rtcGeometry, + RTC_BUFFER_TYPE_INDEX, + 0, + RTC_FORMAT_UINT4, + sizeof(unsigned)*4, + 1)); + index[0] = 0; index[1] = 1; index[2] = 2; index[3] = 3; + + auto ctx = std::make_unique(); + ctx->light = this; + rtcSetGeometryTimeStepCount(_lightData.rtcGeometry, 1); + rtcCommitGeometry(_lightData.rtcGeometry); + _lightData.rtcMeshId = rtcAttachGeometry(scene, _lightData.rtcGeometry); + if (_lightData.rtcMeshId == RTC_INVALID_GEOMETRY_ID) { + TF_WARN("could not create rect mesh for %s", GetId().GetAsString().c_str()); + } else { + rtcSetGeometryUserData(_lightData.rtcGeometry, ctx.release()); + } + } + } +} + HdDirtyBits HdEmbree_Light::GetInitialDirtyBitsMask() const { @@ -227,10 +300,22 @@ void HdEmbree_Light::Finalize(HdRenderParam *renderParam) { auto* embreeParam = static_cast(renderParam); + RTCScene scene = embreeParam->AcquireSceneForEdit(); - // Remove from renderer's light map + // First, remove from renderer's light map HdEmbreeRenderer *renderer = embreeParam->GetRenderer(); renderer->RemoveLight(GetId(), this); + + // Then clean up the associated embree objects + if (_lightData.rtcMeshId != RTC_INVALID_GEOMETRY_ID) { + delete static_cast( + rtcGetGeometryUserData(_lightData.rtcGeometry)); + + rtcDetachGeometry(scene, _lightData.rtcMeshId); + rtcReleaseGeometry(_lightData.rtcGeometry); + _lightData.rtcMeshId = RTC_INVALID_GEOMETRY_ID; + _lightData.rtcGeometry = nullptr; + } } PXR_NAMESPACE_CLOSE_SCOPE diff --git a/pxr/imaging/plugin/hdEmbree/light.h b/pxr/imaging/plugin/hdEmbree/light.h index 05ab262cb2..99d29837d6 100644 --- a/pxr/imaging/plugin/hdEmbree/light.h +++ b/pxr/imaging/plugin/hdEmbree/light.h @@ -88,7 +88,11 @@ struct HdEmbree_LightData HdEmbree_LightVariant lightVariant; bool normalize = false; bool visible = true; + bool visible_camera = true; + bool visible_shadow = true; HdEmbree_Shaping shaping; + unsigned rtcMeshId = RTC_INVALID_GEOMETRY_ID; + RTCGeometry rtcGeometry = nullptr; }; class HdEmbree_Light final : public HdLight @@ -118,6 +122,8 @@ class HdEmbree_Light final : public HdLight } private: + void _PopulateRtcLight(RTCDevice device, RTCScene scene); + HdEmbree_LightData _lightData; }; diff --git a/pxr/imaging/plugin/hdEmbree/mesh.cpp b/pxr/imaging/plugin/hdEmbree/mesh.cpp index 195f83a633..bb4515cf56 100644 --- a/pxr/imaging/plugin/hdEmbree/mesh.cpp +++ b/pxr/imaging/plugin/hdEmbree/mesh.cpp @@ -894,6 +894,7 @@ HdEmbreeMesh::_PopulateRtMesh(HdSceneDelegate* sceneDelegate, HdEmbreeInstanceContext *ctx = new HdEmbreeInstanceContext; ctx->rootScene = _rtcMeshScene; ctx->instanceId = i; + ctx->light = nullptr; rtcSetGeometryUserData(geom,ctx); _rtcInstanceGeometries[i] = geom; } diff --git a/pxr/imaging/plugin/hdEmbree/renderer.cpp b/pxr/imaging/plugin/hdEmbree/renderer.cpp index 23e4c72e4b..356ff2450b 100644 --- a/pxr/imaging/plugin/hdEmbree/renderer.cpp +++ b/pxr/imaging/plugin/hdEmbree/renderer.cpp @@ -422,6 +422,20 @@ _SampleCylinder(GfMatrix4f const& xf, GfMatrix3f const& normalXform, }; } +_ShapeSample +_IntersectAreaLight(HdEmbree_LightData const& light, RTCRayHit const& rayHit) +{ + // XXX: just rect lights at the moment, need to do the others + auto const& rect = std::get(light.lightVariant); + + return _ShapeSample { + _CalculateHitPosition(rayHit), + GfVec3f(rayHit.hit.Ng_x, rayHit.hit.Ng_y, rayHit.hit.Ng_z), + GfVec2f(1.0f - rayHit.hit.u, rayHit.hit.v), + _AreaRect(light.xformLightToWorld, rect.width, rect.height) + }; +} + GfVec3f _EvalLightBasic(HdEmbree_LightData const& light) { @@ -1275,6 +1289,43 @@ _CosineWeightedDirection(GfVec2f const& uniform_float) return dir; } +bool +HdEmbreeRenderer::_RayShouldContinue(RTCRayHit const& rayHit) const { + if (rayHit.hit.geomID == RTC_INVALID_GEOMETRY_ID) { + // missed, don't continue + return false; + } + + if (rayHit.hit.instID[0] == RTC_INVALID_GEOMETRY_ID) { + // not hit an instance, but a "raw" geometry. This should be a light + const HdEmbreeInstanceContext *instanceContext = + static_cast( + rtcGetGeometryUserData(rtcGetGeometry(_scene, + rayHit.hit.geomID))); + + if (instanceContext->light == nullptr) { + // if this isn't a light, don't know what this is + return false; + } + + auto const& light = instanceContext->light->LightData(); + + if ((rayHit.ray.mask & HdEmbree_RayMask::Camera) + && !light.visible_camera) { + return true; + } else if ((rayHit.ray.mask & HdEmbree_RayMask::Shadow) + && !light.visible_shadow) { + return true; + } else { + return false; + } + } + + // XXX: otherwise this is a regular geo. we should handle visibility here + // too eventually + return false; +} + void HdEmbreeRenderer::_TraceRay(unsigned int x, unsigned int y, GfVec3f const &origin, GfVec3f const &dir, @@ -1306,6 +1357,13 @@ HdEmbreeRenderer::_TraceRay(unsigned int x, unsigned int y, rayHit.hit.Ng_z = -rayHit.hit.Ng_z; } + if (_RayShouldContinue(rayHit)) { + GfVec3f hitPos = _CalculateHitPosition(rayHit); + + _TraceRay(x, y, hitPos + dir * _rayHitContinueBias, dir, random); + return; + } + // Write AOVs to attachments that aren't converged. for (size_t i = 0; i < _aovBindings.size(); ++i) { HdEmbreeRenderBuffer *renderBuffer = @@ -1361,6 +1419,11 @@ HdEmbreeRenderer::_ComputeId(RTCRayHit const& rayHit, TfToken const& idType, return false; } + if (rayHit.hit.instID[0] == RTC_INVALID_GEOMETRY_ID) { + // not hit an instance, but a "raw" geometry. This should be a light + return false; + } + // Get the instance and prototype context structures for the hit prim. // We don't use embree's multi-level instancing; we // flatten everything in hydra. So instID[0] should always be correct. @@ -1401,6 +1464,11 @@ HdEmbreeRenderer::_ComputeDepth(RTCRayHit const& rayHit, return false; } + if (rayHit.hit.instID[0] == RTC_INVALID_GEOMETRY_ID) { + // not hit an instance, but a "raw" geometry. This should be a light + return false; + } + if (clip) { GfVec3f hitPos = _CalculateHitPosition(rayHit); @@ -1424,6 +1492,11 @@ HdEmbreeRenderer::_ComputeNormal(RTCRayHit const& rayHit, return false; } + if (rayHit.hit.instID[0] == RTC_INVALID_GEOMETRY_ID) { + // not hit an instance, but a "raw" geometry. This should be a light + return false; + } + // We don't use embree's multi-level instancing; we // flatten everything in hydra. So instID[0] should always be correct. const HdEmbreeInstanceContext *instanceContext = @@ -1462,6 +1535,11 @@ HdEmbreeRenderer::_ComputePrimvar(RTCRayHit const& rayHit, return false; } + if (rayHit.hit.instID[0] == RTC_INVALID_GEOMETRY_ID) { + // not hit an instance, but a "raw" geometry. This should be a light + return false; + } + // We don't use embree's multi-level instancing; we // flatten everything in hydra. So instID[0] should always be correct. const HdEmbreeInstanceContext *instanceContext = @@ -1548,6 +1626,30 @@ HdEmbreeRenderer::_ComputeColor(RTCRayHit const& rayHit, return domeColor; } + if (rayHit.hit.instID[0] == RTC_INVALID_GEOMETRY_ID) { + // if it's not an instance then it's almost certainly a light + const HdEmbreeInstanceContext *instanceContext = + static_cast( + rtcGetGeometryUserData(rtcGetGeometry(_scene, + rayHit.hit.geomID))); + + // if we hit a light, just evaluate the light directly + if (instanceContext->light != nullptr) { + auto const& light = instanceContext->light->LightData(); + _ShapeSample ss = _IntersectAreaLight(light, rayHit); + _LightSample ls = _EvalAreaLight(light, ss, + GfVec3f(rayHit.ray.org_x, rayHit.ray.org_y, rayHit.ray.org_z)); + + return GfVec4f(ls.Li[0], ls.Li[1], ls.Li[2], 1.0f); + } else { + // should never get here. magenta warning! + TF_WARN("Unexpected runtime state - hit an an embree instance " + "that wasn't a geo or light"); + return GfVec4f(1.0f, 0.0f, 1.0f, 1.0f); + } + + } + // Get the instance and prototype context structures for the hit prim. // We don't use embree's multi-level instancing; we // flatten everything in hydra. So instID[0] should always be correct. From 64da5d0242f80c175b01ea2238092e75eaeb46bc Mon Sep 17 00:00:00 2001 From: Paul Molodowitch Date: Mon, 29 Jul 2024 23:03:30 -0700 Subject: [PATCH 11/19] [hdEmbree] add pxrPbrt/pbrUtils.h A few small utility functions from pbrt-v4 --- LICENSE.txt | 17 ++++++ .../plugin/hdEmbree/pxrPbrt/pbrtUtils.h | 52 +++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 pxr/imaging/plugin/hdEmbree/pxrPbrt/pbrtUtils.h diff --git a/LICENSE.txt b/LICENSE.txt index d3bb1fa530..06a5b356ee 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -508,6 +508,23 @@ Redistributions in binary form must reproduce the above copyright notice, this l THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +============================================================ +pbrt (sampling functions in hdEmbree/pxrPbrt/pbrUtils.h) +============================================================ + +Copyright(c) 1998-2020 Matt Pharr, Wenzel Jakob, and Greg Humphreys. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. ============================================================ Draco diff --git a/pxr/imaging/plugin/hdEmbree/pxrPbrt/pbrtUtils.h b/pxr/imaging/plugin/hdEmbree/pxrPbrt/pbrtUtils.h new file mode 100644 index 0000000000..b7fcf4741d --- /dev/null +++ b/pxr/imaging/plugin/hdEmbree/pxrPbrt/pbrtUtils.h @@ -0,0 +1,52 @@ +// pbrt is Copyright(c) 1998-2020 Matt Pharr, Wenzel Jakob, and Greg Humphreys. +// The pbrt source code is licensed under the Apache License, Version 2.0. +// SPDX: Apache-2.0 + +#ifndef PXR_IMAGING_PLUGIN_HD_EMBREE_PBRT_UTILS_H +#define PXR_IMAGING_PLUGIN_HD_EMBREE_PBRT_UTILS_H + +#include "pxr/pxr.h" + +#include "pxr/base/arch/math.h" +#include "pxr/base/gf/vec2f.h" +#include "pxr/base/gf/vec3f.h" + +PXR_NAMESPACE_OPEN_SCOPE + +namespace pxr_pbrt { + +template +constexpr T pi = static_cast(M_PI); + +// Ported from PBRT +inline GfVec3f +SphericalDirection(float sinTheta, float cosTheta, float phi) +{ + return GfVec3f(GfClamp(sinTheta, -1.0f, 1.0f) * GfCos(phi), + GfClamp(sinTheta, -1.0f, 1.0f) * GfSin(phi), + GfClamp(cosTheta, -1.0f, 1.0f)); +} + +// Ported from PBRT +inline GfVec3f +SampleUniformCone(GfVec2f const& u, float angle) +{ + float cosAngle = GfCos(angle); + float cosTheta = (1.0f - u[0]) + u[0] * cosAngle; + float sinTheta = GfSqrt(GfMax(0.0f, 1.0f - cosTheta*cosTheta)); + float phi = u[1] * 2.0f * pi; + return SphericalDirection(sinTheta, cosTheta, phi); +} + +// Ported from PBRT +inline float +InvUniformConePDF(float angle) +{ + return 2.0f * pi * (1.0f - GfCos(angle)); +} + +} // namespace pxr_pbrt + +PXR_NAMESPACE_CLOSE_SCOPE + +#endif // PXR_IMAGING_PLUGIN_HD_EMBREE_PBRT_UTILS_H \ No newline at end of file From d2840eb64545eabcc6099bea73a45df8f32d376d Mon Sep 17 00:00:00 2001 From: Paul Molodowitch Date: Mon, 29 Jul 2024 23:05:19 -0700 Subject: [PATCH 12/19] [hdEmbree] add distant light support --- pxr/imaging/plugin/hdEmbree/CMakeLists.txt | 3 ++ pxr/imaging/plugin/hdEmbree/light.cpp | 8 +++ pxr/imaging/plugin/hdEmbree/light.h | 6 +++ .../plugin/hdEmbree/renderDelegate.cpp | 3 ++ pxr/imaging/plugin/hdEmbree/renderer.cpp | 50 +++++++++++++++++++ 5 files changed, 70 insertions(+) diff --git a/pxr/imaging/plugin/hdEmbree/CMakeLists.txt b/pxr/imaging/plugin/hdEmbree/CMakeLists.txt index c3c92aa552..98f745631e 100644 --- a/pxr/imaging/plugin/hdEmbree/CMakeLists.txt +++ b/pxr/imaging/plugin/hdEmbree/CMakeLists.txt @@ -49,6 +49,9 @@ pxr_plugin(hdEmbree debugCodes implicitSurfaceSceneIndexPlugin + PRIVATE_HEADERS + pxrPbrt/pbrtUtils.h + RESOURCE_FILES plugInfo.json diff --git a/pxr/imaging/plugin/hdEmbree/light.cpp b/pxr/imaging/plugin/hdEmbree/light.cpp index d83d07b59f..2388d346fe 100644 --- a/pxr/imaging/plugin/hdEmbree/light.cpp +++ b/pxr/imaging/plugin/hdEmbree/light.cpp @@ -97,6 +97,8 @@ HdEmbree_Light::HdEmbree_Light(SdfPath const& id, TfToken const& lightType) _lightData.lightVariant = HdEmbree_Cylinder(); } else if (lightType == HdSprimTypeTokens->diskLight) { _lightData.lightVariant = HdEmbree_Disk(); + } else if (lightType == HdSprimTypeTokens->distantLight) { + _lightData.lightVariant = HdEmbree_Distant(); } else if (lightType == HdSprimTypeTokens->domeLight) { _lightData.lightVariant = HdEmbree_Dome(); } else if (lightType == HdSprimTypeTokens->rectLight) { @@ -180,6 +182,12 @@ HdEmbree_Light::Sync(HdSceneDelegate *sceneDelegate, sceneDelegate->GetLightParamValue(id, HdLightTokens->radius) .GetWithDefault(0.5f), }; + } else if constexpr (std::is_same_v) { + typedLight = HdEmbree_Distant{ + float(GfDegreesToRadians( + sceneDelegate->GetLightParamValue(id, HdLightTokens->angle) + .GetWithDefault(0.53f) / 2.0f)), + }; } else if constexpr (std::is_same_v) { typedLight = HdEmbree_Dome{}; _SyncLightTexture(id, _lightData, sceneDelegate); diff --git a/pxr/imaging/plugin/hdEmbree/light.h b/pxr/imaging/plugin/hdEmbree/light.h index 99d29837d6..3f756ebedf 100644 --- a/pxr/imaging/plugin/hdEmbree/light.h +++ b/pxr/imaging/plugin/hdEmbree/light.h @@ -35,6 +35,11 @@ struct HdEmbree_Disk float radius; }; +struct HdEmbree_Distant +{ + float halfAngleRadians; +}; + // Needed for HdEmbree_LightVariant struct HdEmbree_Dome {}; @@ -54,6 +59,7 @@ using HdEmbree_LightVariant = std::variant< HdEmbree_UnknownLight, HdEmbree_Cylinder, HdEmbree_Disk, + HdEmbree_Distant, HdEmbree_Dome, HdEmbree_Rect, HdEmbree_Sphere>; diff --git a/pxr/imaging/plugin/hdEmbree/renderDelegate.cpp b/pxr/imaging/plugin/hdEmbree/renderDelegate.cpp index 27cad002da..3050fc6d03 100644 --- a/pxr/imaging/plugin/hdEmbree/renderDelegate.cpp +++ b/pxr/imaging/plugin/hdEmbree/renderDelegate.cpp @@ -38,6 +38,7 @@ const TfTokenVector HdEmbreeRenderDelegate::SUPPORTED_SPRIM_TYPES = HdPrimTypeTokens->extComputation, HdPrimTypeTokens->cylinderLight, HdPrimTypeTokens->diskLight, + HdPrimTypeTokens->distantLight, HdPrimTypeTokens->domeLight, HdPrimTypeTokens->rectLight, HdPrimTypeTokens->sphereLight, @@ -338,6 +339,7 @@ HdEmbreeRenderDelegate::CreateSprim(TfToken const& typeId, } else if (typeId == HdPrimTypeTokens->extComputation) { return new HdExtComputation(sprimId); } else if (typeId == HdPrimTypeTokens->light || + typeId == HdPrimTypeTokens->distantLight || typeId == HdPrimTypeTokens->diskLight || typeId == HdPrimTypeTokens->domeLight || typeId == HdPrimTypeTokens->rectLight || @@ -361,6 +363,7 @@ HdEmbreeRenderDelegate::CreateFallbackSprim(TfToken const& typeId) } else if (typeId == HdPrimTypeTokens->extComputation) { return new HdExtComputation(SdfPath::EmptyPath()); } else if (typeId == HdPrimTypeTokens->light || + typeId == HdPrimTypeTokens->distantLight || typeId == HdPrimTypeTokens->diskLight || typeId == HdPrimTypeTokens->domeLight || typeId == HdPrimTypeTokens->rectLight || diff --git a/pxr/imaging/plugin/hdEmbree/renderer.cpp b/pxr/imaging/plugin/hdEmbree/renderer.cpp index 356ff2450b..226549c41c 100644 --- a/pxr/imaging/plugin/hdEmbree/renderer.cpp +++ b/pxr/imaging/plugin/hdEmbree/renderer.cpp @@ -9,6 +9,7 @@ #include "pxr/imaging/plugin/hdEmbree/config.h" #include "pxr/imaging/plugin/hdEmbree/light.h" #include "pxr/imaging/plugin/hdEmbree/mesh.h" +#include "pxr/imaging/plugin/hdEmbree/pxrPbrt/pbrtUtils.h" #include "pxr/imaging/plugin/hdEmbree/renderBuffer.h" #include "pxr/imaging/hd/perfLog.h" @@ -449,6 +450,51 @@ _EvalLightBasic(HdEmbree_LightData const& light) return Le; } +_LightSample +_EvalDistantLight(HdEmbree_LightData const& light, GfVec3f const& position, + float u1, float u2) +{ + auto const& distant = std::get(light.lightVariant); + + GfVec3f Le = _EvalLightBasic(light); + + if (distant.halfAngleRadians > 0.0f) + { + if (light.normalize) + { + float sinTheta = sinf(distant.halfAngleRadians); + Le /= _Sqr(sinTheta) * _pi; + } + + // There's an implicit double-negation of the wI direction here + GfVec3f localDir = pxr_pbrt::SampleUniformCone(GfVec2f(u1, u2), + distant.halfAngleRadians); + GfVec3f wI = light.xformLightToWorld.TransformDir(localDir); + wI.Normalize(); + + return _LightSample { + Le, + wI, + std::numeric_limits::max(), + pxr_pbrt::InvUniformConePDF(distant.halfAngleRadians) + }; + } + else + { + // delta case, infinite pdf + GfVec3f wI = light.xformLightToWorld.TransformDir( + GfVec3f(0.0f, 0.0f, 1.0f)); + wI.Normalize(); + + return _LightSample { + Le, + wI, + std::numeric_limits::max(), + 1.0f, + }; + } +} + _LightSample _EvalAreaLight(HdEmbree_LightData const& light, _ShapeSample const& ss, GfVec3f const& position) @@ -615,6 +661,10 @@ class _LightSampler { return _EvalDomeLight(_lightData, _normal, _u1, _u2); } + _LightSample operator()(HdEmbree_Distant const& distant) { + return _EvalDistantLight(_lightData, _hitPosition, _u1, _u2); + } + private: _LightSampler(HdEmbree_LightData const& lightData, GfVec3f const& hitPosition, From 1a044a11ac9216d86efb17d80816c4bf624d8a55 Mon Sep 17 00:00:00 2001 From: Paul Molodowitch Date: Wed, 24 Apr 2024 12:45:53 -0700 Subject: [PATCH 13/19] [hdEmbree] ies.h / ies.cpp: add unaltered These are taken unaltered from cycles, v4.1.1 - https://projects.blender.org/blender/cycles/src/tag/v4.1.1/src/util/ies.h - https://projects.blender.org/blender/cycles/src/tag/v4.1.1/src/util/ies.cpp These are not yet built / used, but merely introduce the files for use in the codebase later. --- pxr/imaging/plugin/hdEmbree/pxrIES/ies.cpp | 410 +++++++++++++++++++++ pxr/imaging/plugin/hdEmbree/pxrIES/ies.h | 46 +++ 2 files changed, 456 insertions(+) create mode 100644 pxr/imaging/plugin/hdEmbree/pxrIES/ies.cpp create mode 100644 pxr/imaging/plugin/hdEmbree/pxrIES/ies.h diff --git a/pxr/imaging/plugin/hdEmbree/pxrIES/ies.cpp b/pxr/imaging/plugin/hdEmbree/pxrIES/ies.cpp new file mode 100644 index 0000000000..a6725cc049 --- /dev/null +++ b/pxr/imaging/plugin/hdEmbree/pxrIES/ies.cpp @@ -0,0 +1,410 @@ +/* SPDX-FileCopyrightText: 2011-2022 Blender Foundation + * + * SPDX-License-Identifier: Apache-2.0 */ + +#include + +#include "util/foreach.h" +#include "util/ies.h" +#include "util/math.h" +#include "util/string.h" + +CCL_NAMESPACE_BEGIN + +// NOTE: For some reason gcc-7.2 does not instantiate this version of the +// allocator here (used in IESTextParser). Works fine for gcc-6, gcc-7.3 and gcc-8. +// +// TODO(sergey): Get to the root of this issue, or confirm this is a compiler +// issue. +template class GuardedAllocator; + +bool IESFile::load(const string &ies) +{ + clear(); + if (!parse(ies) || !process()) { + clear(); + return false; + } + return true; +} + +void IESFile::clear() +{ + intensity.clear(); + v_angles.clear(); + h_angles.clear(); +} + +int IESFile::packed_size() +{ + if (v_angles.size() && h_angles.size() > 0) { + return 2 + h_angles.size() + v_angles.size() + h_angles.size() * v_angles.size(); + } + return 0; +} + +void IESFile::pack(float *data) +{ + if (v_angles.size() && h_angles.size()) { + *(data++) = __int_as_float(h_angles.size()); + *(data++) = __int_as_float(v_angles.size()); + + memcpy(data, &h_angles[0], h_angles.size() * sizeof(float)); + data += h_angles.size(); + memcpy(data, &v_angles[0], v_angles.size() * sizeof(float)); + data += v_angles.size(); + + for (int h = 0; h < intensity.size(); h++) { + memcpy(data, &intensity[h][0], v_angles.size() * sizeof(float)); + data += v_angles.size(); + } + } +} + +class IESTextParser { + public: + string text; + char *data; + bool error; + + IESTextParser(const string &str) : text(str), error(false) + { + std::replace(text.begin(), text.end(), ',', ' '); + data = strstr(&text[0], "\nTILT="); + } + + bool eof() + { + return (data == NULL) || (data[0] == '\0'); + } + + bool has_error() + { + return error; + } + + double get_double() + { + if (eof()) { + error = true; + return 0.0; + } + char *old_data = data; + double val = strtod(data, &data); + if (data == old_data) { + data = NULL; + error = true; + return 0.0; + } + return val; + } + + long get_long() + { + if (eof()) { + error = true; + return 0; + } + char *old_data = data; + long val = strtol(data, &data, 10); + if (data == old_data) { + data = NULL; + error = true; + return 0; + } + return val; + } +}; + +bool IESFile::parse(const string &ies) +{ + if (ies.empty()) { + return false; + } + + IESTextParser parser(ies); + if (parser.eof()) { + return false; + } + + /* Handle the tilt data block. */ + if (strncmp(parser.data, "\nTILT=INCLUDE", 13) == 0) { + parser.data += 13; + parser.get_double(); /* Lamp to Luminaire geometry */ + int num_tilt = parser.get_long(); /* Amount of tilt angles and factors */ + /* Skip over angles and factors. */ + for (int i = 0; i < 2 * num_tilt; i++) { + parser.get_double(); + } + } + else { + /* Skip to next line. */ + parser.data = strstr(parser.data + 1, "\n"); + } + + if (parser.eof()) { + return false; + } + parser.data++; + + parser.get_long(); /* Number of lamps */ + parser.get_double(); /* Lumens per lamp */ + double factor = parser.get_double(); /* Candela multiplier */ + int v_angles_num = parser.get_long(); /* Number of vertical angles */ + int h_angles_num = parser.get_long(); /* Number of horizontal angles */ + type = (IESType)parser.get_long(); /* Photometric type */ + + if (type != TYPE_A && type != TYPE_B && type != TYPE_C) { + return false; + } + + parser.get_long(); /* Unit of the geometry data */ + parser.get_double(); /* Width */ + parser.get_double(); /* Length */ + parser.get_double(); /* Height */ + factor *= parser.get_double(); /* Ballast factor */ + factor *= parser.get_double(); /* Ballast-Lamp Photometric factor */ + parser.get_double(); /* Input Watts */ + + /* Intensity values in IES files are specified in candela (lumen/sr), a photometric quantity. + * Cycles expects radiometric quantities, though, which requires a conversion. + * However, the Luminous efficacy (ratio of lumens per Watt) depends on the spectral distribution + * of the light source since lumens take human perception into account. + * Since this spectral distribution is not known from the IES file, a typical one must be + * assumed. The D65 standard illuminant has a Luminous efficacy of 177.83, which is used here to + * convert to Watt/sr. A more advanced approach would be to add a Blackbody Temperature input to + * the node and numerically integrate the Luminous efficacy from the resulting spectral + * distribution. Also, the Watt/sr value must be multiplied by 4*pi to get the Watt value that + * Cycles expects for lamp strength. Therefore, the conversion here uses 4*pi/177.83 as a Candela + * to Watt factor. + */ + factor *= 0.0706650768394; + + v_angles.reserve(v_angles_num); + for (int i = 0; i < v_angles_num; i++) { + v_angles.push_back((float)parser.get_double()); + } + + h_angles.reserve(h_angles_num); + for (int i = 0; i < h_angles_num; i++) { + h_angles.push_back((float)parser.get_double()); + } + + intensity.resize(h_angles_num); + for (int i = 0; i < h_angles_num; i++) { + intensity[i].reserve(v_angles_num); + for (int j = 0; j < v_angles_num; j++) { + intensity[i].push_back((float)(factor * parser.get_double())); + } + } + + return !parser.has_error(); +} + +static bool angle_close(float a, float b) +{ + return fabsf(a - b) < 1e-4f; +} + +/* Processing functions to turn file contents into the format that Cycles expects. + * Handles type conversion (the output format is based on Type C), symmetry/mirroring, + * value shifting etc. + * Note that this code is much more forgiving than the spec. For example, in type A and B, + * the range of vertical angles officially must be either exactly 0°-90° or -90°-90°. + * However, in practice, IES files are all over the place. Therefore, the handling is as + * flexible as possible, and tries to turn any input into something useful. */ + +void IESFile::process_type_b() +{ + /* According to the standard, Type B defines a different coordinate system where the polar axis + * is horizontal, not vertical. + * To avoid over complicating the conversion logic, we just transpose the angles and use the + * regular Type A/C coordinate system. Users can just rotate the light to get the "proper" + * orientation. */ + vector> newintensity; + newintensity.resize(v_angles.size()); + for (int i = 0; i < v_angles.size(); i++) { + newintensity[i].reserve(h_angles.size()); + for (int j = 0; j < h_angles.size(); j++) { + newintensity[i].push_back(intensity[j][i]); + } + } + intensity.swap(newintensity); + h_angles.swap(v_angles); + + if (angle_close(h_angles[0], 0.0f)) { + /* File angles cover 0°-90°. Mirror that to -90°-90°, and shift to 0°-180° to match Cycles. */ + vector new_h_angles; + vector> new_intensity; + int hnum = h_angles.size(); + new_h_angles.reserve(2 * hnum - 1); + new_intensity.reserve(2 * hnum - 1); + for (int i = hnum - 1; i > 0; i--) { + new_h_angles.push_back(90.0f - h_angles[i]); + new_intensity.push_back(intensity[i]); + } + for (int i = 0; i < hnum; i++) { + new_h_angles.push_back(90.0f + h_angles[i]); + new_intensity.push_back(intensity[i]); + } + h_angles.swap(new_h_angles); + intensity.swap(new_intensity); + } + else { + /* File angles cover -90°-90°. Shift to 0°-180° to match Cycles. */ + for (int i = 0; i < h_angles.size(); i++) { + h_angles[i] += 90.0f; + } + } + + if (angle_close(v_angles[0], 0.0f)) { + /* File angles cover 0°-90°. Mirror that to -90°-90°, and shift to 0°-180° to match Cycles. */ + vector new_v_angles; + int hnum = h_angles.size(); + int vnum = v_angles.size(); + new_v_angles.reserve(2 * vnum - 1); + for (int i = vnum - 1; i > 0; i--) { + new_v_angles.push_back(90.0f - v_angles[i]); + } + for (int i = 0; i < vnum; i++) { + new_v_angles.push_back(90.0f + v_angles[i]); + } + for (int i = 0; i < hnum; i++) { + vector new_intensity; + new_intensity.reserve(2 * vnum - 1); + for (int j = vnum - 1; j > 0; j--) { + new_intensity.push_back(intensity[i][j]); + } + new_intensity.insert(new_intensity.end(), intensity[i].begin(), intensity[i].end()); + intensity[i].swap(new_intensity); + } + v_angles.swap(new_v_angles); + } + else { + /* File angles cover -90°-90°. Shift to 0°-180° to match Cycles. */ + for (int i = 0; i < v_angles.size(); i++) { + v_angles[i] += 90.0f; + } + } +} + +void IESFile::process_type_a() +{ + /* Convert vertical angles - just a simple offset. */ + for (int i = 0; i < v_angles.size(); i++) { + v_angles[i] += 90.0f; + } + + vector new_h_angles; + new_h_angles.reserve(h_angles.size()); + vector> new_intensity; + new_intensity.reserve(h_angles.size()); + + /* Type A goes from -90° to 90°, which is mapped to 270° to 90° respectively in Type C. */ + for (int i = h_angles.size() - 1; i >= 0; i--) { + new_h_angles.push_back(180.0f - h_angles[i]); + new_intensity.push_back(intensity[i]); + } + + /* If the file angles start at 0°, we need to mirror around that. + * Since the negative input range (which we generate here) maps to 180° to 270°, + * it comes after the original entries in the output. */ + if (angle_close(h_angles[0], 0.0f)) { + new_h_angles.reserve(2 * h_angles.size() - 1); + new_intensity.reserve(2 * h_angles.size() - 1); + for (int i = 1; i < h_angles.size(); i++) { + new_h_angles.push_back(180.0f + h_angles[i]); + new_intensity.push_back(intensity[i]); + } + } + + h_angles.swap(new_h_angles); + intensity.swap(new_intensity); +} + +void IESFile::process_type_c() +{ + if (angle_close(h_angles[0], 90.0f)) { + /* Some files are stored from 90° to 270°, so rotate them to the regular 0°-180° range. */ + for (int i = 0; i < h_angles.size(); i++) { + h_angles[i] -= 90.0f; + } + } + + if (h_angles.size() == 1) { + h_angles[0] = 0.0f; + h_angles.push_back(360.0f); + intensity.push_back(intensity[0]); + } + + if (angle_close(h_angles[h_angles.size() - 1], 90.0f)) { + /* Only one quadrant is defined, so we need to mirror twice (from one to two, then to four). + * Since the two->four mirroring step might also be required if we get an input of two + * quadrants, we only do the first mirror here and later do the second mirror in either case. + */ + int hnum = h_angles.size(); + for (int i = hnum - 2; i >= 0; i--) { + h_angles.push_back(180.0f - h_angles[i]); + intensity.push_back(intensity[i]); + } + } + + if (angle_close(h_angles[h_angles.size() - 1], 180.0f)) { + /* Mirror half to the full range. */ + int hnum = h_angles.size(); + for (int i = hnum - 2; i >= 0; i--) { + h_angles.push_back(360.0f - h_angles[i]); + intensity.push_back(intensity[i]); + } + } + + /* Some files skip the 360° entry (contrary to standard) because it's supposed to be identical to + * the 0° entry. If the file has a discernible order in its spacing, just fix this. */ + if (angle_close(h_angles[0], 0.0f) && !angle_close(h_angles[h_angles.size() - 1], 360.0f)) { + int hnum = h_angles.size(); + float last_step = h_angles[hnum - 1] - h_angles[hnum - 2]; + float first_step = h_angles[1] - h_angles[0]; + float gap_step = 360.0f - h_angles[hnum - 1]; + if (angle_close(last_step, gap_step) || angle_close(first_step, gap_step)) { + h_angles.push_back(360.0f); + intensity.push_back(intensity[0]); + } + } +} + +bool IESFile::process() +{ + if (h_angles.size() == 0 || v_angles.size() == 0) { + return false; + } + + if (type == TYPE_A) { + process_type_a(); + } + else if (type == TYPE_B) { + process_type_b(); + } + else if (type == TYPE_C) { + process_type_c(); + } + else { + return false; + } + + /* Convert from deg to rad. */ + for (int i = 0; i < v_angles.size(); i++) { + v_angles[i] *= M_PI_F / 180.f; + } + for (int i = 0; i < h_angles.size(); i++) { + h_angles[i] *= M_PI_F / 180.f; + } + + return true; +} + +IESFile::~IESFile() +{ + clear(); +} + +CCL_NAMESPACE_END diff --git a/pxr/imaging/plugin/hdEmbree/pxrIES/ies.h b/pxr/imaging/plugin/hdEmbree/pxrIES/ies.h new file mode 100644 index 0000000000..8c506befdd --- /dev/null +++ b/pxr/imaging/plugin/hdEmbree/pxrIES/ies.h @@ -0,0 +1,46 @@ +/* SPDX-FileCopyrightText: 2011-2022 Blender Foundation + * + * SPDX-License-Identifier: Apache-2.0 */ + +#ifndef __UTIL_IES_H__ +#define __UTIL_IES_H__ + +#include "util/string.h" +#include "util/vector.h" + +CCL_NAMESPACE_BEGIN + +class IESFile { + public: + IESFile() {} + ~IESFile(); + + int packed_size(); + void pack(float *data); + + bool load(const string &ies); + void clear(); + + protected: + bool parse(const string &ies); + bool process(); + void process_type_a(); + void process_type_b(); + void process_type_c(); + + /* The brightness distribution is stored in spherical coordinates. + * The horizontal angles correspond to theta in the regular notation + * and always span the full range from 0° to 360°. + * The vertical angles correspond to phi and always start at 0°. */ + vector v_angles, h_angles; + /* The actual values are stored here, with every entry storing the values + * of one horizontal segment. */ + vector> intensity; + + /* Types of angle representation in IES files. */ + enum IESType { TYPE_A = 3, TYPE_B = 2, TYPE_C = 1 } type; +}; + +CCL_NAMESPACE_END + +#endif /* __UTIL_IES_H__ */ From 8a6e0ced26872ea988e1194debe5dd4ab67e519f Mon Sep 17 00:00:00 2001 From: Paul Molodowitch Date: Wed, 22 May 2024 12:22:37 -0700 Subject: [PATCH 14/19] [hdEmbree] ies.h / ies.cpp: patch, and add pxr-IES.patch and README.md --- pxr/imaging/plugin/hdEmbree/pxrIES/README.md | 32 +++++ pxr/imaging/plugin/hdEmbree/pxrIES/ies.cpp | 44 +++++-- pxr/imaging/plugin/hdEmbree/pxrIES/ies.h | 25 +++- .../plugin/hdEmbree/pxrIES/pxr-IES.patch | 123 ++++++++++++++++++ 4 files changed, 203 insertions(+), 21 deletions(-) create mode 100644 pxr/imaging/plugin/hdEmbree/pxrIES/README.md create mode 100644 pxr/imaging/plugin/hdEmbree/pxrIES/pxr-IES.patch diff --git a/pxr/imaging/plugin/hdEmbree/pxrIES/README.md b/pxr/imaging/plugin/hdEmbree/pxrIES/README.md new file mode 100644 index 0000000000..307db52715 --- /dev/null +++ b/pxr/imaging/plugin/hdEmbree/pxrIES/README.md @@ -0,0 +1,32 @@ +# IES utilities + +Utilities for reading and using .ies files (IESNA LM-63 Format), which are used +to describe lights. + +The files `ies.h` and `ies.cpp` are originally from +[Cycles](https://www.cycles-renderer.org/), a path-traced renderer that is a +spinoff of the larger [Blender](https://projects.blender.org/blender/blender/) +project, though available with in own repository, and via the Apache 2.0 +license: + +- https://projects.blender.org/blender/cycles +- https://projects.blender.org/blender/cycles/src/branch/main/LICENSE + +## Version + +v4.1.1 ( 234fa733d30a0e49cd10b2c92091500103a1150a ) + +## Setup + +When updating IES, the following steps should be followed: + +1. Copy `src/util/ies.h` and `src/util/ies.cpp` over the + copies in `pxr/imaging/plugin/hdEmbree/pxrIES`. +2. Apply `pxr-IES.patch` to update the source files with modifications for USD, + ie, from the USD repo root folder: + + ```sh + patch -p1 -i pxr/imaging/plugin/hdEmbree/pxrIES/pxr-IES.patch + ``` +3. Commit your changes, noting the exact version of blender that the new ies + files were copied from. \ No newline at end of file diff --git a/pxr/imaging/plugin/hdEmbree/pxrIES/ies.cpp b/pxr/imaging/plugin/hdEmbree/pxrIES/ies.cpp index a6725cc049..243680d918 100644 --- a/pxr/imaging/plugin/hdEmbree/pxrIES/ies.cpp +++ b/pxr/imaging/plugin/hdEmbree/pxrIES/ies.cpp @@ -2,21 +2,22 @@ * * SPDX-License-Identifier: Apache-2.0 */ +#include "ies.h" #include +#include -#include "util/foreach.h" -#include "util/ies.h" -#include "util/math.h" -#include "util/string.h" +#define _USE_MATH_DEFINES +#include -CCL_NAMESPACE_BEGIN +#if !defined(M_PI) +#define M_PI 3.14159265358979323846 +#endif -// NOTE: For some reason gcc-7.2 does not instantiate this version of the -// allocator here (used in IESTextParser). Works fine for gcc-6, gcc-7.3 and gcc-8. -// -// TODO(sergey): Get to the root of this issue, or confirm this is a compiler -// issue. -template class GuardedAllocator; +#define M_PI_F M_PI + +PXR_NAMESPACE_OPEN_SCOPE + +namespace pxr_ccl { bool IESFile::load(const string &ies) { @@ -43,11 +44,23 @@ int IESFile::packed_size() return 0; } + +static float sizet_to_float(const size_t source_size_t) noexcept +{ + int intermediate_int = static_cast(source_size_t); + float dest_float; + + static_assert(sizeof(intermediate_int) == sizeof(dest_float), + "Size of source and destination for memcpy must be identical"); + std::memcpy(&dest_float, &intermediate_int, sizeof(float)); + return dest_float; +} + void IESFile::pack(float *data) { if (v_angles.size() && h_angles.size()) { - *(data++) = __int_as_float(h_angles.size()); - *(data++) = __int_as_float(v_angles.size()); + *(data++) = sizet_to_float(h_angles.size()); + *(data++) = sizet_to_float(v_angles.size()); memcpy(data, &h_angles[0], h_angles.size() * sizeof(float)); data += h_angles.size(); @@ -407,4 +420,7 @@ IESFile::~IESFile() clear(); } -CCL_NAMESPACE_END +} // namespace pxr_ccl + +PXR_NAMESPACE_CLOSE_SCOPE + diff --git a/pxr/imaging/plugin/hdEmbree/pxrIES/ies.h b/pxr/imaging/plugin/hdEmbree/pxrIES/ies.h index 8c506befdd..0bbae712ff 100644 --- a/pxr/imaging/plugin/hdEmbree/pxrIES/ies.h +++ b/pxr/imaging/plugin/hdEmbree/pxrIES/ies.h @@ -2,13 +2,21 @@ * * SPDX-License-Identifier: Apache-2.0 */ -#ifndef __UTIL_IES_H__ -#define __UTIL_IES_H__ +#ifndef PXR_IMAGING_PLUGIN_HD_EMBREE_PXRIES_IES_H +#define PXR_IMAGING_PLUGIN_HD_EMBREE_PXRIES_IES_H -#include "util/string.h" -#include "util/vector.h" -CCL_NAMESPACE_BEGIN +#include +#include + +#include "pxr/pxr.h" + +PXR_NAMESPACE_OPEN_SCOPE + +namespace pxr_ccl { + +using std::string; +using std::vector; class IESFile { public: @@ -24,6 +32,7 @@ class IESFile { protected: bool parse(const string &ies); bool process(); + void process_type_a(); void process_type_b(); void process_type_c(); @@ -41,6 +50,8 @@ class IESFile { enum IESType { TYPE_A = 3, TYPE_B = 2, TYPE_C = 1 } type; }; -CCL_NAMESPACE_END +} /* namespace pxr_ccl */ + +PXR_NAMESPACE_CLOSE_SCOPE -#endif /* __UTIL_IES_H__ */ +#endif /* PXR_IMAGING_PLUGIN_HD_EMBREE_PXRIES_IES_H */ diff --git a/pxr/imaging/plugin/hdEmbree/pxrIES/pxr-IES.patch b/pxr/imaging/plugin/hdEmbree/pxrIES/pxr-IES.patch new file mode 100644 index 0000000000..50f8ead381 --- /dev/null +++ b/pxr/imaging/plugin/hdEmbree/pxrIES/pxr-IES.patch @@ -0,0 +1,123 @@ +diff --git a/pxr/imaging/plugin/hdEmbree/pxrIES/ies.cpp b/pxr/imaging/plugin/hdEmbree/pxrIES/ies.cpp +index a6725cc04..243680d91 100644 +--- a/pxr/imaging/plugin/hdEmbree/pxrIES/ies.cpp ++++ b/pxr/imaging/plugin/hdEmbree/pxrIES/ies.cpp +@@ -2,21 +2,22 @@ + * + * SPDX-License-Identifier: Apache-2.0 */ + ++#include "ies.h" + #include ++#include + +-#include "util/foreach.h" +-#include "util/ies.h" +-#include "util/math.h" +-#include "util/string.h" ++#define _USE_MATH_DEFINES ++#include + +-CCL_NAMESPACE_BEGIN ++#if !defined(M_PI) ++#define M_PI 3.14159265358979323846 ++#endif + +-// NOTE: For some reason gcc-7.2 does not instantiate this version of the +-// allocator here (used in IESTextParser). Works fine for gcc-6, gcc-7.3 and gcc-8. +-// +-// TODO(sergey): Get to the root of this issue, or confirm this is a compiler +-// issue. +-template class GuardedAllocator; ++#define M_PI_F M_PI ++ ++PXR_NAMESPACE_OPEN_SCOPE ++ ++namespace pxr_ccl { + + bool IESFile::load(const string &ies) + { +@@ -43,11 +44,23 @@ int IESFile::packed_size() + return 0; + } + ++ ++static float sizet_to_float(const size_t source_size_t) noexcept ++{ ++ int intermediate_int = static_cast(source_size_t); ++ float dest_float; ++ ++ static_assert(sizeof(intermediate_int) == sizeof(dest_float), ++ "Size of source and destination for memcpy must be identical"); ++ std::memcpy(&dest_float, &intermediate_int, sizeof(float)); ++ return dest_float; ++} ++ + void IESFile::pack(float *data) + { + if (v_angles.size() && h_angles.size()) { +- *(data++) = __int_as_float(h_angles.size()); +- *(data++) = __int_as_float(v_angles.size()); ++ *(data++) = sizet_to_float(h_angles.size()); ++ *(data++) = sizet_to_float(v_angles.size()); + + memcpy(data, &h_angles[0], h_angles.size() * sizeof(float)); + data += h_angles.size(); +@@ -407,4 +420,7 @@ IESFile::~IESFile() + clear(); + } + +-CCL_NAMESPACE_END ++} // namespace pxr_ccl ++ ++PXR_NAMESPACE_CLOSE_SCOPE ++ +diff --git a/pxr/imaging/plugin/hdEmbree/pxrIES/ies.h b/pxr/imaging/plugin/hdEmbree/pxrIES/ies.h +index 8c506befd..0bbae712f 100644 +--- a/pxr/imaging/plugin/hdEmbree/pxrIES/ies.h ++++ b/pxr/imaging/plugin/hdEmbree/pxrIES/ies.h +@@ -2,13 +2,21 @@ + * + * SPDX-License-Identifier: Apache-2.0 */ + +-#ifndef __UTIL_IES_H__ +-#define __UTIL_IES_H__ ++#ifndef PXR_IMAGING_PLUGIN_HD_EMBREE_PXRIES_IES_H ++#define PXR_IMAGING_PLUGIN_HD_EMBREE_PXRIES_IES_H + +-#include "util/string.h" +-#include "util/vector.h" + +-CCL_NAMESPACE_BEGIN ++#include ++#include ++ ++#include "pxr/pxr.h" ++ ++PXR_NAMESPACE_OPEN_SCOPE ++ ++namespace pxr_ccl { ++ ++using std::string; ++using std::vector; + + class IESFile { + public: +@@ -24,6 +32,7 @@ class IESFile { + protected: + bool parse(const string &ies); + bool process(); ++ + void process_type_a(); + void process_type_b(); + void process_type_c(); +@@ -41,6 +50,8 @@ class IESFile { + enum IESType { TYPE_A = 3, TYPE_B = 2, TYPE_C = 1 } type; + }; + +-CCL_NAMESPACE_END ++} /* namespace pxr_ccl */ ++ ++PXR_NAMESPACE_CLOSE_SCOPE + +-#endif /* __UTIL_IES_H__ */ ++#endif /* PXR_IMAGING_PLUGIN_HD_EMBREE_PXRIES_IES_H */ From b1765a86c6d2239da7ddec83ae1b7931d6894290 Mon Sep 17 00:00:00 2001 From: Paul Molodowitch Date: Wed, 5 Jun 2024 14:21:40 -0700 Subject: [PATCH 15/19] [hdEmbree] build ies.cpp --- pxr/imaging/plugin/hdEmbree/CMakeLists.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pxr/imaging/plugin/hdEmbree/CMakeLists.txt b/pxr/imaging/plugin/hdEmbree/CMakeLists.txt index 98f745631e..22ba8bc76b 100644 --- a/pxr/imaging/plugin/hdEmbree/CMakeLists.txt +++ b/pxr/imaging/plugin/hdEmbree/CMakeLists.txt @@ -50,8 +50,12 @@ pxr_plugin(hdEmbree implicitSurfaceSceneIndexPlugin PRIVATE_HEADERS + pxrIES/ies.h pxrPbrt/pbrtUtils.h + CPPFILES + pxrIES/ies.cpp + RESOURCE_FILES plugInfo.json From e6200648637ca7e777c2e718462240a463fd077d Mon Sep 17 00:00:00 2001 From: Paul Molodowitch Date: Wed, 5 Jun 2024 16:28:51 -0700 Subject: [PATCH 16/19] [hdEmbree] ies: disable cycles' candela-to-watt conversion factor by default ...since, unlike Cycles, we intend to work in candela --- pxr/imaging/plugin/hdEmbree/pxrIES/ies.cpp | 4 ++++ .../plugin/hdEmbree/pxrIES/pxr-IES.patch | 22 +++++++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/pxr/imaging/plugin/hdEmbree/pxrIES/ies.cpp b/pxr/imaging/plugin/hdEmbree/pxrIES/ies.cpp index 243680d918..eadcb51698 100644 --- a/pxr/imaging/plugin/hdEmbree/pxrIES/ies.cpp +++ b/pxr/imaging/plugin/hdEmbree/pxrIES/ies.cpp @@ -179,6 +179,8 @@ bool IESFile::parse(const string &ies) factor *= parser.get_double(); /* Ballast-Lamp Photometric factor */ parser.get_double(); /* Input Watts */ +#ifdef PXR_IES_USE_CANDELA_TO_WATT_MULTIPLIER + /* Intensity values in IES files are specified in candela (lumen/sr), a photometric quantity. * Cycles expects radiometric quantities, though, which requires a conversion. * However, the Luminous efficacy (ratio of lumens per Watt) depends on the spectral distribution @@ -193,6 +195,8 @@ bool IESFile::parse(const string &ies) */ factor *= 0.0706650768394; +#endif //PXR_IES_USE_CANDELA_TO_WATT_MULTIPLIER + v_angles.reserve(v_angles_num); for (int i = 0; i < v_angles_num; i++) { v_angles.push_back((float)parser.get_double()); diff --git a/pxr/imaging/plugin/hdEmbree/pxrIES/pxr-IES.patch b/pxr/imaging/plugin/hdEmbree/pxrIES/pxr-IES.patch index 50f8ead381..1420f27361 100644 --- a/pxr/imaging/plugin/hdEmbree/pxrIES/pxr-IES.patch +++ b/pxr/imaging/plugin/hdEmbree/pxrIES/pxr-IES.patch @@ -1,5 +1,5 @@ diff --git a/pxr/imaging/plugin/hdEmbree/pxrIES/ies.cpp b/pxr/imaging/plugin/hdEmbree/pxrIES/ies.cpp -index a6725cc04..243680d91 100644 +index a6725cc04..eadcb5169 100644 --- a/pxr/imaging/plugin/hdEmbree/pxrIES/ies.cpp +++ b/pxr/imaging/plugin/hdEmbree/pxrIES/ies.cpp @@ -2,21 +2,22 @@ @@ -62,7 +62,25 @@ index a6725cc04..243680d91 100644 memcpy(data, &h_angles[0], h_angles.size() * sizeof(float)); data += h_angles.size(); -@@ -407,4 +420,7 @@ IESFile::~IESFile() +@@ -166,6 +179,8 @@ bool IESFile::parse(const string &ies) + factor *= parser.get_double(); /* Ballast-Lamp Photometric factor */ + parser.get_double(); /* Input Watts */ + ++#ifdef PXR_IES_USE_CANDELA_TO_WATT_MULTIPLIER ++ + /* Intensity values in IES files are specified in candela (lumen/sr), a photometric quantity. + * Cycles expects radiometric quantities, though, which requires a conversion. + * However, the Luminous efficacy (ratio of lumens per Watt) depends on the spectral distribution +@@ -180,6 +195,8 @@ bool IESFile::parse(const string &ies) + */ + factor *= 0.0706650768394; + ++#endif //PXR_IES_USE_CANDELA_TO_WATT_MULTIPLIER ++ + v_angles.reserve(v_angles_num); + for (int i = 0; i < v_angles_num; i++) { + v_angles.push_back((float)parser.get_double()); +@@ -407,4 +424,7 @@ IESFile::~IESFile() clear(); } From 85ebf2cee6d1c8adff5399eb94e02986d550022c Mon Sep 17 00:00:00 2001 From: Paul Molodowitch Date: Wed, 5 Jun 2024 17:23:34 -0700 Subject: [PATCH 17/19] [hdEmbree] ies: add PxrIESFile subclass, which calculates power via integration --- pxr/imaging/plugin/hdEmbree/CMakeLists.txt | 2 + pxr/imaging/plugin/hdEmbree/pxrIES/pxrIES.cpp | 78 +++++++++++++++++++ pxr/imaging/plugin/hdEmbree/pxrIES/pxrIES.h | 47 +++++++++++ 3 files changed, 127 insertions(+) create mode 100644 pxr/imaging/plugin/hdEmbree/pxrIES/pxrIES.cpp create mode 100644 pxr/imaging/plugin/hdEmbree/pxrIES/pxrIES.h diff --git a/pxr/imaging/plugin/hdEmbree/CMakeLists.txt b/pxr/imaging/plugin/hdEmbree/CMakeLists.txt index 22ba8bc76b..8e4a5d68b8 100644 --- a/pxr/imaging/plugin/hdEmbree/CMakeLists.txt +++ b/pxr/imaging/plugin/hdEmbree/CMakeLists.txt @@ -51,10 +51,12 @@ pxr_plugin(hdEmbree PRIVATE_HEADERS pxrIES/ies.h + pxrIES/pxrIES.h pxrPbrt/pbrtUtils.h CPPFILES pxrIES/ies.cpp + pxrIES/pxrIES.cpp RESOURCE_FILES plugInfo.json diff --git a/pxr/imaging/plugin/hdEmbree/pxrIES/pxrIES.cpp b/pxr/imaging/plugin/hdEmbree/pxrIES/pxrIES.cpp new file mode 100644 index 0000000000..407ae08e67 --- /dev/null +++ b/pxr/imaging/plugin/hdEmbree/pxrIES/pxrIES.cpp @@ -0,0 +1,78 @@ +// +// Copyright 2024 Pixar +// +// Licensed under the terms set forth in the LICENSE.txt file available at +// https://openusd.org/license. +// +#include "pxr/imaging/plugin/hdEmbree/pxrIES/pxrIES.h" + +#include + + +#define _USE_MATH_DEFINES +#include + +#if !defined(M_PI) +#define M_PI 3.14159265358979323846 +#endif + + +PXR_NAMESPACE_OPEN_SCOPE + +bool +PxrIESFile::load(std::string const& ies) // non-virtual "override" +{ + clear(); + if (!Base::load(ies)) { + return false; + } + pxr_extra_process(); + return true; +} + +void +PxrIESFile::clear() // non-virtual "override" +{ + Base::clear(); + _power = 0; +} + +void +PxrIESFile::pxr_extra_process() +{ + // find max v_angle delta, as a way to estimate whether the distribution + // is over a hemisphere or sphere + const auto [v_angleMin, v_angleMax] = std::minmax_element( + v_angles.cbegin(), v_angles.cend()); + + // does the distribution cover the whole sphere? + bool is_sphere = false; + if ((*v_angleMax - *v_angleMin) > (M_PI / 2 + 0.1 /* fudge factor*/)) { + is_sphere = true; + } + + _power = 0; + + // integrate the intensity over solid angle to get power + for (size_t h = 0; h < h_angles.size() - 1; ++h) { + for (size_t v = 0; v < v_angles.size() - 1; ++v) { + // approximate dimensions of the patch + float dh = h_angles[h + 1] - h_angles[h]; + float dv = v_angles[v + 1] - v_angles[v]; + // bilinearly interpolate intensity at the patch center + float i0 = (intensity[h][v] + intensity[h][v + 1]) / 2; + float i1 = + (intensity[h + 1][v] + intensity[h + 1][v + 1]) / 2; + float center_intensity = (i0 + i1) / 2; + // solid angle of the patch + float dS = dh * dv * sinf(v_angles[v] + dv / 2); + _power += dS * center_intensity; + } + } + + // ...and divide by surface area of a unit sphere (or hemisphere) + // (this result matches Karma & RIS) + _power /= M_PI * (is_sphere ? 4 : 2); +} + +PXR_NAMESPACE_CLOSE_SCOPE diff --git a/pxr/imaging/plugin/hdEmbree/pxrIES/pxrIES.h b/pxr/imaging/plugin/hdEmbree/pxrIES/pxrIES.h new file mode 100644 index 0000000000..a406b62531 --- /dev/null +++ b/pxr/imaging/plugin/hdEmbree/pxrIES/pxrIES.h @@ -0,0 +1,47 @@ +// +// Copyright 2024 Pixar +// +// Licensed under the terms set forth in the LICENSE.txt file available at +// https://openusd.org/license. +// +#ifndef PXR_IMAGING_PLUGIN_HD_EMBREE_PXRIES_PXRIES_H +#define PXR_IMAGING_PLUGIN_HD_EMBREE_PXRIES_PXRIES_H + +#include "pxr/pxr.h" +#include "pxr/imaging/plugin/hdEmbree/pxrIES/ies.h" + +#include + +PXR_NAMESPACE_OPEN_SCOPE + +/// \class PxrIESFile +/// +/// Extends / overrides some functionality of standard IESFile. +/// +class PxrIESFile : public pxr_ccl::IESFile { +private: + using Base = pxr_ccl::IESFile; + +public: + + bool load(std::string const& ies); // non-virtual "override" + void clear(); // non-virtual "override" + + /// \brief The light's power, as calculated when parsing + inline float power() const + { + return _power; + } + +protected: + // Extra processing we do on-top of the "standard" process() from IESFile + void pxr_extra_process(); + +private: + + float _power = 0; +}; + +PXR_NAMESPACE_CLOSE_SCOPE + +#endif // PXR_IMAGING_PLUGIN_HD_EMBREE_PXRIES_PXRIES_H From fd70f4c32fc6fe5841ec2425a6c06eb00bab2f2c Mon Sep 17 00:00:00 2001 From: Anders Langlands Date: Tue, 21 May 2024 12:22:04 -0700 Subject: [PATCH 18/19] [hdEmbree] PxrIESFile: add eval() and valid() methods --- pxr/imaging/plugin/hdEmbree/pxrIES/pxrIES.cpp | 104 ++++++++++++++++-- pxr/imaging/plugin/hdEmbree/pxrIES/pxrIES.h | 10 ++ 2 files changed, 107 insertions(+), 7 deletions(-) diff --git a/pxr/imaging/plugin/hdEmbree/pxrIES/pxrIES.cpp b/pxr/imaging/plugin/hdEmbree/pxrIES/pxrIES.cpp index 407ae08e67..b05dbe78af 100644 --- a/pxr/imaging/plugin/hdEmbree/pxrIES/pxrIES.cpp +++ b/pxr/imaging/plugin/hdEmbree/pxrIES/pxrIES.cpp @@ -6,6 +6,8 @@ // #include "pxr/imaging/plugin/hdEmbree/pxrIES/pxrIES.h" +#include "pxr/base/gf/math.h" + #include @@ -16,11 +18,43 @@ #define M_PI 3.14159265358979323846 #endif +namespace { + +// ------------------------------------------------------------------------- +// Constants +// ------------------------------------------------------------------------- + +template +constexpr T _pi = static_cast(M_PI); + +constexpr float _hemisphereFudgeFactor = 0.1f; + +// ------------------------------------------------------------------------- +// Utility functions +// ------------------------------------------------------------------------- + + +float +_linearstep(float x, float a, float b) +{ + if (x <= a) { + return 0.0f; + } + + if (x >= b) { + return 1.0f; + } + + return (x - a) / (b - a); +} + +} // anonymous namespace + PXR_NAMESPACE_OPEN_SCOPE bool -PxrIESFile::load(std::string const& ies) // non-virtual "override" +PxrIESFile::load(const std::string &ies) // non-virtual "override" { clear(); if (!Base::load(ies)) { @@ -47,7 +81,8 @@ PxrIESFile::pxr_extra_process() // does the distribution cover the whole sphere? bool is_sphere = false; - if ((*v_angleMax - *v_angleMin) > (M_PI / 2 + 0.1 /* fudge factor*/)) { + if ((*v_angleMax - *v_angleMin) + > (_pi / 2.0f + _hemisphereFudgeFactor)) { is_sphere = true; } @@ -60,19 +95,74 @@ PxrIESFile::pxr_extra_process() float dh = h_angles[h + 1] - h_angles[h]; float dv = v_angles[v + 1] - v_angles[v]; // bilinearly interpolate intensity at the patch center - float i0 = (intensity[h][v] + intensity[h][v + 1]) / 2; + float i0 = (intensity[h][v] + intensity[h][v + 1]) / 2.0f; float i1 = - (intensity[h + 1][v] + intensity[h + 1][v + 1]) / 2; - float center_intensity = (i0 + i1) / 2; + (intensity[h + 1][v] + intensity[h + 1][v + 1]) / 2.0f; + float center_intensity = (i0 + i1) / 2.0f; // solid angle of the patch - float dS = dh * dv * sinf(v_angles[v] + dv / 2); + float dS = dh * dv * sinf(v_angles[v] + dv / 2.0f); _power += dS * center_intensity; } } // ...and divide by surface area of a unit sphere (or hemisphere) // (this result matches Karma & RIS) - _power /= M_PI * (is_sphere ? 4 : 2); + _power /= _pi * (is_sphere ? 4.0f : 2.0f); +} + +float +PxrIESFile::eval(float theta, float phi, float angleScale) const +{ + int hi = -1; + int vi = -1; + float dh = 0.0f; + float dv = 0.0f; + + phi = GfMod(phi, 2.0f * _pi); + for (size_t i = 0; i < h_angles.size() - 1; ++i) { + if (phi >= h_angles[i] && phi < h_angles[i + 1]) { + hi = i; + dh = _linearstep(phi, h_angles[i], h_angles[i + 1]); + break; + } + } + + // This formula matches Renderman's behavior + + // Scale with origin at "top" (ie, 180 degress / pi), by a factor + // of 1 / (1 + angleScale), offset so that angleScale = 0 yields the + // identity function. + const float profileScale = 1.0f + angleScale; + theta = (theta - _pi) / profileScale + _pi; + theta = GfClamp(theta, 0.0f, _pi); + + if (theta < 0) { + // vi = 0; + // dv = 0; + return 0.0f; + } else if (theta >= _pi) { + vi = v_angles.size() - 2; + dv = 1; + } else { + for (size_t i = 0; i < v_angles.size() - 1; ++i) { + if (theta >= v_angles[i] && theta < v_angles[i + 1]) { + vi = i; + dv = _linearstep(theta, v_angles[i], v_angles[i + 1]); + break; + } + } + } + + if (hi == -1 || vi == -1) { + // XXX: need to indicate error somehow here + return 0.0f; + } + + // XXX: This should be a cubic interpolation + float i0 = GfLerp(dv, intensity[hi][vi], intensity[hi][vi + 1]); + float i1 = GfLerp(dv, intensity[hi + 1][vi], intensity[hi + 1][vi + 1]); + + return GfLerp(dh, i0, i1); } PXR_NAMESPACE_CLOSE_SCOPE diff --git a/pxr/imaging/plugin/hdEmbree/pxrIES/pxrIES.h b/pxr/imaging/plugin/hdEmbree/pxrIES/pxrIES.h index a406b62531..3378146df2 100644 --- a/pxr/imaging/plugin/hdEmbree/pxrIES/pxrIES.h +++ b/pxr/imaging/plugin/hdEmbree/pxrIES/pxrIES.h @@ -33,6 +33,16 @@ class PxrIESFile : public pxr_ccl::IESFile { return _power; } + // returns true if the IES files was successfully loaded and processed and + // is ready to evaluate + bool valid() const + { + return !intensity.empty(); + } + + // evaluate the IES file for the given spherical coordinates + float eval(float theta, float phi, float angleScale) const; + protected: // Extra processing we do on-top of the "standard" process() from IESFile void pxr_extra_process(); From cf4f1d3e5605a469d0c5766d6a9d1d682fa41a85 Mon Sep 17 00:00:00 2001 From: Paul Molodowitch Date: Mon, 29 Jul 2024 23:27:05 -0700 Subject: [PATCH 19/19] [hdEmbree] add lighting support for IES files --- pxr/imaging/plugin/hdEmbree/light.cpp | 36 ++++++++++++++++++++++++ pxr/imaging/plugin/hdEmbree/light.h | 10 +++++++ pxr/imaging/plugin/hdEmbree/renderer.cpp | 24 ++++++++++++++++ 3 files changed, 70 insertions(+) diff --git a/pxr/imaging/plugin/hdEmbree/light.cpp b/pxr/imaging/plugin/hdEmbree/light.cpp index 2388d346fe..6885d20965 100644 --- a/pxr/imaging/plugin/hdEmbree/light.cpp +++ b/pxr/imaging/plugin/hdEmbree/light.cpp @@ -233,6 +233,42 @@ HdEmbree_Light::Sync(HdSceneDelegate *sceneDelegate, _lightData.shaping.coneSoftness = value.UncheckedGet(); } + if (const auto value = sceneDelegate->GetLightParamValue( + id, HdLightTokens->shapingIesFile); + value.IsHolding()) { + SdfAssetPath iesAssetPath = value.UncheckedGet(); + std::string iesPath = iesAssetPath.GetResolvedPath(); + if (iesPath.empty()) { + iesPath = iesAssetPath.GetAssetPath(); + } + + if (!iesPath.empty()) { + std::ifstream in(iesPath); + if (!in.is_open()) { + TF_WARN("could not open ies file %s", iesPath.c_str()); + } else { + std::stringstream buffer; + buffer << in.rdbuf(); + + if (!_lightData.shaping.ies.iesFile.load(buffer.str())) { + TF_WARN("could not load ies file %s", iesPath.c_str()); + } + } + } + } + + if (const auto value = sceneDelegate->GetLightParamValue( + id, HdLightTokens->shapingIesNormalize); + value.IsHolding()) { + _lightData.shaping.ies.normalize = value.UncheckedGet(); + } + + if (const auto value = sceneDelegate->GetLightParamValue( + id, HdLightTokens->shapingIesAngleScale); + value.IsHolding()) { + _lightData.shaping.ies.angleScale = value.UncheckedGet(); + } + _PopulateRtcLight(device, scene); HdEmbreeRenderer *renderer = embreeRenderParam->GetRenderer(); diff --git a/pxr/imaging/plugin/hdEmbree/light.h b/pxr/imaging/plugin/hdEmbree/light.h index 3f756ebedf..1020de3cea 100644 --- a/pxr/imaging/plugin/hdEmbree/light.h +++ b/pxr/imaging/plugin/hdEmbree/light.h @@ -7,6 +7,8 @@ #ifndef PXR_IMAGING_PLUGIN_HD_EMBREE_LIGHT_H #define PXR_IMAGING_PLUGIN_HD_EMBREE_LIGHT_H +#include "pxr/imaging/plugin/hdEmbree/pxrIES/pxrIES.h" + #include "pxr/base/gf/vec3f.h" #include "pxr/base/gf/matrix3f.h" #include "pxr/base/gf/matrix4f.h" @@ -71,12 +73,20 @@ struct HdEmbree_LightTexture int height = 0; }; +struct HdEmbree_IES +{ + PxrIESFile iesFile; + bool normalize = false; + float angleScale = 0.0f; +}; + struct HdEmbree_Shaping { GfVec3f focusTint; float focus = 0.0f; float coneAngle = 180.0f; float coneSoftness = 0.0f; + HdEmbree_IES ies; }; struct HdEmbree_LightData diff --git a/pxr/imaging/plugin/hdEmbree/renderer.cpp b/pxr/imaging/plugin/hdEmbree/renderer.cpp index 226549c41c..17d4367273 100644 --- a/pxr/imaging/plugin/hdEmbree/renderer.cpp +++ b/pxr/imaging/plugin/hdEmbree/renderer.cpp @@ -437,6 +437,27 @@ _IntersectAreaLight(HdEmbree_LightData const& light, RTCRayHit const& rayHit) }; } +float +_EvalIES(HdEmbree_LightData const& light, GfVec3f const& wI) +{ + HdEmbree_IES const& ies = light.shaping.ies; + + if (!ies.iesFile.valid()) { + // Either none specified or there was an error loading. In either case, + // just ignore + return 1.0f; + } + + // emission direction in light space + GfVec3f wE = light.xformWorldToLight.TransformDir(wI).GetNormalized(); + + float theta = _Theta(wE); + float phi = _Phi(wE); + float norm = ies.normalize ? ies.iesFile.power() : 1.0f; + + return ies.iesFile.eval(theta, phi, ies.angleScale) / norm; +} + GfVec3f _EvalLightBasic(HdEmbree_LightData const& light) { @@ -543,6 +564,9 @@ _EvalAreaLight(HdEmbree_LightData const& light, _ShapeSample const& ss, const float thetaOffZ = acosf(cosThetaOffZ); Le *= 1.0f - _Smoothstep(thetaOffZ, GfRange1f(thetaSoft, thetaCone)); + // Apply IES + Le *= _EvalIES(light, wI); + return _LightSample { Le, wI,