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/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"), 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()); } diff --git a/pxr/imaging/plugin/hdEmbree/CMakeLists.txt b/pxr/imaging/plugin/hdEmbree/CMakeLists.txt index 43aa0826e5..8e4a5d68b8 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 @@ -45,8 +46,18 @@ pxr_plugin(hdEmbree renderParam.h PRIVATE_CLASSES + debugCodes implicitSurfaceSceneIndexPlugin + 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/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/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 new file mode 100644 index 0000000000..6885d20965 --- /dev/null +++ b/pxr/imaging/plugin/hdEmbree/light.cpp @@ -0,0 +1,365 @@ +// +// 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 +#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 + +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()) { + 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(); + } 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) { + // 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 + RTCScene scene = embreeRenderParam->AcquireSceneForEdit(); + RTCDevice device = embreeRenderParam->GetEmbreeDevice(); + + 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.diffuse = sceneDelegate->GetLightParamValue( + id, HdLightTokens->diffuse).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); + _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 + 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_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); + } else if constexpr (std::is_same_v) { + typedLight = HdEmbree_Rect{ + sceneDelegate->GetLightParamValue(id, HdLightTokens->width) + .Get(), + 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) + .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(); + } + + 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(); + 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 +{ + return HdLight::AllDirty; +} + +void +HdEmbree_Light::Finalize(HdRenderParam *renderParam) +{ + auto* embreeParam = static_cast(renderParam); + RTCScene scene = embreeParam->AcquireSceneForEdit(); + + // 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 new file mode 100644 index 0000000000..1020de3cea --- /dev/null +++ b/pxr/imaging/plugin/hdEmbree/light.h @@ -0,0 +1,149 @@ +// +// 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/imaging/plugin/hdEmbree/pxrIES/pxrIES.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_Distant +{ + float halfAngleRadians; +}; + +// Needed for HdEmbree_LightVariant +struct HdEmbree_Dome +{}; + +struct HdEmbree_Rect +{ + float width; + float height; +}; + +struct HdEmbree_Sphere +{ + float radius; +}; + +using HdEmbree_LightVariant = std::variant< + HdEmbree_UnknownLight, + HdEmbree_Cylinder, + HdEmbree_Disk, + HdEmbree_Distant, + HdEmbree_Dome, + HdEmbree_Rect, + HdEmbree_Sphere>; + +struct HdEmbree_LightTexture +{ + std::vector pixels; + int width = 0; + 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 +{ + GfMatrix4f xformLightToWorld; + GfMatrix3f normalXformLightToWorld; + GfMatrix4f xformWorldToLight; + GfVec3f color; + HdEmbree_LightTexture texture; + float intensity = 1.0f; + float diffuse = 1.0f; + float exposure = 0.0f; + float colorTemperature = 6500.0f; + bool enableColorTemperature = false; + 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 +{ +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; + } + + bool IsDome() const { + return std::holds_alternative(_lightData.lightVariant); + } + +private: + void _PopulateRtcLight(RTCDevice device, RTCScene scene); + + HdEmbree_LightData _lightData; +}; + + +PXR_NAMESPACE_CLOSE_SCOPE + +#endif \ No newline at end of file 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/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/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 new file mode 100644 index 0000000000..eadcb51698 --- /dev/null +++ b/pxr/imaging/plugin/hdEmbree/pxrIES/ies.cpp @@ -0,0 +1,430 @@ +/* SPDX-FileCopyrightText: 2011-2022 Blender Foundation + * + * SPDX-License-Identifier: Apache-2.0 */ + +#include "ies.h" +#include +#include + +#define _USE_MATH_DEFINES +#include + +#if !defined(M_PI) +#define M_PI 3.14159265358979323846 +#endif + +#define M_PI_F M_PI + +PXR_NAMESPACE_OPEN_SCOPE + +namespace pxr_ccl { + +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; +} + + +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++) = 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(); + 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 */ + +#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 + * 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; + +#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()); + } + + 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(); +} + +} // 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 new file mode 100644 index 0000000000..0bbae712ff --- /dev/null +++ b/pxr/imaging/plugin/hdEmbree/pxrIES/ies.h @@ -0,0 +1,57 @@ +/* SPDX-FileCopyrightText: 2011-2022 Blender Foundation + * + * SPDX-License-Identifier: Apache-2.0 */ + +#ifndef PXR_IMAGING_PLUGIN_HD_EMBREE_PXRIES_IES_H +#define PXR_IMAGING_PLUGIN_HD_EMBREE_PXRIES_IES_H + + +#include +#include + +#include "pxr/pxr.h" + +PXR_NAMESPACE_OPEN_SCOPE + +namespace pxr_ccl { + +using std::string; +using std::vector; + +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; +}; + +} /* namespace pxr_ccl */ + +PXR_NAMESPACE_CLOSE_SCOPE + +#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..1420f27361 --- /dev/null +++ b/pxr/imaging/plugin/hdEmbree/pxrIES/pxr-IES.patch @@ -0,0 +1,141 @@ +diff --git a/pxr/imaging/plugin/hdEmbree/pxrIES/ies.cpp b/pxr/imaging/plugin/hdEmbree/pxrIES/ies.cpp +index a6725cc04..eadcb5169 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(); +@@ -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(); + } + +-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 */ diff --git a/pxr/imaging/plugin/hdEmbree/pxrIES/pxrIES.cpp b/pxr/imaging/plugin/hdEmbree/pxrIES/pxrIES.cpp new file mode 100644 index 0000000000..b05dbe78af --- /dev/null +++ b/pxr/imaging/plugin/hdEmbree/pxrIES/pxrIES.cpp @@ -0,0 +1,168 @@ +// +// 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 "pxr/base/gf/math.h" + +#include + + +#define _USE_MATH_DEFINES +#include + +#if !defined(M_PI) +#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(const std::string &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) + > (_pi / 2.0f + _hemisphereFudgeFactor)) { + 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.0f; + float i1 = + (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.0f); + _power += dS * center_intensity; + } + } + + // ...and divide by surface area of a unit sphere (or hemisphere) + // (this result matches Karma & RIS) + _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 new file mode 100644 index 0000000000..3378146df2 --- /dev/null +++ b/pxr/imaging/plugin/hdEmbree/pxrIES/pxrIES.h @@ -0,0 +1,57 @@ +// +// 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; + } + + // 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(); + +private: + + float _power = 0; +}; + +PXR_NAMESPACE_CLOSE_SCOPE + +#endif // PXR_IMAGING_PLUGIN_HD_EMBREE_PXRIES_PXRIES_H 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 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..3050fc6d03 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,12 @@ const TfTokenVector HdEmbreeRenderDelegate::SUPPORTED_SPRIM_TYPES = { HdPrimTypeTokens->camera, HdPrimTypeTokens->extComputation, + HdPrimTypeTokens->cylinderLight, + HdPrimTypeTokens->diskLight, + HdPrimTypeTokens->distantLight, + HdPrimTypeTokens->domeLight, + HdPrimTypeTokens->rectLight, + HdPrimTypeTokens->sphereLight, }; const TfTokenVector HdEmbreeRenderDelegate::SUPPORTED_BPRIM_TYPES = @@ -147,7 +154,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 +237,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 +338,14 @@ 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->distantLight || + typeId == HdPrimTypeTokens->diskLight || + typeId == HdPrimTypeTokens->domeLight || + 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 +362,14 @@ 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->distantLight || + typeId == HdPrimTypeTokens->diskLight || + typeId == HdPrimTypeTokens->domeLight || + 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 88d5e79093..17d4367273 100644 --- a/pxr/imaging/plugin/hdEmbree/renderer.cpp +++ b/pxr/imaging/plugin/hdEmbree/renderer.cpp @@ -6,26 +6,224 @@ // #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/pxrPbrt/pbrtUtils.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 +// ------------------------------------------------------------------------- +// 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 + +// ------------------------------------------------------------------------- +// 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 // ------------------------------------------------------------------------- @@ -38,6 +236,479 @@ _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; +}; + +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) +{ + // 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 + }; +} + +_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) + }; +} + +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) +{ + // 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)); + } + 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) +{ + // 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); + + // 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 + 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)); + + // Apply IES + Le *= _EvalIES(light, wI); + + return _LightSample { + Le, + wI, + dist, + invPdfW + }; +} + +_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, + 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); + } + + _LightSample operator()(HdEmbree_Dome const& dome) { + 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, + 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 @@ -130,6 +801,29 @@ HdEmbreeRenderer::SetAovBindings( _aovBindingsNeedValidation = true; } + +void +HdEmbreeRenderer::AddLight(SdfPath const& lightPath, + HdEmbree_Light* light) +{ + ScopedLock lightsWriteLock(_lightsWriteMutex); + _lightMap[lightPath] = light; + + if (light->IsDome()) { + _domes.push_back(light); + } +} + +void +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 HdEmbreeRenderer::_ValidateAovBindings() { @@ -371,7 +1065,7 @@ _IsContained(const GfRect2i &rect, int width, int height) } void -HdEmbreeRenderer::Render(HdRenderThread *renderThread) +HdEmbreeRenderer::_PreRenderSetup() { _completedSamples.store(0); @@ -422,9 +1116,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 @@ -453,6 +1152,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, @@ -533,7 +1235,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) { @@ -557,7 +1261,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) { @@ -571,25 +1274,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. @@ -607,7 +1310,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]; @@ -619,25 +1324,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]. @@ -648,7 +1354,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; @@ -657,6 +1363,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, @@ -665,7 +1408,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); @@ -686,6 +1431,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 = @@ -741,16 +1493,23 @@ 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. 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(); @@ -779,6 +1538,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); @@ -802,15 +1566,23 @@ 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 = 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); @@ -837,31 +1609,42 @@ 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 = 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; } @@ -869,13 +1652,76 @@ 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; + 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; + } + + 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. @@ -883,18 +1729,22 @@ HdEmbreeRenderer::_ComputeColor(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))); // 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( @@ -903,12 +1753,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); } } @@ -918,38 +1768,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); + + // 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 gets modulated by an ambient occlusion term. - float aoLightIntensity = - _ComputeAmbientOcclusion(hitPos, normal, random); + // 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) { @@ -962,12 +1837,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()); @@ -1024,4 +1899,66 @@ 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 + 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 + * cosOffNormal + * 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..e5a8426136 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,11 @@ class HdEmbreeRenderer final // How many samples have been completed. std::atomic _completedSamples; + + // Lights + mutable WriteMutex _lightsWriteMutex; // protects the 2 below + std::map _lightMap; + std::vector _domes; }; PXR_NAMESPACE_CLOSE_SCOPE