From d91d50539a725ce3337a854efde45b1eca92d913 Mon Sep 17 00:00:00 2001 From: Owen O'Malley Date: Wed, 14 Jan 2026 22:18:23 -0800 Subject: [PATCH] Add testrender volumes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Alongside the rest of the initialization for subpixel_radiance, an empty MediumStack is initialized. It stores the medium params with pointers in two arrays. One tracks the entry order and the other sorted based on priority. There is a priority handling scheme that will shuffle the incumbent medium pointers to give high priority (lower integer priorities) mediums lower indices. When processing BSDF closures, intersections with MxDielectric or MxGeneralizedSchlick may only be added when priority < current_priority or are both 0 (the precious priority). MediumStack exposes an integrate method that operates on aggregated parameters from all media sharing the same priority as the topmost medium; these aggregated parameters are stored on the stack and updated whenever media are added. A CDF built from these parameters is then sampled to select one of the overlapping media for scattering. At present, only Henyey–Greenstein phase functions are supported, implemented as a BSDF subclass and wraps the implementation in BSDL. Signed-off-by: Owen O'Malley --- .github/workflows/ci.yml | 3 +- src/cmake/testing.cmake | 5 +- src/libbsdl/include/BSDL/tools.h | 6 + src/testrender/optixraytracer.cpp | 2 +- src/testrender/shading.cpp | 191 +++++++++-- src/testrender/shading.h | 309 +++++++++++++++++- src/testrender/simpleraytracer.cpp | 25 +- src/testshade/optixgridrender.cpp | 2 +- testsuite/render-mx-anisotropic-vdf/OPTIX | 0 .../render-mx-anisotropic-vdf/anisotropic.osl | 22 ++ .../render-mx-anisotropic-vdf/envmap.osl | 19 ++ testsuite/render-mx-anisotropic-vdf/matte.osl | 19 ++ .../ref/out-linux-alt.exr | Bin 0 -> 139959 bytes .../ref/out-macos-alt.exr | Bin 0 -> 139962 bytes .../ref/out-optix-alt.exr | Bin 0 -> 139996 bytes .../render-mx-anisotropic-vdf/ref/out.exr | Bin 0 -> 139959 bytes testsuite/render-mx-anisotropic-vdf/run.py | 10 + testsuite/render-mx-anisotropic-vdf/scene.xml | 64 ++++ .../ref/out-optix-alt.exr | Bin 245354 -> 241152 bytes .../render-mx-dielectric-glass/ref/out.exr | Bin 245367 -> 241164 bytes .../render-mx-medium-vdf-glass/emitter.osl | 23 ++ .../render-mx-medium-vdf-glass/glossy.osl | 35 ++ .../render-mx-medium-vdf-glass/matte.osl | 19 ++ .../ref/out-linux-alt.exr | Bin 0 -> 159670 bytes .../ref/out-macos-alt.exr | Bin 0 -> 159973 bytes .../render-mx-medium-vdf-glass/ref/out.exr | Bin 0 -> 159670 bytes testsuite/render-mx-medium-vdf-glass/run.py | 10 + .../render-mx-medium-vdf-glass/scene.xml | 91 ++++++ testsuite/render-mx-medium-vdf/envmap.osl | 19 ++ testsuite/render-mx-medium-vdf/matte.osl | 19 ++ testsuite/render-mx-medium-vdf/medium.osl | 31 ++ .../ref/out-linux-alt.exr | Bin 0 -> 121495 bytes .../ref/out-macos-alt.exr | Bin 0 -> 121505 bytes testsuite/render-mx-medium-vdf/ref/out.exr | Bin 0 -> 121495 bytes testsuite/render-mx-medium-vdf/run.py | 10 + testsuite/render-mx-medium-vdf/scene.xml | 78 +++++ 36 files changed, 963 insertions(+), 49 deletions(-) create mode 100644 testsuite/render-mx-anisotropic-vdf/OPTIX create mode 100644 testsuite/render-mx-anisotropic-vdf/anisotropic.osl create mode 100644 testsuite/render-mx-anisotropic-vdf/envmap.osl create mode 100644 testsuite/render-mx-anisotropic-vdf/matte.osl create mode 100755 testsuite/render-mx-anisotropic-vdf/ref/out-linux-alt.exr create mode 100755 testsuite/render-mx-anisotropic-vdf/ref/out-macos-alt.exr create mode 100755 testsuite/render-mx-anisotropic-vdf/ref/out-optix-alt.exr create mode 100755 testsuite/render-mx-anisotropic-vdf/ref/out.exr create mode 100755 testsuite/render-mx-anisotropic-vdf/run.py create mode 100644 testsuite/render-mx-anisotropic-vdf/scene.xml create mode 100644 testsuite/render-mx-medium-vdf-glass/emitter.osl create mode 100644 testsuite/render-mx-medium-vdf-glass/glossy.osl create mode 100644 testsuite/render-mx-medium-vdf-glass/matte.osl create mode 100755 testsuite/render-mx-medium-vdf-glass/ref/out-linux-alt.exr create mode 100644 testsuite/render-mx-medium-vdf-glass/ref/out-macos-alt.exr create mode 100755 testsuite/render-mx-medium-vdf-glass/ref/out.exr create mode 100755 testsuite/render-mx-medium-vdf-glass/run.py create mode 100644 testsuite/render-mx-medium-vdf-glass/scene.xml create mode 100644 testsuite/render-mx-medium-vdf/envmap.osl create mode 100644 testsuite/render-mx-medium-vdf/matte.osl create mode 100644 testsuite/render-mx-medium-vdf/medium.osl create mode 100755 testsuite/render-mx-medium-vdf/ref/out-linux-alt.exr create mode 100755 testsuite/render-mx-medium-vdf/ref/out-macos-alt.exr create mode 100755 testsuite/render-mx-medium-vdf/ref/out.exr create mode 100755 testsuite/render-mx-medium-vdf/run.py create mode 100644 testsuite/render-mx-medium-vdf/scene.xml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3cee601622..ca00b86372 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -377,7 +377,8 @@ jobs: setenvs: export CMAKE_BUILD_TYPE=Debug LLVM_VERSION=14.0.0 LLVM_DISTRO_NAME=ubuntu-18.04 PUGIXML_VERSION=v1.9 - CTEST_TEST_TIMEOUT=240 + CTEST_TEST_TIMEOUT=1200 + OSL_CMAKE_FLAGS="-DOSL_TEST_BIG_TIMEOUT=1200" - desc: gcc10/C++17 llvm14 oiio-2.5 avx2 nametag: linux-2021ish-gcc10-llvm14 runner: ubuntu-22.04 diff --git a/src/cmake/testing.cmake b/src/cmake/testing.cmake index 5b5b4327d7..05e01fa58b 100644 --- a/src/cmake/testing.cmake +++ b/src/cmake/testing.cmake @@ -362,6 +362,7 @@ macro (osl_add_all_tests) render-cornell render-displacement render-furnace-diffuse + render-mx-anisotropic-vdf render-mx-furnace-burley-diffuse render-mx-furnace-oren-nayar render-mx-furnace-sheen @@ -371,7 +372,9 @@ macro (osl_add_all_tests) render-mx-generalized-schlick render-mx-generalized-schlick-glass render-mx-layer render-mx-sheen - render-microfacet render-oren-nayar + render-mx-medium-vdf + render-mx-medium-vdf-glass + render-microfacet render-oren-nayar render-spi-thinlayer render-uv render-veachmis render-ward render-raytypes diff --git a/src/libbsdl/include/BSDL/tools.h b/src/libbsdl/include/BSDL/tools.h index 1ada926446..3aed2cb759 100644 --- a/src/libbsdl/include/BSDL/tools.h +++ b/src/libbsdl/include/BSDL/tools.h @@ -12,6 +12,12 @@ BSDL_ENTER_NAMESPACE +BSDL_INLINE float +MAX(float a, float b) +{ + return std::max(a, b); +} + BSDL_INLINE float MAX_ABS_XYZ(const Imath::V3f& v) { diff --git a/src/testrender/optixraytracer.cpp b/src/testrender/optixraytracer.cpp index 492430712c..5f6037a8ca 100644 --- a/src/testrender/optixraytracer.cpp +++ b/src/testrender/optixraytracer.cpp @@ -1175,7 +1175,7 @@ OptixRaytracer::processPrintfBuffer(void* buffer_data, size_t buffer_size) if (format[j] == '%') { fmt_string = "%"; bool format_end_found = false; - for (size_t i = 0; !format_end_found; i++) { + while (!format_end_found) { j++; fmt_string += format[j]; switch (format[j]) { diff --git a/src/testrender/shading.cpp b/src/testrender/shading.cpp index 7e9b0b3b20..1137215543 100644 --- a/src/testrender/shading.cpp +++ b/src/testrender/shading.cpp @@ -4,6 +4,7 @@ #include "shading.h" +#include "OSL/oslconfig.h" #include #include "optics.h" #include "sampling.h" @@ -16,6 +17,7 @@ #include #include #include +#include #include using namespace OSL; @@ -1147,6 +1149,52 @@ struct Transparent final : public BSDF { } }; +struct HenyeyGreenstein : public bsdl::spi::VolumeLobe { + using Base = bsdl::spi::VolumeLobe; + + OSL_HOSTDEVICE HenyeyGreenstein(const Data& data, const Vec3& wo, + float path_roughness) + : Base(this, + bsdl::BsdfGlobals(wo, + Vec3(0), // Nf + Vec3(0), // Ngf + false, path_roughness, + 1.0f, // outer_ior + 0), // hero wavelength off + data) + { + } + + OSL_HOSTDEVICE BSDF::Sample eval(const Vec3& wo, const Vec3& wi) const + { + bsdl::Sample s = Base::eval_impl(Base::frame.local(wo), + Base::frame.local(wi)); + return { wi, s.weight.toRGB(0), s.pdf, s.roughness }; + } + OSL_HOSTDEVICE BSDF::Sample sample(const Vec3& wo, float rx, float ry, + float rz) const + { + bsdl::Sample s = Base::sample_impl(Base::frame.local(wo), + { rx, ry, rz }); + return { Base::frame.world(s.wi), s.weight.toRGB(0), s.pdf, + s.roughness }; + } +}; + + +BSDF::Sample +MediumParams::sample_phase_func(const Vec3& wo, float rx, float ry, + float rz) const +{ + if (is_vaccum()) { + return { Vec3(1.0f), Color3(1.0f), 0.0f, 0.0f }; + } + + HenyeyGreenstein::Data data { medium_g, medium_g, 0.0f }; + HenyeyGreenstein phase_func(data, wo, 0.0f); + return phase_func.sample(wo, rx, ry, rz); +} + OSL_HOSTDEVICE Color3 evaluate_layer_opacity(const ShaderGlobalsType& sg, float path_roughness, const ClosureColor* closure) @@ -1235,8 +1283,8 @@ evaluate_layer_opacity(const ShaderGlobalsType& sg, float path_roughness, OSL_HOSTDEVICE void process_medium_closure(const ShaderGlobalsType& sg, float path_roughness, - ShadingResult& result, const ClosureColor* closure, - const Color3& w) + ShadingResult& result, MediumStack& medium_stack, + const ClosureColor* closure, const Color3& w) { if (!closure) return; @@ -1278,37 +1326,86 @@ process_medium_closure(const ShaderGlobalsType& sg, float path_roughness, const ClosureComponent* comp = closure->as_comp(); Color3 cw = weight * comp->w; const auto& params = *comp->as(); - result.sigma_t = cw * params.extinction; - result.sigma_s = params.albedo * result.sigma_t; - result.medium_g = params.anisotropy; - closure = nullptr; + result.medium_data.sigma_t = cw * params.extinction; + result.medium_data.sigma_s = params.albedo + * result.medium_data.sigma_t; + result.medium_data.medium_g = params.anisotropy; + result.medium_data.priority = 0; // always intersect + + // clamp sigma_s to be less than sigma_t + result.medium_data.sigma_s + = { std::min(result.medium_data.sigma_s.x, + result.medium_data.sigma_t.x), + std::min(result.medium_data.sigma_s.y, + result.medium_data.sigma_t.y), + std::min(result.medium_data.sigma_s.z, + result.medium_data.sigma_t.z) }; + + closure = nullptr; break; } case MX_MEDIUM_VDF_ID: { const ClosureComponent* comp = closure->as_comp(); Color3 cw = weight * comp->w; const auto& params = *comp->as(); - result.sigma_t = { -OIIO::fast_log(params.transmission_color.x), - -OIIO::fast_log(params.transmission_color.y), - -OIIO::fast_log(params.transmission_color.z) }; - // NOTE: closure weight scales the extinction parameter - result.sigma_t *= cw / params.transmission_depth; - result.sigma_s = params.albedo * result.sigma_t; - result.medium_g = params.anisotropy; - // TODO: properly track a medium stack here ... - result.refraction_ior = sg.backfacing ? 1.0f / params.ior - : params.ior; - result.priority = params.priority; - closure = nullptr; + + // when both albedo and transmission_color are black, this is + // a vacuum medium used only to carry the IOR for dielectric + // surfaces. + bool is_vacuum = is_black(params.albedo) + && is_black(params.transmission_color); + + if (is_vacuum) { + result.medium_data.sigma_t = Color3(0.0f); + result.medium_data.sigma_s = Color3(0.0f); + } else { + constexpr float epsilon = 1e-10f; + const Color3& t_color = params.transmission_color; + result.medium_data.sigma_t + = Color3(-OIIO::fast_log(fmaxf(t_color.x, epsilon)), + -OIIO::fast_log(fmaxf(t_color.y, epsilon)), + -OIIO::fast_log(fmaxf(t_color.z, epsilon))); + + result.medium_data.sigma_t *= cw / params.transmission_depth; + result.medium_data.sigma_s = params.albedo + * result.medium_data.sigma_t; + + // clamp sigma_s to be less than sigma_t + result.medium_data.sigma_s + = { std::min(result.medium_data.sigma_s.x, + result.medium_data.sigma_t.x), + std::min(result.medium_data.sigma_s.y, + result.medium_data.sigma_t.y), + std::min(result.medium_data.sigma_s.z, + result.medium_data.sigma_t.z) }; + } + + result.medium_data.medium_g = params.anisotropy; + + result.medium_data.refraction_ior = sg.backfacing + ? 1.0f / params.ior + : params.ior; + result.medium_data.priority = params.priority; + + closure = nullptr; break; } case MxDielectric::closureid(): { const ClosureComponent* comp = closure->as_comp(); const MxDielectric::Data& params = *comp->as(); if (!is_black(weight * comp->w * params.refr_tint)) { - // TODO: properly track a medium stack here ... - result.refraction_ior = sg.backfacing ? 1.0f / params.IOR - : params.IOR; + float new_ior = sg.backfacing ? 1.0f / params.IOR : params.IOR; + + result.medium_data.refraction_ior = new_ior; + + const MediumParams* current_params + = medium_stack.get_current_params(); + if (current_params + && result.medium_data.priority + <= current_params->priority) { + result.medium_data.refraction_ior + = current_params->refraction_ior; + } } closure = nullptr; break; @@ -1318,13 +1415,23 @@ process_medium_closure(const ShaderGlobalsType& sg, float path_roughness, const MxGeneralizedSchlick::Data& params = *comp->as(); if (!is_black(weight * comp->w * params.refr_tint)) { - // TODO: properly track a medium stack here ... float avg_F0 = clamp((params.F0.x + params.F0.y + params.F0.z) / 3.0f, 0.0f, 0.99f); float sqrt_F0 = sqrtf(avg_F0); float ior = (1 + sqrt_F0) / (1 - sqrt_F0); - result.refraction_ior = sg.backfacing ? 1.0f / ior : ior; + float new_ior = sg.backfacing ? 1.0f / ior : ior; + + result.medium_data.refraction_ior = new_ior; + + const MediumParams* current_params + = medium_stack.get_current_params(); + if (current_params + && result.medium_data.priority + <= current_params->priority) { + result.medium_data.refraction_ior + = current_params->refraction_ior; + } } closure = nullptr; break; @@ -1341,8 +1448,9 @@ process_medium_closure(const ShaderGlobalsType& sg, float path_roughness, // recursively walk through the closure tree, creating bsdfs as we go OSL_HOSTDEVICE void process_bsdf_closure(const ShaderGlobalsType& sg, float path_roughness, - ShadingResult& result, const ClosureColor* closure, - const Color3& w, bool light_only) + ShadingResult& result, MediumStack& medium_stack, + const ClosureColor* closure, const Color3& w, + bool light_only) { static const ustringhash uh_ggx("ggx"); static const ustringhash uh_beckmann("beckmann"); @@ -1472,16 +1580,29 @@ process_bsdf_closure(const ShaderGlobalsType& sg, float path_roughness, case MxDielectric::closureid(): { const MxDielectric::Data& params = *comp->as(); - ok = result.bsdf.add_bsdf(cw, params, -sg.I, - sg.backfacing, - path_roughness); + + if (medium_stack.false_intersection_with( + result.medium_data)) { + ok = result.bsdf.add_bsdf(cw); + } else { + ok = result.bsdf.add_bsdf(cw, params, + -sg.I, + sg.backfacing, + path_roughness); + } break; } case MxGeneralizedSchlick::closureid(): { const MxGeneralizedSchlick::Data& params = *comp->as(); - ok = result.bsdf.add_bsdf( - cw, params, -sg.I, sg.backfacing, path_roughness); + + if (medium_stack.false_intersection_with( + result.medium_data)) { + ok = result.bsdf.add_bsdf(cw); + } else { + ok = result.bsdf.add_bsdf( + cw, params, -sg.I, sg.backfacing, path_roughness); + } break; } case MxConductor::closureid(): { @@ -1574,11 +1695,14 @@ process_bsdf_closure(const ShaderGlobalsType& sg, float path_roughness, OSL_HOSTDEVICE void process_closure(const ShaderGlobalsType& sg, float path_roughness, - ShadingResult& result, const ClosureColor* Ci, bool light_only) + ShadingResult& result, MediumStack& medium_stack, + const ClosureColor* Ci, bool light_only) { if (!light_only) - process_medium_closure(sg, path_roughness, result, Ci, Color3(1)); - process_bsdf_closure(sg, path_roughness, result, Ci, Color3(1), light_only); + process_medium_closure(sg, path_roughness, result, medium_stack, Ci, + Color3(1)); + process_bsdf_closure(sg, path_roughness, result, medium_stack, Ci, + Color3(1), light_only); } OSL_HOSTDEVICE Vec3 @@ -1639,5 +1763,4 @@ BSDF::sample_vrtl(const Vec3& wo, float rx, float ry, float rz) const return dispatch([&](auto bsdf) { return bsdf.sample(wo, rx, ry, rz); }); } - OSL_NAMESPACE_END diff --git a/src/testrender/shading.h b/src/testrender/shading.h index 34a8dae57c..3c311c32e4 100644 --- a/src/testrender/shading.h +++ b/src/testrender/shading.h @@ -14,10 +14,11 @@ #include "bsdl_config.h" #include +#include "bvh.h" #include "optics.h" +#include "raytracer.h" #include "sampling.h" - OSL_NAMESPACE_BEGIN @@ -242,6 +243,7 @@ struct Refraction; struct ZeltnerBurleySheen; struct CharlieSheen; struct SpiThinLayer; +struct HenyeyGreenstein; // StaticVirtual generates a switch/case dispatch method for us given // a list of possible subtypes. We just need to forward declare them. @@ -250,7 +252,7 @@ using AbstractBSDF = bsdl::StaticVirtual< MicrofacetBeckmannRefl, MicrofacetBeckmannRefr, MicrofacetBeckmannBoth, MicrofacetGGXRefl, MicrofacetGGXRefr, MicrofacetGGXBoth, MxConductor, MxDielectric, MxGeneralizedSchlick, MxSheen, MxBurleyDiffuse, - MxOrenNayarDiffuse, MxTranslucent, SpiThinLayer>; + MxOrenNayarDiffuse, MxTranslucent, SpiThinLayer, HenyeyGreenstein>; // Then we just need to inherit from AbstractBSDF @@ -434,22 +436,311 @@ struct CompositeBSDF { int num_bsdfs, num_bytes; }; +struct MediumParams { + // Initialize to vaccum by default + Color3 sigma_t = Color3(0.0f); // extinction coefficient + Color3 sigma_s = Color3(0.0f); // scattering + float medium_g = 0.0f; // volumetric anisotropy + float refraction_ior = 1.0f; + int priority = 0; + + OSL_HOSTDEVICE bool is_vaccum() const + { + return sigma_t.x <= 0.0f && sigma_t.y <= 0.0f && sigma_t.z <= 0.0f; + } + + OSL_HOSTDEVICE bool is_special_priority() const { return priority == 0; } + + OSL_HOSTDEVICE BSDF::Sample sample_phase_func(const Vec3& wo, float rx, + float ry, float rz) const; +}; + +struct MediumStack { + OSL_HOSTDEVICE MediumStack() : depth(0), pool_size(0) {} + + OSL_HOSTDEVICE const MediumParams* get_current_params() const + { + if (depth > 0) { + return mediums[0]; + } + return nullptr; + } + + OSL_HOSTDEVICE bool in_medium() const { return depth > 0; } + + OSL_HOSTDEVICE int size() const { return depth; } + + OSL_HOSTDEVICE void compute_current_params() + { + // compute data for overlapping mediums + // called if a new medium was added to the stack + MediumParams new_params; + num_overlapping = 0; + + for (int i = 0; i < depth; i++) { + const MediumParams& params_i = *mediums[i]; + + if (i == 0) { + // current params has to have the same priority as mediums[0] + new_params.priority = params_i.priority; + } + if (params_i.priority != new_params.priority) { + continue; + } + overlapping_medium_indices[num_overlapping] = i; + new_params.sigma_t += params_i.sigma_t; + new_params.sigma_s += params_i.sigma_s; + + float avg_sigma_s_i = (params_i.sigma_s.x + params_i.sigma_s.y + + params_i.sigma_s.z) + / 3.0f; + cdf[num_overlapping] + = (num_overlapping > 0 ? cdf[num_overlapping - 1] : 0.0f) + + avg_sigma_s_i; + num_overlapping++; + } + + if (num_overlapping > 1 && !new_params.is_vaccum()) { + float total_cdf = cdf[num_overlapping - 1]; + if (total_cdf > 0.0f) { + for (int i = 0; i < num_overlapping; i++) { + cdf[i] /= total_cdf; + } + } + } + + // clamp sigma_s to be less than sigma_t + new_params.sigma_s + = { std::min(new_params.sigma_s.x, new_params.sigma_t.x), + std::min(new_params.sigma_s.y, new_params.sigma_t.y), + std::min(new_params.sigma_s.z, new_params.sigma_t.z) }; + + current_params = new_params; + } + + OSL_HOSTDEVICE bool integrate(Ray& r, Sampler& sampler, Intersection& hit, + Color3& path_weight, Color3& path_radiance, + float& bsdf_pdf) + { + if (depth <= 0) { + return false; + } + + if (current_params.is_vaccum()) { + return false; + } + + Vec3 weighted_sigma_s = path_weight * current_params.sigma_s + / current_params.sigma_t; + float channel_weights[3] = { weighted_sigma_s.x, weighted_sigma_s.y, + weighted_sigma_s.z }; + + float total = channel_weights[0] + channel_weights[1] + + channel_weights[2]; + if (total <= 0.0f) { + // pure absorption: apply transmittance to the surface and continue + Color3 tr = transmittance(current_params.sigma_t, hit.t); + path_weight *= tr; + return false; + } + float inv_total = 1.0f / total; + channel_weights[0] *= inv_total; + channel_weights[1] *= inv_total; + channel_weights[2] *= inv_total; + + // Used for channel selection, distance, and cdf sampling + Vec3 rand = sampler.get(); + + // pick a channel using the cdf of channel_weights + int channel; + float rand_channel = rand.y; + if (rand_channel < channel_weights[0]) { + channel = 0; + } else if (rand_channel < channel_weights[0] + channel_weights[1]) { + channel = 1; + } else { + channel = 2; + } + + // sample distance along the ray using the selected channel's sigma_t + float sigma_t_channel = current_params.sigma_t[channel]; + float t_volume = -logf(1.0f - rand.x) / sigma_t_channel; + + bool scatter = t_volume < hit.t; + float t = scatter ? t_volume : hit.t; + + Color3 tr = transmittance(current_params.sigma_t, t); + + Color3 density = scatter ? (current_params.sigma_t * tr) : tr; + float pdf = density.x * channel_weights[0] + + density.y * channel_weights[1] + + density.z * channel_weights[2]; + + if (pdf <= 0.0f) { + return false; + } + + if (scatter) { + path_weight *= (tr * current_params.sigma_s) / pdf; + } else { + path_weight *= (tr / pdf); + return false; + } + + // scattering pick a new direction + r.origin = r.point(t_volume); + + int index = 0; + if (num_overlapping > 1) { + for (index = 0; index < num_overlapping - 1; ++index) { + if (rand.z < cdf[index]) { + break; + } + } + } + + int medium_index = overlapping_medium_indices[index]; + + Vec3 rand_phase = sampler.get(); + BSDF::Sample phase_sample = mediums[medium_index]->sample_phase_func( + -r.direction, rand_phase.x, rand_phase.y, rand_phase.z); + + if (phase_sample.pdf > 0.0f) { + path_weight *= phase_sample.weight; + r.direction = phase_sample.wi; + bsdf_pdf = phase_sample.pdf; + return true; + } + + return false; + } + + OSL_HOSTDEVICE bool add_medium(MediumParams new_params) + { + if (depth >= MaxEntries || pool_size >= MaxEntries) + return false; + + MediumParams* p = &pool[pool_size++]; + *p = new_params; + + // find insertion point (sorted by descending priority) + int insert_pos = depth; + for (int i = 0; i < depth; ++i) { + if (new_params.priority > mediums[i]->priority) { + insert_pos = i; + break; + } + } + + // shift pointers to make room + for (int j = depth; j > insert_pos; --j) { + mediums[j] = mediums[j - 1]; + } + + mediums[insert_pos] = p; + entry_order[depth] = p; + + depth++; + compute_current_params(); + + return true; + } + + OSL_HOSTDEVICE void pop_medium() + { + if (depth <= 0) + return; + + depth--; + + // find the most recently added medium + MediumParams* p = entry_order[depth]; + int sorted_index = -1; + for (int i = 0; i <= depth; ++i) { + if (mediums[i] == p) { + sorted_index = i; + break; + } + } + + if (sorted_index < 0) + return; // shouldn't happen + + // shift pointers down to fill the gap + for (int j = sorted_index; j < depth; ++j) { + mediums[j] = mediums[j + 1]; + } + + // reclaim pool if this was the last allocated entry + if (p == &pool[pool_size - 1]) + pool_size--; + + compute_current_params(); + } + + OSL_HOSTDEVICE bool + false_intersection_with(const MediumParams& entrant_params) + { + const MediumParams* current = get_current_params(); + if (!current) + return false; + + // special priority will always record an intersection + if (entrant_params.is_special_priority() + && current->is_special_priority()) + return false; + + // same non-special priority: never record an intersection + if (entrant_params.priority == current->priority) + return true; + + return entrant_params.priority > current->priority; + } + + OSL_HOSTDEVICE Color3 transmittance(const Color3& sigma_t, + float distance) const + { // Beer-Lambert law + return Color3(expf(-sigma_t.x * distance), expf(-sigma_t.y * distance), + expf(-sigma_t.z * distance)); + } + + /// Never try to copy this struct because it would invalidate the medium pointers + OSL_HOSTDEVICE MediumStack(const MediumStack& c) = delete; + OSL_HOSTDEVICE MediumStack& operator=(const MediumStack& c) = delete; + + enum { MaxEntries = 8 }; + + MediumParams pool[MaxEntries]; // stable storage + MediumParams* mediums[MaxEntries]; // sorted by descending priority + MediumParams* entry_order[MaxEntries]; // LIFO order (pointer identity) + + float cdf[MaxEntries]; + int overlapping_medium_indices[MaxEntries]; + + MediumParams current_params; + + int depth; + int pool_size; + int num_overlapping; +}; + struct ShadingResult { Color3 Le = Color3(0.0f); CompositeBSDF bsdf = {}; - // medium data - Color3 sigma_s = Color3(0.0f); - Color3 sigma_t = Color3(0.0f); - float medium_g = 0.0f; // volumetric anisotropy - float refraction_ior = 1.0f; - int priority = 0; + // Tracks medium properties for both volumetric and dielectric surface boundaries. + // The priority is used to resolve a 'winner' against neighboring + // dielectric boundaries in `process_closure`. For false intersections, + // winner IOR is used and surface shading is skipped. + MediumParams medium_data = {}; }; + void register_closures(ShadingSystem* shadingsys); OSL_HOSTDEVICE void process_closure(const OSL::ShaderGlobals& sg, float path_roughness, - ShadingResult& result, const ClosureColor* Ci, bool light_only); + ShadingResult& result, MediumStack& medium_stack, + const ClosureColor* Ci, bool light_only); OSL_HOSTDEVICE Vec3 process_background_closure(const ClosureColor* Ci); diff --git a/src/testrender/simpleraytracer.cpp b/src/testrender/simpleraytracer.cpp index 600d46bdb8..0f1258be9c 100644 --- a/src/testrender/simpleraytracer.cpp +++ b/src/testrender/simpleraytracer.cpp @@ -969,6 +969,8 @@ SimpleRaytracer::subpixel_radiance(float x, float y, Sampler& sampler, Color3 path_radiance(0, 0, 0); int prev_id = -1; float bsdf_pdf = inf; // camera ray has only one possible direction + MediumStack medium_stack; + for (int b = 0; b <= max_bounces; b++) { ShaderGlobalsType sg; @@ -994,6 +996,11 @@ SimpleRaytracer::subpixel_radiance(float x, float y, Sampler& sampler, break; } + if (medium_stack.integrate(r, sampler, hit, path_weight, path_radiance, + bsdf_pdf)) { + continue; + } + // construct a shader globals for the hit point globals_from_hit(sg, r, hit.t, hit.id, hit.u, hit.v); @@ -1032,8 +1039,8 @@ SimpleRaytracer::subpixel_radiance(float x, float y, Sampler& sampler, #endif ShadingResult result; bool last_bounce = b == max_bounces; - process_closure(sg, r.roughness, result, (const ClosureColor*)sg.Ci, - last_bounce); + process_closure(sg, r.roughness, result, medium_stack, + (const ClosureColor*)sg.Ci, last_bounce); #ifndef __CUDACC__ const size_t lightprims_size = m_lightprims.size(); @@ -1144,6 +1151,7 @@ SimpleRaytracer::subpixel_radiance(float x, float y, Sampler& sampler, #endif ShadingResult light_result; process_closure(light_sg, r.roughness, light_result, + medium_stack, (const ClosureColor*)light_sg.Ci, true); // accumulate contribution path_radiance += contrib * light_result.Le; @@ -1162,6 +1170,18 @@ SimpleRaytracer::subpixel_radiance(float x, float y, Sampler& sampler, // Just simply use roughness as spread slope r.spread = std::max(r.spread, p.roughness); r.roughness = p.roughness; + + // has the sammpled sampled dir crossed surface? + bool transmitted = sg.Ng.dot(p.wi) < 0; + + if (transmitted) { + if (!sg.backfacing) { // if entering and crossing surface + medium_stack.add_medium(result.medium_data); + } else { + medium_stack.pop_medium(); + } + } + if (!(path_weight.x > 0) && !(path_weight.y > 0) && !(path_weight.z > 0)) break; // filter out all 0's or NaNs @@ -1172,6 +1192,7 @@ SimpleRaytracer::subpixel_radiance(float x, float y, Sampler& sampler, } + OSL_HOSTDEVICE Color3 SimpleRaytracer::antialias_pixel(int x, int y, ShadingContext* ctx) { diff --git a/src/testshade/optixgridrender.cpp b/src/testshade/optixgridrender.cpp index 86a80cbe05..9ba0030f61 100644 --- a/src/testshade/optixgridrender.cpp +++ b/src/testshade/optixgridrender.cpp @@ -1013,7 +1013,7 @@ OptixGridRenderer::processPrintfBuffer(void* buffer_data, size_t buffer_size) if (format[j] == '%') { fmt_string = "%"; bool format_end_found = false; - for (size_t i = 0; !format_end_found; i++) { + while (!format_end_found) { j++; fmt_string += format[j]; switch (format[j]) { diff --git a/testsuite/render-mx-anisotropic-vdf/OPTIX b/testsuite/render-mx-anisotropic-vdf/OPTIX new file mode 100644 index 0000000000..e69de29bb2 diff --git a/testsuite/render-mx-anisotropic-vdf/anisotropic.osl b/testsuite/render-mx-anisotropic-vdf/anisotropic.osl new file mode 100644 index 0000000000..170f2486d8 --- /dev/null +++ b/testsuite/render-mx-anisotropic-vdf/anisotropic.osl @@ -0,0 +1,22 @@ +// Copyright Contributors to the Open Shading Language project. +// SPDX-License-Identifier: BSD-3-Clause +// https://github.com/AcademySoftwareFoundation/OpenShadingLanguage + + +volume +anisotropic + [[ string description = "Volume medium material" ]] +( + color albedo = 1.0 + [[ string description = "Scattering color", + float UImin = 0, float UImax = 1 ]], + color extinction = 1.0 + [[ string description = "Transmission color (volume absorption tint)", + float UImin = 0, float UImax = 1 ]], + float anisotropy = 0.0 + [[ string description = "Directionality of scattering (-1=back, 0=isotropic, 1=forward)", + float UImin = -1, float UImax = 1 ]] + ) +{ + Ci = transparent() + anisotropic_vdf(albedo, extinction, anisotropy); +} \ No newline at end of file diff --git a/testsuite/render-mx-anisotropic-vdf/envmap.osl b/testsuite/render-mx-anisotropic-vdf/envmap.osl new file mode 100644 index 0000000000..14f09243b2 --- /dev/null +++ b/testsuite/render-mx-anisotropic-vdf/envmap.osl @@ -0,0 +1,19 @@ +// Copyright Contributors to the Open Shading Language project. +// SPDX-License-Identifier: BSD-3-Clause +// https://github.com/AcademySoftwareFoundation/OpenShadingLanguage + +shader envmap( + color sky = color(0.7,0.8,0.9), + color horizon = color(0.3,0.6,0.9), + color sun = color(0.9,0.8,0.7), + vector sun_dir = vector(0,1,1), + float sun_angle = 5.0) +{ + vector dir = normalize(I); + vector sd = normalize(sun_dir); + color c = mix(horizon, sky, dir.y * dir.y); + float cos_ang = cos(radians(sun_angle / 2)); + if (dot(dir, normalize(sun_dir)) > cos_ang) + c += sun / (1 - cos_ang); // normalize + Ci = c * background(); +} diff --git a/testsuite/render-mx-anisotropic-vdf/matte.osl b/testsuite/render-mx-anisotropic-vdf/matte.osl new file mode 100644 index 0000000000..a8c6f187e4 --- /dev/null +++ b/testsuite/render-mx-anisotropic-vdf/matte.osl @@ -0,0 +1,19 @@ +// Copyright Contributors to the Open Shading Language project. +// SPDX-License-Identifier: BSD-3-Clause +// https://github.com/AcademySoftwareFoundation/OpenShadingLanguage + + +surface +matte + [[ string description = "Lambertian diffuse material" ]] +( + float Kd = 1 + [[ string description = "Diffuse scaling", + float UImin = 0, float UIsoftmax = 1 ]], + color Cs = 1 + [[ string description = "Base color", + float UImin = 0, float UImax = 1 ]] + ) +{ + Ci = Kd * Cs * diffuse (N); +} diff --git a/testsuite/render-mx-anisotropic-vdf/ref/out-linux-alt.exr b/testsuite/render-mx-anisotropic-vdf/ref/out-linux-alt.exr new file mode 100755 index 0000000000000000000000000000000000000000..e6dda98ffba380020d78b5b6dfa7ab4f1a3a6586 GIT binary patch literal 139959 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