diff --git a/66_HLSLBxDFTests/CMakeLists.txt b/66_HLSLBxDFTests/CMakeLists.txt index d26a90205..608d46d8f 100644 --- a/66_HLSLBxDFTests/CMakeLists.txt +++ b/66_HLSLBxDFTests/CMakeLists.txt @@ -1,28 +1,54 @@ set(NBL_INCLUDE_SEARCH_DIRECTORIES - "${CMAKE_CURRENT_SOURCE_DIR}/include" + "${CMAKE_CURRENT_SOURCE_DIR}/include" ) include(common RESULT_VARIABLE RES) if(NOT RES) - message(FATAL_ERROR "common.cmake not found. Should be in {repo_root}/cmake directory") + message(FATAL_ERROR "common.cmake not found. Should be in {repo_root}/cmake directory") endif() nbl_create_executable_project("" "" "${NBL_INCLUDE_SEARCH_DIRECTORIES}" "" "${NBL_EXECUTABLE_PROJECT_CREATION_PCH_TARGET}") if(NBL_EMBED_BUILTIN_RESOURCES) - set(_BR_TARGET_ ${EXECUTABLE_NAME}_builtinResourceData) - set(RESOURCE_DIR "app_resources") + set(_BR_TARGET_ ${EXECUTABLE_NAME}_builtinResourceData) + set(RESOURCE_DIR "app_resources") - get_filename_component(_SEARCH_DIRECTORIES_ "${CMAKE_CURRENT_SOURCE_DIR}" ABSOLUTE) - get_filename_component(_OUTPUT_DIRECTORY_SOURCE_ "${CMAKE_CURRENT_BINARY_DIR}/src" ABSOLUTE) - get_filename_component(_OUTPUT_DIRECTORY_HEADER_ "${CMAKE_CURRENT_BINARY_DIR}/include" ABSOLUTE) + get_filename_component(_SEARCH_DIRECTORIES_ "${CMAKE_CURRENT_SOURCE_DIR}" ABSOLUTE) + get_filename_component(_OUTPUT_DIRECTORY_SOURCE_ "${CMAKE_CURRENT_BINARY_DIR}/src" ABSOLUTE) + get_filename_component(_OUTPUT_DIRECTORY_HEADER_ "${CMAKE_CURRENT_BINARY_DIR}/include" ABSOLUTE) file(GLOB_RECURSE BUILTIN_RESOURCE_FILES RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}/${RESOURCE_DIR}" CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/${RESOURCE_DIR}/*") foreach(RES_FILE ${BUILTIN_RESOURCE_FILES}) LIST_BUILTIN_RESOURCE(RESOURCES_TO_EMBED "${RES_FILE}") endforeach() - ADD_CUSTOM_BUILTIN_RESOURCES(${_BR_TARGET_} RESOURCES_TO_EMBED "${_SEARCH_DIRECTORIES_}" "${RESOURCE_DIR}" "nbl::this_example::builtin" "${_OUTPUT_DIRECTORY_HEADER_}" "${_OUTPUT_DIRECTORY_SOURCE_}") + ADD_CUSTOM_BUILTIN_RESOURCES(${_BR_TARGET_} RESOURCES_TO_EMBED "${_SEARCH_DIRECTORIES_}" "${RESOURCE_DIR}" "nbl::this_example::builtin" "${_OUTPUT_DIRECTORY_HEADER_}" "${_OUTPUT_DIRECTORY_SOURCE_}") - LINK_BUILTIN_RESOURCES_TO_TARGET(${EXECUTABLE_NAME} ${_BR_TARGET_}) -endif() \ No newline at end of file + LINK_BUILTIN_RESOURCES_TO_TARGET(${EXECUTABLE_NAME} ${_BR_TARGET_}) +endif() + +set(OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/auto-gen") + +set(JSON [=[ +[ + { + "INPUT": "app_resources/test_compile.comp.hlsl", + "KEY": "shader", + "COMPILE_OPTIONS": ["-T", "cs_6_8"], + "DEPENDS": [], + "CAPS": [] + } +] +]=]) + +NBL_CREATE_NSC_COMPILE_RULES( + TARGET ${EXECUTABLE_NAME}SPIRV + LINK_TO ${EXECUTABLE_NAME} + BINARY_DIR ${OUTPUT_DIRECTORY} + MOUNT_POINT_DEFINE NBL_THIS_EXAMPLE_BUILD_MOUNT_POINT + COMMON_OPTIONS -I ${CMAKE_CURRENT_SOURCE_DIR} + OUTPUT_VAR KEYS + INCLUDE nbl/this_example/builtin/build/spirv/keys.hpp + NAMESPACE nbl::this_example::builtin::build + INPUTS ${JSON} +) diff --git a/66_HLSLBxDFTests/app_resources/config.json b/66_HLSLBxDFTests/app_resources/config.json new file mode 100644 index 000000000..a97ac67db --- /dev/null +++ b/66_HLSLBxDFTests/app_resources/config.json @@ -0,0 +1,31 @@ +{ + "logInfo": false, + "TestJacobian": { + "runs": 10, + "verbose": true + }, + "TestReciprocity": { + "runs": 10, + "verbose": true + }, + "TestBucket": { + "runs": 10, + "samples": 500 + }, + "TestChi2": { + "runs": 10, + "samples": 1000000, + "thetaSplits": 80, + "phiSplits": 160, + "writeFrequencies": 1 + }, + "TestNDF": { + "runs": 10, + "verbose": true + }, + "TestCTGenerateH": { + "runs": 10, + "samples": 1000000, + "immediateFail": false + } +} diff --git a/66_HLSLBxDFTests/app_resources/test_compile.comp.hlsl b/66_HLSLBxDFTests/app_resources/test_compile.comp.hlsl new file mode 100644 index 000000000..c293e65bb --- /dev/null +++ b/66_HLSLBxDFTests/app_resources/test_compile.comp.hlsl @@ -0,0 +1,103 @@ +#include "nbl/builtin/hlsl/cpp_compat.hlsl" + +#include "nbl/builtin/hlsl/bxdf/common.hlsl" +#include "nbl/builtin/hlsl/bxdf/reflection.hlsl" +#include "nbl/builtin/hlsl/bxdf/transmission.hlsl" + +[[vk::binding(0,0)]] RWStructuredBuffer buff; + +using namespace nbl::hlsl; + +using spectral_t = vector; +using ray_dir_info_t = bxdf::ray_dir_info::SBasic; +using iso_interaction = bxdf::surface_interactions::SIsotropic; +using aniso_interaction = bxdf::surface_interactions::SAnisotropic; +using sample_t = bxdf::SLightSample; +using iso_cache = bxdf::SIsotropicMicrofacetCache; +using aniso_cache = bxdf::SAnisotropicMicrofacetCache; +using quotient_pdf_t = sampling::quotient_and_pdf; + +using iso_config_t = bxdf::SConfiguration; +using aniso_config_t = bxdf::SConfiguration; +using iso_microfacet_config_t = bxdf::SMicrofacetConfiguration; +using aniso_microfacet_config_t = bxdf::SMicrofacetConfiguration; + +[numthreads(64,1,1)] +void main(uint32_t3 ID : SV_DispatchThreadID) +{ + bxdf::reflection::SLambertian lambertianBRDF; + bxdf::reflection::SOrenNayar orenNayarBRDF; + bxdf::reflection::SDeltaDistribution deltaDistBRDF; + bxdf::reflection::SBeckmannIsotropic beckmannIsoBRDF; + bxdf::reflection::SBeckmannAnisotropic beckmannAnisoBRDF; + bxdf::reflection::SGGXIsotropic ggxIsoBRDF; + bxdf::reflection::SGGXAnisotropic ggxAnisoBRDF; + bxdf::reflection::SIridescent iridBRDF; + + bxdf::transmission::SLambertian lambertianBSDF; + bxdf::transmission::SOrenNayar orenNayarBSDF; + bxdf::transmission::SSmoothDielectric smoothDielectricBSDF; + bxdf::transmission::SThinSmoothDielectric thinSmoothDielectricBSDF; + bxdf::transmission::SDeltaDistribution deltaDistBSDF; + bxdf::transmission::SBeckmannDielectricIsotropic beckmannIsoBSDF; + bxdf::transmission::SBeckmannDielectricAnisotropic beckmannAnisoBSDF; + bxdf::transmission::SGGXDielectricIsotropic ggxIsoBSDF; + bxdf::transmission::SGGXDielectricAnisotropic ggxAnisoBSDF; + bxdf::transmission::SIridescent iridBSDF; + + + // do some nonsense calculations, but call all the relevant functions + ray_dir_info_t V; + V.direction = nbl::hlsl::normalize(float3(1, 1, 1)); + const float3 N = float3(0, 1, 0); + float3 T, B; + math::frisvad(N, T, B); + const float3 u = float3(0.5, 0.5, 0); + + iso_interaction isointer = iso_interaction::create(V, N); + aniso_interaction anisointer = aniso_interaction::create(isointer, T, B); + aniso_cache cache; + + float3 L = float3(0,0,0); + float3 q = float3(0,0,0); + sample_t s = lambertianBRDF.generate(anisointer, u.xy); + L += s.L.direction; + + s = orenNayarBRDF.generate(anisointer, u.xy); + L += s.L.direction; + + quotient_pdf_t qp = orenNayarBRDF.quotient_and_pdf(s, isointer); + L -= qp.quotient; + + s = beckmannAnisoBRDF.generate(anisointer, u.xy, cache); + L += s.L.direction; + + qp = beckmannAnisoBRDF.quotient_and_pdf(s, anisointer, cache); + L -= qp.quotient; + + s = ggxAnisoBRDF.generate(anisointer, u.xy, cache); + L += s.L.direction; + + qp = iridBRDF.quotient_and_pdf(s, anisointer, cache); + L -= qp.quotient; + + qp = ggxAnisoBRDF.quotient_and_pdf(s, anisointer, cache); + L -= qp.quotient; + + s = lambertianBSDF.generate(anisointer, u); + L += s.L.direction; + + s = thinSmoothDielectricBSDF.generate(anisointer, u); + L += s.L.direction; + + qp = thinSmoothDielectricBSDF.quotient_and_pdf(s, isointer); + L -= qp.quotient; + + s = ggxAnisoBSDF.generate(anisointer, u, cache); + L += s.L.direction; + + qp = ggxAnisoBSDF.quotient_and_pdf(s, anisointer, cache); + L -= qp.quotient; + + buff[ID.x] = L; +} diff --git a/66_HLSLBxDFTests/app_resources/test_components.hlsl b/66_HLSLBxDFTests/app_resources/test_components.hlsl new file mode 100644 index 000000000..b41334541 --- /dev/null +++ b/66_HLSLBxDFTests/app_resources/test_components.hlsl @@ -0,0 +1,334 @@ +#ifndef BXDFTESTS_TEST_COMPONENTS_HLSL +#define BXDFTESTS_TEST_COMPONENTS_HLSL + +#include "tests_common.hlsl" + +template // only for cook torrance bxdfs +struct TestNDF : TestBxDF +{ + using base_t = TestBxDFBase; + using this_t = TestNDF; + using traits_t = bxdf::traits; + + TestResult compute() + { + aniso_cache dummy; + iso_cache dummy_iso; + + float32_t3 ux = base_t::rc.u + float32_t3(eps,0,0); + float32_t3 uy = base_t::rc.u + float32_t3(0,eps,0); + + NBL_IF_CONSTEXPR(traits_t::type == bxdf::BT_BRDF && traits_t::IsMicrofacet) + { + NBL_IF_CONSTEXPR(aniso) + { + s = base_t::bxdf.generate(base_t::anisointer, base_t::rc.u.xy, cache); + sx = base_t::bxdf.generate(base_t::anisointer, ux.xy, dummy); + sy = base_t::bxdf.generate(base_t::anisointer, uy.xy, dummy); + } + else + { + s = base_t::bxdf.generate(base_t::isointer, base_t::rc.u.xy, isocache); + sx = base_t::bxdf.generate(base_t::isointer, ux.xy, dummy_iso); + sy = base_t::bxdf.generate(base_t::isointer, uy.xy, dummy_iso); + } + } + NBL_IF_CONSTEXPR(traits_t::type == bxdf::BT_BSDF && traits_t::IsMicrofacet) + { + NBL_IF_CONSTEXPR(aniso) + { + s = base_t::bxdf.generate(base_t::anisointer, base_t::rc.u, cache); + sx = base_t::bxdf.generate(base_t::anisointer, ux, dummy); + sy = base_t::bxdf.generate(base_t::anisointer, uy, dummy); + } + else + { + s = base_t::bxdf.generate(base_t::isointer, base_t::rc.u, isocache); + sx = base_t::bxdf.generate(base_t::isointer, ux, dummy_iso); + sy = base_t::bxdf.generate(base_t::isointer, uy, dummy_iso); + } + } + + if (!BxDF::ndf_type::GuaranteedVNDF && !(s.isValid() && sx.isValid() && sy.isValid())) + return BTR_INVALID_TEST_CONFIG; + + using ndf_type = typename base_t::bxdf_t::ndf_type; + using quant_type = typename ndf_type::quant_type; + using quant_query_type = typename ndf_type::quant_query_type; + using dg1_query_type = typename ndf_type::dg1_query_type; + using fresnel_type = typename base_t::bxdf_t::fresnel_type; + + float reflectance; + bool isNdfInfinity; + bool transmitted; + NBL_IF_CONSTEXPR(aniso) + { + dg1_query_type dq = base_t::bxdf.ndf.template createDG1Query(base_t::anisointer, cache); + fresnel_type _f = base_t::bxdf_t::__getOrientedFresnel(base_t::bxdf.fresnel, base_t::anisointer.getNdotV()); + quant_query_type qq = bxdf::impl::quant_query_helper::template __call(base_t::bxdf.ndf, _f, base_t::anisointer, cache); + quant_type DG1 = base_t::bxdf.ndf.template DG1(dq, qq, s, base_t::anisointer, isNdfInfinity); + dg1 = DG1.projectedLightMeasure; + + float VdotH = cache.getVdotH(); + NBL_IF_CONSTEXPR (traits_t::type == bxdf::BT_BSDF) + VdotH = hlsl::abs(VdotH); + reflectance = _f(VdotH)[0]; + transmitted = cache.isTransmission(); + } + else + { + dg1_query_type dq = base_t::bxdf.ndf.template createDG1Query(base_t::isointer, isocache); + fresnel_type _f = base_t::bxdf_t::__getOrientedFresnel(base_t::bxdf.fresnel, base_t::isointer.getNdotV()); + quant_query_type qq = bxdf::impl::quant_query_helper::template __call(base_t::bxdf.ndf, _f, base_t::isointer, isocache); + quant_type DG1 = base_t::bxdf.ndf.template DG1(dq, qq, s, base_t::isointer, isNdfInfinity); + dg1 = DG1.projectedLightMeasure; + + float VdotH = isocache.getVdotH(); + NBL_IF_CONSTEXPR(traits_t::type == bxdf::BT_BSDF) + VdotH = hlsl::abs(VdotH); + reflectance = _f(VdotH)[0]; + transmitted = isocache.isTransmission(); + } + + if (isNdfInfinity) + return BTR_INVALID_TEST_CONFIG; + + if (reflectance < 0.f || reflectance > 1.f) + { +#ifndef __HLSL_VERSION + if (verbose) + base_t::errMsg += std::format("reflectance={}, eta={}, transmitted={}", reflectance, base_t::rc.eta.x, transmitted ? "true" : "false"); +#endif + return BTR_ERROR_REFLECTANCE_OUT_OF_RANGE; + } + + return BTR_NONE; + } + + TestResult test() + { + if (traits_t::type == bxdf::BT_BRDF) + { + if (base_t::isointer.getNdotV() <= bit_cast(numeric_limits::min)) + return BTR_INVALID_TEST_CONFIG; + } + else if (traits_t::type == bxdf::BT_BSDF) + { + if (hlsl::abs(base_t::isointer.getNdotV()) <= bit_cast(numeric_limits::min)) + return BTR_INVALID_TEST_CONFIG; + } + + TestResult res = compute(); + if (res != BTR_NONE) + return res; + + // get jacobian + float32_t2x2 m = float32_t2x2( + sx.getTdotL() - s.getTdotL(), sy.getTdotL() - s.getTdotL(), + sx.getBdotL() - s.getBdotL(), sy.getBdotL() - s.getBdotL() + ); + float det = nbl::hlsl::determinant(m) / (eps * eps); + + float jacobi_dg1_ndoth = det * dg1 / hlsl::abs(s.getNdotL()); + const bool alphaIsZero = checkZero(base_t::rc.alpha, 1e-3); + if (!alphaIsZero && !checkZero(det, 1e-3) && !checkZero(jacobi_dg1_ndoth - 1.f, 0.1)) + { +#ifndef __HLSL_VERSION + if (verbose) + base_t::errMsg += std::format("VdotH={}, NdotV={}, LdotH={}, NdotL={}, NdotH={}, eta={}, alpha=[{},{}] Jacobian={}, DG1={}, Jacobian*DG1={}", + aniso ? cache.getVdotH() : isocache.getVdotH(), aniso ? base_t::anisointer.getNdotV() : base_t::isointer.getNdotV(), + aniso ? cache.getLdotH() : isocache.getLdotH(), s.getNdotL(), NdotH, base_t::rc.eta.x, base_t::rc.alpha.x, base_t::rc.alpha.y, + det, dg1, jacobi_dg1_ndoth); +#endif + return BTR_ERROR_JACOBIAN_TEST_FAIL; + } + + return BTR_NONE; + } + + static void run(NBL_CONST_REF_ARG(STestInitParams) initparams, NBL_REF_ARG(FailureCallback) cb) + { + this_t t; + t.init(initparams.halfSeed); + t.rc.halfSeed = initparams.halfSeed; + t.verbose = initparams.verbose; + t.initBxDF(t.rc); + + TestResult e = t.test(); + if (e != BTR_NONE) + cb.__call(e, t, initparams.logInfo); + } + + float eps = 1e-4; + sample_t s, sx, sy; + aniso_cache cache; + iso_cache isocache; + float dg1, NdotH; + bool verbose; +}; + +template +struct TestCTGenerateH : TestBxDF +{ + using base_t = TestBxDFBase; + using this_t = TestCTGenerateH; + using traits_t = bxdf::traits; + + TestResult compute() + { + counter.reset(); + + sample_t s; + iso_cache isocache; + aniso_cache cache; + for (uint32_t i = 0; i < numSamples; i++) + { + float32_t3 u = ConvertToFloat01::__call(base_t::rc.rng_vec<3>()); + u.x = hlsl::clamp(u.x, base_t::rc.eps, 1.f-base_t::rc.eps); + u.y = hlsl::clamp(u.y, base_t::rc.eps, 1.f-base_t::rc.eps); + + NBL_IF_CONSTEXPR(traits_t::type == bxdf::BT_BRDF && !traits_t::IsMicrofacet) + { + s = base_t::bxdf.generate(base_t::anisointer, u.xy); + } + NBL_IF_CONSTEXPR(traits_t::type == bxdf::BT_BRDF && traits_t::IsMicrofacet) + { + NBL_IF_CONSTEXPR(aniso) + s = base_t::bxdf.generate(base_t::anisointer, u.xy, cache); + else + s = base_t::bxdf.generate(base_t::isointer, u.xy, isocache); + } + NBL_IF_CONSTEXPR(traits_t::type == bxdf::BT_BSDF && !traits_t::IsMicrofacet) + { + s = base_t::bxdf.generate(base_t::anisointer, u); + } + NBL_IF_CONSTEXPR(traits_t::type == bxdf::BT_BSDF && traits_t::IsMicrofacet) + { + NBL_IF_CONSTEXPR(aniso) + s = base_t::bxdf.generate(base_t::anisointer, u, cache); + else + s = base_t::bxdf.generate(base_t::isointer, u, isocache); + } + + if (!BxDF::ndf_type::GuaranteedVNDF && !s.isValid()) + continue; + + bool transmitted; + float NdotV, VdotH; + float dotProductVdotL, VdotL; + NBL_IF_CONSTEXPR(aniso) + { + NdotV = base_t::anisointer.getNdotV(); + VdotH = cache.getVdotH(); + transmitted = cache.isTransmission(); + dotProductVdotL = hlsl::dot(base_t::anisointer.getV().getDirection(), s.getL().getDirection()); + VdotL = cache.getVdotL(); + } + else + { + NdotV = base_t::isointer.getNdotV(); + VdotH = isocache.getVdotH(); + transmitted = isocache.isTransmission(); + dotProductVdotL = hlsl::dot(base_t::isointer.getV().getDirection(), s.getL().getDirection()); + VdotL = isocache.getVdotL(); + } + + if (!(NdotV * VdotH >= 0.f)) + { + if (immediateFail) + { + base_t::errMsg += std::format("first failed case (NdotV*VdotH): i={}, u=[{},{},{}] NdotV={}, VdotH={}", i, u.x, u.y, u.z, NdotV, VdotH); + return BTR_ERROR_GENERATED_H_INVALID; + } + else + { + counter.NdotVVdotHfail++; + transmitted ? counter.transmitted++ : counter.reflected++; + } + } + + if (!checkZero(dotProductVdotL - VdotL, 1e-4)) + { + if (immediateFail) + { + base_t::errMsg += std::format("first failed case (compare VdotL): i={}, u=[{},{},{}] {}!={}", i, u.x, u.y, u.z, dotProductVdotL, VdotL); + return BTR_ERROR_GENERATED_H_INVALID; + } + else + { + counter.VdotLfail++; + transmitted ? counter.transmitted++ : counter.reflected++; + } + } + + counter.total++; + } + + float totalFails = counter.totalFails(); + if (totalFails > 0) + { + base_t::errMsg += std::format("fail count={} out of {} valid samples: [{}] NdotV*VdotH, [{}] compare VdotL, [{}] transmitted, [{}] reflected, alpha=[{},{}]", + totalFails, counter.total, counter.NdotVVdotHfail, counter.VdotLfail, + counter.transmitted, counter.reflected, base_t::rc.alpha.x, base_t::rc.alpha.y); + return BTR_ERROR_GENERATED_H_INVALID; + } + + return BTR_NONE; + } + + TestResult test() + { + if (traits_t::type == bxdf::BT_BRDF) + if (base_t::isointer.getNdotV() <= numeric_limits::min) + return BTR_INVALID_TEST_CONFIG; + else if (traits_t::type == bxdf::BT_BSDF) + if (hlsl::abs(base_t::isointer.getNdotV()) <= numeric_limits::min) + return BTR_INVALID_TEST_CONFIG; + + TestResult res = compute(); + if (res != BTR_NONE) + return res; + + return BTR_NONE; + } + + static void run(NBL_CONST_REF_ARG(STestInitParams) initparams, NBL_REF_ARG(FailureCallback) cb) + { + this_t t; + t.init(initparams.halfSeed); + t.rc.halfSeed = initparams.halfSeed; + t.numSamples = initparams.samples; + t.immediateFail = initparams.immediateFail; + t.initBxDF(t.rc); + + TestResult e = t.test(); + if (e != BTR_NONE) + cb.__call(e, t, initparams.logInfo); + } + + struct Counter + { + uint32_t NdotVVdotHfail; + uint32_t VdotLfail; + uint32_t reflected; + uint32_t transmitted; + uint32_t total; + + void reset() + { + NdotVVdotHfail = 0; + VdotLfail = 0; + reflected = 0; + transmitted = 0; + total = 0; + } + + float totalFails() { return NdotVVdotHfail + VdotLfail; } + }; + + bool immediateFail = false; + uint32_t numSamples = 1000000; + Counter counter; +}; + +#endif diff --git a/66_HLSLBxDFTests/app_resources/tests.hlsl b/66_HLSLBxDFTests/app_resources/tests.hlsl index 6f67c359f..6b511d975 100644 --- a/66_HLSLBxDFTests/app_resources/tests.hlsl +++ b/66_HLSLBxDFTests/app_resources/tests.hlsl @@ -1,479 +1,359 @@ #ifndef BXDFTESTS_TESTS_HLSL #define BXDFTESTS_TESTS_HLSL -#include "nbl/builtin/hlsl/cpp_compat.hlsl" +#include "tests_common.hlsl" -#include "nbl/builtin/hlsl/random/xoroshiro.hlsl" -#include "nbl/builtin/hlsl/random/pcg.hlsl" -#include "nbl/builtin/hlsl/sampling/uniform.hlsl" -#include "nbl/builtin/hlsl/bxdf/common.hlsl" -#include "nbl/builtin/hlsl/bxdf/reflection.hlsl" -#include "nbl/builtin/hlsl/bxdf/transmission.hlsl" - -#ifndef __HLSL_VERSION -#include -#endif - -namespace nbl -{ -namespace hlsl -{ - -using ray_dir_info_t = bxdf::ray_dir_info::SBasic; -using iso_interaction = bxdf::surface_interactions::SIsotropic; -using aniso_interaction = bxdf::surface_interactions::SAnisotropic; -using sample_t = bxdf::SLightSample; -using iso_cache = bxdf::SIsotropicMicrofacetCache; -using aniso_cache = bxdf::SAnisotropicMicrofacetCache; -using quotient_pdf_t = bxdf::quotient_and_pdf; -using spectral_t = vector; - -using bool32_t3 = vector; - -// uint32_t pcg_hash(uint32_t v) -// { -// uint32_t state = v * 747796405u + 2891336453u; -// uint32_t word = ((state >> ((state >> 28u) + 4u)) ^ state) * 277803737u; -// return (word >> 22u) ^ word; -// } - -// uint32_t2 pcg2d_hash(uint32_t v) -// { -// return uint32_t2(pcg_hash(v), pcg_hash(v+1)); -// } - -namespace impl -{ - -inline float rngFloat01(NBL_REF_ARG(nbl::hlsl::Xoroshiro64Star) rng) -{ - return (float)rng() / numeric_limits::max; -} - -template -struct RNGUniformDist; - -template<> -struct RNGUniformDist +template +struct TestJacobian : TestBxDF { - static float32_t __call(NBL_REF_ARG(nbl::hlsl::Xoroshiro64Star) rng) - { - return rngFloat01(rng); - } -}; + using base_t = TestBxDFBase; + using this_t = TestJacobian; + using traits_t = bxdf::traits; -template -struct RNGUniformDist> -{ - static vector __call(NBL_REF_ARG(nbl::hlsl::Xoroshiro64Star) rng) + TestResult compute() { - vector retval; - for (int i = 0; i < N; i++) - retval[i] = rngFloat01(rng); - return retval; - } -}; - -} + aniso_cache cache, dummy; + iso_cache isocache, dummy_iso; -template -T rngUniformDist(NBL_REF_ARG(nbl::hlsl::Xoroshiro64Star) rng) -{ - return impl::RNGUniformDist::__call(rng); -} + float32_t3 ux = base_t::rc.u + float32_t3(base_t::rc.eps,0,0); + float32_t3 uy = base_t::rc.u + float32_t3(0,base_t::rc.eps,0); + NBL_IF_CONSTEXPR(traits_t::type == bxdf::BT_BRDF && !traits_t::IsMicrofacet) + { + s = base_t::bxdf.generate(base_t::isointer, base_t::rc.u.xy); + sx = base_t::bxdf.generate(base_t::isointer, ux.xy); + sy = base_t::bxdf.generate(base_t::isointer, uy.xy); + } + NBL_IF_CONSTEXPR(traits_t::type == bxdf::BT_BRDF && traits_t::IsMicrofacet) + { + NBL_IF_CONSTEXPR(aniso) + { + s = base_t::bxdf.generate(base_t::anisointer, base_t::rc.u.xy, cache); + sx = base_t::bxdf.generate(base_t::anisointer, ux.xy, dummy); + sy = base_t::bxdf.generate(base_t::anisointer, uy.xy, dummy); + } + else + { + s = base_t::bxdf.generate(base_t::isointer, base_t::rc.u.xy, isocache); + sx = base_t::bxdf.generate(base_t::isointer, ux.xy, dummy_iso); + sy = base_t::bxdf.generate(base_t::isointer, uy.xy, dummy_iso); + } + } + NBL_IF_CONSTEXPR(traits_t::type == bxdf::BT_BSDF && !traits_t::IsMicrofacet) + { + s = base_t::bxdf.generate(base_t::anisointer, base_t::rc.u); + sx = base_t::bxdf.generate(base_t::anisointer, ux); + sy = base_t::bxdf.generate(base_t::anisointer, uy); + } + NBL_IF_CONSTEXPR(traits_t::type == bxdf::BT_BSDF && traits_t::IsMicrofacet) + { + NBL_IF_CONSTEXPR(aniso) + { + s = base_t::bxdf.generate(base_t::anisointer, base_t::rc.u, cache); + sx = base_t::bxdf.generate(base_t::anisointer, ux, dummy); + sy = base_t::bxdf.generate(base_t::anisointer, uy, dummy); + } + else + { + s = base_t::bxdf.generate(base_t::isointer, base_t::rc.u, isocache); + sx = base_t::bxdf.generate(base_t::isointer, ux, dummy_iso); + sy = base_t::bxdf.generate(base_t::isointer, uy, dummy_iso); + } + } -struct SBxDFTestResources -{ - static SBxDFTestResources create(uint32_t2 seed) - { - SBxDFTestResources retval; - retval.rng = nbl::hlsl::Xoroshiro64Star::construct(seed); - retval.u = float32_t3(rngUniformDist(retval.rng), 0.0); + // TODO: might want to distinguish between invalid H and sample produced below hemisphere + if (!(s.isValid() && sx.isValid() && sy.isValid())) + return BTR_INVALID_TEST_CONFIG; - retval.V.direction = nbl::hlsl::normalize(uniform_sphere_generate(rngUniformDist(retval.rng))); - retval.N = nbl::hlsl::normalize(uniform_sphere_generate(rngUniformDist(retval.rng))); - - float32_t2x3 tb = math::frisvad(retval.N); -#ifndef __HLSL_VERSION - const float angle = 2 * numbers::pi * rngUniformDist(retval.rng); - glm::quat rot = glm::angleAxis(angle, retval.N); - retval.T = rot * tb[0]; - retval.B = rot * tb[1]; -#else - retval.T = tb[0]; - retval.B = tb[1]; -#endif + NBL_IF_CONSTEXPR(!traits_t::IsMicrofacet) + { + pdf = base_t::bxdf.quotient_and_pdf(s, base_t::isointer); + bsdf = float32_t3(base_t::bxdf.eval(s, base_t::isointer)); + transmitted = base_t::isointer.getNdotV() * s.getNdotL() < 0.f; + } + NBL_IF_CONSTEXPR(traits_t::IsMicrofacet) + { + NBL_IF_CONSTEXPR(aniso) + { + pdf = base_t::bxdf.quotient_and_pdf(s, base_t::anisointer, cache); + bsdf = float32_t3(base_t::bxdf.eval(s, base_t::anisointer, cache)); + transmitted = cache.isTransmission(); + } + else + { + pdf = base_t::bxdf.quotient_and_pdf(s, base_t::isointer, isocache); + bsdf = float32_t3(base_t::bxdf.eval(s, base_t::isointer, isocache)); + transmitted = isocache.isTransmission(); + } + } - retval.alpha.x = rngUniformDist(retval.rng); - retval.alpha.y = rngUniformDist(retval.rng); - retval.eta = 1.3; - retval.ior = float32_t2(1.3, 2.0); - retval.luma_coeff = float32_t3(0.2126, 0.7152, 0.0722); // luma coefficients for Rec. 709 - return retval; + return BTR_NONE; } - // epsilon - float eps = 1e-3; - - nbl::hlsl::Xoroshiro64Star rng; - ray_dir_info_t V; - float32_t3 N; - float32_t3 T; - float32_t3 B; - - float32_t3 u; - float32_t2 alpha; - float eta; - float32_t2 ior; - float32_t3 luma_coeff; -}; - -enum ErrorType : uint32_t -{ - NOERR = 0, - NEGATIVE_VAL, // pdf/quotient/eval < 0 - PDF_ZERO, // pdf = 0 - QUOTIENT_INF, // quotient -> inf - JACOBIAN, - PDF_EVAL_DIFF, - RECIPROCITY -}; - -struct TestBase -{ - void init(uint32_t2 seed) + TestResult test() { - rc = SBxDFTestResources::create(seed); - - isointer = iso_interaction::create(rc.V, rc.N); - anisointer = aniso_interaction::create(isointer, rc.T, rc.B); - } - - virtual void compute() {} - - SBxDFTestResources rc; + if (traits_t::type == bxdf::BT_BRDF) + { + if (base_t::isointer.getNdotV() <= bit_cast(numeric_limits::min)) + return BTR_INVALID_TEST_CONFIG; + } + else if (traits_t::type == bxdf::BT_BSDF) + { + if (hlsl::abs(base_t::isointer.getNdotV()) <= bit_cast(numeric_limits::min)) + return BTR_INVALID_TEST_CONFIG; + } - iso_interaction isointer; - aniso_interaction anisointer; + TestResult res = compute(); + if (res != BTR_NONE) + return res; -#ifndef __HLSL_VERSION - std::string name = "base"; -#endif -}; - -struct FailureCallback -{ - virtual void __call(ErrorType error, NBL_REF_ARG(TestBase) failedFor) {} -}; + if (checkLt(bsdf, hlsl::promote(0.0)) || checkLt(pdf.quotient, hlsl::promote(0.0)) || pdf.pdf < 0.0) + return BTR_ERROR_NEGATIVE_VAL; -template -struct TestBxDFBase : TestBase -{ - BxDF bxdf; -}; + if (checkZero(pdf.pdf, 1e-5) && !checkZero(pdf.quotient, 1e-5)) // something generated cannot have 0 probability of getting generated + return BTR_ERROR_GENERATED_SAMPLE_NON_POSITIVE_PDF; -template -struct TestBxDF : TestBxDFBase -{ - using base_t = TestBxDFBase; + if (!checkLt(pdf.quotient, hlsl::promote(bit_cast(numeric_limits::infinity)))) // importance sampler's job to prevent inf + return BTR_ERROR_QUOTIENT_INF; - void initBxDF(SBxDFTestResources _rc) - { - base_t::bxdf = BxDF::create(); // default to lambertian bxdf -#ifndef __HLSL_VERSION - base_t::name = "Lambertian BxDF"; -#endif - } -}; + if (checkZero(bsdf, 1e-5) || checkZero(pdf.quotient, 1e-5)) + return BTR_NONE; // likely to be that a bad sample was produced, unless it's a mixture/delta BxDF -template<> -struct TestBxDF> : TestBxDFBase> -{ - using base_t = TestBxDFBase>; + if (hlsl::isnan(pdf.pdf)) + return BTR_NONE; - void initBxDF(SBxDFTestResources _rc) - { - base_t::bxdf = bxdf::reflection::SOrenNayarBxDF::create(_rc.alpha.x); -#ifndef __HLSL_VERSION - base_t::name = "OrenNayar BRDF"; -#endif - } -}; + // get jacobian + float32_t2x2 m = float32_t2x2( + sx.getTdotL() - s.getTdotL(), sy.getTdotL() - s.getTdotL(), + sx.getBdotL() - s.getBdotL(), sy.getBdotL() - s.getBdotL() + ); + float det = nbl::hlsl::determinant(m); -template<> -struct TestBxDF> : TestBxDFBase> -{ - using base_t = TestBxDFBase>; + // infinite PDF and zero jacobian are both valid behaviors + if (!checkZero(det, 1e-3) && !checkZero(det * pdf.pdf / s.getNdotL(), 1e-4)) + return BTR_ERROR_JACOBIAN_TEST_FAIL; - template - void initBxDF(SBxDFTestResources _rc) - { - if (aniso) - { - base_t::bxdf = bxdf::reflection::SBeckmannBxDF::create(rc.alpha.x,rc.alpha.y,(float32_t3)(rc.ior.x),(float32_t3)(rc.ior.y)); -#ifndef __HLSL_VERSION - base_t::name = "Beckmann Aniso BRDF"; -#endif - } - else + float32_t3 quo_pdf = pdf.value(); + if (!hlsl::isinf(pdf.pdf) && !checkEq(quo_pdf, bsdf, 1e-4)) { - base_t::bxdf = bxdf::reflection::SBeckmannBxDF::create(rc.alpha.x,(float32_t3)(rc.ior.x),(float32_t3)(rc.ior.y)); #ifndef __HLSL_VERSION - base_t::name = "Beckmann BRDF"; + if (verbose) + base_t::errMsg += std::format("transmitted={}, quotient*pdf=[{},{},{}] eval=[{},{},{}]", + transmitted ? "true" : "false", + quo_pdf.x, quo_pdf.y, quo_pdf.z, + bsdf.x, bsdf.y, bsdf.z); #endif + return BTR_ERROR_PDF_EVAL_DIFF; } - } -}; - -template<> -struct TestBxDF> : TestBxDFBase> -{ - using base_t = TestBxDFBase>; - template - void initBxDF(SBxDFTestResources _rc) - { - if (aniso) - { - base_t::bxdf = bxdf::reflection::SGGXBxDF::create(rc.alpha.x,rc.alpha.y,(float32_t3)(rc.ior.x),(float32_t3)(rc.ior.y)); -#ifndef __HLSL_VERSION - base_t::name = "GGX Aniso BRDF"; -#endif - } - else - { - base_t::bxdf = bxdf::reflection::SGGXBxDF::create(rc.alpha.x,(float32_t3)(rc.ior.x),(float32_t3)(rc.ior.y)); -#ifndef __HLSL_VERSION - base_t::name = "GGX BRDF"; -#endif - } + return BTR_NONE; } -}; -template<> -struct TestBxDF> : TestBxDFBase> -{ - using base_t = TestBxDFBase>; - - void initBxDF(SBxDFTestResources _rc) + static void run(NBL_CONST_REF_ARG(STestInitParams) initparams, NBL_REF_ARG(FailureCallback) cb) { - base_t::bxdf = bxdf::transmission::SSmoothDielectricBxDF::create(rc.eta); -#ifndef __HLSL_VERSION - base_t::name = "Smooth dielectric BSDF"; -#endif + this_t t; + t.init(initparams.halfSeed); + t.rc.halfSeed = initparams.halfSeed; + t.verbose = initparams.verbose; + t.initBxDF(t.rc); + + TestResult e = t.test(); + if (e != BTR_NONE) + cb.__call(e, t, initparams.logInfo); } -}; -template<> -struct TestBxDF> : TestBxDFBase> -{ - using base_t = TestBxDFBase>; - - void initBxDF(SBxDFTestResources _rc) - { - base_t::bxdf = bxdf::transmission::SSmoothDielectricBxDF::create(float32_t3(rc.eta * rc.eta),rc.luma_coeff); -#ifndef __HLSL_VERSION - base_t::name = "Thin smooth dielectric BSDF"; -#endif - } + sample_t s, sx, sy; + quotient_pdf_t pdf; + float32_t3 bsdf; + bool transmitted; + bool verbose; }; -template<> -struct TestBxDF> : TestBxDFBase> +template +struct TestReciprocity : TestBxDF { - using base_t = TestBxDFBase>; - - template - void initBxDF(SBxDFTestResources _rc) - { - if (aniso) - { - base_t::bxdf = bxdf::transmission::SBeckmannDielectricBxDF::create(rc.eta,rc.alpha.x,rc.alpha.y); -#ifndef __HLSL_VERSION - base_t::name = "Beckmann Dielectric Aniso BSDF"; -#endif - } - else - { - base_t::bxdf = bxdf::transmission::SBeckmannDielectricBxDF::create(rc.eta,rc.alpha.x); -#ifndef __HLSL_VERSION - base_t::name = "Beckmann Dielectric BSDF"; -#endif - } - } -}; + using base_t = TestBxDFBase; + using this_t = TestReciprocity; + using traits_t = bxdf::traits; -template<> -struct TestBxDF> : TestBxDFBase> -{ - using base_t = TestBxDFBase>; + using iso_interaction_t = typename BxDF::isotropic_interaction_type; + using aniso_interaction_t = typename BxDF::anisotropic_interaction_type; - template - void initBxDF(SBxDFTestResources _rc) + TestResult compute() { - if (aniso) + aniso_cache cache, rec_cache; + iso_cache isocache, rec_isocache; + + NBL_IF_CONSTEXPR(traits_t::type == bxdf::BT_BSDF && traits_t::IsMicrofacet) { - base_t::bxdf = bxdf::transmission::SGGXDielectricBxDF::create(rc.eta,rc.alpha.x,rc.alpha.y); -#ifndef __HLSL_VERSION - base_t::name = "GGX Dielectric Aniso BSDF"; -#endif + isointer = iso_interaction_t::template copy(base_t::isointer); + anisointer = aniso_interaction_t::template copy(base_t::anisointer); } else { - base_t::bxdf = bxdf::transmission::SGGXDielectricBxDF::create(rc.eta,rc.alpha.x); -#ifndef __HLSL_VERSION - base_t::name = "GGX Dielectric BSDF"; -#endif + isointer = base_t::isointer; + anisointer = base_t::anisointer; } - } -}; - - -template -struct is_basic_brdf : bool_constant< - is_same>::value || - is_same>::value -> {}; - -template -struct is_microfacet_brdf : bool_constant< - is_same>::value || - is_same>::value -> {}; - -template -struct is_basic_bsdf : bool_constant< - is_same>::value || - is_same>::value || - is_same>::value -> {}; - -template -struct is_microfacet_bsdf : bool_constant< - is_same>::value || - is_same>::value -> {}; - -template -NBL_CONSTEXPR_INLINE_NSPC_SCOPE_VAR bool is_basic_brdf_v = is_basic_brdf::value; -template -NBL_CONSTEXPR_INLINE_NSPC_SCOPE_VAR bool is_microfacet_brdf_v = is_microfacet_brdf::value; -template -NBL_CONSTEXPR_INLINE_NSPC_SCOPE_VAR bool is_basic_bsdf_v = is_basic_bsdf::value; -template -NBL_CONSTEXPR_INLINE_NSPC_SCOPE_VAR bool is_microfacet_bsdf_v = is_microfacet_bsdf::value; - -template -struct TestUOffset : TestBxDF -{ - using base_t = TestBxDFBase; - using this_t = TestUOffset; - - void compute() override - { - aniso_cache cache, dummy; - - float32_t3 ux = base_t::rc.u + float32_t3(base_t::rc.eps,0,0); - float32_t3 uy = base_t::rc.u + float32_t3(0,base_t::rc.eps,0); - - if NBL_CONSTEXPR_FUNC (is_basic_brdf_v) + NBL_IF_CONSTEXPR(traits_t::type == bxdf::BT_BRDF && !traits_t::IsMicrofacet) { - s = base_t::bxdf.generate(base_t::anisointer, base_t::rc.u.xy); - sx = base_t::bxdf.generate(base_t::anisointer, ux.xy); - sy = base_t::bxdf.generate(base_t::anisointer, uy.xy); + s = base_t::bxdf.generate(anisointer, base_t::rc.u.xy); } - if NBL_CONSTEXPR_FUNC (is_microfacet_brdf_v) + NBL_IF_CONSTEXPR(traits_t::type == bxdf::BT_BRDF && traits_t::IsMicrofacet) { - s = base_t::bxdf.generate(base_t::anisointer, base_t::rc.u.xy, cache); - sx = base_t::bxdf.generate(base_t::anisointer, ux.xy, dummy); - sy = base_t::bxdf.generate(base_t::anisointer, uy.xy, dummy); + NBL_IF_CONSTEXPR(aniso) + { + s = base_t::bxdf.generate(anisointer, base_t::rc.u.xy, cache); + } + else + { + s = base_t::bxdf.generate(isointer, base_t::rc.u.xy, isocache); + } } - if NBL_CONSTEXPR_FUNC (is_basic_bsdf_v) + NBL_IF_CONSTEXPR(traits_t::type == bxdf::BT_BSDF && !traits_t::IsMicrofacet) { - s = base_t::bxdf.generate(base_t::anisointer, base_t::rc.u); - sx = base_t::bxdf.generate(base_t::anisointer, ux); - sy = base_t::bxdf.generate(base_t::anisointer, uy); + s = base_t::bxdf.generate(anisointer, base_t::rc.u); } - if NBL_CONSTEXPR_FUNC (is_microfacet_bsdf_v) + NBL_IF_CONSTEXPR(traits_t::type == bxdf::BT_BSDF && traits_t::IsMicrofacet) { - s = base_t::bxdf.generate(base_t::anisointer, base_t::rc.u, cache); - sx = base_t::bxdf.generate(base_t::anisointer, ux, dummy); - sy = base_t::bxdf.generate(base_t::anisointer, uy, dummy); + NBL_IF_CONSTEXPR(aniso) + { + s = base_t::bxdf.generate(anisointer, base_t::rc.u, cache); + } + else + { + s = base_t::bxdf.generate(isointer, base_t::rc.u, isocache); + } } + + // TODO: might want to distinguish between invalid H and sample produced below hemisphere + if (!s.isValid()) + return BTR_INVALID_TEST_CONFIG; + + float32_t3x3 toTangentSpace = anisointer.getToTangentSpace(); + ray_dir_info_t rec_V = s.getL(); + ray_dir_info_t rec_localV = rec_V.transform(toTangentSpace); + ray_dir_info_t rec_localL = base_t::rc.V.transform(toTangentSpace); + rec_s = sample_t::createFromTangentSpace(rec_localL, anisointer.getFromTangentSpace()); + + rec_isointer = iso_interaction_t::create(rec_V, base_t::rc.N); + rec_isointer.luminosityContributionHint = isointer.luminosityContributionHint; + rec_anisointer = aniso_interaction_t::create(rec_isointer, base_t::rc.T, base_t::rc.B); + rec_cache = cache; + rec_cache.iso_cache.VdotH = cache.iso_cache.getLdotH(); + rec_cache.iso_cache.LdotH = cache.iso_cache.getVdotH(); + rec_isocache = isocache; + rec_isocache.VdotH = isocache.getLdotH(); + rec_isocache.LdotH = isocache.getVdotH(); - if NBL_CONSTEXPR_FUNC (is_basic_brdf_v || is_basic_bsdf_v) + NBL_IF_CONSTEXPR(!traits_t::IsMicrofacet) { - pdf = base_t::bxdf.quotient_and_pdf(s, base_t::isointer); - bsdf = float32_t3(base_t::bxdf.eval(s, base_t::isointer)); + bsdf = float32_t3(base_t::bxdf.eval(s, isointer)); + rec_bsdf = float32_t3(base_t::bxdf.eval(rec_s, rec_isointer)); } - if NBL_CONSTEXPR_FUNC (is_microfacet_brdf_v || is_microfacet_bsdf_v) + NBL_IF_CONSTEXPR(traits_t::type == bxdf::BT_BRDF && traits_t::IsMicrofacet) { - if NBL_CONSTEXPR_FUNC (aniso) + NBL_IF_CONSTEXPR(aniso) { - pdf = base_t::bxdf.quotient_and_pdf(s, base_t::anisointer, cache); - bsdf = float32_t3(base_t::bxdf.eval(s, base_t::anisointer, cache)); + bsdf = float32_t3(base_t::bxdf.eval(s, anisointer, cache)); + rec_bsdf = float32_t3(base_t::bxdf.eval(rec_s, rec_anisointer, rec_cache)); } else { - iso_cache isocache = (iso_cache)cache; - pdf = base_t::bxdf.quotient_and_pdf(s, base_t::isointer, isocache); - bsdf = float32_t3(base_t::bxdf.eval(s, base_t::isointer, isocache)); + bsdf = float32_t3(base_t::bxdf.eval(s, isointer, isocache)); + rec_bsdf = float32_t3(base_t::bxdf.eval(rec_s, rec_isointer, rec_isocache)); + } + } + NBL_IF_CONSTEXPR(traits_t::type == bxdf::BT_BSDF && traits_t::IsMicrofacet) + { + NBL_IF_CONSTEXPR(aniso) + { + anisointer.isotropic.pathOrigin = bxdf::PathOrigin::PO_SENSOR; + bsdf = float32_t3(base_t::bxdf.eval(s, anisointer, cache)); + rec_anisointer.isotropic.pathOrigin = bxdf::PathOrigin::PO_LIGHT; + rec_bsdf = float32_t3(base_t::bxdf.eval(rec_s, rec_anisointer, rec_cache)); + } + else + { + isointer.pathOrigin = bxdf::PathOrigin::PO_SENSOR; + bsdf = float32_t3(base_t::bxdf.eval(s, isointer, isocache)); + rec_isointer.pathOrigin = bxdf::PathOrigin::PO_LIGHT; + rec_bsdf = float32_t3(base_t::bxdf.eval(rec_s, rec_isointer, rec_isocache)); } } - } - ErrorType test() - { - compute(); + transmitted = aniso ? cache.isTransmission() : isocache.isTransmission(); - if (nbl::hlsl::abs(pdf.pdf) < base_t::rc.eps) // something generated cannot have 0 probability of getting generated - return PDF_ZERO; +#ifndef __HLSL_VERSION + if (verbose) + base_t::errMsg += std::format("isTransmission: {}, NdotV: {}, NdotL: {}, VdotH: {}, LdotH: {}, NdotH: {}", + transmitted ? "true" : "false", + isointer.getNdotV(), s.getNdotL(), + aniso ? cache.getVdotH() : isocache.getVdotH(), aniso ? cache.getLdotH() : isocache.getLdotH(), aniso ? cache.getAbsNdotH() : isocache.getAbsNdotH()); +#endif - if (!all(pdf.quotient < (float32_t3)numeric_limits::infinity)) // importance sampler's job to prevent inf - return QUOTIENT_INF; + return BTR_NONE; + } + + TestResult test() + { + if (traits_t::type == bxdf::BT_BRDF) + { + if (base_t::isointer.getNdotV() <= bit_cast(numeric_limits::min)) + return BTR_INVALID_TEST_CONFIG; + } + else if (traits_t::type == bxdf::BT_BSDF) + { + if (hlsl::abs(base_t::isointer.getNdotV()) <= bit_cast(numeric_limits::min)) + return BTR_INVALID_TEST_CONFIG; + } - if (all(nbl::hlsl::abs(bsdf) < (float32_t3)base_t::rc.eps) || all(pdf.quotient < (float32_t3)base_t::rc.eps)) - return NOERR; // produces an "impossible" sample + TestResult res = compute(); + if (res != BTR_NONE) + return res; - // get jacobian - float32_t2x2 m = float32_t2x2(sx.TdotL - s.TdotL, sy.TdotL - s.TdotL, sx.BdotL - s.BdotL, sy.BdotL - s.BdotL); - float det = nbl::hlsl::determinant(m); + if (checkZero(bsdf, 1e-5)) + return BTR_NONE; // produces an "impossible" sample - bool jacobian_test = nbl::hlsl::abs(det*pdf.pdf/s.NdotL) < base_t::rc.eps; - if (!jacobian_test) - return JACOBIAN; + if (checkLt(bsdf, (float32_t3)0.0)) + return BTR_ERROR_NEGATIVE_VAL; - bool32_t3 diff_test = nbl::hlsl::max(pdf.value() / bsdf, bsdf / pdf.value()) <= (float32_t3)(1 + base_t::rc.eps); - if (!all(diff_test)) - return PDF_EVAL_DIFF; + float32_t3 a = bsdf / hlsl::abs(s.getNdotL()); + float32_t3 b = rec_bsdf / hlsl::abs(rec_s.getNdotL()); + if (!(a == b)) // avoid division by 0 + if (!checkEq(a, b, 1e-2)) + { +#ifndef __HLSL_VERSION + if (verbose) + base_t::errMsg += std::format(" front=[{},{},{}] rec=[{},{},{}]", + a.x, a.y, a.z, + b.x, b.y, b.z); +#endif + return BTR_ERROR_NO_RECIPROCITY; + } - return NOERR; + return BTR_NONE; } - static void run(uint32_t seed, NBL_REF_ARG(FailureCallback) cb) + static void run(NBL_CONST_REF_ARG(STestInitParams) initparams, NBL_REF_ARG(FailureCallback) cb) { - uint32_t2 state = pcg32x2(seed); - this_t t; - t.init(state); - if NBL_CONSTEXPR_FUNC (is_microfacet_brdf_v || is_microfacet_bsdf_v) - t.template initBxDF(t.rc); - else - t.initBxDF(t.rc); + t.init(initparams.halfSeed); + t.rc.halfSeed = initparams.halfSeed; + t.verbose = initparams.verbose; + t.initBxDF(t.rc); - ErrorType e = t.test(); - if (e != NOERR) - cb.__call(e, t); + TestResult e = t.test(); + if (e != BTR_NONE) + cb.__call(e, t, initparams.logInfo); } - sample_t s, sx, sy; - quotient_pdf_t pdf; - float32_t3 bsdf; + sample_t s, rec_s; + float32_t3 bsdf, rec_bsdf; + iso_interaction_t isointer, rec_isointer; + aniso_interaction_t anisointer, rec_anisointer; + bool transmitted; + bool verbose; }; -} -} - -#endif \ No newline at end of file +#endif diff --git a/66_HLSLBxDFTests/app_resources/tests_common.hlsl b/66_HLSLBxDFTests/app_resources/tests_common.hlsl new file mode 100644 index 000000000..cb2e7222b --- /dev/null +++ b/66_HLSLBxDFTests/app_resources/tests_common.hlsl @@ -0,0 +1,656 @@ +#ifndef BXDFTESTS_TESTS_COMMON_HLSL +#define BXDFTESTS_TESTS_COMMON_HLSL + +#include "nbl/builtin/hlsl/cpp_compat.hlsl" + +#include "nbl/builtin/hlsl/random/xoroshiro.hlsl" +#include "nbl/builtin/hlsl/random/pcg.hlsl" +#include "nbl/builtin/hlsl/random/dim_adaptor_recursive.hlsl" +#include "nbl/builtin/hlsl/sampling/uniform_spheres.hlsl" +#include "nbl/builtin/hlsl/math/linalg/transform.hlsl" +#include "nbl/builtin/hlsl/math/linalg/fast_affine.hlsl" +#include "nbl/builtin/hlsl/math/polar.hlsl" +#include "nbl/builtin/hlsl/bxdf/common.hlsl" +#include "nbl/builtin/hlsl/bxdf/reflection.hlsl" +#include "nbl/builtin/hlsl/bxdf/transmission.hlsl" +#include "nbl/builtin/hlsl/bxdf/bxdf_traits.hlsl" +#include "nbl/builtin/hlsl/colorspace/encodeCIEXYZ.hlsl" + +using namespace nbl; +using namespace hlsl; + +using spectral_t = hlsl::vector; +using ray_dir_info_t = bxdf::ray_dir_info::SBasic; +using iso_interaction = bxdf::surface_interactions::SIsotropic; +using aniso_interaction = bxdf::surface_interactions::SAnisotropic; +using sample_t = bxdf::SLightSample; +using iso_cache = bxdf::SIsotropicMicrofacetCache; +using aniso_cache = bxdf::SAnisotropicMicrofacetCache; +using quotient_pdf_t = sampling::quotient_and_pdf; + +using iso_config_t = bxdf::SConfiguration; +using aniso_config_t = bxdf::SConfiguration; +using iso_microfacet_config_t = bxdf::SMicrofacetConfiguration; +using aniso_microfacet_config_t = bxdf::SMicrofacetConfiguration; + +using bool32_t3 = hlsl::vector; + +template) +struct ConvertToFloat01 +{ + using ret_t = conditional_t::Dimension==1, float, hlsl::vector::Dimension> >; + + static ret_t __call(T x) + { + return ret_t(x) / hlsl::promote(numeric_limits::max); + } +}; + +template +bool checkEq(T a, T b, float32_t eps) +{ + T _a = hlsl::max(hlsl::abs(a), hlsl::promote(1e-5)); + T _b = hlsl::max(hlsl::abs(b), hlsl::promote(1e-5)); + return nbl::hlsl::all::Dimension> >(nbl::hlsl::max(_a / _b, _b / _a) <= hlsl::promote(1 + eps)); +} + +template<> +bool checkEq(float32_t a, float32_t b, float32_t eps) +{ + float32_t _a = hlsl::max(hlsl::abs(a), 1e-5f); // prevent divide by 0 + float32_t _b = hlsl::max(hlsl::abs(b), 1e-5f); + return nbl::hlsl::max(_a / _b, _b / _a) <= float32_t(1 + eps); +} + +template +bool checkLt(T a, T b) +{ + return nbl::hlsl::all::Dimension> >(a < b); +} + +template +bool checkZero(T a, float32_t eps) +{ + return nbl::hlsl::all::Dimension> >(nbl::hlsl::abs(a) < hlsl::promote(eps)); +} + +template<> +bool checkZero(float32_t a, float32_t eps) +{ + return nbl::hlsl::abs(a) < eps; +} + +struct SBxDFTestResources +{ + static SBxDFTestResources create(uint32_t _halfseed) + { + random::PCG32 pcg = random::PCG32::construct(_halfseed); + uint32_t2 seed = nbl::hlsl::random::DimAdaptorRecursive::__call(pcg); + + SBxDFTestResources retval; + retval.rng = nbl::hlsl::Xoroshiro64Star::construct(seed); + retval.u = ConvertToFloat01::__call(retval.rng_vec<3>()); + retval.u.x = hlsl::clamp(retval.u.x, retval.eps, 1.f-retval.eps); + retval.u.y = hlsl::clamp(retval.u.y, retval.eps, 1.f-retval.eps); + + retval.V.direction = nbl::hlsl::normalize(sampling::UniformSphere::generate(ConvertToFloat01::__call(retval.rng_vec<2>()))); + retval.N = nbl::hlsl::normalize(sampling::UniformSphere::generate(ConvertToFloat01::__call(retval.rng_vec<2>()))); + + float32_t3 tangent, bitangent; + math::frisvad(retval.N, tangent, bitangent); + tangent = nbl::hlsl::normalize(tangent); + bitangent = nbl::hlsl::normalize(bitangent); + + const float angle = 2.0f * numbers::pi * ConvertToFloat01::__call(retval.rng()); + float32_t4x4 rot = math::linalg::promote_affine<4, 4>(math::linalg::rotation_mat(angle, retval.N)); + retval.T = mul(rot, float32_t4(tangent,1)).xyz; + retval.B = mul(rot, float32_t4(bitangent,1)).xyz; + + retval.alpha.x = ConvertToFloat01::__call(retval.rng()); + retval.alpha.y = ConvertToFloat01::__call(retval.rng()); + retval.eta = ConvertToFloat01::__call(retval.rng_vec<3>()) * hlsl::promote(1.5) + hlsl::promote(1.1); // range [1.1,2.6], also only do eta = eta/1.0 (air) + retval.etak = ConvertToFloat01::__call(retval.rng_vec<3>()) * hlsl::promote(1.5) + hlsl::promote(1.1); // same as above + retval.luma_coeff = colorspace::scRGBtoXYZ[1]; + + retval.Dinc = ConvertToFloat01::__call(retval.rng()) * 2400.0f + 100.0f; + retval.etaThinFilm = ConvertToFloat01::__call(retval.rng()) * 0.5 + 1.1f; // range [1.1,1.6] + return retval; + } + + template + hlsl::vector rng_vec() + { + // don't construct an adaptor, use a static call which takes base RNG by reference, so modifies its state while producing numbers + return nbl::hlsl::random::DimAdaptorRecursive::__call(rng); + } + + float eps = 1e-3; // epsilon + uint32_t halfSeed; // init state seed, for debugging + + nbl::hlsl::Xoroshiro64Star rng; + ray_dir_info_t V; + float32_t3 N; + float32_t3 T; + float32_t3 B; + + float32_t3 u; + float32_t2 alpha; + float32_t3 eta; + float32_t3 etak; + float32_t3 luma_coeff; + + // thin film stuff; + float Dinc; // in nm [100, 2500] + float etaThinFilm; +}; + +// refer to config to see which params are used in which test +struct STestInitParams +{ + bool logInfo; + uint32_t halfSeed; // state used to get vec2 seed from hash, default: iteration no. + uint32_t samples; // num samples generated for distribution tests, e.g. chi2, bucket, etc. + uint32_t thetaSplits; + uint32_t phiSplits; + uint16_t writeFrequencies; + bool immediateFail; + bool verbose; +}; + +enum TestResult +{ + BTR_NOBREAK = 0, + BTR_NONE = 1, + BTR_PRINT_MSG = 2, + + BTR_ERROR_NEGATIVE_VAL = -1, // pdf/quotient/eval < 0 + BTR_ERROR_GENERATED_SAMPLE_NON_POSITIVE_PDF = -2, // pdf = 0 + BTR_ERROR_QUOTIENT_INF = -3, // quotient -> inf + BTR_ERROR_JACOBIAN_TEST_FAIL = -4, // jacobian * pdf != 0 + BTR_ERROR_PDF_EVAL_DIFF = -5, // quotient * pdf != eval + BTR_ERROR_NO_RECIPROCITY = -6, // eval(incoming) != eval(outgoing) + BTR_ERROR_GENERATED_H_INVALID = -7, // generated H is invalid + BTR_ERROR_REFLECTANCE_OUT_OF_RANGE = -8, // reflectance not [0, 1] + + BTR_INVALID_TEST_CONFIG = -9 +}; + +struct TestBase +{ + void init(uint32_t halfSeed) + { + rc = SBxDFTestResources::create(halfSeed); + + isointer = iso_interaction::create(rc.V, rc.N); + isointer.luminosityContributionHint = rc.luma_coeff; + anisointer = aniso_interaction::create(isointer, rc.T, rc.B); + } + + SBxDFTestResources rc; + + iso_interaction isointer; + aniso_interaction anisointer; + +#ifndef __HLSL_VERSION + std::string name = "base"; + std::string errMsg = ""; +#endif +}; + +template +struct FailureCallback +{ + virtual void __call(TestResult error, NBL_REF_ARG(TestT) failedFor, bool logInfo) {} +}; + +template +struct TestBxDFBase : TestBase +{ + using bxdf_t = BxDF; + BxDF bxdf; +}; + +template +struct TestBxDF : TestBxDFBase +{ + using base_t = TestBxDFBase; + + void initBxDF(SBxDFTestResources _rc) + { + // default to lambertian bxdf +#ifndef __HLSL_VERSION + base_t::name = "Lambertian BxDF"; +#endif + } +}; + +template<> +struct TestBxDF> : TestBxDFBase> +{ + using base_t = TestBxDFBase>; + + void initBxDF(SBxDFTestResources _rc) + { + base_t::bxdf_t::creation_type params; + params.A = _rc.alpha.x; + base_t::bxdf = bxdf::reflection::SOrenNayar::create(params); +#ifndef __HLSL_VERSION + base_t::name = "OrenNayar BRDF"; +#endif + } +}; + +template<> +struct TestBxDF> : TestBxDFBase> +{ + using base_t = TestBxDFBase>; + + void initBxDF(SBxDFTestResources _rc) + { +#ifndef __HLSL_VERSION + base_t::name = "Delta Distribution BRDF"; +#endif + } +}; + +template<> +struct TestBxDF> : TestBxDFBase> +{ + using base_t = TestBxDFBase>; + + void initBxDF(SBxDFTestResources _rc) + { + base_t::bxdf.ndf = base_t::bxdf_t::ndf_type::create(_rc.alpha.x); + base_t::bxdf.fresnel = base_t::bxdf_t::fresnel_type::create(_rc.eta,_rc.etak); +#ifndef __HLSL_VERSION + base_t::name = "Beckmann BRDF"; +#endif + } +}; + +template<> +struct TestBxDF> : TestBxDFBase> +{ + using base_t = TestBxDFBase>; + + void initBxDF(SBxDFTestResources _rc) + { + base_t::bxdf.ndf = base_t::bxdf_t::ndf_type::create(_rc.alpha.x, _rc.alpha.y); + base_t::bxdf.fresnel = base_t::bxdf_t::fresnel_type::create(_rc.eta,_rc.etak); +#ifndef __HLSL_VERSION + base_t::name = "Beckmann Aniso BRDF"; +#endif + } +}; + +template<> +struct TestBxDF> : TestBxDFBase> +{ + using base_t = TestBxDFBase>; + + void initBxDF(SBxDFTestResources _rc) + { + base_t::bxdf.ndf = base_t::bxdf_t::ndf_type::create(_rc.alpha.x); + base_t::bxdf.fresnel = base_t::bxdf_t::fresnel_type::create(_rc.eta,_rc.etak); +#ifndef __HLSL_VERSION + base_t::name = "GGX BRDF"; +#endif + } +}; + +template<> +struct TestBxDF> : TestBxDFBase> +{ + using base_t = TestBxDFBase>; + + void initBxDF(SBxDFTestResources _rc) + { + base_t::bxdf.ndf = base_t::bxdf_t::ndf_type::create(_rc.alpha.x, _rc.alpha.y); + base_t::bxdf.fresnel = base_t::bxdf_t::fresnel_type::create(_rc.eta,_rc.etak); +#ifndef __HLSL_VERSION + base_t::name = "GGX Aniso BRDF"; +#endif + } +}; + +template<> +struct TestBxDF> : TestBxDFBase> +{ + using base_t = TestBxDFBase>; + + void initBxDF(SBxDFTestResources _rc) + { + base_t::bxdf.ndf = base_t::bxdf_t::ndf_type::create(_rc.alpha.x); + using creation_params_t = base_t::bxdf_t::fresnel_type::creation_params_type; + creation_params_t params; + params.Dinc = _rc.Dinc; + params.ior1 = hlsl::promote(1.0); + params.ior2 = hlsl::promote(_rc.etaThinFilm); + params.ior3 = _rc.eta; + params.iork3 = _rc.etak; + base_t::bxdf.fresnel = base_t::bxdf_t::fresnel_type::create(params); +#ifndef __HLSL_VERSION + base_t::name = "Iridescent BRDF"; +#endif + } +}; + +template<> +struct TestBxDF> : TestBxDFBase> +{ + using base_t = TestBxDFBase>; + + void initBxDF(SBxDFTestResources _rc) + { + base_t::bxdf_t::creation_type params; + params.A = _rc.alpha.x; + base_t::bxdf = bxdf::transmission::SOrenNayar::create(params); +#ifndef __HLSL_VERSION + base_t::name = "OrenNayar BSDF"; +#endif + } +}; + +template<> +struct TestBxDF> : TestBxDFBase> +{ + using base_t = TestBxDFBase>; + + void initBxDF(SBxDFTestResources _rc) + { + base_t::bxdf.orientedEta = bxdf::fresnel::OrientedEtas::create(base_t::isointer.getNdotV(bxdf::BxDFClampMode::BCM_ABS), hlsl::promote(_rc.eta.x)); +#ifndef __HLSL_VERSION + base_t::name = "Smooth dielectric BSDF"; +#endif + } +}; + +template<> +struct TestBxDF> : TestBxDFBase> +{ + using base_t = TestBxDFBase>; + + void initBxDF(SBxDFTestResources _rc) + { + using spectral_type = typename base_t::bxdf_t::spectral_type; + base_t::bxdf.fresnel = bxdf::fresnel::Dielectric::create(bxdf::fresnel::OrientedEtas::create(base_t::isointer.getNdotV(bxdf::BxDFClampMode::BCM_ABS), hlsl::promote(_rc.eta.x))); +#ifndef __HLSL_VERSION + base_t::name = "Thin smooth dielectric BSDF"; +#endif + } +}; + +template<> +struct TestBxDF> : TestBxDFBase> +{ + using base_t = TestBxDFBase>; + + void initBxDF(SBxDFTestResources _rc) + { +#ifndef __HLSL_VERSION + base_t::name = "Delta Distribution BSDF"; +#endif + } +}; + +template +struct TestBxDF> : TestBxDFBase> +{ + using base_t = TestBxDFBase>; + + void initBxDF(SBxDFTestResources _rc) + { + bxdf::fresnel::OrientedEtas orientedEta = bxdf::fresnel::OrientedEtas::create(1.0, hlsl::promote(_rc.eta.x)); + base_t::bxdf.ndf = base_t::bxdf_t::ndf_type::create(_rc.alpha.x); + base_t::bxdf.fresnel = base_t::bxdf_t::fresnel_type::create(orientedEta); +#ifndef __HLSL_VERSION + base_t::name = "Beckmann Dielectric BSDF"; +#endif + } +}; + +template +struct TestBxDF> : TestBxDFBase> +{ + using base_t = TestBxDFBase>; + + void initBxDF(SBxDFTestResources _rc) + { + bxdf::fresnel::OrientedEtas orientedEta = bxdf::fresnel::OrientedEtas::create(1.0, hlsl::promote(_rc.eta.x)); + base_t::bxdf.ndf = base_t::bxdf_t::ndf_type::create(_rc.alpha.x, _rc.alpha.y); + base_t::bxdf.fresnel = base_t::bxdf_t::fresnel_type::create(orientedEta); +#ifndef __HLSL_VERSION + base_t::name = "Beckmann Dielectric Aniso BSDF"; +#endif + } +}; + +template +struct TestBxDF> : TestBxDFBase> +{ + using base_t = TestBxDFBase>; + + void initBxDF(SBxDFTestResources _rc) + { + bxdf::fresnel::OrientedEtas orientedEta = bxdf::fresnel::OrientedEtas::create(1.0, hlsl::promote(_rc.eta.x)); + base_t::bxdf.ndf = base_t::bxdf_t::ndf_type::create(_rc.alpha.x); + base_t::bxdf.fresnel = base_t::bxdf_t::fresnel_type::create(orientedEta); +#ifndef __HLSL_VERSION + base_t::name = "GGX Dielectric BSDF"; +#endif + } +}; + +template +struct TestBxDF> : TestBxDFBase> +{ + using base_t = TestBxDFBase>; + + void initBxDF(SBxDFTestResources _rc) + { + bxdf::fresnel::OrientedEtas orientedEta = bxdf::fresnel::OrientedEtas::create(1.0, hlsl::promote(_rc.eta.x)); + base_t::bxdf.ndf = base_t::bxdf_t::ndf_type::create(_rc.alpha.x, _rc.alpha.y); + base_t::bxdf.fresnel = base_t::bxdf_t::fresnel_type::create(orientedEta); +#ifndef __HLSL_VERSION + base_t::name = "GGX Dielectric Aniso BSDF"; +#endif + } +}; + +template +struct TestBxDF> : TestBxDFBase> +{ + using base_t = TestBxDFBase>; + + void initBxDF(SBxDFTestResources _rc) + { + base_t::bxdf.ndf = base_t::bxdf_t::ndf_type::create(_rc.alpha.x); + using creation_params_t = base_t::bxdf_t::fresnel_type::creation_params_type; + creation_params_t params; + params.Dinc = _rc.Dinc; + params.ior1 = hlsl::promote(1.0); + params.ior2 = hlsl::promote(_rc.etaThinFilm); + params.ior3 = hlsl::promote(_rc.eta.x); + base_t::bxdf.fresnel = base_t::bxdf_t::fresnel_type::create(params); +#ifndef __HLSL_VERSION + base_t::name = "Iridescent BSDF"; +#endif + } +}; + + +namespace reciprocity_test_impl +{ +template && concepts::FloatingPointLikeVectorial) +struct SIsotropic +{ + using this_t = SIsotropic; + using ray_dir_info_type = RayDirInfo; + using scalar_type = typename RayDirInfo::scalar_type; + using vector3_type = typename RayDirInfo::vector3_type; + using spectral_type = Spectrum; + + // WARNING: Changed since GLSL, now arguments need to be normalized! + static this_t create(NBL_CONST_REF_ARG(RayDirInfo) normalizedV, const vector3_type normalizedN) + { + this_t retval; + retval.V = normalizedV; + retval.N = normalizedN; + retval.NdotV = nbl::hlsl::dot(retval.N, retval.V.getDirection()); + retval.NdotV2 = retval.NdotV * retval.NdotV; + retval.luminosityContributionHint = hlsl::promote(1.0); + + return retval; + } + + template) + static this_t copy(NBL_CONST_REF_ARG(I) other) + { + this_t retval; + retval.V = other.getV(); + retval.N = other.getN(); + retval.NdotV = other.getNdotV(); + retval.NdotV2 = other.getNdotV2(); + retval.pathOrigin = bxdf::PathOrigin::PO_SENSOR; + retval.luminosityContributionHint = other.luminosityContributionHint; + return retval; + } + + RayDirInfo getV() NBL_CONST_MEMBER_FUNC { return V; } + vector3_type getN() NBL_CONST_MEMBER_FUNC { return N; } + scalar_type getNdotV(bxdf::BxDFClampMode _clamp = bxdf::BxDFClampMode::BCM_NONE) NBL_CONST_MEMBER_FUNC + { + return bxdf::conditionalAbsOrMax(NdotV, _clamp); + } + scalar_type getNdotV2() NBL_CONST_MEMBER_FUNC { return NdotV2; } + + bxdf::PathOrigin getPathOrigin() NBL_CONST_MEMBER_FUNC { return pathOrigin; } + spectral_type getLuminosityContributionHint() NBL_CONST_MEMBER_FUNC { return luminosityContributionHint; } + + RayDirInfo V; + vector3_type N; + scalar_type NdotV; + scalar_type NdotV2; + bxdf::PathOrigin pathOrigin; + spectral_type luminosityContributionHint; +}; + +template) +struct SAnisotropic +{ + using this_t = SAnisotropic; + using isotropic_interaction_type = IsotropicInteraction; + using ray_dir_info_type = typename isotropic_interaction_type::ray_dir_info_type; + using scalar_type = typename ray_dir_info_type::scalar_type; + using vector3_type = typename ray_dir_info_type::vector3_type; + using matrix3x3_type = hlsl::matrix; + using spectral_type = typename isotropic_interaction_type::spectral_type; + + // WARNING: Changed since GLSL, now arguments need to be normalized! + static this_t create( + NBL_CONST_REF_ARG(isotropic_interaction_type) isotropic, + const vector3_type normalizedT, + const vector3_type normalizedB + ) + { + this_t retval; + retval.isotropic = isotropic; + + retval.T = normalizedT; + retval.B = normalizedB; + + retval.TdotV = nbl::hlsl::dot(retval.isotropic.getV().getDirection(), retval.T); + retval.BdotV = nbl::hlsl::dot(retval.isotropic.getV().getDirection(), retval.B); + + return retval; + } + static this_t create(NBL_CONST_REF_ARG(isotropic_interaction_type) isotropic, const vector3_type normalizedT) + { + return create(isotropic, normalizedT, cross(isotropic.getN(), normalizedT)); + } + static this_t create(NBL_CONST_REF_ARG(isotropic_interaction_type) isotropic) + { + vector3_type T, B; + math::frisvad(isotropic.getN(), T, B); + return create(isotropic, nbl::hlsl::normalize(T), nbl::hlsl::normalize(B)); + } + + static this_t create(NBL_CONST_REF_ARG(ray_dir_info_type) normalizedV, const vector3_type normalizedN) + { + isotropic_interaction_type isotropic = isotropic_interaction_type::create(normalizedV, normalizedN); + return create(isotropic); + } + + template) + static this_t copy(NBL_CONST_REF_ARG(I) other) + { + this_t retval; + retval.isotropic = isotropic_interaction_type::template copy(other.isotropic); + retval.T = other.getT(); + retval.B = other.getB(); + retval.TdotV = other.getTdotV(); + retval.BdotV = other.getBdotV(); + return retval; + } + + ray_dir_info_type getV() NBL_CONST_MEMBER_FUNC { return isotropic.getV(); } + vector3_type getN() NBL_CONST_MEMBER_FUNC { return isotropic.getN(); } + scalar_type getNdotV(bxdf::BxDFClampMode _clamp = bxdf::BxDFClampMode::BCM_NONE) NBL_CONST_MEMBER_FUNC { return isotropic.getNdotV(_clamp); } + scalar_type getNdotV2() NBL_CONST_MEMBER_FUNC { return isotropic.getNdotV2(); } + bxdf::PathOrigin getPathOrigin() NBL_CONST_MEMBER_FUNC { return isotropic.getPathOrigin(); } + spectral_type getLuminosityContributionHint() NBL_CONST_MEMBER_FUNC { return isotropic.getLuminosityContributionHint(); } + + vector3_type getT() NBL_CONST_MEMBER_FUNC { return T; } + vector3_type getB() NBL_CONST_MEMBER_FUNC { return B; } + scalar_type getTdotV() NBL_CONST_MEMBER_FUNC { return TdotV; } + scalar_type getTdotV2() NBL_CONST_MEMBER_FUNC { const scalar_type t = getTdotV(); return t*t; } + scalar_type getBdotV() NBL_CONST_MEMBER_FUNC { return BdotV; } + scalar_type getBdotV2() NBL_CONST_MEMBER_FUNC { const scalar_type t = getBdotV(); return t*t; } + + vector3_type getTangentSpaceV() NBL_CONST_MEMBER_FUNC { return vector3_type(TdotV, BdotV, isotropic.getNdotV()); } + matrix3x3_type getToTangentSpace() NBL_CONST_MEMBER_FUNC { return matrix3x3_type(T, B, isotropic.getN()); } + matrix3x3_type getFromTangentSpace() NBL_CONST_MEMBER_FUNC { return nbl::hlsl::transpose(matrix3x3_type(T, B, isotropic.getN())); } + + isotropic_interaction_type isotropic; + vector3_type T; + vector3_type B; + scalar_type TdotV; + scalar_type BdotV; +}; + + +template +struct CustomIsoMicrofacetConfiguration; + +template +NBL_BOOL_CONCEPT CustomMicrofacetConfigIso = bxdf::LightSample && bxdf::surface_interactions::Isotropic && !bxdf::surface_interactions::Anisotropic && bxdf::CreatableIsotropicMicrofacetCache && !bxdf::AnisotropicMicrofacetCache && concepts::FloatingPointLikeVectorial; + +template +NBL_PARTIAL_REQ_TOP(CustomMicrofacetConfigIso) +struct CustomIsoMicrofacetConfiguration) > +#undef MICROFACET_CONF_ISO +{ + NBL_CONSTEXPR_STATIC_INLINE bool IsAnisotropic = false; + + using scalar_type = typename LS::scalar_type; + using ray_dir_info_type = typename LS::ray_dir_info_type; + using vector2_type = hlsl::vector; + using vector3_type = hlsl::vector; + using monochrome_type = hlsl::vector; + using matrix3x3_type = hlsl::matrix; + using isotropic_interaction_type = Interaction; + using anisotropic_interaction_type = reciprocity_test_impl::SAnisotropic; + using sample_type = LS; + using spectral_type = Spectrum; + using quotient_pdf_type = sampling::quotient_and_pdf; + using isocache_type = MicrofacetCache; + using anisocache_type = bxdf::SAnisotropicMicrofacetCache; +}; +} + +using rectest_iso_interaction = reciprocity_test_impl::SIsotropic; +using rectest_aniso_interaction = reciprocity_test_impl::SAnisotropic; +using rectest_iso_microfacet_config_t = reciprocity_test_impl::CustomIsoMicrofacetConfiguration; +using rectest_aniso_microfacet_config_t = bxdf::SMicrofacetConfiguration; + +#endif diff --git a/66_HLSLBxDFTests/main.cpp b/66_HLSLBxDFTests/main.cpp index f91ed5f02..a91249049 100644 --- a/66_HLSLBxDFTests/main.cpp +++ b/66_HLSLBxDFTests/main.cpp @@ -1,73 +1,489 @@ #include -#include +#include #include +#include +#include -#include +#include "nbl/examples/examples.hpp" +#include "nbl/this_example/builtin/build/spirv/keys.hpp" +using namespace nbl; +using namespace core; +using namespace system; +using namespace asset; +using namespace video; using namespace nbl::hlsl; +using namespace nbl::examples; +#include "app_resources/test_components.hlsl" #include "app_resources/tests.hlsl" +#include "tests.h" +#include "nbl/builtin/hlsl/math/angle_adding.hlsl" +#include "nbl/builtin/hlsl/bxdf/ndf.hlsl" +#include "nbl/builtin/hlsl/bxdf/fresnel.hlsl" -struct PrintFailureCallback : FailureCallback +#include "nlohmann/json.hpp" + +using json = nlohmann::json; + +#define FOR_EACH_BEGIN_EX(r, ex) std::for_each(ex, r.begin(), r.end(), [&](uint32_t i) { +#define FOR_EACH_BEGIN(r) std::for_each(std::execution::par_unseq, r.begin(), r.end(), [&](uint32_t i) { +#define FOR_EACH_END }); + +#define RUN_TEST_OF_TYPE(TEST_TYPE, INIT_PARAMS) {\ + PrintFailureCallback cb;\ + cb.logger = m_logger;\ + BOOST_PP_REMOVE_PARENS(TEST_TYPE)::run(INIT_PARAMS, cb);\ +}\ + +#define RUN_CHI2_TEST_WRITE_EXR(TEST_TYPE, INIT_PARAMS) {\ + PrintFailureCallback cb;\ + cb.logger = m_logger;\ + BOOST_PP_REMOVE_PARENS(TEST_TYPE) t;\ + t.init(initparams.halfSeed);\ + t.rc.halfSeed = initparams.halfSeed;\ + t.numSamples = initparams.samples;\ + t.thetaSplits = initparams.thetaSplits;\ + t.phiSplits = initparams.phiSplits;\ + t.write_frequencies = static_cast(initparams.writeFrequencies);\ + t.initBxDF(t.rc);\ + TestResult e = t.test();\ + if (e != BTR_INVALID_TEST_CONFIG)\ + {\ + if (e != BTR_NONE)\ + {\ + if (initparams.writeFrequencies >= BOOST_PP_REMOVE_PARENS(TEST_TYPE)::WFE_WRITE_ERRORS)\ + writeToEXR(t);\ + cb.__call(e, t, initparams.logInfo);\ + }\ + else if (initparams.writeFrequencies == BOOST_PP_REMOVE_PARENS(TEST_TYPE)::WFE_WRITE_ALL)\ + writeToEXR(t);\ + }\ +}\ + +class HLSLBxDFTests final : public application_templates::MonoDeviceApplication, public BuiltinResourcesApplication { - void __call(ErrorType error, NBL_REF_ARG(TestBase) failedFor) override + using device_base_t = application_templates::MonoDeviceApplication; + using asset_base_t = BuiltinResourcesApplication; + +public: + HLSLBxDFTests(const path& _localInputCWD, const path& _localOutputCWD, const path& _sharedInputCWD, const path& _sharedOutputCWD) : + system::IApplicationFramework(_localInputCWD, _localOutputCWD, _sharedInputCWD, _sharedOutputCWD) {} + + inline bool onAppInitialized(smart_refctd_ptr&& system) override + { + if (!device_base_t::onAppInitialized(smart_refctd_ptr(system))) + return false; + + if (!asset_base_t::onAppInitialized(std::move(system))) + return false; + + std::ifstream f("../app_resources/config.json"); + if (f.fail()) + { + m_logger->log("could not open config file", ILogger::ELL_ERROR); + return false; + } + try + { + testconfigs = json::parse(f); + } + catch (json::parse_error& ex) + { + m_logger->log("parse_error.%d failed to parse config file at byte %u: %s", ILogger::ELL_ERROR, ex.id, ex.byte, ex.what()); + return false; + } + + // test compile with dxc + { + IAssetLoader::SAssetLoadParams lp = {}; + lp.logger = m_logger.get(); + lp.workingDirectory = "app_resources"; // virtual root + auto key = nbl::this_example::builtin::build::get_spirv_key<"shader">(m_device.get()); + auto bundle = m_assetMgr->getAsset(key.c_str(), lp); + + const auto assets = bundle.getContents(); + if (assets.empty()) + m_logger->log("Could not load shader!", ILogger::ELL_ERROR); + + // Cast down the asset to its proper type + auto shader = IAsset::castDown(assets[0]); + + if (!shader) + m_logger->log("compile shader test failed!", ILogger::ELL_ERROR); + } + + // test concepts, not comprehensive + static_assert(bxdf::surface_interactions::Isotropic); + static_assert(bxdf::surface_interactions::Isotropic); + static_assert(bxdf::surface_interactions::Anisotropic); + + static_assert(bxdf::CreatableIsotropicMicrofacetCache); + static_assert(bxdf::ReadableIsotropicMicrofacetCache); + static_assert(bxdf::AnisotropicMicrofacetCache); + + using ndf_beckmann_t = bxdf::ndf::Beckmann; + static_assert(bxdf::ndf::NDF); + using ndf_ggx_t = bxdf::ndf::GGX; + static_assert(bxdf::ndf::NDF); + + using fresnel_schlick_t = bxdf::fresnel::Schlick; + static_assert(bxdf::fresnel::Fresnel); + + static_assert(bxdf::bxdf_concepts::IsotropicBxDF>); + static_assert(bxdf::bxdf_concepts::IsotropicBxDF>); + static_assert(bxdf::bxdf_concepts::IsotropicBxDF>); + + static_assert(bxdf::bxdf_concepts::MicrofacetBxDF>); + static_assert(bxdf::bxdf_concepts::MicrofacetBxDF>); + static_assert(bxdf::bxdf_concepts::MicrofacetBxDF>); + static_assert(bxdf::bxdf_concepts::MicrofacetBxDF>); + + runTests(); + + return true; + } + + void workLoopBody() override {} + + bool keepRunning() override { return false; } + + bool onAppTerminated() override + { + return device_base_t::onAppTerminated(); + } + +private: + template + struct PrintFailureCallback : FailureCallback { - switch (error) + void __call(TestResult error, NBL_REF_ARG(TestT) failedFor, bool logInfo) override + { + switch (error) + { + case BTR_INVALID_TEST_CONFIG: + if (logInfo) + logger->log("seed %u: %s skipping test due to invalid NdotV/NdotL config", ILogger::ELL_INFO, failedFor.rc.halfSeed, failedFor.name.c_str()); + break; + case BTR_ERROR_NEGATIVE_VAL: + logger->log("seed %u: %s pdf/quotient/eval < 0", ILogger::ELL_ERROR, failedFor.rc.halfSeed, failedFor.name.c_str()); + break; + case BTR_ERROR_GENERATED_SAMPLE_NON_POSITIVE_PDF: + logger->log("seed %u: %s generated sample has pdf = 0", ILogger::ELL_ERROR, failedFor.rc.halfSeed, failedFor.name.c_str()); + break; + case BTR_ERROR_QUOTIENT_INF: + logger->log("seed %u: %s quotient -> inf", ILogger::ELL_ERROR, failedFor.rc.halfSeed, failedFor.name.c_str()); + break; + case BTR_ERROR_JACOBIAN_TEST_FAIL: + logger->log("seed %u: %s failed the jacobian * pdf test %s", ILogger::ELL_ERROR, failedFor.rc.halfSeed, failedFor.name.c_str(), failedFor.errMsg.c_str()); + break; + case BTR_ERROR_PDF_EVAL_DIFF: + logger->log("seed %u: %s quotient * pdf != eval %s", ILogger::ELL_ERROR, failedFor.rc.halfSeed, failedFor.name.c_str(), failedFor.errMsg.c_str()); + break; + case BTR_ERROR_NO_RECIPROCITY: + logger->log("seed %u: %s failed the reciprocity test %s", ILogger::ELL_ERROR, failedFor.rc.halfSeed, failedFor.name.c_str(), failedFor.errMsg.c_str()); + break; + case BTR_ERROR_REFLECTANCE_OUT_OF_RANGE: + logger->log("seed %u: %s reflectance not between 0 and 1 %s", ILogger::ELL_ERROR, failedFor.rc.halfSeed, failedFor.name.c_str(), failedFor.errMsg.c_str()); + break; + case BTR_PRINT_MSG: + logger->log("seed %u: %s error message %s", ILogger::ELL_ERROR, failedFor.rc.halfSeed, failedFor.name.c_str(), failedFor.errMsg.c_str()); + break; + case BTR_ERROR_GENERATED_H_INVALID: + logger->log("seed %u: %s failed invalid H configuration generated %s", ILogger::ELL_WARNING, failedFor.rc.halfSeed, failedFor.name.c_str(), failedFor.errMsg.c_str()); + break; + default: + logger->log("seed %u: %s unknown error", ILogger::ELL_ERROR, failedFor.rc.halfSeed, failedFor.name.c_str()); + } + +#ifdef _NBL_DEBUG + for (volatile bool repeat = true; IsDebuggerPresent() && repeat && error < BTR_NOBREAK; ) + { + repeat = false; + _NBL_DEBUG_BREAK_IF(true); + failedFor.compute(); + } +#endif + } + + smart_refctd_ptr logger; + }; + + void runTests() + { + const bool logInfo = testconfigs["logInfo"]; + + // test jacobian * pdf + uint32_t runs = testconfigs["TestJacobian"]["runs"]; + auto rJacobian = std::ranges::views::iota(0u, runs); + FOR_EACH_BEGIN(rJacobian) + STestInitParams initparams{ .logInfo = logInfo }; + initparams.halfSeed = i; + initparams.verbose = testconfigs["TestJacobian"]["verbose"]; + + RUN_TEST_OF_TYPE((TestJacobian>), initparams); + RUN_TEST_OF_TYPE((TestJacobian>), initparams); + RUN_TEST_OF_TYPE((TestJacobian>), initparams); + RUN_TEST_OF_TYPE((TestJacobian, false>), initparams); + RUN_TEST_OF_TYPE((TestJacobian, true>), initparams); + RUN_TEST_OF_TYPE((TestJacobian, false>), initparams); + RUN_TEST_OF_TYPE((TestJacobian, true>), initparams); + RUN_TEST_OF_TYPE((TestJacobian, false>), initparams); + + RUN_TEST_OF_TYPE((TestJacobian>), initparams); + RUN_TEST_OF_TYPE((TestJacobian>), initparams); + RUN_TEST_OF_TYPE((TestJacobian >), initparams); + RUN_TEST_OF_TYPE((TestJacobian >), initparams); + RUN_TEST_OF_TYPE((TestJacobian>), initparams); + RUN_TEST_OF_TYPE((TestJacobian, false>), initparams); + RUN_TEST_OF_TYPE((TestJacobian, true>), initparams); + RUN_TEST_OF_TYPE((TestJacobian, false>), initparams); + RUN_TEST_OF_TYPE((TestJacobian, true>), initparams); + RUN_TEST_OF_TYPE((TestJacobian, false>), initparams); + FOR_EACH_END + + + // test reciprocity + runs = testconfigs["TestReciprocity"]["runs"]; + auto rReciprocity = std::ranges::views::iota(0u, runs); + FOR_EACH_BEGIN(rReciprocity) + STestInitParams initparams{ .logInfo = logInfo }; + initparams.halfSeed = i; + initparams.verbose = testconfigs["TestReciprocity"]["verbose"]; + + RUN_TEST_OF_TYPE((TestReciprocity>), initparams); + RUN_TEST_OF_TYPE((TestReciprocity>), initparams); + RUN_TEST_OF_TYPE((TestReciprocity>), initparams); + RUN_TEST_OF_TYPE((TestReciprocity, false>), initparams); + RUN_TEST_OF_TYPE((TestReciprocity, true>), initparams); + RUN_TEST_OF_TYPE((TestReciprocity, false>), initparams); + RUN_TEST_OF_TYPE((TestReciprocity, true>), initparams); + RUN_TEST_OF_TYPE((TestReciprocity, false>), initparams); + + RUN_TEST_OF_TYPE((TestReciprocity>), initparams); + RUN_TEST_OF_TYPE((TestReciprocity>), initparams); + RUN_TEST_OF_TYPE((TestReciprocity >), initparams); + RUN_TEST_OF_TYPE((TestReciprocity >), initparams); + RUN_TEST_OF_TYPE((TestReciprocity>), initparams); + RUN_TEST_OF_TYPE((TestReciprocity, false>), initparams); + RUN_TEST_OF_TYPE((TestReciprocity, true>), initparams); + RUN_TEST_OF_TYPE((TestReciprocity, false>), initparams); + RUN_TEST_OF_TYPE((TestReciprocity, true>), initparams); + RUN_TEST_OF_TYPE((TestReciprocity, false>), initparams); + FOR_EACH_END + + + // test buckets of inf + runs = testconfigs["TestBucket"]["runs"]; + auto rBucket = std::ranges::views::iota(0u, runs); + FOR_EACH_BEGIN(rBucket) + STestInitParams initparams{ .logInfo = logInfo }; + initparams.halfSeed = i; + initparams.samples = testconfigs["TestBucket"]["samples"]; + + RUN_TEST_OF_TYPE((TestBucket>), initparams); + RUN_TEST_OF_TYPE((TestBucket>), initparams); + RUN_TEST_OF_TYPE((TestBucket, false>), initparams); + RUN_TEST_OF_TYPE((TestBucket, true>), initparams); + RUN_TEST_OF_TYPE((TestBucket, false>), initparams); + RUN_TEST_OF_TYPE((TestBucket, true>), initparams); + RUN_TEST_OF_TYPE((TestBucket, false>), initparams); + + RUN_TEST_OF_TYPE((TestBucket>), initparams); + RUN_TEST_OF_TYPE((TestBucket>), initparams); + RUN_TEST_OF_TYPE((TestBucket, false>), initparams); + RUN_TEST_OF_TYPE((TestBucket, true>), initparams); + RUN_TEST_OF_TYPE((TestBucket, false>), initparams); + RUN_TEST_OF_TYPE((TestBucket, true>), initparams); + RUN_TEST_OF_TYPE((TestBucket, false>), initparams); + FOR_EACH_END + + + // chi2 test for sampling and pdf + runs = testconfigs["TestChi2"]["runs"]; + auto rChi2 = std::ranges::views::iota(0u, runs); + FOR_EACH_BEGIN_EX(rChi2, std::execution::par_unseq) + STestInitParams initparams{ .logInfo = logInfo }; + initparams.halfSeed = i; + initparams.samples = testconfigs["TestChi2"]["samples"]; + initparams.thetaSplits = testconfigs["TestChi2"]["thetaSplits"]; + initparams.phiSplits = testconfigs["TestChi2"]["phiSplits"]; + initparams.writeFrequencies = testconfigs["TestChi2"]["writeFrequencies"]; + + RUN_CHI2_TEST_WRITE_EXR((TestChi2>), initparams); + RUN_CHI2_TEST_WRITE_EXR((TestChi2>), initparams); + RUN_CHI2_TEST_WRITE_EXR((TestChi2, false>), initparams); + RUN_CHI2_TEST_WRITE_EXR((TestChi2, true>), initparams); + RUN_CHI2_TEST_WRITE_EXR((TestChi2, false>), initparams); + RUN_CHI2_TEST_WRITE_EXR((TestChi2, true>), initparams); + RUN_CHI2_TEST_WRITE_EXR((TestChi2, false>), initparams); + + RUN_CHI2_TEST_WRITE_EXR((TestChi2>), initparams); + RUN_CHI2_TEST_WRITE_EXR((TestChi2>), initparams); + RUN_CHI2_TEST_WRITE_EXR((TestChi2, false>), initparams); + RUN_CHI2_TEST_WRITE_EXR((TestChi2, true>), initparams); + RUN_CHI2_TEST_WRITE_EXR((TestChi2, false>), initparams); + RUN_CHI2_TEST_WRITE_EXR((TestChi2, true>), initparams); + RUN_CHI2_TEST_WRITE_EXR((TestChi2, false>), initparams); + FOR_EACH_END + + // testing ndf jacobian * dg1, ONLY for cook torrance bxdfs + runs = testconfigs["TestNDF"]["runs"]; + auto rNdf = std::ranges::views::iota(0u, runs); + FOR_EACH_BEGIN(rNdf) + STestInitParams initparams{ .logInfo = logInfo }; + initparams.halfSeed = i; + initparams.verbose = testconfigs["TestNDF"]["verbose"]; + + RUN_TEST_OF_TYPE((TestNDF, false>), initparams); + RUN_TEST_OF_TYPE((TestNDF, true>), initparams); + RUN_TEST_OF_TYPE((TestNDF, false>), initparams); + RUN_TEST_OF_TYPE((TestNDF, true>), initparams); + + RUN_TEST_OF_TYPE((TestNDF, false>), initparams); + RUN_TEST_OF_TYPE((TestNDF, true>), initparams); + RUN_TEST_OF_TYPE((TestNDF, false>), initparams); + RUN_TEST_OF_TYPE((TestNDF, true>), initparams); + FOR_EACH_END + + // test generated H that NdotV*VdotH>=0.0, VdotL calculation + runs = testconfigs["TestCTGenerateH"]["runs"]; + auto rGenerateH = std::ranges::views::iota(0u, runs); + FOR_EACH_BEGIN_EX(rGenerateH, std::execution::par_unseq) + STestInitParams initparams{ .logInfo = logInfo }; + initparams.halfSeed = i; + initparams.samples = testconfigs["TestCTGenerateH"]["samples"]; + initparams.immediateFail = testconfigs["TestCTGenerateH"]["immediateFail"]; + + RUN_TEST_OF_TYPE((TestCTGenerateH, false>), initparams); + RUN_TEST_OF_TYPE((TestCTGenerateH, true>), initparams); + RUN_TEST_OF_TYPE((TestCTGenerateH, false>), initparams); + RUN_TEST_OF_TYPE((TestCTGenerateH, true>), initparams); + + RUN_TEST_OF_TYPE((TestCTGenerateH, false>), initparams); + RUN_TEST_OF_TYPE((TestCTGenerateH, true>), initparams); + RUN_TEST_OF_TYPE((TestCTGenerateH, false>), initparams); + RUN_TEST_OF_TYPE((TestCTGenerateH, true>), initparams); + FOR_EACH_END + + // test arccos angle sums { - case NEGATIVE_VAL: - fprintf(stderr, "%s pdf/quotient/eval < 0\n", failedFor.name.c_str()); - break; - case PDF_ZERO: - fprintf(stderr, "%s pdf = 0\n", failedFor.name.c_str()); - break; - case QUOTIENT_INF: - fprintf(stderr, "%s quotient -> inf\n", failedFor.name.c_str()); - break; - case JACOBIAN: - fprintf(stderr, "%s failed the jacobian * pdf test\n", failedFor.name.c_str()); - break; - case PDF_EVAL_DIFF: - fprintf(stderr, "%s quotient * pdf - eval not 0\n", failedFor.name.c_str()); - break; - case RECIPROCITY: - fprintf(stderr, "%s failed the reprocity test\n", failedFor.name.c_str()); - break; - default: - fprintf(stderr, "%s unknown error\n", failedFor.name.c_str()); + Xoroshiro64Star rng = Xoroshiro64Star::construct(uint32_t2(4, 2)); + math::sincos_accumulator angle_adder; + + auto Sin = [&](const float cosA) -> float + { + return nbl::hlsl::sqrt(1.f - cosA * cosA); + }; + + for (uint32_t i = 0; i < 10; i++) + { + const float a = ConvertToFloat01::__call(rng()) * 2.f - 1.f; + const float b = ConvertToFloat01::__call(rng()) * 2.f - 1.f; + const float c = ConvertToFloat01::__call(rng()) * 2.f - 1.f; + const float d = ConvertToFloat01::__call(rng()) * 2.f - 1.f; + + const float exAB = acos(a) + acos(b); + angle_adder = math::sincos_accumulator::create(a, Sin(a)); + angle_adder.addAngle(b, Sin(b)); + float res = angle_adder.getSumofArccos(); + if (!checkEq(res, exAB, 1e-3)) + fprintf(stderr, "[ERROR] angle adding (2 angles) failed! expected %f, got %f\n", exAB, res); + + const float exABCD = exAB + acos(c) + acos(d); + angle_adder = math::sincos_accumulator::create(a, Sin(a)); + angle_adder.addAngle(b, Sin(b)); + angle_adder.addAngle(c, Sin(c)); + angle_adder.addAngle(d, Sin(d)); + res = angle_adder.getSumofArccos(); + if (!checkEq(res, exABCD, 1e-3)) + fprintf(stderr, "[ERROR] angle adding (4 angles) failed! expected %f, got %f\n", exABCD, res); + } } + } + + template + static smart_refctd_ptr writeToCPUImage(const Chi2Test& test) + { + const uint32_t totalWidth = test.phiSplits; + const uint32_t totalHeight = 2 * test.thetaSplits; + const auto format = E_FORMAT::EF_R32G32B32A32_SFLOAT; - for (volatile bool repeat = true; IsDebuggerPresent() && repeat; ) + IImage::SCreationParams imageParams = {}; + imageParams.type = IImage::E_TYPE::ET_2D; + imageParams.format = format; + imageParams.extent = { totalWidth, totalHeight, 1 }; + imageParams.mipLevels = 1; + imageParams.arrayLayers = 1; + imageParams.samples = ICPUImage::ESCF_1_BIT; + imageParams.usage = IImage::EUF_SAMPLED_BIT; + + smart_refctd_ptr image = ICPUImage::create(std::move(imageParams)); + assert(image); + + const size_t bufferSize = totalWidth * totalHeight * getTexelOrBlockBytesize(format); { - repeat = false; - __debugbreak(); - failedFor.compute(); + auto imageRegions = make_refctd_dynamic_array>(1ull); + auto& region = imageRegions->front(); + region.bufferImageHeight = 0u; + region.bufferOffset = 0ull; + region.bufferRowLength = totalWidth; + region.imageExtent = { totalWidth, totalHeight, 1 }; + region.imageOffset = { 0u, 0u, 0u }; + region.imageSubresource.aspectMask = IImage::EAF_COLOR_BIT; + region.imageSubresource.baseArrayLayer = 0u; + region.imageSubresource.layerCount = 1; + region.imageSubresource.mipLevel = 0; + + image->setBufferAndRegions(ICPUBuffer::create({ bufferSize }), std::move(imageRegions)); } + + uint8_t* bytePtr = reinterpret_cast(image->getBuffer()->getPointer()); + + // write sample count from generate, top half + for (uint64_t j = 0u; j < test.thetaSplits; ++j) + for (uint64_t i = 0u; i < test.phiSplits; ++i) + { + float32_t3 pixelColor = hlsl::visualization::Turbo::map(test.countFreq[j * test.phiSplits + i] / test.maxCountFreq); + double decodedPixel[4] = { pixelColor[0], pixelColor[1], pixelColor[2], 1 }; + + const uint64_t pixelIndex = j * test.phiSplits + i; + asset::encodePixelsRuntime(format, bytePtr + pixelIndex * asset::getTexelOrBlockBytesize(format), decodedPixel); + } + + // write values of pdf, bottom half + for (uint64_t j = 0u; j < test.thetaSplits; ++j) + for (uint64_t i = 0u; i < test.phiSplits; ++i) + { + float32_t3 pixelColor = hlsl::visualization::Turbo::map(test.integrateFreq[j * test.phiSplits + i] / test.maxIntFreq); + double decodedPixel[4] = { pixelColor[0], pixelColor[1], pixelColor[2], 1 }; + + const uint64_t pixelIndex = (test.thetaSplits + j) * test.phiSplits + i; + asset::encodePixelsRuntime(format, bytePtr + pixelIndex * asset::getTexelOrBlockBytesize(format), decodedPixel); + } + + return image; } + + template + void writeToEXR(const Chi2Test& test) + { + std::string filename = std::format("chi2test_{}_{}.exr", test.rc.halfSeed, test.name); + + auto cpuImage = writeToCPUImage(test); + ICPUImageView::SCreationParams imgViewParams; + imgViewParams.flags = static_cast(0u); + imgViewParams.format = cpuImage->getCreationParameters().format; + imgViewParams.image = smart_refctd_ptr(cpuImage); + imgViewParams.viewType = ICPUImageView::ET_2D; + imgViewParams.subresourceRange = { static_cast(0u),0u,1u,0u,1u }; + smart_refctd_ptr imageView = ICPUImageView::create(std::move(imgViewParams)); + + IAssetWriter::SAssetWriteParams wp(imageView.get()); + m_assetMgr->writeAsset(filename, wp); + } + + json testconfigs; }; -int main(int argc, char** argv) -{ - std::cout << std::fixed << std::setprecision(4); - - const uint32_t state = 69u; - - PrintFailureCallback cb; - - // test u offset, 2 axis - TestUOffset>::run(state, cb); - TestUOffset>::run(state, cb); - TestUOffset,false>::run(state, cb); - TestUOffset,true>::run(state, cb); - TestUOffset,false>::run(state, cb); - TestUOffset,true>::run(state, cb); - - TestUOffset>::run(state, cb); - //TestUOffset>::run(state); - //TestUOffset>::run(state); - TestUOffset,false>::run(state, cb); - TestUOffset,true>::run(state, cb); - TestUOffset,false>::run(state, cb); - TestUOffset,true>::run(state, cb); - - return 0; -} \ No newline at end of file +NBL_MAIN_FUNC(HLSLBxDFTests) diff --git a/66_HLSLBxDFTests/tests.h b/66_HLSLBxDFTests/tests.h new file mode 100644 index 000000000..463571a53 --- /dev/null +++ b/66_HLSLBxDFTests/tests.h @@ -0,0 +1,540 @@ +#ifndef BXDFTESTS_TESTS_H +#define BXDFTESTS_TESTS_H +// cpp only tests + +#define GLM_ENABLE_EXPERIMENTAL +#include +#include +#include +#include +#include +#include + +#include "app_resources/tests_common.hlsl" +#include "nbl/builtin/hlsl/visualization/turbo.hlsl" +#include "nbl/builtin/hlsl/math/quadrature/adaptive_simpson.hlsl" + +// because unordered_map -- next time, do fixed size array of atomic offsets and linked lists (for readback and verification on cpu) +template +struct TestBucket : TestBxDF +{ + using base_t = TestBxDFBase; + using this_t = TestBucket; + using traits_t = bxdf::traits; + + void clearBuckets() + { + for (float y = -1.0f; y < 1.0f; y += stride) + { + for (float x = -1.0f; x < 1.0f; x += stride) + { + buckets[float32_t2(x, y)] = 0; + } + } + } + + float bin(float a) + { + float diff = std::fmod(a, stride); + float b = (a < 0) ? -stride : 0.0f; + return a - diff + b; + } + + TestResult compute() + { + clearBuckets(); + + aniso_cache cache; + iso_cache isocache; + + sample_t s; + quotient_pdf_t pdf; + //float32_t3 bsdf; + + for (uint32_t i = 0; i < numSamples; i++) + { + float32_t3 u = ConvertToFloat01::__call(base_t::rc.rng_vec<3>()); + + NBL_IF_CONSTEXPR(traits_t::type == bxdf::BT_BRDF && !traits_t::IsMicrofacet) + { + s = base_t::bxdf.generate(base_t::anisointer, u.xy); + } + NBL_IF_CONSTEXPR(traits_t::type == bxdf::BT_BRDF && traits_t::IsMicrofacet) + { + NBL_IF_CONSTEXPR(aniso) + { + s = base_t::bxdf.generate(base_t::anisointer, u.xy, cache); + } + else + { + s = base_t::bxdf.generate(base_t::isointer, u.xy, isocache); + } + } + NBL_IF_CONSTEXPR(traits_t::type == bxdf::BT_BSDF && !traits_t::IsMicrofacet) + { + s = base_t::bxdf.generate(base_t::anisointer, u); + } + NBL_IF_CONSTEXPR(traits_t::type == bxdf::BT_BSDF && traits_t::IsMicrofacet) + { + NBL_IF_CONSTEXPR(aniso) + { + s = base_t::bxdf.generate(base_t::anisointer, u, cache); + } + else + { + s = base_t::bxdf.generate(base_t::isointer, u, isocache); + } + } + + if (!s.isValid()) + continue; + + NBL_IF_CONSTEXPR(!traits_t::IsMicrofacet) + { + pdf = base_t::bxdf.quotient_and_pdf(s, base_t::isointer); + //bsdf = float32_t3(base_t::bxdf.eval(s, base_t::isointer)); + } + NBL_IF_CONSTEXPR(traits_t::IsMicrofacet) + { + NBL_IF_CONSTEXPR(aniso) + { + pdf = base_t::bxdf.quotient_and_pdf(s, base_t::anisointer, cache); + //bsdf = float32_t3(base_t::bxdf.eval(s, base_t::anisointer, cache)); + } + else + { + pdf = base_t::bxdf.quotient_and_pdf(s, base_t::isointer, isocache); + //bsdf = float32_t3(base_t::bxdf.eval(s, base_t::isointer, isocache)); + } + } + + // put s into bucket + float32_t3x3 toTangentSpace = base_t::anisointer.getToTangentSpace(); + const ray_dir_info_t localL = s.getL().transform(toTangentSpace); + math::Polar polarCoords = math::Polar::createFromCartesian(localL.getDirection()); + float32_t2 bucket = float32_t2(bin(polarCoords.theta * numbers::inv_pi), bin(polarCoords.phi * 0.5f * numbers::inv_pi)); + + if (pdf.pdf == bit_cast(numeric_limits::infinity)) + buckets[bucket] += 1; + } + + // double check this conversion makes sense + for (auto const& b : buckets) { + if (!selective || b.second > 0) + { + math::Polar polarCoords; + polarCoords.theta = b.first.x * numbers::pi; + polarCoords.phi = b.first.y * 2.f * numbers::pi; + const float32_t3 v = polarCoords.getCartesian(); + base_t::errMsg += std::format("({:.3f},{:.3f},{:.3f}): {}\n", v.x, v.y, v.z, b.second); + } + } + return BTR_NONE; + } + + TestResult test() + { + if (traits_t::type == bxdf::BT_BRDF) + { + if (base_t::isointer.getNdotV() <= bit_cast(numeric_limits::min)) + return BTR_INVALID_TEST_CONFIG; + } + else if (traits_t::type == bxdf::BT_BSDF) + { + if (hlsl::abs(base_t::isointer.getNdotV()) <= bit_cast(numeric_limits::min)) + return BTR_INVALID_TEST_CONFIG; + } + + TestResult res = compute(); + if (res != BTR_NONE) + return res; + + return (base_t::errMsg.length() == 0) ? BTR_NONE : BTR_PRINT_MSG; + } + + static void run(NBL_CONST_REF_ARG(STestInitParams) initparams, NBL_REF_ARG(FailureCallback) cb) + { + this_t t; + t.init(initparams.halfSeed); + t.rc.halfSeed = initparams.halfSeed; + t.numSamples = initparams.samples; + t.initBxDF(t.rc); + + TestResult e = t.test(); + if (e != BTR_NONE) + cb.__call(e, t, initparams.logInfo); + } + + bool selective = true; // print only buckets with count > 0 + float stride = 0.2f; + uint32_t numSamples = 500; + std::unordered_map> buckets; +}; + +inline float adaptiveSimpson(const std::function& f, float x0, float x1, float eps = 1e-6, int depth = 6) +{ + int count = 0; + std::function integrate = + [&](float a, float b, float c, float fa, float fb, float fc, float I, float eps, int depth) + { + float d = 0.5f * (a + b); + float e = 0.5f * (b + c); + float fd = f(d); + float fe = f(e); + + float h = c - a; + float I0 = (1.0f / 12.0f) * h * (fa + 4 * fd + fb); + float I1 = (1.0f / 12.0f) * h * (fb + 4 * fe + fc); + float Ip = I0 + I1; + count++; + + if (depth <= 0 || std::abs(Ip - I) < 15 * eps) + return Ip + (1.0f / 15.0f) * (Ip - I); + + return integrate(a, d, b, fa, fd, fb, I0, .5f * eps, depth - 1) + + integrate(b, e, c, fb, fe, fc, I1, .5f * eps, depth - 1); + }; + + float a = x0; + float b = 0.5f * (x0 + x1); + float c = x1; + float fa = f(a); + float fb = f(b); + float fc = f(c); + float I = (c - a) * (1.0f / 6.0f) * (fa + 4.f * fb + fc); + return integrate(a, b, c, fa, fb, fc, I, eps, depth); +} + +inline float adaptiveSimpson2D(const std::function& f, float32_t2 x0, float32_t2 x1, float eps = 1e-6, int depth = 6) +{ + const auto integrate = [&](float y) -> float + { + return adaptiveSimpson(std::bind(f, std::placeholders::_1, y), x0.x, x1.x, eps, depth); + }; + return adaptiveSimpson(integrate, x0.y, x1.y, eps, depth); +} + +template +struct CalculatePdfSinTheta +{ + using traits_t = bxdf::traits; + + float __call(float theta, float phi) + { + float cosTheta = std::cos(theta), sinTheta = std::sin(theta); + float cosPhi = std::cos(phi), sinPhi = std::sin(phi); + + ray_dir_info_t L; + L.direction = hlsl::normalize(float32_t3(sinTheta * cosPhi, sinTheta * sinPhi, cosTheta)); + float32_t3 N = anisointer.getN(); + float NdotL = hlsl::dot(N, L.direction); + + const float32_t3 T = anisointer.getT(); + const float32_t3 B = anisointer.getB(); + sample_t s = sample_t::create(L, T, B, NdotL); + + float tmpeta = 1.f; + NBL_IF_CONSTEXPR(traits_t::IsMicrofacet) + { + const float NdotV = anisointer.getNdotV(); + NBL_IF_CONSTEXPR(traits_t::type == bxdf::BT_BRDF) + if (NdotV < 0.f) return 0.f; + + const float NdotL = s.getNdotL(); + if (NdotV * NdotL < 0.f) + tmpeta = NdotV < 0.f ? 1.f / eta : eta; + float32_t3 H = hlsl::normalize(V.getDirection() + L.getDirection() * tmpeta); + float VdotH = hlsl::dot(V.getDirection(), H); + if (NdotV * VdotH < 0.f) + { + H = -H; + VdotH = -VdotH; + } + + cache.iso_cache.VdotH = VdotH; + cache.iso_cache.LdotH = hlsl::dot(L.getDirection(), H); + cache.iso_cache.VdotL = hlsl::dot(V.getDirection(), L.getDirection()); + cache.iso_cache.absNdotH = hlsl::abs(hlsl::dot(N, H)); + cache.iso_cache.NdotH2 = cache.iso_cache.absNdotH * cache.iso_cache.absNdotH; + + if (!cache.isValid(bxdf::fresnel::OrientedEtas >::create(1.f, hlsl::promote >(tmpeta)))) + return 0.f; + + cache.fillTangents(T, B, H); + } + + float pdf; + NBL_IF_CONSTEXPR(!traits_t::IsMicrofacet) + { + pdf = bxdf.pdf(s, isointer); + } + NBL_IF_CONSTEXPR(traits_t::IsMicrofacet) + { + NBL_IF_CONSTEXPR(aniso) + { + pdf = bxdf.pdf(s, anisointer, cache); + } + else + { + pdf = bxdf.pdf(s, isointer, cache.iso_cache); + } + } + + return pdf * sinTheta; + } + + BxDF bxdf; + ray_dir_info_t V; + iso_interaction isointer; + aniso_interaction anisointer; + aniso_cache cache; + float eta; +}; + +// adapted from pbrt chi2 test: https://github.com/mmp/pbrt-v4/blob/792aaaa08d97dbedf11a3bb23e246b6443d847b4/src/pbrt/bsdfs_test.cpp#L280 +template +struct TestChi2 : TestBxDF +{ + using base_t = TestBxDFBase; + using this_t = TestChi2; + using traits_t = bxdf::traits; + + void clearBuckets() + { + const uint32_t freqSize = thetaSplits * phiSplits; + countFreq.resize(freqSize); + std::fill(countFreq.begin(), countFreq.end(), 0); + integrateFreq.resize(freqSize); + std::fill(integrateFreq.begin(), integrateFreq.end(), 0); + maxCountFreq = 0.f; + maxIntFreq = 0.f; + } + + double chi2CDF(double x, int dof) + { + if (dof < 1 || x < 0) + { + return 0.0; + } + else if (dof == 2) + { + return 1.0 - hlsl::exp(-0.5 * x); + } + else + { + return hlsl::gamma(0.5 * dof, 0.5 * x); + } + } + + enum WriteFrequenciesToEXR : uint16_t + { + WFE_DONT_WRITE = 0, + WFE_WRITE_ERRORS = 1, + WFE_WRITE_ALL = 2 + }; + + TestResult compute() + { + clearBuckets(); + + float thetaFactor = thetaSplits * numbers::inv_pi; + float phiFactor = phiSplits * 0.5f * numbers::inv_pi; + + sample_t s; + iso_cache isocache; + aniso_cache cache; + for (uint32_t i = 0; i < numSamples; i++) + { + float32_t3 u = ConvertToFloat01::__call(base_t::rc.rng_vec<3>()); + + NBL_IF_CONSTEXPR(traits_t::type == bxdf::BT_BRDF && !traits_t::IsMicrofacet) + { + s = base_t::bxdf.generate(base_t::anisointer, u.xy); + } + NBL_IF_CONSTEXPR(traits_t::type == bxdf::BT_BRDF && traits_t::IsMicrofacet) + { + if NBL_CONSTEXPR_FUNC(aniso) + s = base_t::bxdf.generate(base_t::anisointer, u.xy, cache); + else + s = base_t::bxdf.generate(base_t::isointer, u.xy, isocache); + } + NBL_IF_CONSTEXPR(traits_t::type == bxdf::BT_BSDF && !traits_t::IsMicrofacet) + { + s = base_t::bxdf.generate(base_t::anisointer, u); + } + NBL_IF_CONSTEXPR(traits_t::type == bxdf::BT_BSDF && traits_t::IsMicrofacet) + { + if NBL_CONSTEXPR_FUNC(aniso) + s = base_t::bxdf.generate(base_t::anisointer, u, cache); + else + s = base_t::bxdf.generate(base_t::isointer, u, isocache); + } + + if (!s.isValid()) + continue; + + // put s into bucket + math::Polar polarCoords = math::Polar::createFromCartesian(s.getL().getDirection()); + polarCoords.theta *= thetaFactor; + polarCoords.phi *= phiFactor; + if (polarCoords.phi < 0) + polarCoords.phi += 2.f * numbers::pi * phiFactor; + + int thetaBin = clamp((int)std::floor(polarCoords.theta), 0, thetaSplits - 1); + int phiBin = clamp((int)std::floor(polarCoords.phi), 0, phiSplits - 1); + + uint32_t freqidx = thetaBin * phiSplits + phiBin; + countFreq[freqidx] += 1; + + if (write_frequencies && maxCountFreq < countFreq[freqidx]) + maxCountFreq = countFreq[freqidx]; + } + + thetaFactor = 1.f / thetaFactor; + phiFactor = 1.f / phiFactor; + + uint32_t intidx = 0; + for (int i = 0; i < thetaSplits; i++) + { + for (int j = 0; j < phiSplits; j++) + { + uint32_t lastidx = intidx; + CalculatePdfSinTheta pdfSinTheta; + pdfSinTheta.bxdf = base_t::bxdf; + pdfSinTheta.V = base_t::rc.V; + pdfSinTheta.isointer = base_t::isointer; + pdfSinTheta.anisointer = base_t::anisointer; + pdfSinTheta.eta = base_t::rc.eta.x; + integrateFreq[intidx++] = numSamples * math::quadrature::AdaptiveSimpson2D, float>::__call( + pdfSinTheta, float32_t2(i * thetaFactor, j * phiFactor), float32_t2((i + 1) * thetaFactor, (j + 1) * phiFactor)); + + if (write_frequencies && maxIntFreq < integrateFreq[lastidx]) + maxIntFreq = integrateFreq[lastidx]; + } + } + + return BTR_NONE; + } + + TestResult test() + { + if (traits_t::type == bxdf::BT_BRDF) + if (base_t::isointer.getNdotV() <= numeric_limits::min) + return BTR_INVALID_TEST_CONFIG; + else if (traits_t::type == bxdf::BT_BSDF) + if (hlsl::abs(base_t::isointer.getNdotV()) <= numeric_limits::min) + return BTR_INVALID_TEST_CONFIG; + + TestResult res = compute(); + if (res != BTR_NONE) + return res; + + // chi2 + std::vector cells(thetaSplits * phiSplits); + for (uint32_t i = 0; i < cells.size(); i++) + { + cells[i].expFreq = integrateFreq[i]; + cells[i].index = i; + } + std::sort(cells.begin(), cells.end(), [](const Cell& a, const Cell& b) + { + return a.expFreq < b.expFreq; + }); + + float pooledFreqs = 0, pooledExpFreqs = 0, chsq = 0; + int pooledCells = 0, dof = 0; + + for (const Cell& c : cells) + { + if (integrateFreq[c.index] == 0) + { + if (countFreq[c.index] > numSamples * 1e-5) + { + base_t::errMsg = std::format("expected frequency of 0 for c but found {} samples", countFreq[c.index]); + return BTR_PRINT_MSG; + } + } + else if (integrateFreq[c.index] < minFreq) + { + pooledFreqs += countFreq[c.index]; + pooledExpFreqs += integrateFreq[c.index]; + pooledCells++; + } + else if (pooledExpFreqs > 0 && pooledExpFreqs < minFreq) + { + pooledFreqs += countFreq[c.index]; + pooledExpFreqs += integrateFreq[c.index]; + pooledCells++; + } + else + { + float diff = countFreq[c.index] - integrateFreq[c.index]; + chsq += (diff * diff) / integrateFreq[c.index]; + dof++; + } + } + + if (pooledExpFreqs > 0 || pooledFreqs > 0) + { + float diff = pooledFreqs - pooledExpFreqs; + chsq += (diff * diff) / pooledExpFreqs; + dof++; + } + dof -= 1; + + if (dof <= 0) + { + base_t::errMsg = std::format("degrees of freedom {} too low", dof); + return BTR_PRINT_MSG; + } + + float pval = 1.0f - static_cast(chi2CDF(chsq, dof)); + float alpha = 1.0f - std::pow(1.0f - threshold, 1.0f / numTests); + + if (pval < alpha || !std::isfinite(pval)) + { + base_t::errMsg = std::format("chi2 test: rejected the null hypothesis (p-value = {:.3f}, significance level = {:.3f}", pval, alpha); + return BTR_PRINT_MSG; + } + + return BTR_NONE; + } + + static void run(NBL_CONST_REF_ARG(STestInitParams) initparams, NBL_REF_ARG(FailureCallback) cb) + { + this_t t; + t.init(initparams.halfSeed); + t.rc.halfSeed = initparams.halfSeed; + t.numSamples = initparams.samples; + t.thetaSplits = initparams.thetaSplits; + t.phiSplits = initparams.phiSplits; + t.write_frequencies = static_cast(initparams.writeFrequencies); + t.initBxDF(t.rc); + + TestResult e = t.test(); + if (e != BTR_NONE) + cb.__call(e, t, initparams.logInfo); + } + + struct Cell { + float expFreq; + uint32_t index; + }; + + uint32_t thetaSplits = 80; + uint32_t phiSplits = 160; + uint32_t numSamples = 1000000; + + uint32_t threshold = 1e-2; + uint32_t minFreq = 5; + uint32_t numTests = 5; + + WriteFrequenciesToEXR write_frequencies; + float maxCountFreq; + float maxIntFreq; + + std::vector countFreq; + std::vector integrateFreq; +}; + +#endif