diff --git a/66_HLSLBxDFTests/CMakeLists.txt b/66_HLSLBxDFTests/CMakeLists.txt index d26a90205..66168ba88 100644 --- a/66_HLSLBxDFTests/CMakeLists.txt +++ b/66_HLSLBxDFTests/CMakeLists.txt @@ -1,28 +1,32 @@ 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() + +add_dependencies(${EXECUTABLE_NAME} OpenEXR) +target_link_libraries(${EXECUTABLE_NAME} PRIVATE OpenEXR) +target_include_directories(${EXECUTABLE_NAME} PUBLIC $) diff --git a/66_HLSLBxDFTests/app_resources/config.json b/66_HLSLBxDFTests/app_resources/config.json new file mode 100644 index 000000000..a329006e5 --- /dev/null +++ b/66_HLSLBxDFTests/app_resources/config.json @@ -0,0 +1,20 @@ +{ + "logInfo": false, + "TestJacobian": { + "runs": 10 + }, + "TestReciprocity": { + "runs": 10 + }, + "TestBucket": { + "runs": 10, + "samples": 500 + }, + "TestChi2": { + "runs": 5, + "samples": 1000000, + "thetaSplits": 80, + "phiSplits": 160, + "writeFrequencies": 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..916b5e281 --- /dev/null +++ b/66_HLSLBxDFTests/app_resources/test_compile.comp.hlsl @@ -0,0 +1,92 @@ +#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" +#include "nbl/builtin/hlsl/bxdf/bxdf_traits.hlsl" + +[[vk::binding(0,0)]] RWStructuredBuffer buff; + +using namespace nbl::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 = sampling::quotient_and_pdf; +using spectral_t = vector; + +[numthreads(WORKGROUP_SIZE,1,1)] +void main(uint32_t3 ID : SV_DispatchThreadID) +{ + bxdf::reflection::SLambertianBxDF lambertianBRDF; + bxdf::reflection::SOrenNayarBxDF orenNayarBRDF; + bxdf::reflection::SBeckmannBxDF beckmannBRDF; + bxdf::reflection::SGGXBxDF ggxBRDF; + + bxdf::transmission::SLambertianBxDF lambertianBSDF; + bxdf::transmission::SSmoothDielectricBxDF smoothDielectricBSDF; + bxdf::transmission::SSmoothDielectricBxDF thinSmoothDielectricBSDF; + bxdf::transmission::SBeckmannDielectricBxDF beckmannBSDF; + bxdf::transmission::SGGXDielectricBxDF ggxBSDF; + + + // 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; + + bxdf::reflection::SOrenNayarBxDF::params_isotropic_t params0 = bxdf::reflection::SOrenNayarBxDF::params_isotropic_t::create(s, isointer, bxdf::BxDFClampMode::BCM_MAX); + quotient_pdf_t qp = orenNayarBRDF.quotient_and_pdf(params0); + L -= qp.quotient; + + s = beckmannBRDF.generate(anisointer, u.xy, cache); + L += s.L.direction; + + bxdf::reflection::SBeckmannBxDF::params_anisotropic_t params1 = bxdf::reflection::SBeckmannBxDF::params_anisotropic_t::create(s, anisointer, cache, bxdf::BxDFClampMode::BCM_MAX); + qp = beckmannBRDF.quotient_and_pdf(params1); + L -= qp.quotient; + + s = ggxBRDF.generate(anisointer, u.xy, cache); + L += s.L.direction; + + bxdf::reflection::SGGXBxDF::params_anisotropic_t params2 = bxdf::reflection::SGGXBxDF::params_anisotropic_t::create(s, anisointer, cache, bxdf::BxDFClampMode::BCM_MAX); + qp = ggxBRDF.quotient_and_pdf(params2); + L -= qp.quotient; + + s = lambertianBSDF.generate(anisointer, u); + L += s.L.direction; + + s = thinSmoothDielectricBSDF.generate(anisointer, u); + L += s.L.direction; + + bxdf::transmission::SSmoothDielectricBxDF::params_isotropic_t params3 = bxdf::transmission::SSmoothDielectricBxDF::params_isotropic_t::create(s, isointer, bxdf::BxDFClampMode::BCM_ABS); + qp = thinSmoothDielectricBSDF.quotient_and_pdf(params3); + L -= qp.quotient; + + s = ggxBSDF.generate(anisointer, u, cache); + L += s.L.direction; + + bxdf::transmission::SGGXDielectricBxDF::params_anisotropic_t params4 = bxdf::transmission::SGGXDielectricBxDF::params_anisotropic_t::create(s, anisointer, cache, bxdf::BxDFClampMode::BCM_ABS); + qp = ggxBSDF.quotient_and_pdf(params4); + L -= qp.quotient; + + buff[ID.x] = L; +} \ No newline at end of file diff --git a/66_HLSLBxDFTests/app_resources/tests.hlsl b/66_HLSLBxDFTests/app_resources/tests.hlsl index 256ed3ce9..b0fa7d643 100644 --- a/66_HLSLBxDFTests/app_resources/tests.hlsl +++ b/66_HLSLBxDFTests/app_resources/tests.hlsl @@ -5,13 +5,37 @@ #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.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" #ifndef __HLSL_VERSION #include +#include +#include +#include +#include +#include +#include + +#include "ImfRgbaFile.h" +#include "ImfArray.h" +#include "ImfHeader.h" + +#include "ImfNamespace.h" +#include + +#include "nlohmann/json.hpp" + +namespace IMF = Imf; +namespace IMATH = Imath; + +using namespace IMF; +using namespace IMATH; +using json = nlohmann::json; #endif namespace nbl @@ -21,27 +45,15 @@ 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 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 aniso_cache = bxdf::SAnisotropicMicrofacetCache; +using quotient_pdf_t = sampling::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 { @@ -82,6 +94,46 @@ T rngUniformDist(NBL_REF_ARG(nbl::hlsl::Xoroshiro64Star) rng) return impl::RNGUniformDist::__call(rng); } +template +bool checkEq(T a, T b, float32_t eps) +{ + return nbl::hlsl::all::Dimension> >(nbl::hlsl::max(a / b, b / a) <= (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) < (T)eps); +} + +template<> +bool checkZero(float32_t a, float32_t eps) +{ + return nbl::hlsl::abs(a) < eps; +} + +#ifndef __HLSL_VERSION +// because atan2 is not in tgmath.hlsl yet + +// takes in normalized vectors +inline float32_t3 polarToCartesian(float32_t2 theta_phi) +{ + return float32_t3(std::cos(theta_phi.y) * std::cos(theta_phi.x), + std::sin(theta_phi.y) * std::cos(theta_phi.x), + std::sin(theta_phi.x)); +} + +inline float32_t2 cartesianToPolar(float32_t3 coords) +{ + return float32_t2(std::acos(clamp(coords.z, -1, 1)), std::atan2(coords.y, coords.x)); +} +#endif struct SBxDFTestResources { @@ -91,18 +143,21 @@ struct SBxDFTestResources retval.rng = nbl::hlsl::Xoroshiro64Star::construct(seed); retval.u = float32_t3(rngUniformDist(retval.rng), 0.0); - retval.V.direction = nbl::hlsl::normalize(uniform_sphere_generate(rngUniformDist(retval.rng))); - retval.N = nbl::hlsl::normalize(uniform_sphere_generate(rngUniformDist(retval.rng))); + retval.V.direction = nbl::hlsl::normalize(sampling::UniformSphere::generate(rngUniformDist(retval.rng))); + retval.N = nbl::hlsl::normalize(sampling::UniformSphere::generate(rngUniformDist(retval.rng))); - float32_t2x3 tb = math::frisvad(retval.N); + float32_t3 tangent, bitangent; + math::frisvad(retval.N, tangent, bitangent); + tangent = nbl::hlsl::normalize(tangent); + bitangent = nbl::hlsl::normalize(bitangent); #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]; + retval.T = rot * tangent; + retval.B = rot * bitangent; #else - retval.T = tb[0]; - retval.B = tb[1]; + retval.T = tangent; + retval.B = bitangent; #endif retval.alpha.x = rngUniformDist(retval.rng); @@ -113,8 +168,8 @@ struct SBxDFTestResources return retval; } - // epsilon - float eps = 1e-3; + float eps = 1e-3; // epsilon + uint32_t state; // init state seed, for debugging nbl::hlsl::Xoroshiro64Star rng; ray_dir_info_t V; @@ -129,15 +184,29 @@ struct SBxDFTestResources float32_t3 luma_coeff; }; +struct STestInitParams +{ + bool logInfo; + uint32_t state; + uint32_t samples; + uint32_t thetaSplits; + uint32_t phiSplits; + bool writeFrequencies; +}; + 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 + BET_NONE = 0, + BET_NEGATIVE_VAL, // pdf/quotient/eval < 0 + BET_PDF_ZERO, // pdf = 0 + BET_QUOTIENT_INF, // quotient -> inf + BET_JACOBIAN, + BET_PDF_EVAL_DIFF, + BET_RECIPROCITY, + + BET_NOBREAK, // not an error code, ones after this don't break + BET_INVALID, + BET_PRINT_MSG }; struct TestBase @@ -150,7 +219,7 @@ struct TestBase anisointer = aniso_interaction::create(isointer, rc.T, rc.B); } - virtual void compute() {} + virtual ErrorType compute() { return BET_NONE; } SBxDFTestResources rc; @@ -159,12 +228,13 @@ struct TestBase #ifndef __HLSL_VERSION std::string name = "base"; + std::string errMsg = ""; #endif }; struct FailureCallback { - virtual void __call(ErrorType error, NBL_REF_ARG(TestBase) failedFor) {} + virtual void __call(ErrorType error, NBL_REF_ARG(TestBase) failedFor, bool logInfo) {} }; template @@ -202,23 +272,23 @@ struct TestBxDF -struct TestBxDF> : TestBxDFBase> +struct TestBxDF> : TestBxDFBase> { - using base_t = TestBxDFBase>; + using base_t = TestBxDFBase>; 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)); + 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 { - base_t::bxdf = bxdf::reflection::SBeckmannBxDF::create(rc.alpha.x,(float32_t3)(rc.ior.x),(float32_t3)(rc.ior.y)); + 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"; #endif @@ -227,23 +297,23 @@ struct TestBxDF -struct TestBxDF> : TestBxDFBase> +struct TestBxDF> : TestBxDFBase> { - using base_t = 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)); + 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)); + 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 @@ -252,13 +322,13 @@ struct TestBxDF -struct TestBxDF> : TestBxDFBase> +struct TestBxDF> : TestBxDFBase> { - using base_t = TestBxDFBase>; + using base_t = TestBxDFBase>; void initBxDF(SBxDFTestResources _rc) { - base_t::bxdf = bxdf::transmission::SSmoothDielectricBxDF::create(rc.eta); + base_t::bxdf = bxdf::transmission::SSmoothDielectricBxDF::create(rc.eta); #ifndef __HLSL_VERSION base_t::name = "Smooth dielectric BSDF"; #endif @@ -266,13 +336,13 @@ struct TestBxDF -struct TestBxDF> : TestBxDFBase> +struct TestBxDF> : TestBxDFBase> { - using base_t = 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); + 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 @@ -280,23 +350,23 @@ struct TestBxDF -struct TestBxDF> : TestBxDFBase> +struct TestBxDF> : TestBxDFBase> { - using base_t = TestBxDFBase>; + 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); + 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); + base_t::bxdf = bxdf::transmission::SBeckmannDielectricBxDF::create(rc.eta,rc.alpha.x); #ifndef __HLSL_VERSION base_t::name = "Beckmann Dielectric BSDF"; #endif @@ -305,23 +375,23 @@ struct TestBxDF -struct TestBxDF> : TestBxDFBase> +struct TestBxDF> : TestBxDFBase> { - using base_t = TestBxDFBase>; + using base_t = TestBxDFBase>; template void initBxDF(SBxDFTestResources _rc) { if (aniso) { - base_t::bxdf = bxdf::transmission::SGGXDielectricBxDF::create(rc.eta,rc.alpha.x,rc.alpha.y); + 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 } else { - base_t::bxdf = bxdf::transmission::SGGXDielectricBxDF::create(rc.eta,rc.alpha.x); + base_t::bxdf = bxdf::transmission::SGGXDielectricBxDF::create(rc.eta,rc.alpha.x); #ifndef __HLSL_VERSION base_t::name = "GGX Dielectric BSDF"; #endif @@ -338,21 +408,21 @@ struct is_basic_brdf : bool_constant< template struct is_microfacet_brdf : bool_constant< - is_same>::value || - is_same>::value + is_same>::value || + is_same>::value > {}; template struct is_basic_bsdf : bool_constant< is_same>::value || - is_same>::value || - is_same>::value + is_same>::value || + is_same>::value > {}; template struct is_microfacet_bsdf : bool_constant< - is_same>::value || - is_same>::value + is_same>::value || + is_same>::value > {}; template @@ -366,106 +436,157 @@ NBL_CONSTEXPR bool is_microfacet_bsdf_v = is_microfacet_bsdf::value; template -struct TestUOffset : TestBxDF +struct TestJacobian : TestBxDF { using base_t = TestBxDFBase; - using this_t = TestUOffset; + using this_t = TestJacobian; + using params_t = conditional_t; - void compute() override + virtual ErrorType compute() override { aniso_cache cache, dummy; + iso_cache isocache; + params_t params; 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) { - 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(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); + params = params_t::create(s, base_t::isointer, bxdf::BCM_MAX); } if NBL_CONSTEXPR_FUNC (is_microfacet_brdf_v) { 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); + + if NBL_CONSTEXPR_FUNC (aniso) + params = params_t::create(s, base_t::anisointer, cache, bxdf::BCM_MAX); + else + { + isocache = cache.iso_cache; + params = params_t::create(s, base_t::isointer, isocache, bxdf::BCM_MAX); + } } if NBL_CONSTEXPR_FUNC (is_basic_bsdf_v) { 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); + params = params_t::create(s, base_t::isointer, bxdf::BCM_ABS); } if NBL_CONSTEXPR_FUNC (is_microfacet_bsdf_v) { 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); + + if NBL_CONSTEXPR_FUNC (aniso) + params = params_t::create(s, base_t::anisointer, cache, bxdf::BCM_ABS); + else + { + isocache = cache.iso_cache; + params = params_t::create(s, base_t::isointer, isocache, bxdf::BCM_ABS); + } } - + + // TODO: add checks with need clamp trait + if (bxdf::traits::type == bxdf::BT_BRDF) + { + if (s.getNdotL() <= bit_cast(numeric_limits::min)) + return BET_INVALID; + } + else if (bxdf::traits::type == bxdf::BT_BSDF) + { + if (abs(s.getNdotL()) <= bit_cast(numeric_limits::min)) + return BET_INVALID; + } + if NBL_CONSTEXPR_FUNC (is_basic_brdf_v || is_basic_bsdf_v) { - pdf = base_t::bxdf.quotient_and_pdf(s, base_t::isointer); - bsdf = float32_t3(base_t::bxdf.eval(s, base_t::isointer)); + pdf = base_t::bxdf.quotient_and_pdf(params); + bsdf = float32_t3(base_t::bxdf.eval(params)); } if NBL_CONSTEXPR_FUNC (is_microfacet_brdf_v || is_microfacet_bsdf_v) { if NBL_CONSTEXPR_FUNC (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)); + pdf = base_t::bxdf.quotient_and_pdf(params); + bsdf = float32_t3(base_t::bxdf.eval(params)); } 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)); + pdf = base_t::bxdf.quotient_and_pdf(params); + bsdf = float32_t3(base_t::bxdf.eval(params)); } } + + return BET_NONE; } ErrorType test() { - compute(); + if (bxdf::traits::type == bxdf::BT_BRDF) + { + if (base_t::isointer.getNdotV() <= bit_cast(numeric_limits::min)) + return BET_INVALID; + } + else if (bxdf::traits::type == bxdf::BT_BSDF) + { + if (abs(base_t::isointer.getNdotV()) <= bit_cast(numeric_limits::min)) + return BET_INVALID; + } + + ErrorType res = compute(); + if (res != BET_NONE) + return res; + + if (checkZero(pdf.pdf, 1e-5)) // something generated cannot have 0 probability of getting generated + return BET_PDF_ZERO; - if (nbl::hlsl::abs(pdf.pdf) < base_t::rc.eps) // something generated cannot have 0 probability of getting generated - return PDF_ZERO; + if (!checkLt(pdf.quotient, (float32_t3)bit_cast(numeric_limits::infinity))) // importance sampler's job to prevent inf + return BET_QUOTIENT_INF; - if (!all(pdf.quotient < (float32_t3)numeric_limits::infinity)) // importance sampler's job to prevent inf - return QUOTIENT_INF; + if (checkZero(bsdf, 1e-5) || checkZero(pdf.quotient, 1e-5)) + return BET_NONE; // produces an "impossible" sample - 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 + if (checkLt(bsdf, (float32_t3)0.0) || checkLt(pdf.quotient, (float32_t3)0.0) || pdf.pdf < 0.0) + return BET_NEGATIVE_VAL; - // get jacobian - float32_t2x2 m = float32_t2x2(sx.TdotL - s.TdotL, sy.TdotL - s.TdotL, sx.BdotL - s.BdotL, sy.BdotL - s.BdotL); + // get BET_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); - bool jacobian_test = nbl::hlsl::abs(det*pdf.pdf/s.NdotL) < base_t::rc.eps; - if (!jacobian_test) - return JACOBIAN; + if (!checkZero(det * pdf.pdf / s.getNdotL(), 1e-5)) + return BET_JACOBIAN; - 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; + if (!checkEq(pdf.value(), bsdf, 5e-2)) + return BET_PDF_EVAL_DIFF; - return NOERR; + return BET_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); + random::PCG32 pcg = random::PCG32::construct(initparams.state); + random::DimAdaptorRecursive rand2d = random::DimAdaptorRecursive::construct(pcg); + uint32_t2 state = rand2d(); this_t t; t.init(state); + t.rc.state = initparams.state; if NBL_CONSTEXPR_FUNC (is_microfacet_brdf_v || is_microfacet_bsdf_v) t.template initBxDF(t.rc); else t.initBxDF(t.rc); ErrorType e = t.test(); - if (e != NOERR) - cb.__call(e, t); + if (e != BET_NONE) + cb.__call(e, t, initparams.logInfo); } sample_t s, sx, sy; @@ -473,6 +594,813 @@ struct TestUOffset : TestBxDF float32_t3 bsdf; }; +template +struct TestReciprocity : TestBxDF +{ + using base_t = TestBxDFBase; + using this_t = TestReciprocity; + using params_t = conditional_t; + + virtual ErrorType compute() override + { + aniso_cache cache, rec_cache; + iso_cache isocache, rec_isocache; + + if NBL_CONSTEXPR_FUNC (is_basic_brdf_v) + { + s = base_t::bxdf.generate(base_t::anisointer, base_t::rc.u.xy); + params = params_t::create(s, base_t::isointer, bxdf::BCM_MAX); + } + if NBL_CONSTEXPR_FUNC (is_microfacet_brdf_v) + { + s = base_t::bxdf.generate(base_t::anisointer, base_t::rc.u.xy, cache); + + if NBL_CONSTEXPR_FUNC (aniso) + params = params_t::create(s, base_t::anisointer, cache, bxdf::BCM_MAX); + else + { + isocache = cache.iso_cache; + params = params_t::create(s, base_t::isointer, isocache, bxdf::BCM_MAX); + } + } + if NBL_CONSTEXPR_FUNC (is_basic_bsdf_v) + { + s = base_t::bxdf.generate(base_t::anisointer, base_t::rc.u); + params = params_t::create(s, base_t::isointer, bxdf::BCM_ABS); + } + if NBL_CONSTEXPR_FUNC (is_microfacet_bsdf_v) + { + s = base_t::bxdf.generate(base_t::anisointer, base_t::rc.u, cache); + + if NBL_CONSTEXPR_FUNC (aniso) + params = params_t::create(s, base_t::anisointer, cache, bxdf::BCM_ABS); + else + { + isocache = cache.iso_cache; + params = params_t::create(s, base_t::isointer, isocache, bxdf::BCM_ABS); + } + } + + // TODO: add checks with need clamp trait + if (bxdf::traits::type == bxdf::BT_BRDF) + { + if (s.getNdotL() <= bit_cast(numeric_limits::min)) + return BET_INVALID; + } + else if (bxdf::traits::type == bxdf::BT_BSDF) + { + if (abs(s.getNdotL()) <= bit_cast(numeric_limits::min)) + return BET_INVALID; + } + + float32_t3x3 toTangentSpace = base_t::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_localV.direction, rec_localL, base_t::anisointer.getFromTangentSpace()); + + iso_interaction rec_isointer = iso_interaction::create(rec_V, base_t::rc.N); + aniso_interaction rec_anisointer = aniso_interaction::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 = rec_cache.iso_cache; + + if NBL_CONSTEXPR_FUNC (is_basic_brdf_v) + rec_params = params_t::create(rec_s, rec_isointer, bxdf::BCM_MAX); + if NBL_CONSTEXPR_FUNC (is_microfacet_brdf_v) + { + if NBL_CONSTEXPR_FUNC (aniso) + rec_params = params_t::create(rec_s, rec_anisointer, rec_cache, bxdf::BCM_MAX); + else + { + rec_isocache = rec_cache.iso_cache; + rec_params = params_t::create(rec_s, rec_isointer, rec_isocache, bxdf::BCM_MAX); + } + } + if NBL_CONSTEXPR_FUNC (is_basic_bsdf_v) + rec_params = params_t::create(rec_s, rec_isointer, bxdf::BCM_ABS); + if NBL_CONSTEXPR_FUNC (is_microfacet_bsdf_v) + { + if NBL_CONSTEXPR_FUNC (aniso) + rec_params = params_t::create(rec_s, rec_anisointer, rec_cache, bxdf::BCM_ABS); + else + { + rec_isocache = rec_cache.iso_cache; + rec_params = params_t::create(rec_s, rec_isointer, rec_isocache, bxdf::BCM_ABS); + } + } + + if NBL_CONSTEXPR_FUNC (is_basic_brdf_v || is_basic_bsdf_v) + { + bsdf = float32_t3(base_t::bxdf.eval(params)); + rec_bsdf = float32_t3(base_t::bxdf.eval(rec_params)); + } + if NBL_CONSTEXPR_FUNC (is_microfacet_brdf_v || is_microfacet_bsdf_v) + { + if NBL_CONSTEXPR_FUNC (aniso) + { + bsdf = float32_t3(base_t::bxdf.eval(params)); + rec_bsdf = float32_t3(base_t::bxdf.eval(rec_params)); + } + else + { + bsdf = float32_t3(base_t::bxdf.eval(params)); + rec_bsdf = float32_t3(base_t::bxdf.eval(rec_params)); + } + } + + return BET_NONE; + } + + ErrorType test() + { + if (bxdf::traits::type == bxdf::BT_BRDF) + { + if (base_t::isointer.getNdotV() <= bit_cast(numeric_limits::min)) + return BET_INVALID; + } + else if (bxdf::traits::type == bxdf::BT_BSDF) + { + if (abs(base_t::isointer.getNdotV()) <= bit_cast(numeric_limits::min)) + return BET_INVALID; + } + + ErrorType res = compute(); + if (res != BET_NONE) + return res; + + if (checkZero(bsdf, 1e-5)) + return BET_NONE; // produces an "impossible" sample + + if (checkLt(bsdf, (float32_t3)0.0)) + return BET_NEGATIVE_VAL; + + float32_t3 a = bsdf * nbl::hlsl::abs(params.getNdotV()); + float32_t3 b = rec_bsdf * nbl::hlsl::abs(rec_params.getNdotV()); + if (!(a == b)) // avoid division by 0 + if (!checkEq(a, b, 1e-2)) + return BET_RECIPROCITY; + + return BET_NONE; + } + + static void run(NBL_CONST_REF_ARG(STestInitParams) initparams, NBL_REF_ARG(FailureCallback) cb) + { + random::PCG32 pcg = random::PCG32::construct(initparams.state); + random::DimAdaptorRecursive rand2d = random::DimAdaptorRecursive::construct(pcg); + uint32_t2 state = rand2d(); + + this_t t; + t.init(state); + t.rc.state = initparams.state; + if NBL_CONSTEXPR_FUNC (is_microfacet_brdf_v || is_microfacet_bsdf_v) + t.template initBxDF(t.rc); + else + t.initBxDF(t.rc); + + ErrorType e = t.test(); + if (e != BET_NONE) + cb.__call(e, t, initparams.logInfo); + } + + sample_t s, rec_s; + float32_t3 bsdf, rec_bsdf; + params_t params, rec_params; +}; + +#ifndef __HLSL_VERSION // because unordered_map +template +struct TestBucket : TestBxDF +{ + using base_t = TestBxDFBase; + using this_t = TestBucket; + using params_t = conditional_t; + + 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; + } + + virtual ErrorType compute() override + { + clearBuckets(); + + aniso_cache cache; + iso_cache isocache; + params_t params; + + sample_t s; + quotient_pdf_t pdf; + float32_t3 bsdf; + + for (uint32_t i = 0; i < numSamples; i++) + { + float32_t3 u = float32_t3(rngUniformDist(base_t::rc.rng), 0.0); + + if NBL_CONSTEXPR_FUNC (is_basic_brdf_v) + { + s = base_t::bxdf.generate(base_t::anisointer, u.xy); + params = params_t::create(s, base_t::isointer, bxdf::BCM_MAX); + } + if NBL_CONSTEXPR_FUNC (is_microfacet_brdf_v) + { + s = base_t::bxdf.generate(base_t::anisointer, u.xy, cache); + + if NBL_CONSTEXPR_FUNC (aniso) + params = params_t::create(s, base_t::anisointer, cache, bxdf::BCM_MAX); + else + { + isocache = cache.iso_cache; + params = params_t::create(s, base_t::isointer, isocache, bxdf::BCM_MAX); + } + } + if NBL_CONSTEXPR_FUNC (is_basic_bsdf_v) + { + s = base_t::bxdf.generate(base_t::anisointer, u); + params = params_t::create(s, base_t::isointer, bxdf::BCM_ABS); + } + if NBL_CONSTEXPR_FUNC (is_microfacet_bsdf_v) + { + s = base_t::bxdf.generate(base_t::anisointer, u, cache); + + if NBL_CONSTEXPR_FUNC (aniso) + params = params_t::create(s, base_t::anisointer, cache, bxdf::BCM_ABS); + else + { + isocache = cache.iso_cache; + params = params_t::create(s, base_t::isointer, isocache, bxdf::BCM_ABS); + } + } + + if NBL_CONSTEXPR_FUNC (is_basic_brdf_v || is_basic_bsdf_v) + { + pdf = base_t::bxdf.quotient_and_pdf(params); + bsdf = float32_t3(base_t::bxdf.eval(params)); + } + if NBL_CONSTEXPR_FUNC (is_microfacet_brdf_v || is_microfacet_bsdf_v) + { + if NBL_CONSTEXPR_FUNC (aniso) + { + pdf = base_t::bxdf.quotient_and_pdf(params); + bsdf = float32_t3(base_t::bxdf.eval(params)); + } + else + { + pdf = base_t::bxdf.quotient_and_pdf(params); + bsdf = float32_t3(base_t::bxdf.eval(params)); + } + } + + // put s into bucket + float32_t3x3 toTangentSpace = base_t::anisointer.getToTangentSpace(); + const ray_dir_info_t localL = s.getL().transform(toTangentSpace); + const float32_t2 coords = cartesianToPolar(localL.direction); + float32_t2 bucket = float32_t2(bin(coords.x * numbers::inv_pi), bin(coords.y * 0.5f * numbers::inv_pi)); + + if (pdf.pdf == bit_cast(numeric_limits::infinity)) + buckets[bucket] += 1; + } + +#ifndef __HLSL_VERSION + // double check this conversion makes sense + for (auto const& b : buckets) { + if (!selective || b.second > 0) + { + const float32_t3 v = polarToCartesian(b.first * float32_t2(1, 2) * numbers::pi); + base_t::errMsg += std::format("({:.3f},{:.3f},{:.3f}): {}\n", v.x, v.y, v.z, b.second); + } + } +#endif + return BET_NONE; + } + + ErrorType test() + { + if (bxdf::traits::type == bxdf::BT_BRDF) + { + if (base_t::isointer.getNdotV() <= bit_cast(numeric_limits::min)) + return BET_INVALID; + } + else if (bxdf::traits::type == bxdf::BT_BSDF) + { + if (abs(base_t::isointer.getNdotV()) <= bit_cast(numeric_limits::min)) + return BET_INVALID; + } + + ErrorType res = compute(); + if (res != BET_NONE) + return res; + + return (base_t::errMsg.length() == 0) ? BET_NONE : BET_PRINT_MSG; + } + + static void run(NBL_CONST_REF_ARG(STestInitParams) initparams, NBL_REF_ARG(FailureCallback) cb) + { + random::PCG32 pcg = random::PCG32::construct(initparams.state); + random::DimAdaptorRecursive rand2d = random::DimAdaptorRecursive::construct(pcg); + uint32_t2 state = rand2d(); + + this_t t; + t.init(state); + t.rc.state = initparams.state; + t.numSamples = initparams.samples; + if NBL_CONSTEXPR_FUNC (is_microfacet_brdf_v || is_microfacet_bsdf_v) + t.template initBxDF(t.rc); + else + t.initBxDF(t.rc); + + ErrorType e = t.test(); + if (e != BET_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); +} + +// 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 params_t = conditional_t; + + 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); + } + + double RLGamma(double a, double x) { + const double epsilon = 0.000000000000001; + const double big = 4503599627370496.0; + const double bigInv = 2.22044604925031308085e-16; + assert(a >= 0 && x >= 0); + + if (x == 0) + return 0.0f; + + double ax = (a * std::log(x)) - x - std::lgamma(a); + if (ax < -709.78271289338399) + return a < x ? 1.0 : 0.0; + + if (x <= 1 || x <= a) + { + double r2 = a; + double c2 = 1; + double ans2 = 1; + + do { + r2 = r2 + 1; + c2 = c2 * x / r2; + ans2 += c2; + } while ((c2 / ans2) > epsilon); + + return std::exp(ax) * ans2 / a; + } + + int c = 0; + double y = 1 - a; + double z = x + y + 1; + double p3 = 1; + double q3 = x; + double p2 = x + 1; + double q2 = z * x; + double ans = p2 / q2; + double error; + + do { + c++; + y += 1; + z += 2; + double yc = y * c; + double p = (p2 * z) - (p3 * yc); + double q = (q2 * z) - (q3 * yc); + + if (q != 0) + { + double nextans = p / q; + error = std::abs((ans - nextans) / nextans); + ans = nextans; + } + else + { + error = 1; + } + + p3 = p2; + p2 = p; + q3 = q2; + q2 = q; + + if (std::abs(p) > big) + { + p3 *= bigInv; + p2 *= bigInv; + q3 *= bigInv; + q2 *= bigInv; + } + } while (error > epsilon); + + return 1.0 - (std::exp(ax) * ans); + } + + double chi2CDF(double x, int dof) + { + if (dof < 1 || x < 0) + { + return 0.0; + } + else if (dof == 2) + { + return 1.0 - std::exp(-0.5 * x); + } + else + { + return RLGamma(0.5 * dof, 0.5 * x); + } + } + + Imf::Rgba mapColor(float v, float vmin, float vmax) + { + Imf::Rgba c(1, 1, 1); + float diff = vmax - vmin; + v = clamp(v, vmin, vmax); + + if (v < (vmin + 0.25f * diff)) + { + c.r = 0; + c.g = 4.f * (v - vmin) / diff; + } + else if (v < (vmin + 0.5f * diff)) + { + c.r = 0; + c.b = 1.f + 4.f * (vmin + 0.25f * diff - v) / diff; + } + else if (v < (vmin + 0.75f * diff)) + { + c.r = 4.f * (v - vmin - 0.5f * diff) / diff; + c.b = 0; + } + else + { + c.g = 1.f + 4.f * (vmin + 0.75f * diff - v) / diff; + c.b = 0; + } + + return c; + } + + void writeToEXR() + { + std::string filename = std::format("chi2test_{}_{}.exr", base_t::rc.state, base_t::name); + + int totalWidth = phiSplits; + int totalHeight = 2 * thetaSplits + 1; + float maxFreq = max(maxCountFreq, maxIntFreq); + + Array2D pixels(totalWidth, totalHeight); + for (int y = 0; y < thetaSplits; y++) + for (int x = 0; x < phiSplits; x++) + pixels[y][x] = mapColor(countFreq[y * phiSplits + x], 0.f, maxFreq); + + // for (int x = 0; x < phiSplits; x++) + // pixels[thetaSplits][x] = Rgba(1, 1, 1); + + for (int y = 0; y < thetaSplits; y++) + for (int x = 0; x < phiSplits; x++) + pixels[thetaSplits + y][x] = mapColor(integrateFreq[y * phiSplits + x], 0.f, maxFreq); + + Header header(totalWidth, totalHeight); + RgbaOutputFile file(filename.c_str(), header, WRITE_RGBA); + file.setFrameBuffer(&pixels[0][0], 1, totalWidth+1); + file.writePixels(totalHeight); + } + + virtual ErrorType compute() override + { + clearBuckets(); + + float thetaFactor = thetaSplits * numbers::inv_pi; + float phiFactor = phiSplits * 0.5f * numbers::inv_pi; + + sample_t s; + aniso_cache cache; + for (uint32_t i = 0; i < numSamples; i++) + { + float32_t3 u = float32_t3(rngUniformDist(base_t::rc.rng), 0.0); + + if NBL_CONSTEXPR_FUNC (is_basic_brdf_v) + { + s = base_t::bxdf.generate(base_t::anisointer, u.xy); + } + if NBL_CONSTEXPR_FUNC (is_microfacet_brdf_v) + { + s = base_t::bxdf.generate(base_t::anisointer, u.xy, cache); + } + if NBL_CONSTEXPR_FUNC (is_basic_bsdf_v) + { + s = base_t::bxdf.generate(base_t::anisointer, u); + } + if NBL_CONSTEXPR_FUNC (is_microfacet_bsdf_v) + { + s = base_t::bxdf.generate(base_t::anisointer, u, cache); + } + + // put s into bucket + float32_t2 coords = cartesianToPolar(s.getL().getDirection()) * float32_t2(thetaFactor, phiFactor); + if (coords.y < 0) + coords.y += 2.f * numbers::pi * phiFactor; + + int thetaBin = clamp((int)std::floor(coords.x), 0, thetaSplits - 1); + int phiBin = clamp((int)std::floor(coords.y), 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; + integrateFreq[intidx++] = numSamples * adaptiveSimpson2D([&](float theta, float phi) -> float + { + float cosTheta = std::cos(theta), sinTheta = std::sin(theta); + float cosPhi = std::cos(phi), sinPhi = std::sin(phi); + + float32_t3x3 toTangentSpace = base_t::anisointer.getToTangentSpace(); + ray_dir_info_t localV = base_t::rc.V.transform(toTangentSpace); + ray_dir_info_t L; + L.direction = float32_t3(sinTheta * cosPhi, sinTheta * sinPhi, cosTheta); + ray_dir_info_t localL = L.transform(toTangentSpace); + sample_t s = sample_t::createFromTangentSpace(localV.direction, localL, base_t::anisointer.getFromTangentSpace()); + + params_t params; + if NBL_CONSTEXPR_FUNC (is_basic_brdf_v) + { + params = params_t::create(s, base_t::isointer, bxdf::BCM_MAX); + } + if NBL_CONSTEXPR_FUNC (is_microfacet_brdf_v) + { + aniso_cache cache = aniso_cache::template createForReflection(base_t::anisointer, s); + + if NBL_CONSTEXPR_FUNC (aniso) + params = params_t::create(s, base_t::anisointer, cache, bxdf::BCM_MAX); + else + { + iso_cache isocache = cache.iso_cache; + params = params_t::create(s, base_t::isointer, isocache, bxdf::BCM_MAX); + } + } + if NBL_CONSTEXPR_FUNC (is_basic_bsdf_v) + { + params = params_t::create(s, base_t::isointer, bxdf::BCM_ABS); + } + if NBL_CONSTEXPR_FUNC (is_microfacet_bsdf_v) + { + aniso_cache cache = aniso_cache::template createForReflection(base_t::anisointer, s); + + if NBL_CONSTEXPR_FUNC (aniso) + params = params_t::create(s, base_t::anisointer, cache, bxdf::BCM_ABS); + else + { + iso_cache isocache = cache.iso_cache; + params = params_t::create(s, base_t::isointer, isocache, bxdf::BCM_ABS); + } + } + + quotient_pdf_t pdf; + if NBL_CONSTEXPR_FUNC (is_basic_brdf_v || is_basic_bsdf_v) + { + pdf = base_t::bxdf.quotient_and_pdf(params); + } + if NBL_CONSTEXPR_FUNC (is_microfacet_brdf_v || is_microfacet_bsdf_v) + { + if NBL_CONSTEXPR_FUNC (aniso) + { + pdf = base_t::bxdf.quotient_and_pdf(params); + } + else + { + pdf = base_t::bxdf.quotient_and_pdf(params); + } + } + return pdf.pdf == bit_cast(numeric_limits::infinity) ? 0.0 : pdf.pdf * sinTheta; + }, + float32_t2(i * thetaFactor, j * phiFactor), float32_t2((i + 1) * thetaFactor, (j + 1) * phiFactor)); + + if (write_frequencies && maxIntFreq < integrateFreq[lastidx]) + maxIntFreq = integrateFreq[lastidx]; + } + } + + return BET_NONE; + } + + ErrorType test() + { + if (bxdf::traits::type == bxdf::BT_BRDF) + if (base_t::isointer.getNdotV() <= numeric_limits::min) + return BET_INVALID; + else if (bxdf::traits::type == bxdf::BT_BSDF) + if (abs(base_t::isointer.getNdotV()) <= numeric_limits::min) + return BET_INVALID; + + ErrorType res = compute(); + if (res != BET_NONE) + return res; + + if (write_frequencies) + writeToEXR(); + + // 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 BET_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 BET_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 BET_PRINT_MSG; + } + + return BET_NONE; + } + + static void run(NBL_CONST_REF_ARG(STestInitParams) initparams, NBL_REF_ARG(FailureCallback) cb) + { + random::PCG32 pcg = random::PCG32::construct(initparams.state); + random::DimAdaptorRecursive rand2d = random::DimAdaptorRecursive::construct(pcg); + uint32_t2 state = rand2d(); + + this_t t; + t.init(state); + t.rc.state = initparams.state; + t.numSamples = initparams.samples; + t.thetaSplits = initparams.thetaSplits; + t.phiSplits = initparams.phiSplits; + t.write_frequencies = initparams.writeFrequencies; + if NBL_CONSTEXPR_FUNC (is_microfacet_brdf_v || is_microfacet_bsdf_v) + t.template initBxDF(t.rc); + else + t.initBxDF(t.rc); + + ErrorType e = t.test(); + if (e != BET_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; + + bool write_frequencies = true; + float maxCountFreq; + float maxIntFreq; + + std::vector countFreq; + std::vector integrateFreq; +}; +#endif + } } diff --git a/66_HLSLBxDFTests/main.cpp b/66_HLSLBxDFTests/main.cpp index f91ed5f02..e2da0539d 100644 --- a/66_HLSLBxDFTests/main.cpp +++ b/66_HLSLBxDFTests/main.cpp @@ -1,73 +1,276 @@ #include -#include +#include #include +#include +#include #include +#include "nbl/system/CColoredStdoutLoggerANSI.h" +#include "nbl/system/IApplicationFramework.h" +using namespace nbl; +using namespace core; +using namespace system; +using namespace asset; +using namespace video; using namespace nbl::hlsl; #include "app_resources/tests.hlsl" struct PrintFailureCallback : FailureCallback { - void __call(ErrorType error, NBL_REF_ARG(TestBase) failedFor) override + void __call(ErrorType error, NBL_REF_ARG(TestBase) failedFor, bool logInfo) override { switch (error) { - case NEGATIVE_VAL: - fprintf(stderr, "%s pdf/quotient/eval < 0\n", failedFor.name.c_str()); + case BET_INVALID: + if (logInfo) + fprintf(stderr, "[INFO] seed %u: %s skipping test due to invalid NdotV/NdotL config\n", failedFor.rc.state, failedFor.name.c_str()); break; - case PDF_ZERO: - fprintf(stderr, "%s pdf = 0\n", failedFor.name.c_str()); + case BET_NEGATIVE_VAL: + fprintf(stderr, "[ERROR] seed %u: %s pdf/quotient/eval < 0\n", failedFor.rc.state, failedFor.name.c_str()); break; - case QUOTIENT_INF: - fprintf(stderr, "%s quotient -> inf\n", failedFor.name.c_str()); + case BET_PDF_ZERO: + fprintf(stderr, "[ERROR] seed %u: %s pdf = 0\n", failedFor.rc.state, failedFor.name.c_str()); break; - case JACOBIAN: - fprintf(stderr, "%s failed the jacobian * pdf test\n", failedFor.name.c_str()); + case BET_QUOTIENT_INF: + fprintf(stderr, "[ERROR] seed %u: %s quotient -> inf\n", failedFor.rc.state, failedFor.name.c_str()); break; - case PDF_EVAL_DIFF: - fprintf(stderr, "%s quotient * pdf - eval not 0\n", failedFor.name.c_str()); + case BET_JACOBIAN: + fprintf(stderr, "[ERROR] seed %u: %s failed the jacobian * pdf test\n", failedFor.rc.state, failedFor.name.c_str()); break; - case RECIPROCITY: - fprintf(stderr, "%s failed the reprocity test\n", failedFor.name.c_str()); + case BET_PDF_EVAL_DIFF: + fprintf(stderr, "[ERROR] seed %u: %s quotient * pdf != eval\n", failedFor.rc.state, failedFor.name.c_str()); + break; + case BET_RECIPROCITY: + fprintf(stderr, "[ERROR] seed %u: %s failed the reciprocity test\n", failedFor.rc.state, failedFor.name.c_str()); + break; + case BET_PRINT_MSG: + fprintf(stderr, "[ERROR] seed %u: %s error message\n%s\n", failedFor.rc.state, failedFor.name.c_str(), failedFor.errMsg.c_str()); break; default: - fprintf(stderr, "%s unknown error\n", failedFor.name.c_str()); + fprintf(stderr, "[ERROR] seed %u: %s unknown error\n", failedFor.rc.state, failedFor.name.c_str()); } - for (volatile bool repeat = true; IsDebuggerPresent() && repeat; ) +#ifdef _NBL_DEBUG + for (volatile bool repeat = true; IsDebuggerPresent() && repeat && error < BET_NOBREAK; ) { repeat = false; - __debugbreak(); + _NBL_DEBUG_BREAK_IF(true); failedFor.compute(); } +#endif } }; +#define FOR_EACH_BEGIN(r) std::for_each(std::execution::par_unseq, r.begin(), r.end(), [&](uint32_t i) { +#define FOR_EACH_END }); + int main(int argc, char** argv) { std::cout << std::fixed << std::setprecision(4); - const uint32_t state = 69u; + std::ifstream f("../app_resources/config.json"); + if (f.fail()) + { + fprintf(stderr, "[ERROR] could not open config file\n"); + return -1; + } + json testconfigs; + try + { + testconfigs = json::parse(f); + } + catch (json::parse_error& ex) + { + fprintf(stderr, "[ERROR] parse_error.%d failed to parse config file at byte %u: %s\n", ex.id, ex.byte, ex.what()); + return -1; + } + + // test compile with dxc + { + smart_refctd_ptr m_system = system::IApplicationFramework::createSystem(); + smart_refctd_ptr m_logger = core::make_smart_refctd_ptr(system::ILogger::DefaultLogMask()); + m_logger->log("Logger Created!", system::ILogger::ELL_INFO); + smart_refctd_ptr m_assetMgr = make_smart_refctd_ptr(smart_refctd_ptr(m_system)); + + path CWD = system::path(argv[0]).parent_path().generic_string() + "/"; + path localInputCWD = CWD / "../"; + auto resourceArchive = +#ifdef NBL_EMBED_BUILTIN_RESOURCES + make_smart_refctd_ptr(smart_refctd_ptr(m_logger)); +#else + make_smart_refctd_ptr(localInputCWD/"app_resources", smart_refctd_ptr(m_logger), m_system.get()); +#endif + m_system->mount(std::move(resourceArchive), "app_resources"); + + constexpr uint32_t WorkgroupSize = 256; + const std::string WorkgroupSizeAsStr = std::to_string(WorkgroupSize); + const std::string filePath = "app_resources/test_compile.comp.hlsl"; + + IAssetLoader::SAssetLoadParams lparams = {}; + lparams.logger = m_logger.get(); + lparams.workingDirectory = ""; + auto bundle = m_assetMgr->getAsset(filePath, lparams); + if (bundle.getContents().empty() || bundle.getAssetType() != IAsset::ET_SHADER) + { + m_logger->log("Shader %s not found!", ILogger::ELL_ERROR, filePath); + exit(-1); + } + + const auto assets = bundle.getContents(); + assert(assets.size() == 1); + smart_refctd_ptr shaderSrc = IAsset::castDown(assets[0]); + + smart_refctd_ptr shader = shaderSrc; + auto compiler = make_smart_refctd_ptr(smart_refctd_ptr(m_system)); + CHLSLCompiler::SOptions options = {}; + options.stage = asset::IShader::E_SHADER_STAGE::ESS_COMPUTE; + options.debugInfoFlags |= IShaderCompiler::E_DEBUG_INFO_FLAGS::EDIF_LINE_BIT; + options.spirvOptimizer = nullptr; + // if you don't set the logger and source identifier you'll have no meaningful errors + options.preprocessorOptions.sourceIdentifier = shaderSrc->getFilepathHint(); + options.preprocessorOptions.logger = m_logger.get(); + options.preprocessorOptions.includeFinder = compiler->getDefaultIncludeFinder(); + const IShaderCompiler::SMacroDefinition WorkgroupSizeDefine = { "WORKGROUP_SIZE", WorkgroupSizeAsStr }; + options.preprocessorOptions.extraDefines = { &WorkgroupSizeDefine,&WorkgroupSizeDefine + 1 }; + if (!(shader = compiler->compileToSPIRV((const char*)shaderSrc->getContent()->getPointer(), options))) + fprintf(stderr, "[ERROR] compile shader test failed!\n"); + } + + assert(bxdf::surface_interactions::Isotropic); + assert(bxdf::surface_interactions::Isotropic); + assert(bxdf::surface_interactions::Anisotropic); + + assert(bxdf::CreatableIsotropicMicrofacetCache); + assert(bxdf::ReadableIsotropicMicrofacetCache); + assert(bxdf::AnisotropicMicrofacetCache); + const bool logInfo = testconfigs["logInfo"]; 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); + // 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.state = i; + + TestJacobian>::run(initparams, cb); + TestJacobian>::run(initparams, cb); + TestJacobian, false>::run(initparams, cb); + TestJacobian, true>::run(initparams, cb); + TestJacobian, false>::run(initparams, cb); + TestJacobian,true>::run(initparams, cb); + + TestJacobian>::run(initparams, cb); + TestJacobian >::run(initparams, cb); + TestJacobian >::run(initparams, cb); + TestJacobian, false>::run(initparams, cb); + TestJacobian, true>::run(initparams, cb); + TestJacobian, false>::run(initparams, cb); + TestJacobian,true>::run(initparams, cb); + 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.state = i; + + TestReciprocity>::run(initparams, cb); + TestReciprocity>::run(initparams, cb); + TestReciprocity, false>::run(initparams, cb); + TestReciprocity, true>::run(initparams, cb); + TestReciprocity, false>::run(initparams, cb); + TestReciprocity, true>::run(initparams, cb); + + TestReciprocity>::run(initparams, cb); + TestReciprocity>::run(initparams, cb); + TestReciprocity>::run(initparams, cb); + TestReciprocity, false>::run(initparams, cb); + TestReciprocity, true>::run(initparams, cb); + TestReciprocity, false>::run(initparams, cb); + TestReciprocity, true>::run(initparams, cb); + FOR_EACH_END + + + // test buckets of inf + // NOTE: can safely ignore any errors for smooth dielectric BxDFs because pdf SHOULD be inf + runs = testconfigs["TestBucket"]["runs"]; + auto rBucket = std::ranges::views::iota(0u, runs); + FOR_EACH_BEGIN(rBucket) + STestInitParams initparams{ .logInfo = logInfo }; + initparams.state = i; + initparams.samples = testconfigs["TestBucket"]["samples"]; + + TestBucket>::run(initparams, cb); + TestBucket>::run(initparams, cb); + TestBucket, false>::run(initparams, cb); + TestBucket, true>::run(initparams, cb); + TestBucket, false>::run(initparams, cb); + TestBucket, true>::run(initparams, cb); + + TestBucket>::run(initparams, cb); + //TestBucket>::run(initparams, cb); + //TestBucket>::run(initparams, cb); + TestBucket, false>::run(initparams, cb); + TestBucket, true>::run(initparams, cb); + TestBucket, false>::run(initparams, cb); + TestBucket, true>::run(initparams, cb); + FOR_EACH_END + + + // chi2 test for sampling and pdf + runs = testconfigs["TestChi2"]["runs"]; + auto rChi2 = std::ranges::views::iota(0u, runs); + FOR_EACH_BEGIN(rChi2) + STestInitParams initparams{ .logInfo = logInfo }; + initparams.state = i; + initparams.samples = testconfigs["TestChi2"]["samples"]; + initparams.thetaSplits = testconfigs["TestChi2"]["thetaSplits"]; + initparams.phiSplits = testconfigs["TestChi2"]["phiSplits"]; + initparams.writeFrequencies = testconfigs["TestChi2"]["writeFrequencies"]; + + TestChi2>::run(initparams, cb); + TestChi2>::run(initparams, cb); + TestChi2, false>::run(initparams, cb); + TestChi2, true>::run(initparams, cb); + TestChi2, false>::run(initparams, cb); + TestChi2, true>::run(initparams, cb); + + TestChi2>::run(initparams, cb); + //TestChi2>::run(initparams, cb); + //TestChi2>::run(initparams, cb); + TestChi2, false>::run(initparams, cb); + TestChi2, true>::run(initparams, cb); + TestChi2, false>::run(initparams, cb); + TestChi2, true>::run(initparams, cb); + FOR_EACH_END + + // test arccos angle sums + { + Xoroshiro64Star rng = Xoroshiro64Star::construct(uint32_t2(4, 2)); + for (uint32_t i = 0; i < 10; i++) + { + const float a = rng() * numbers::pi; + const float b = rng() * numbers::pi; + const float c = rng() * numbers::pi; + const float d = rng() * numbers::pi; + + const float exAB = acos(a) + acos(b); + float res = math::getSumofArccosAB(a, b); + if (res != exAB) + fprintf(stderr, "[ERROR] math::getSumofArccosAB failed! expected %f, got %f\n", exAB, res); + + const float exABCD = exAB + acos(c) + acos(d); + res = math::getSumofArccosABCD(a, b, c, d); + if (res != exABCD) + fprintf(stderr, "[ERROR] math::getSumofArccosABCD failed! expected %f, got %f\n", exABCD, res); + } + } return 0; } \ No newline at end of file