From 0c02f22da13f720c655f05806c20fbbe261fa8fe Mon Sep 17 00:00:00 2001 From: Thomas Arcila <134677+tarcila@users.noreply.github.com> Date: Fri, 14 Nov 2025 20:20:32 -0500 Subject: [PATCH 01/11] Update visrtx device json Denoiser parameters were outdated for non directlight renderers --- devices/rtx/device/visrtx_device.json | 202 ++++++++++++++++++-------- 1 file changed, 143 insertions(+), 59 deletions(-) diff --git a/devices/rtx/device/visrtx_device.json b/devices/rtx/device/visrtx_device.json index 1847661a..703c3bc9 100644 --- a/devices/rtx/device/visrtx_device.json +++ b/devices/rtx/device/visrtx_device.json @@ -63,7 +63,9 @@ "parameters": [ { "name": "sampleLimit", - "types": ["ANARI_INT32"], + "types": [ + "ANARI_INT32" + ], "tags": [], "default": 128, "minimum": 0, @@ -71,50 +73,68 @@ }, { "name": "denoise", - "types": ["ANARI_BOOL"], + "types": [ + "ANARI_BOOL" + ], "tags": [], "default": false, "description": "enable the OptiX denoiser" }, { "name": "denoiseMode", - "types": ["ANARI_STRING"], + "types": [ + "ANARI_STRING" + ], "tags": [], "default": "color", - "values": ["color", "colorAlbedo", "colorAlbedoNormal"], + "values": [ + "color", + "colorAlbedo", + "colorAlbedoNormal" + ], "description": "mode controlling buffers given to the denoiser" }, { "name": "tonemap", - "types": ["ANARI_BOOL"], + "types": [ + "ANARI_BOOL" + ], "tags": [], "default": true, "description": "enable internal tonemapping during sample accumulation" }, { "name": "premultiplyBackground", - "types": ["ANARI_BOOL"], + "types": [ + "ANARI_BOOL" + ], "tags": [], "default": false, "description": "pre-multiply alpha channel with background color" }, { "name": "cullTriangleBackfaces", - "types": ["ANARI_BOOL"], + "types": [ + "ANARI_BOOL" + ], "tags": [], "default": false, "description": "enable triangle back face culling" }, { "name": "checkerboarding", - "types": ["ANARI_BOOL"], + "types": [ + "ANARI_BOOL" + ], "tags": [], "default": false, "description": "use checkerboarding to lower frame latency" }, { "name": "pixelSamples", - "types": ["ANARI_INT32"], + "types": [ + "ANARI_INT32" + ], "tags": [], "default": 1, "minimum": 1, @@ -122,7 +142,9 @@ }, { "name": "maxRayDepth", - "types": ["ANARI_INT32"], + "types": [ + "ANARI_INT32" + ], "tags": [], "default": 5, "minimum": 1, @@ -130,7 +152,9 @@ }, { "name": "ambientSamples", - "types": ["ANARI_INT32"], + "types": [ + "ANARI_INT32" + ], "tags": [], "default": 1, "minimum": 0, @@ -138,7 +162,9 @@ }, { "name": "ambientOcclusionDistance", - "types": ["ANARI_FLOAT32"], + "types": [ + "ANARI_FLOAT32" + ], "tags": [], "default": 1e20, "minimum": 0, @@ -146,7 +172,9 @@ }, { "name": "lightFalloff", - "types": ["ANARI_FLOAT32"], + "types": [ + "ANARI_FLOAT32" + ], "tags": [], "default": 1.0, "minimum": 0.0, @@ -155,7 +183,9 @@ }, { "name": "volumeSamplingRate", - "types": ["ANARI_FLOAT32"], + "types": [ + "ANARI_FLOAT32" + ], "tags": [], "default": 0.125, "minimum": 0.001, @@ -164,7 +194,9 @@ }, { "name": "volumeSamplingRateShadows", - "types": ["ANARI_FLOAT32"], + "types": [ + "ANARI_FLOAT32" + ], "tags": [], "default": 0.0125, "minimum": 0.0001, @@ -179,7 +211,9 @@ "parameters": [ { "name": "sampleLimit", - "types": ["ANARI_INT32"], + "types": [ + "ANARI_INT32" + ], "tags": [], "default": 128, "minimum": 0, @@ -187,56 +221,68 @@ }, { "name": "denoise", - "types": ["ANARI_BOOL"], + "types": [ + "ANARI_BOOL" + ], "tags": [], "default": false, "description": "enable the OptiX denoiser" }, { - "name": "denoiseAlbedo", - "types": ["ANARI_BOOL"], - "tags": [], - "default": false, - "description": "enable albedo use by the OptiX denoiser" - }, - { - "name": "denoiseNormal", - "types": ["ANARI_BOOL"], + "name": "denoiseMode", + "types": [ + "ANARI_STRING" + ], "tags": [], - "default": false, - "description": "enable normal use by the OptiX denoiser" + "default": "color", + "values": [ + "color", + "colorAlbedo", + "colorAlbedoNormal" + ], + "description": "mode controlling buffers given to the denoiser" }, { "name": "tonemap", - "types": ["ANARI_BOOL"], + "types": [ + "ANARI_BOOL" + ], "tags": [], "default": true, "description": "enable internal tonemapping during sample accumulation" }, { "name": "premultiplyBackground", - "types": ["ANARI_BOOL"], + "types": [ + "ANARI_BOOL" + ], "tags": [], "default": false, "description": "pre-multiply alpha channel with background color" }, { "name": "cullTriangleBackfaces", - "types": ["ANARI_BOOL"], + "types": [ + "ANARI_BOOL" + ], "tags": [], "default": false, "description": "enable triangle back face culling" }, { "name": "checkerboarding", - "types": ["ANARI_BOOL"], + "types": [ + "ANARI_BOOL" + ], "tags": [], "default": false, "description": "use checkerboarding to lower frame latency" }, { "name": "pixelSamples", - "types": ["ANARI_INT32"], + "types": [ + "ANARI_INT32" + ], "tags": [], "default": 1, "minimum": 1, @@ -244,7 +290,9 @@ }, { "name": "ambientSamples", - "types": ["ANARI_INT32"], + "types": [ + "ANARI_INT32" + ], "tags": [], "default": 1, "minimum": 0, @@ -252,7 +300,9 @@ }, { "name": "ambientOcclusionDistance", - "types": ["ANARI_FLOAT32"], + "types": [ + "ANARI_FLOAT32" + ], "tags": [], "default": 1e20, "minimum": 0, @@ -260,7 +310,9 @@ }, { "name": "volumeSamplingRate", - "types": ["ANARI_FLOAT32"], + "types": [ + "ANARI_FLOAT32" + ], "tags": [], "default": 0.125, "minimum": 0.001, @@ -275,61 +327,83 @@ "parameters": [ { "name": "sampleLimit", - "types": ["ANARI_INT32"], + "types": [ + "ANARI_INT32" + ], "tags": [], - "default": 128, + "default": 512, "minimum": 0, "description": "stop refining the frame after this number of samples" }, { "name": "denoise", - "types": ["ANARI_BOOL"], + "types": [ + "ANARI_BOOL" + ], "tags": [], "default": false, "description": "enable the OptiX denoiser" }, { - "name": "denoiseAlbedo", - "types": ["ANARI_BOOL"], - "tags": [], - "default": false, - "description": "enable albedo use by the OptiX denoiser" - }, - { - "name": "denoiseNormal", - "types": ["ANARI_BOOL"], + "name": "denoiseMode", + "types": [ + "ANARI_STRING" + ], "tags": [], - "default": false, - "description": "enable normal use by the OptiX denoiser" + "default": "color", + "values": [ + "color", + "colorAlbedo", + "colorAlbedoNormal" + ], + "description": "mode controlling buffers given to the denoiser" }, { "name": "tonemap", - "types": ["ANARI_BOOL"], + "types": [ + "ANARI_BOOL" + ], "tags": [], "default": true, "description": "enable internal tonemapping during sample accumulation" }, { "name": "cullTriangleBackfaces", - "types": ["ANARI_BOOL"], + "types": [ + "ANARI_BOOL" + ], "tags": [], "default": false, "description": "enable triangle back face culling" }, { "name": "checkerboarding", - "types": ["ANARI_BOOL"], + "types": [ + "ANARI_BOOL" + ], "tags": [], "default": false, "description": "use checkerboarding to lower frame latency" }, { "name": "pixelSamples", - "types": ["ANARI_INT32"], + "types": [ + "ANARI_INT32" + ], "tags": [], "default": 1, "minimum": 1, "description": "samples per-pixel" + }, + { + "name": "maxRayDepth", + "types": [ + "ANARI_INT32" + ], + "tags": [], + "default": 3, + "minimum": 1, + "description": "Maximum number of ray bounces" } ] }, @@ -339,28 +413,36 @@ "parameters": [ { "name": "cullTriangleBackfaces", - "types": ["ANARI_BOOL"], + "types": [ + "ANARI_BOOL" + ], "tags": [], "default": false, "description": "enable triangle back face culling" }, { "name": "tonemap", - "types": ["ANARI_BOOL"], + "types": [ + "ANARI_BOOL" + ], "tags": [], "default": true, "description": "enable internal tonemapping during sample accumulation" }, { "name": "premultiplyBackground", - "types": ["ANARI_BOOL"], + "types": [ + "ANARI_BOOL" + ], "tags": [], "default": false, "description": "pre-multiply alpha channel with background color" }, { "name": "volumeSamplingRate", - "types": ["ANARI_FLOAT32"], + "types": [ + "ANARI_FLOAT32" + ], "tags": [], "default": 0.125, "minimum": 0.001, @@ -375,7 +457,9 @@ "parameters": [ { "name": "method", - "types": ["ANARI_STRING"], + "types": [ + "ANARI_STRING" + ], "tags": [], "default": "primIndex", "values": [ From 973276d71f2df8cf3bac9d5a3fab5aa864950c66 Mon Sep 17 00:00:00 2001 From: Thomas Arcila <134677+tarcila@users.noreply.github.com> Date: Tue, 3 Feb 2026 15:47:16 -0500 Subject: [PATCH 02/11] Add raygen_helpers.h include directive to volumeIntegration.h --- devices/rtx/device/gpu/renderer/raygen_helpers.h | 1 + 1 file changed, 1 insertion(+) diff --git a/devices/rtx/device/gpu/renderer/raygen_helpers.h b/devices/rtx/device/gpu/renderer/raygen_helpers.h index 0d00bddf..d4d5a2d1 100644 --- a/devices/rtx/device/gpu/renderer/raygen_helpers.h +++ b/devices/rtx/device/gpu/renderer/raygen_helpers.h @@ -35,6 +35,7 @@ #include "gpu/intersectRay.h" #include "gpu/renderer/common.h" #include "gpu/shadingState.h" +#include "gpu/volumeIntegration.h" // Shared helper functions for ray generation across renderers From 20d3395157d41816a9325f0070b9cb61db29ff70 Mon Sep 17 00:00:00 2001 From: Thomas Arcila <134677+tarcila@users.noreply.github.com> Date: Tue, 3 Feb 2026 14:16:45 -0500 Subject: [PATCH 03/11] Make sure to normalize nextRay direction --- devices/rtx/device/renderer/DirectLight_ptx.cu | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devices/rtx/device/renderer/DirectLight_ptx.cu b/devices/rtx/device/renderer/DirectLight_ptx.cu index 32c3196c..e5303930 100644 --- a/devices/rtx/device/renderer/DirectLight_ptx.cu +++ b/devices/rtx/device/renderer/DirectLight_ptx.cu @@ -129,7 +129,7 @@ struct DirectLightShadingPolicy + bounceHit.Ng * std::copysignf( bounceHit.epsilon, dot(bounceHit.Ns, nextRay.direction)), - nextRay.direction, + normalize(nextRay.direction), }; // Only check for intersecting surfaces and background as secondary light From 81908e9ba4e524c76a13ccbc3b72fe36cd0c4f33 Mon Sep 17 00:00:00 2001 From: Thomas Arcila <134677+tarcila@users.noreply.github.com> Date: Tue, 3 Feb 2026 10:43:44 -0500 Subject: [PATCH 04/11] Refactor lights into dual-bucket flatten architecture Changes: - All lights instances are flattened into the lightInstances - All hdri lights are in a separate bucket containing only HDRI lights This allows HDRI to participate in Next Event Estimation while maintaining separate access for background rendering. --- devices/rtx/device/gpu/gpu_objects.h | 8 +- devices/rtx/device/gpu/gpu_util.h | 16 +- .../rtx/device/renderer/DirectLight_ptx.cu | 153 +++++++++--------- devices/rtx/device/world/Group.cpp | 5 + devices/rtx/device/world/Group.h | 1 + devices/rtx/device/world/World.cpp | 56 +++++-- devices/rtx/device/world/World.h | 2 +- 7 files changed, 142 insertions(+), 99 deletions(-) diff --git a/devices/rtx/device/gpu/gpu_objects.h b/devices/rtx/device/gpu/gpu_objects.h index 21e7c5d8..c8946325 100644 --- a/devices/rtx/device/gpu/gpu_objects.h +++ b/devices/rtx/device/gpu/gpu_objects.h @@ -612,9 +612,8 @@ struct InstanceVolumeGPUData struct InstanceLightGPUData { - const DeviceObjectIndex *indices; - size_t numLights; - mat4 xfm; + DeviceObjectIndex lightIndex; // Index into registry.lights[] + mat4 xfm; // Transform for this light instance }; // World // @@ -632,7 +631,8 @@ struct WorldGPUData const InstanceLightGPUData *lightInstances; size_t numLightInstances; - DeviceObjectIndex hdri; + const InstanceLightGPUData *hdriLightInstances; + size_t numHdriLightInstances; }; // Renderer // diff --git a/devices/rtx/device/gpu/gpu_util.h b/devices/rtx/device/gpu/gpu_util.h index aaef86ab..aecbb73a 100644 --- a/devices/rtx/device/gpu/gpu_util.h +++ b/devices/rtx/device/gpu/gpu_util.h @@ -304,12 +304,16 @@ VISRTX_DEVICE vec4 getBackgroundImage( VISRTX_DEVICE vec4 getBackground( const FrameGPUData &fd, const vec2 &loc, const vec3 &rayDir) { - const LightGPUData *hdri = - fd.world.hdri != -1 ? &fd.registry.lights[fd.world.hdri] : nullptr; - if (hdri && hdri->hdri.visible) - return vec4(sampleHDRI(*hdri, rayDir), 1.f); - else - return getBackgroundImage(fd.renderer, loc); + // Check HDRI lights bucket for visible environment maps + for (size_t i = 0; i < fd.world.numHdriLightInstances; i++) { + const auto &hdriLight = fd.world.hdriLightInstances[i]; + const auto &light = fd.registry.lights[hdriLight.lightIndex]; + if (light.hdri.visible) + return vec4(sampleHDRI(light, rayDir), 1.f); + } + + // No visible HDRI, use background image/color + return getBackgroundImage(fd.renderer, loc); } VISRTX_DEVICE uint32_t computeGeometryPrimId(const SurfaceHit &hit) diff --git a/devices/rtx/device/renderer/DirectLight_ptx.cu b/devices/rtx/device/renderer/DirectLight_ptx.cu index e5303930..d52b0b9a 100644 --- a/devices/rtx/device/renderer/DirectLight_ptx.cu +++ b/devices/rtx/device/renderer/DirectLight_ptx.cu @@ -39,11 +39,11 @@ #include "gpu/gpu_math.h" #include "gpu/gpu_objects.h" #include "gpu/intersectRay.h" +#include "gpu/renderer/common.h" +#include "gpu/renderer/raygen_helpers.h" #include "gpu/sampleLight.h" #include "gpu/shadingState.h" #include "gpu/shading_api.h" -#include "gpu/renderer/common.h" -#include "gpu/renderer/raygen_helpers.h" namespace visrtx { @@ -53,43 +53,41 @@ DECLARE_FRAME_DATA(frameData) struct DirectLightShadingPolicy { - static VISRTX_DEVICE vec4 shadeSurface(const MaterialShadingState &shadingState, - ScreenSample &ss, - const Ray &ray, - const SurfaceHit &hit) -{ - const auto &rendererParams = frameData.renderer; - const auto &directLightParams = rendererParams.params.directLight; - - auto &world = frameData.world; - - // Compute ambient light contribution // - const float aoFactor = directLightParams.aoSamples > 0 - ? computeAO(ss, - ray, - hit, - rendererParams.occlusionDistance, - directLightParams.aoSamples, - &surfaceAttenuation) - : 1.f; - - vec3 contrib = materialEvaluateEmission(shadingState, -ray.dir); - - // Handle ambient light contribution - if (rendererParams.ambientIntensity > 0.0f) { - contrib += rendererParams.ambientColor * rendererParams.ambientIntensity - * materialEvaluateTint(shadingState); - } - - // Handle all lights contributions - for (size_t i = 0; i < world.numLightInstances; i++) { - auto *inst = world.lightInstances + i; - if (!inst) - continue; + static VISRTX_DEVICE vec4 shadeSurface( + const MaterialShadingState &shadingState, + ScreenSample &ss, + const Ray &ray, + const SurfaceHit &hit) + { + const auto &rendererParams = frameData.renderer; + const auto &directLightParams = rendererParams.params.directLight; + + auto &world = frameData.world; + + // Compute ambient light contribution // + const float aoFactor = directLightParams.aoSamples > 0 + ? computeAO(ss, + ray, + hit, + rendererParams.occlusionDistance, + directLightParams.aoSamples, + &surfaceAttenuation) + : 1.f; + + vec3 contrib = materialEvaluateEmission(shadingState, -ray.dir); + + // Handle ambient light contribution + if (rendererParams.ambientIntensity > 0.0f) { + contrib += rendererParams.ambientColor * rendererParams.ambientIntensity + * materialEvaluateTint(shadingState); + } - for (size_t l = 0; l < inst->numLights; l++) { + // Handle all lights contributions + for (size_t i = 0; i < world.numLightInstances; i++) { + const auto &light = world.lightInstances[i]; const auto lightSample = - sampleLight(ss, hit, inst->indices[l], inst->xfm); + sampleLight(ss, hit, light.lightIndex, light.xfm); + if (lightSample.pdf == 0.0f) continue; @@ -114,50 +112,49 @@ struct DirectLightShadingPolicy contrib += thisLightContrib * attenuation; } - } - // Take AO in account - contrib *= aoFactor; - - // Then proceed with single bounce ray for indirect lighting - SurfaceHit bounceHit = hit; - NextRay nextRay = materialNextRay(shadingState, ray, ss.rs); - if (glm::any(glm::greaterThan( - nextRay.contributionWeight, glm::vec3(MIN_CONTRIBUTION_EPSILON)))) { - Ray bounceRay = { - bounceHit.hitpoint - + bounceHit.Ng - * std::copysignf( - bounceHit.epsilon, dot(bounceHit.Ns, nextRay.direction)), - normalize(nextRay.direction), - }; - - // Only check for intersecting surfaces and background as secondary light - // interactions - bounceHit.foundHit = false; - intersectSurface(ss, bounceRay, RayType::PRIMARY, &bounceHit); - - if (bounceHit.foundHit) { - // We hit something. Gather its contribution, cosine weighted diffuse - // only, we want this to be lightweight. - MaterialShadingState bounceShadingState; - materialInitShading( - &bounceShadingState, frameData, *bounceHit.material, bounceHit); - - auto sampleDir = randomDir(ss.rs, bounceHit.Ns); - auto cosineT = dot(bounceHit.Ns, sampleDir); - auto color = materialEvaluateTint(bounceShadingState) * cosineT; - contrib += color * nextRay.contributionWeight; - } else { - // No hit, get background contribution directly (no surface to weight - // against) - const auto color = getBackground(frameData, ss.screen, bounceRay.dir); - contrib += vec3(color) * nextRay.contributionWeight; + // Take AO in account + contrib *= aoFactor; + + // Then proceed with single bounce ray for indirect lighting + SurfaceHit bounceHit = hit; + NextRay nextRay = materialNextRay(shadingState, ray, ss.rs); + if (glm::any(glm::greaterThan( + nextRay.contributionWeight, glm::vec3(MIN_CONTRIBUTION_EPSILON)))) { + Ray bounceRay = { + bounceHit.hitpoint + + bounceHit.Ng + * std::copysignf( + bounceHit.epsilon, dot(bounceHit.Ns, nextRay.direction)), + normalize(nextRay.direction), + }; + + // Only check for intersecting surfaces and background as secondary light + // interactions + bounceHit.foundHit = false; + intersectSurface(ss, bounceRay, RayType::PRIMARY, &bounceHit); + + if (bounceHit.foundHit) { + // We hit something. Gather its contribution, cosine weighted diffuse + // only, we want this to be lightweight. + MaterialShadingState bounceShadingState; + materialInitShading( + &bounceShadingState, frameData, *bounceHit.material, bounceHit); + + auto sampleDir = randomDir(ss.rs, bounceHit.Ns); + auto cosineT = dot(bounceHit.Ns, sampleDir); + auto color = materialEvaluateTint(bounceShadingState) * cosineT; + contrib += color * nextRay.contributionWeight; + } else { + // No hit, get background contribution directly (no surface to weight + // against) + const auto color = getBackground(frameData, ss.screen, bounceRay.dir); + contrib += vec3(color) * nextRay.contributionWeight; + } } - } - float opacity = evaluateOpacity(shadingState); - return vec4(contrib, opacity); + float opacity = evaluateOpacity(shadingState); + return vec4(contrib, opacity); } }; diff --git a/devices/rtx/device/world/Group.cpp b/devices/rtx/device/world/Group.cpp index 0f56e27b..ed2a5452 100644 --- a/devices/rtx/device/world/Group.cpp +++ b/devices/rtx/device/world/Group.cpp @@ -186,6 +186,11 @@ DeviceObjectIndex Group::firstHDRI() const return m_firstHDRI; } +const std::vector &Group::lights() const +{ + return m_lights; +} + void Group::rebuildSurfaceBVHs() { const auto &state = *deviceState(); diff --git a/devices/rtx/device/world/Group.h b/devices/rtx/device/world/Group.h index 8ce2f42d..5be8474c 100644 --- a/devices/rtx/device/world/Group.h +++ b/devices/rtx/device/world/Group.h @@ -72,6 +72,7 @@ struct Group : public Object Span lightGPUIndices() const; DeviceObjectIndex firstHDRI() const; + const std::vector &lights() const; void rebuildSurfaceBVHs(); void rebuildVolumeBVH(); diff --git a/devices/rtx/device/world/World.cpp b/devices/rtx/device/world/World.cpp index 7ca45d42..90c70ef8 100644 --- a/devices/rtx/device/world/World.cpp +++ b/devices/rtx/device/world/World.cpp @@ -193,7 +193,8 @@ WorldGPUData World::gpuData() const retval.lightInstances = m_instanceLightGPUData.dataDevice(); retval.numLightInstances = m_instanceLightGPUData.size(); - retval.hdri = m_hdri; + retval.hdriLightInstances = m_instanceHdriLightGPUData.dataDevice(); + retval.numHdriLightInstances = m_instanceHdriLightGPUData.size(); return retval; } @@ -456,28 +457,63 @@ void World::buildInstanceVolumeGPUData() void World::buildInstanceLightGPUData() { - m_instanceLightGPUData.resize(m_numLightInstances); + // Calculate total lights + size_t totalLights = 0; + size_t totalHdriLights = 0; - m_hdri = -1; + std::for_each(m_instances.begin(), m_instances.end(), [&](auto *inst) { + auto *group = inst->group(); + if (!group->containsLights()) + return; + + group->rebuildLights(); + const auto &lights = group->lights(); + const size_t numTransforms = inst->numTransforms(); + const DeviceObjectIndex hdriIdx = group->firstHDRI(); + + totalLights += lights.size() * numTransforms; + + // Count HDRI lights separately + for (auto *light : lights) { + if (light->isHDRI()) + totalHdriLights += numTransforms; + } + }); + + // Allocate both arrays + m_instanceLightGPUData.resize(totalLights); + m_instanceHdriLightGPUData.resize(totalHdriLights); + + size_t lightIndex = 0; + size_t hdriIndex = 0; - int instID = 0; std::for_each(m_instances.begin(), m_instances.end(), [&](auto *inst) { auto *group = inst->group(); - auto *li = m_instanceLightGPUData.dataHost(); if (!group->containsLights()) return; group->rebuildLights(); - const auto lgi = group->lightGPUIndices(); - if (m_hdri == -1) - m_hdri = group->firstHDRI(); + auto *lights = m_instanceLightGPUData.dataHost(); + auto *hdris = m_instanceHdriLightGPUData.dataHost(); + + for (size_t t = 0; t < inst->numTransforms(); t++) { + const mat4 xfm = mat4(inst->xfm(t)); + + for (auto *light : group->lights()) { + const auto lightIdx = light->index(); + + lights[lightIndex++] = {lightIdx, xfm}; - for (size_t t = 0; t < inst->numTransforms(); t++) - li[instID++] = {lgi.data(), lgi.size(), mat4(inst->xfm(t))}; + // HDRI lights also go into hdriLights + if (light->isHDRI()) + hdris[hdriIndex++] = {lightIdx, xfm}; + } + } }); m_instanceLightGPUData.upload(); + m_instanceHdriLightGPUData.upload(); } } // namespace visrtx diff --git a/devices/rtx/device/world/World.h b/devices/rtx/device/world/World.h index 483a705a..36e5eda7 100644 --- a/devices/rtx/device/world/World.h +++ b/devices/rtx/device/world/World.h @@ -111,7 +111,7 @@ struct World : public Object // Lights // HostDeviceArray m_instanceLightGPUData; - DeviceObjectIndex m_hdri{-1}; + HostDeviceArray m_instanceHdriLightGPUData; }; } // namespace visrtx From 7d0b057708efcefd31bbdefb5cc7bbee740bb12e Mon Sep 17 00:00:00 2001 From: Thomas Arcila <134677+tarcila@users.noreply.github.com> Date: Tue, 3 Feb 2026 13:34:35 -0500 Subject: [PATCH 05/11] Fix HDRI instance transform in background evaluation When evaluating HDRI for background, the instance transform was being ignored. Actual lighting computation is already correctly using instance transforms. --- devices/rtx/device/gpu/gpu_util.h | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/devices/rtx/device/gpu/gpu_util.h b/devices/rtx/device/gpu/gpu_util.h index aecbb73a..5595001b 100644 --- a/devices/rtx/device/gpu/gpu_util.h +++ b/devices/rtx/device/gpu/gpu_util.h @@ -308,8 +308,13 @@ VISRTX_DEVICE vec4 getBackground( for (size_t i = 0; i < fd.world.numHdriLightInstances; i++) { const auto &hdriLight = fd.world.hdriLightInstances[i]; const auto &light = fd.registry.lights[hdriLight.lightIndex]; - if (light.hdri.visible) - return vec4(sampleHDRI(light, rayDir), 1.f); + if (light.hdri.visible) { + // Transform ray direction from world space to HDRI local space + // For orthonormal matrices, inverse = transpose + const mat3 xfmInv = glm::transpose(mat3(hdriLight.xfm)); + const vec3 localRayDir = xfmInv * rayDir; + return vec4(sampleHDRI(light, localRayDir), 1.f); + } } // No visible HDRI, use background image/color From e7e2cb615cbf13266541ec96c60f9c7d638e860e Mon Sep 17 00:00:00 2001 From: Thomas Arcila <134677+tarcila@users.noreply.github.com> Date: Tue, 3 Feb 2026 13:41:25 -0500 Subject: [PATCH 06/11] Accumulate all visible HDRI contributions for background Changed getBackground() to sum contributions from all visible HDRI lights instead of returning only the first one. This is consistent with how multiple HDRIs contribute to scene lighting. --- devices/rtx/device/gpu/gpu_util.h | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/devices/rtx/device/gpu/gpu_util.h b/devices/rtx/device/gpu/gpu_util.h index 5595001b..2f039d02 100644 --- a/devices/rtx/device/gpu/gpu_util.h +++ b/devices/rtx/device/gpu/gpu_util.h @@ -304,7 +304,10 @@ VISRTX_DEVICE vec4 getBackgroundImage( VISRTX_DEVICE vec4 getBackground( const FrameGPUData &fd, const vec2 &loc, const vec3 &rayDir) { - // Check HDRI lights bucket for visible environment maps + // Accumulate contributions from all visible HDRI lights + vec3 hdriContribution = vec3(0.f); + bool hasVisibleHDRI = false; + for (size_t i = 0; i < fd.world.numHdriLightInstances; i++) { const auto &hdriLight = fd.world.hdriLightInstances[i]; const auto &light = fd.registry.lights[hdriLight.lightIndex]; @@ -313,10 +316,14 @@ VISRTX_DEVICE vec4 getBackground( // For orthonormal matrices, inverse = transpose const mat3 xfmInv = glm::transpose(mat3(hdriLight.xfm)); const vec3 localRayDir = xfmInv * rayDir; - return vec4(sampleHDRI(light, localRayDir), 1.f); + hdriContribution += sampleHDRI(light, localRayDir); + hasVisibleHDRI = true; } } + if (hasVisibleHDRI) + return vec4(hdriContribution, 1.f); + // No visible HDRI, use background image/color return getBackgroundImage(fd.renderer, loc); } From afac91ded92edaab19db9e7452b1161769ad4f59 Mon Sep 17 00:00:00 2001 From: Thomas Arcila <134677+tarcila@users.noreply.github.com> Date: Wed, 4 Feb 2026 12:07:31 -0500 Subject: [PATCH 07/11] Back to diffuse distilling Transmission information can directly be extracted from the main material. --- devices/rtx/libmdl/Core.cpp | 166 ++++++++++++++++-------------------- devices/rtx/libmdl/Core.h | 2 +- 2 files changed, 74 insertions(+), 94 deletions(-) diff --git a/devices/rtx/libmdl/Core.cpp b/devices/rtx/libmdl/Core.cpp index c796cd9b..f8d990a6 100644 --- a/devices/rtx/libmdl/Core.cpp +++ b/devices/rtx/libmdl/Core.cpp @@ -243,38 +243,40 @@ void Core::addBuiltinModule( nonstd::scope_exit finalizeTransaction( [transaction]() { transaction->commit(); }); - auto result = impexpApi->load_module_from_string( - transaction.get(), - std::string(moduleName).c_str(), - std::string(moduleSource).c_str(), - executionContext.get() - ); + auto result = impexpApi->load_module_from_string(transaction.get(), + std::string(moduleName).c_str(), + std::string(moduleSource).c_str(), + executionContext.get()); switch (result) { - case 0: { - logMessage(mi::base::MESSAGE_SEVERITY_INFO, - "Added builtin module {} from source", moduleName); - break; - } - case 1: { - logMessage(mi::base::MESSAGE_SEVERITY_INFO, - "Builtin module {} already exists", - moduleName); - break; - } - case -1: - logMessage(mi::base::MESSAGE_SEVERITY_ERROR, - "Invalid name {} or module source for builtin", moduleName); - break; - case -2: - logMessage(mi::base::MESSAGE_SEVERITY_WARNING, - "Ignoring builtin {} would shadow a file based definition", moduleName); - break; - default: - logMessage(mi::base::MESSAGE_SEVERITY_ERROR, - "Unknown error while adding builtin module {}", moduleName); - logExecutionContextMessages(executionContext.get()); - break; + case 0: { + logMessage(mi::base::MESSAGE_SEVERITY_INFO, + "Added builtin module {} from source", + moduleName); + break; + } + case 1: { + logMessage(mi::base::MESSAGE_SEVERITY_INFO, + "Builtin module {} already exists", + moduleName); + break; + } + case -1: + logMessage(mi::base::MESSAGE_SEVERITY_ERROR, + "Invalid name {} or module source for builtin", + moduleName); + break; + case -2: + logMessage(mi::base::MESSAGE_SEVERITY_WARNING, + "Ignoring builtin {} would shadow a file based definition", + moduleName); + break; + default: + logMessage(mi::base::MESSAGE_SEVERITY_ERROR, + "Unknown error while adding builtin module {}", + moduleName); + logExecutionContextMessages(executionContext.get()); + break; } } @@ -288,9 +290,10 @@ const mi::neuraylib::IModule *Core::loadModule( // If that fails, try and resolve it as a file name. // First considering the module name from the MDL file name. - if (auto name = make_handle(impexpApi->get_mdl_module_name(moduleName.c_str())); + if (auto name = + make_handle(impexpApi->get_mdl_module_name(moduleName.c_str())); name.is_valid_interface()) { - moduleName = name->get_c_str(); + moduleName = name->get_c_str(); } else { // Check if this is a single MDL name, such as OmniPBR.mdl and // resolve it to its equivalent module name, such as ::OmniPBR. @@ -304,7 +307,6 @@ const mi::neuraylib::IModule *Core::loadModule( } } - if (moduleName.empty()) { logMessage(mi::base::MESSAGE_SEVERITY_ERROR, "Cannot resolve module name from {}", @@ -388,14 +390,14 @@ mi::neuraylib::ICompiled_material *Core::getCompiledMaterial( return compiledMaterial; } -mi::neuraylib::ICompiled_material *Core::getDistilledToTransmissivePBR( +mi::neuraylib::ICompiled_material *Core::getDistilledToDiffuse( const mi::neuraylib::ICompiled_material *compiledMaterial) { auto distiller_api = make_handle( m_neuray->get_api_component()); mi::Sint32 result = 0; auto distilledMaterial = distiller_api->distill_material( - compiledMaterial, "transmissive_pbr", nullptr, &result); + compiledMaterial, "diffuse", nullptr, &result); if (result != 0) { logMessage(mi::base::MESSAGE_SEVERITY_ERROR, "Failed to distill material: %i\n", @@ -417,8 +419,7 @@ const mi::neuraylib::ITarget_code *Core::getPtxTargetCode( auto executionContext = make_handle(m_mdlFactory->clone(m_executionContext.get())); - auto distilledMaterial = - make_handle(getDistilledToTransmissivePBR(compiledMaterial)); + auto distilledMaterial = make_handle(getDistilledToDiffuse(compiledMaterial)); // ANARI attributes 0 to 3 const int numTextureSpaces = 4; @@ -450,66 +451,44 @@ const mi::neuraylib::ITarget_code *Core::getPtxTargetCode( {"surface.emission.emission", "mdlEmission"}, {"surface.emission.intensity", "mdlEmissionIntensity"}, {"surface.emission.mode", "mdlEmissionMode"}, + + {"volume.scattering_coefficient", "mdlTransmission"}, + + {"geometry.cutout_opacity", "mdlOpacity"}, }; - // Special case for tint, transmission and opacity. - // For some renderers, we want to be able to access these directly to - // compute some simplified light propagation. - // run something like: - // mdl_distiller_cli -class -tmm -m transmissive_pbr -o - -p - // /path/to/materials/folder ::My::Material - // to get a description of a transmissive pbr material. - static mi::neuraylib::Target_function_description - distilledTransmissivePBRMaterialFunctions[][3] = { - // Generic version for transmissive_pbr - { - {"surface.scattering.base.layer.tint", "mdlTint"}, - {"volume.scattering_coefficient", "mdlTransmission"}, - {"geometry.cutout_opacity", "mdlOpacity"}, - }, - - // Simplified version where tint is rooted directly under scattering. - { - {"surface.scattering.layer.tint", "mdlTint"}, - {"volume.scattering_coefficient", "mdlTransmission"}, - {"geometry.cutout_opacity", "mdlOpacity"}, - }, - // Simplified version where tint is rooted directly under scattering. - { - {"surface.scattering.tint", "mdlTint"}, - {"volume.scattering_coefficient", "mdlTransmission"}, - {"geometry.cutout_opacity", "mdlOpacity"}, - } - - }; + static mi::neuraylib::Target_function_description distilledFunctions[] = { + {"surface.scattering.tint", "mdlTint"}, + }; // Generate target code for the compiled material - for (auto &distilledMaterialFunctions : - distilledTransmissivePBRMaterialFunctions) { - auto linkUnit = make_handle( - ptxBackend->create_link_unit(transaction, executionContext.get())); - linkUnit->add_material(compiledMaterial, - std::data(materialFunctions), - std::size(materialFunctions), - executionContext.get()); - - linkUnit->add_material(distilledMaterial.get(), - std::data(distilledMaterialFunctions), - std::size(distilledMaterialFunctions), - executionContext.get()); - - if (!logExecutionContextMessages(executionContext.get())) - continue; - - auto targetCode = - ptxBackend->translate_link_unit(linkUnit.get(), executionContext.get()); - if (!logExecutionContextMessages(executionContext.get())) - continue; - - return targetCode; - } + auto linkUnit = make_handle( + ptxBackend->create_link_unit(transaction, executionContext.get())); + + // Add main material functions (BSDF, emission, and auxiliary + // albedo/normal/roughness) + linkUnit->add_material(compiledMaterial, + std::data(materialFunctions), + std::size(materialFunctions), + executionContext.get()); - return {}; + if (!logExecutionContextMessages(executionContext.get())) + return {}; + + linkUnit->add_material(distilledMaterial.get(), + std::data(distilledFunctions), + std::size(distilledFunctions), + executionContext.get()); + + if (!logExecutionContextMessages(executionContext.get())) + return {}; + + auto targetCode = + ptxBackend->translate_link_unit(linkUnit.get(), executionContext.get()); + if (!logExecutionContextMessages(executionContext.get())) + return {}; + + return targetCode; } bool Core::logExecutionContextMessages( @@ -616,7 +595,8 @@ auto Core::resolveModule(std::string_view moduleId) -> std::string return resolvedModule->get_module_name(); } else { logMessage(mi::base::MESSAGE_SEVERITY_WARNING, - "Failed to resolve module `{}` using entityResolver\n", moduleId); + "Failed to resolve module `{}` using entityResolver\n", + moduleId); } return {}; diff --git a/devices/rtx/libmdl/Core.h b/devices/rtx/libmdl/Core.h index 8318dfda..d15f1ca4 100644 --- a/devices/rtx/libmdl/Core.h +++ b/devices/rtx/libmdl/Core.h @@ -110,7 +110,7 @@ class Core mi::neuraylib::ICompiled_material *getCompiledMaterial( const mi::neuraylib::IFunction_definition *, bool classCompilation = true); - mi::neuraylib::ICompiled_material *getDistilledToTransmissivePBR( + mi::neuraylib::ICompiled_material *getDistilledToDiffuse( const mi::neuraylib::ICompiled_material *compiledMaterial); const mi::neuraylib::ITarget_code *getPtxTargetCode( From c3c414441586fb0dd1984d70a98723d3ee5fa92e Mon Sep 17 00:00:00 2001 From: Thomas Arcila <134677+tarcila@users.noreply.github.com> Date: Thu, 5 Feb 2026 15:26:35 -0500 Subject: [PATCH 08/11] Update physically_based.mdl so distilling works better This fixes albedo AOV (for demoising) and albedo based renderers such as Raytrace. --- devices/rtx/shaders/visrtx/physically_based.mdl | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/devices/rtx/shaders/visrtx/physically_based.mdl b/devices/rtx/shaders/visrtx/physically_based.mdl index a67b6b47..17b89a86 100644 --- a/devices/rtx/shaders/visrtx/physically_based.mdl +++ b/devices/rtx/shaders/visrtx/physically_based.mdl @@ -457,11 +457,16 @@ export material physically_based_material( ); // Sheen - bsdf dielectricSheenBsdf = ::df::sheen_bsdf( - tint: resolvedSheenColor, - roughness: resolvedSheenRoughness * resolvedSheenRoughness, - multiscatter_tint: color(1.0), - multiscatter: dielectricIridescenceBsdf, + float sheenWeight = ::math::max_value(resolvedSheenColor); + bsdf dielectricSheenBsdf = ::df::weighted_layer( + weight: sheenWeight, + base: dielectricIridescenceBsdf, + layer: ::df::sheen_bsdf( + tint: resolvedSheenColor, + roughness: resolvedSheenRoughness * resolvedSheenRoughness, + multiscatter_tint: color(1.0), + multiscatter: dielectricIridescenceBsdf, + ), ); // Onto the metallic layer From 7e723b46fc08e0aaf35fe50e48a344fee45da14d Mon Sep 17 00:00:00 2001 From: Thomas Arcila <134677+tarcila@users.noreply.github.com> Date: Thu, 5 Feb 2026 15:44:55 -0500 Subject: [PATCH 09/11] Correctly implement transmission approximation for MDL with DirectLight --- devices/rtx/device/material/shaders/MDLShader_ptx.cu | 3 ++- devices/rtx/shaders/visrtx/physically_based.mdl | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/devices/rtx/device/material/shaders/MDLShader_ptx.cu b/devices/rtx/device/material/shaders/MDLShader_ptx.cu index b812e320..cef1e708 100644 --- a/devices/rtx/device/material/shaders/MDLShader_ptx.cu +++ b/devices/rtx/device/material/shaders/MDLShader_ptx.cu @@ -273,7 +273,8 @@ vec3 __direct_callable__evaluateTransmission( const MDLShadingState *shadingState) { return mdlTransmission( - &shadingState->state, &shadingState->resData, shadingState->argBlock); + &shadingState->state, &shadingState->resData, shadingState->argBlock) + * 0.85f; } VISRTX_CALLABLE diff --git a/devices/rtx/shaders/visrtx/physically_based.mdl b/devices/rtx/shaders/visrtx/physically_based.mdl index 17b89a86..97a943bf 100644 --- a/devices/rtx/shaders/visrtx/physically_based.mdl +++ b/devices/rtx/shaders/visrtx/physically_based.mdl @@ -515,6 +515,7 @@ export material physically_based_material( volume: material_volume( absorption_coefficient: resolvedThickness == 0.0f ? color(0.0f) : -math::log(resolvedAttenuationColor) / resolvedAttenuationDistance, + scattering_coefficient: resolvedBaseColor * resolvedTransmission, ), ior: color(ior), geometry: material_geometry( From d959f6e5d15bf16842965d74182ec9db0a6327c0 Mon Sep 17 00:00:00 2001 From: Thomas Arcila <134677+tarcila@users.noreply.github.com> Date: Fri, 14 Nov 2025 11:53:20 -0500 Subject: [PATCH 10/11] Render the albedo AOV as srgb data --- tsd/src/tsd/rendering/pipeline/passes/VisualizeAOVPass.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsd/src/tsd/rendering/pipeline/passes/VisualizeAOVPass.cpp b/tsd/src/tsd/rendering/pipeline/passes/VisualizeAOVPass.cpp index 0bc62cb9..b62aec31 100644 --- a/tsd/src/tsd/rendering/pipeline/passes/VisualizeAOVPass.cpp +++ b/tsd/src/tsd/rendering/pipeline/passes/VisualizeAOVPass.cpp @@ -76,7 +76,7 @@ void computeAlbedoImage(RenderBuffers &b, tsd::math::uint2 size) detail::parallel_for( b.stream, 0u, uint32_t(size.x * size.y), [=] DEVICE_FCN(uint32_t i) { const auto albedo = b.albedo ? b.albedo[i] : tsd::math::float3(0.f); - b.color[i] = helium::cvt_color_to_uint32({albedo, 1.f}); + b.color[i] = helium::cvt_color_to_uint32_srgb({albedo, 1.f}); }); } From 08141a52152c38d943d0eba5ff9551cc1eaa6ccc Mon Sep 17 00:00:00 2001 From: Thomas Arcila <134677+tarcila@users.noreply.github.com> Date: Thu, 5 Feb 2026 17:27:51 -0500 Subject: [PATCH 11/11] Let's keep the default sample count to 128 for PT Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- devices/rtx/device/visrtx_device.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devices/rtx/device/visrtx_device.json b/devices/rtx/device/visrtx_device.json index 703c3bc9..fdbf7d68 100644 --- a/devices/rtx/device/visrtx_device.json +++ b/devices/rtx/device/visrtx_device.json @@ -331,7 +331,7 @@ "ANARI_INT32" ], "tags": [], - "default": 512, + "default": 128, "minimum": 0, "description": "stop refining the frame after this number of samples" },