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 66b5c231a5..4565e9972a 100644 --- a/build_scripts/build_usd.py +++ b/build_scripts/build_usd.py @@ -2730,6 +2730,7 @@ def _JoinVersion(v): OpenVDB support: {enableOpenVDB} OpenImageIO support: {buildOIIO} OpenColorIO support: {buildOCIO} + Embree support: {buildEmbree} PRMan support: {buildPrman} Vulkan support: {enableVulkan} UsdImaging {buildUsdImaging} @@ -2794,6 +2795,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..c3c92aa552 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,6 +46,7 @@ pxr_plugin(hdEmbree renderParam.h PRIVATE_CLASSES + debugCodes implicitSurfaceSceneIndexPlugin RESOURCE_FILES 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..d83d07b59f --- /dev/null +++ b/pxr/imaging/plugin/hdEmbree/light.cpp @@ -0,0 +1,321 @@ +// +// 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->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_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(); + } + + _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..99d29837d6 --- /dev/null +++ b/pxr/imaging/plugin/hdEmbree/light.h @@ -0,0 +1,133 @@ +// +// Copyright 2024 Pixar +// +// Licensed under the terms set forth in the LICENSE.txt file available at +// https://openusd.org/license. +// +#ifndef PXR_IMAGING_PLUGIN_HD_EMBREE_LIGHT_H +#define PXR_IMAGING_PLUGIN_HD_EMBREE_LIGHT_H + +#include "pxr/base/gf/vec3f.h" +#include "pxr/base/gf/matrix3f.h" +#include "pxr/base/gf/matrix4f.h" +#include "pxr/imaging/hd/light.h" + +#include +#include + +#include +#include + +PXR_NAMESPACE_OPEN_SCOPE + +class HdEmbreeRenderer; + +struct HdEmbree_UnknownLight +{}; +struct HdEmbree_Cylinder +{ + float radius; + float length; +}; + +struct HdEmbree_Disk +{ + float radius; +}; + +// 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_Dome, + HdEmbree_Rect, + HdEmbree_Sphere>; + +struct HdEmbree_LightTexture +{ + std::vector pixels; + int width = 0; + int height = 0; +}; + +struct HdEmbree_Shaping +{ + GfVec3f focusTint; + float focus = 0.0f; + float coneAngle = 180.0f; + float coneSoftness = 0.0f; +}; + +struct HdEmbree_LightData +{ + GfMatrix4f xformLightToWorld; + GfMatrix3f normalXformLightToWorld; + GfMatrix4f xformWorldToLight; + GfVec3f color; + 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/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..27cad002da 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,11 @@ const TfTokenVector HdEmbreeRenderDelegate::SUPPORTED_SPRIM_TYPES = { HdPrimTypeTokens->camera, HdPrimTypeTokens->extComputation, + HdPrimTypeTokens->cylinderLight, + HdPrimTypeTokens->diskLight, + HdPrimTypeTokens->domeLight, + HdPrimTypeTokens->rectLight, + HdPrimTypeTokens->sphereLight, }; const TfTokenVector HdEmbreeRenderDelegate::SUPPORTED_BPRIM_TYPES = @@ -147,7 +153,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 +236,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 +337,13 @@ HdEmbreeRenderDelegate::CreateSprim(TfToken const& typeId, return new HdCamera(sprimId); } else if (typeId == HdPrimTypeTokens->extComputation) { return new HdExtComputation(sprimId); + } else if (typeId == HdPrimTypeTokens->light || + typeId == HdPrimTypeTokens->diskLight || + typeId == HdPrimTypeTokens->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 +360,13 @@ HdEmbreeRenderDelegate::CreateFallbackSprim(TfToken const& typeId) return new HdCamera(SdfPath::EmptyPath()); } else if (typeId == HdPrimTypeTokens->extComputation) { return new HdExtComputation(SdfPath::EmptyPath()); + } else if (typeId == HdPrimTypeTokens->light || + typeId == HdPrimTypeTokens->diskLight || + typeId == HdPrimTypeTokens->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..356ff2450b 100644 --- a/pxr/imaging/plugin/hdEmbree/renderer.cpp +++ b/pxr/imaging/plugin/hdEmbree/renderer.cpp @@ -6,26 +6,223 @@ // #include "pxr/imaging/plugin/hdEmbree/renderer.h" -#include "pxr/imaging/plugin/hdEmbree/renderBuffer.h" #include "pxr/imaging/plugin/hdEmbree/config.h" -#include "pxr/imaging/plugin/hdEmbree/context.h" +#include "pxr/imaging/plugin/hdEmbree/light.h" #include "pxr/imaging/plugin/hdEmbree/mesh.h" +#include "pxr/imaging/plugin/hdEmbree/renderBuffer.h" #include "pxr/imaging/hd/perfLog.h" +#include "pxr/base/gf/color.h" +#include "pxr/base/gf/colorSpace.h" #include "pxr/base/gf/matrix3f.h" +#include "pxr/base/gf/range1f.h" #include "pxr/base/gf/vec2f.h" +#include "pxr/base/gf/vec3f.h" #include "pxr/base/work/loops.h" #include "pxr/base/tf/hash.h" +#include +#include +#include + +#include #include +#include +#include #include +// ------------------------------------------------------------------------- +// 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 +235,406 @@ _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) + }; +} + +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 +_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)); + + 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); + } + +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 +727,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 +991,7 @@ _IsContained(const GfRect2i &rect, int width, int height) } void -HdEmbreeRenderer::Render(HdRenderThread *renderThread) +HdEmbreeRenderer::_PreRenderSetup() { _completedSamples.store(0); @@ -422,9 +1042,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 +1078,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 +1161,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 +1187,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 +1200,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 +1236,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 +1250,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 +1280,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 +1289,43 @@ _CosineWeightedDirection(GfVec2f const& uniform_float) return dir; } +bool +HdEmbreeRenderer::_RayShouldContinue(RTCRayHit const& rayHit) const { + if (rayHit.hit.geomID == RTC_INVALID_GEOMETRY_ID) { + // missed, don't continue + return false; + } + + if (rayHit.hit.instID[0] == RTC_INVALID_GEOMETRY_ID) { + // not hit an instance, but a "raw" geometry. This should be a light + const HdEmbreeInstanceContext *instanceContext = + static_cast( + rtcGetGeometryUserData(rtcGetGeometry(_scene, + rayHit.hit.geomID))); + + if (instanceContext->light == nullptr) { + // if this isn't a light, don't know what this is + return false; + } + + auto const& light = instanceContext->light->LightData(); + + if ((rayHit.ray.mask & HdEmbree_RayMask::Camera) + && !light.visible_camera) { + return true; + } else if ((rayHit.ray.mask & HdEmbree_RayMask::Shadow) + && !light.visible_shadow) { + return true; + } else { + return false; + } + } + + // XXX: otherwise this is a regular geo. we should handle visibility here + // too eventually + return false; +} + void HdEmbreeRenderer::_TraceRay(unsigned int x, unsigned int y, GfVec3f const &origin, GfVec3f const &dir, @@ -665,7 +1334,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 +1357,13 @@ HdEmbreeRenderer::_TraceRay(unsigned int x, unsigned int y, rayHit.hit.Ng_z = -rayHit.hit.Ng_z; } + if (_RayShouldContinue(rayHit)) { + GfVec3f hitPos = _CalculateHitPosition(rayHit); + + _TraceRay(x, y, hitPos + dir * _rayHitContinueBias, dir, random); + return; + } + // Write AOVs to attachments that aren't converged. for (size_t i = 0; i < _aovBindings.size(); ++i) { HdEmbreeRenderBuffer *renderBuffer = @@ -741,16 +1419,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 +1464,11 @@ HdEmbreeRenderer::_ComputeDepth(RTCRayHit const& rayHit, return false; } + if (rayHit.hit.instID[0] == RTC_INVALID_GEOMETRY_ID) { + // not hit an instance, but a "raw" geometry. This should be a light + return false; + } + if (clip) { GfVec3f hitPos = _CalculateHitPosition(rayHit); @@ -802,15 +1492,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 +1535,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 +1578,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 +1655,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 +1679,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 +1694,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 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; + + // Lighting gets modulated by an ambient occlusion term. + float aoLightIntensity = + _ComputeAmbientOcclusion(hitPos, normal, random); - // Lighting gets modulated by an ambient occlusion term. - float aoLightIntensity = - _ComputeAmbientOcclusion(hitPos, normal, random); + // XXX: We should support opacity here... - // XXX: We should support opacity here... + lightingColor = GfVec3f(diffuseLight * aoLightIntensity); + } + else + { + // For lighting, default material is 100% white + if (materialColor == _invalidColor) { + materialColor = GfVec3f(1.0f); + } - // Return color * diffuseLight * aoLightIntensity. - GfVec3f finalColor = color * diffuseLight * aoLightIntensity; + lightingColor = _ComputeLighting( + hitPos, normal,random, prototypeContext); + } + const GfVec3f finalColor = GfCompMult(materialColor, lightingColor); - // Clamp colors to [0,1]. + // 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 +1763,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 +1825,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