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..1bbf09ba3 --- /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": false + }, + "TestNDF": { + "runs": 10, + "verbose": true + }, + "TestCTGenerateH": { + "runs": 10, + "samples": 1000000, + "immediateFail": true + } +} 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..fcf510b21 --- /dev/null +++ b/66_HLSLBxDFTests/app_resources/test_compile.comp.hlsl @@ -0,0 +1,98 @@ +#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 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; + +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(WORKGROUP_SIZE,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::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; + + + // 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 = 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; +} \ No newline at end of file diff --git a/66_HLSLBxDFTests/app_resources/test_components.hlsl b/66_HLSLBxDFTests/app_resources/test_components.hlsl new file mode 100644 index 000000000..9631db05d --- /dev/null +++ b/66_HLSLBxDFTests/app_resources/test_components.hlsl @@ -0,0 +1,382 @@ +#ifndef BXDFTESTS_TEST_COMPONENTS_HLSL +#define BXDFTESTS_TEST_COMPONENTS_HLSL + +#include "tests_common.hlsl" + +namespace nbl +{ +namespace hlsl +{ + +template // only for cook torrance bxdfs +struct TestNDF : TestBxDF +{ + using base_t = TestBxDFBase; + using this_t = TestNDF; + using traits_t = bxdf::traits; + + virtual ErrorType compute() override + { + 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); + + if NBL_CONSTEXPR_FUNC (traits_t::type == bxdf::BT_BRDF && traits_t::IsMicrofacet) + { + if NBL_CONSTEXPR_FUNC (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); + } + } + if NBL_CONSTEXPR_FUNC (traits_t::type == bxdf::BT_BSDF && traits_t::IsMicrofacet) + { + if NBL_CONSTEXPR_FUNC (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); + } + } + + // test jacobian is correct + // float32_t3x3 fromTangentSpace = base_t::anisointer.getFromTangentSpace(); + // ray_dir_info_t tmpL; + // tmpL.setDirection(sampling::ProjectedSphere::generate(base_t::rc.u)); + // s = sample_t::createFromTangentSpace(tmpL, fromTangentSpace); + // tmpL.setDirection(sampling::ProjectedSphere::generate(ux)); + // sx = sample_t::createFromTangentSpace(tmpL, fromTangentSpace); + // tmpL.setDirection(sampling::ProjectedSphere::generate(uy)); + // sy = sample_t::createFromTangentSpace(tmpL, fromTangentSpace); + + if (!(s.isValid() && sx.isValid() && sy.isValid())) + return BET_INVALID; + + // TODO: add checks with need clamp trait + if (traits_t::type == bxdf::BT_BRDF) + { + if (s.getNdotL() <= bit_cast(numeric_limits::min)) + return BET_INVALID; + } + else if (traits_t::type == bxdf::BT_BSDF) + { + if (abs(s.getNdotL()) <= bit_cast(numeric_limits::min)) + return BET_INVALID; + } + + 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 transmitted; + NBL_IF_CONSTEXPR(aniso) + { + dg1_query_type dq = base_t::bxdf.ndf.template createDG1Query(base_t::anisointer, cache); + fresnel_type _f = bxdf::impl::getOrientedFresnel::__call(base_t::bxdf.fresnel, base_t::anisointer.getNdotV()); + quant_query_type qq = bxdf::impl::quant_query_helper::template __call(base_t::bxdf.ndf, _f, cache); + quant_type DG1 = base_t::bxdf.ndf.template DG1(dq, qq, s, base_t::anisointer); + dg1 = DG1.microfacetMeasure * hlsl::abs(cache.getVdotH() / base_t::anisointer.getNdotV()); + reflectance = _f(cache.getVdotH())[0]; + NdotH = cache.getAbsNdotH(); + transmitted = cache.isTransmission(); + } + else + { + dg1_query_type dq = base_t::bxdf.ndf.template createDG1Query(base_t::isointer, isocache); + fresnel_type _f = bxdf::impl::getOrientedFresnel::__call(base_t::bxdf.fresnel, base_t::isointer.getNdotV()); + quant_query_type qq = bxdf::impl::quant_query_helper::template __call(base_t::bxdf.ndf, _f, isocache); + quant_type DG1 = base_t::bxdf.ndf.template DG1(dq, qq, s, base_t::isointer); + dg1 = DG1.microfacetMeasure * hlsl::abs(isocache.getVdotH() / base_t::isointer.getNdotV()); + reflectance = _f(isocache.getVdotH())[0]; + NdotH = isocache.getAbsNdotH(); + transmitted = isocache.isTransmission(); + } + + if (transmitted) + { + float eta = base_t::rc.eta.x; + if (base_t::isointer.getNdotV() < 0.f) + eta = 1.f / eta; + + reflectance = transmitted ? 1.f - reflectance : reflectance; + NBL_IF_CONSTEXPR(aniso) + { + float denom = cache.getVdotH() + eta * cache.getLdotH(); + dg1 = dg1 * hlsl::abs(eta * eta * cache.getLdotH()) / (denom * denom); + } + else + { + float denom = isocache.getVdotH() + eta * isocache.getLdotH(); + dg1 = dg1 * hlsl::abs(eta * eta * isocache.getLdotH()) / (denom * denom); + } + } + else + { + NBL_IF_CONSTEXPR(aniso) + dg1 = 0.25f * dg1 / hlsl::abs(cache.getVdotH()); + else + dg1 = 0.25f * dg1 / hlsl::abs(isocache.getVdotH()); + } + + return BET_NONE; + } + + ErrorType test() + { + if (traits_t::type == bxdf::BT_BRDF) + { + if (base_t::isointer.getNdotV() <= bit_cast(numeric_limits::min)) + return BET_INVALID; + } + else if (traits_t::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; + + // 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()); + if (!checkZero(jacobi_dg1_ndoth - 1.f, 5e-2)) + { +#ifndef __HLSL_VERSION + if (verbose) + base_t::errMsg += std::format("VdotH={}, NdotV={}, LdotH={}, NdotL={}, NdotH={}, eta={}, 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, + det, dg1, jacobi_dg1_ndoth); +#endif + return BET_JACOBIAN; + } + + 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.verbose = initparams.verbose; + t.initBxDF(t.rc); + + ErrorType e = t.test(); + if (e != BET_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; +}; + +#ifndef __HLSL_VERSION +template +struct TestCTGenerateH : TestBxDF +{ + using base_t = TestBxDFBase; + using this_t = TestCTGenerateH; + using traits_t = bxdf::traits; + + virtual ErrorType compute() override + { + counter.reset(); + + sample_t s; + iso_cache isocache; + aniso_cache cache; + nbl::hlsl::random::DimAdaptorRecursive rng_vec3 = nbl::hlsl::random::DimAdaptorRecursive::construct(base_t::rc.rng); + for (uint32_t i = 0; i < numSamples; i++) + { + float32_t3 u = ConvertToFloat01::__call(rng_vec3()); + 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); + // u.z = 0.0; + + if NBL_CONSTEXPR_FUNC (traits_t::type == bxdf::BT_BRDF && !traits_t::IsMicrofacet) + { + s = base_t::bxdf.generate(base_t::anisointer, u.xy); + } + if NBL_CONSTEXPR_FUNC (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); + } + if NBL_CONSTEXPR_FUNC (traits_t::type == bxdf::BT_BSDF && !traits_t::IsMicrofacet) + { + s = base_t::bxdf.generate(base_t::anisointer, u); + } + if NBL_CONSTEXPR_FUNC (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; + + 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 BET_GENERATE_H; + } + 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 BET_GENERATE_H; + } + 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 BET_GENERATE_H; + } + + return BET_NONE; + } + + ErrorType test() + { + if (traits_t::type == bxdf::BT_BRDF) + if (base_t::isointer.getNdotV() <= numeric_limits::min) + return BET_INVALID; + else if (traits_t::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; + + 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.immediateFail = initparams.immediateFail; + t.initBxDF(t.rc); + + ErrorType e = t.test(); + if (e != BET_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 + +} +} + +#endif \ No newline at end of file diff --git a/66_HLSLBxDFTests/app_resources/tests.hlsl b/66_HLSLBxDFTests/app_resources/tests.hlsl index 256ed3ce9..9011aa2e5 100644 --- a/66_HLSLBxDFTests/app_resources/tests.hlsl +++ b/66_HLSLBxDFTests/app_resources/tests.hlsl @@ -1,477 +1,1038 @@ #ifndef BXDFTESTS_TESTS_HLSL #define BXDFTESTS_TESTS_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/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 +#include "tests_common.hlsl" 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 +template +struct TestJacobian : TestBxDF { + using base_t = TestBxDFBase; + using this_t = TestJacobian; + using traits_t = bxdf::traits; -inline float rngFloat01(NBL_REF_ARG(nbl::hlsl::Xoroshiro64Star) rng) -{ - return (float)rng() / numeric_limits::max; -} + virtual ErrorType compute() override + { + aniso_cache cache, dummy; + iso_cache isocache, dummy_iso; -template -struct RNGUniformDist; + 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); -template<> -struct RNGUniformDist -{ - static float32_t __call(NBL_REF_ARG(nbl::hlsl::Xoroshiro64Star) rng) - { - return rngFloat01(rng); + if NBL_CONSTEXPR_FUNC (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); + } + if NBL_CONSTEXPR_FUNC (traits_t::type == bxdf::BT_BRDF && traits_t::IsMicrofacet) + { + if NBL_CONSTEXPR_FUNC (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); + } + } + if NBL_CONSTEXPR_FUNC (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); + } + if NBL_CONSTEXPR_FUNC (traits_t::type == bxdf::BT_BSDF && traits_t::IsMicrofacet) + { + if NBL_CONSTEXPR_FUNC (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 (!(s.isValid() && sx.isValid() && sy.isValid())) + return BET_INVALID; + + // TODO: add checks with need clamp trait + if (traits_t::type == bxdf::BT_BRDF) + { + if (s.getNdotL() <= bit_cast(numeric_limits::min)) + return BET_INVALID; + } + else if (traits_t::type == bxdf::BT_BSDF) + { + if (abs(s.getNdotL()) <= bit_cast(numeric_limits::min)) + return BET_INVALID; + } + + if NBL_CONSTEXPR_FUNC (!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; + } + if NBL_CONSTEXPR_FUNC (traits_t::IsMicrofacet) + { + 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)); + 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(); + } + } + + return BET_NONE; } -}; -template -struct RNGUniformDist> -{ - static vector __call(NBL_REF_ARG(nbl::hlsl::Xoroshiro64Star) rng) + ErrorType test() { - vector retval; - for (int i = 0; i < N; i++) - retval[i] = rngFloat01(rng); - return retval; - } -}; + if (traits_t::type == bxdf::BT_BRDF) + { + if (base_t::isointer.getNdotV() <= bit_cast(numeric_limits::min)) + return BET_INVALID; + } + else if (traits_t::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; -template -T rngUniformDist(NBL_REF_ARG(nbl::hlsl::Xoroshiro64Star) rng) -{ - return impl::RNGUniformDist::__call(rng); -} + if (checkZero(pdf.pdf, 1e-5) && !checkZero(pdf.quotient, 1e-5)) // something generated cannot have 0 probability of getting generated + return BET_PDF_ZERO; + if (!checkLt(pdf.quotient, (float32_t3)bit_cast(numeric_limits::infinity))) // importance sampler's job to prevent inf + return BET_QUOTIENT_INF; -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); + if (checkZero(bsdf, 1e-5) || checkZero(pdf.quotient, 1e-5)) + return BET_NONE; // produces an "impossible" sample - 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); + 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.getTdotL() - s.getTdotL(), sy.getTdotL() - s.getTdotL(), + sx.getBdotL() - s.getBdotL(), sy.getBdotL() - s.getBdotL() + ); + float det = nbl::hlsl::determinant(m); + + if (!checkZero(det * pdf.pdf / s.getNdotL(), 1e-4)) + return BET_JACOBIAN; + + float32_t3 quo_pdf = pdf.value(); + if (!checkEq(quo_pdf, bsdf, 1e-4)) + { #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]; + 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 BET_PDF_EVAL_DIFF; + } - 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 BET_NONE; } - // epsilon - float eps = 1e-3; + 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(); - nbl::hlsl::Xoroshiro64Star rng; - ray_dir_info_t V; - float32_t3 N; - float32_t3 T; - float32_t3 B; + this_t t; + t.init(state); + t.rc.state = initparams.state; + t.verbose = initparams.verbose; + t.initBxDF(t.rc); + + ErrorType e = t.test(); + if (e != BET_NONE) + cb.__call(e, t, initparams.logInfo); + } - float32_t3 u; - float32_t2 alpha; - float eta; - float32_t2 ior; - float32_t3 luma_coeff; + sample_t s, sx, sy; + quotient_pdf_t pdf; + float32_t3 bsdf; + bool transmitted; + bool verbose; }; -enum ErrorType : uint32_t +template +struct TestReciprocity : TestBxDF { - NOERR = 0, - NEGATIVE_VAL, // pdf/quotient/eval < 0 - PDF_ZERO, // pdf = 0 - QUOTIENT_INF, // quotient -> inf - JACOBIAN, - PDF_EVAL_DIFF, - RECIPROCITY -}; + using base_t = TestBxDFBase; + using this_t = TestReciprocity; + using traits_t = bxdf::traits; -struct TestBase -{ - void init(uint32_t2 seed) + using iso_interaction_t = typename BxDF::isotropic_interaction_type; + using aniso_interaction_t = typename BxDF::anisotropic_interaction_type; + + virtual ErrorType compute() override { - rc = SBxDFTestResources::create(seed); + aniso_cache cache, rec_cache; + iso_cache isocache, rec_isocache; - isointer = iso_interaction::create(rc.V, rc.N); - anisointer = aniso_interaction::create(isointer, rc.T, rc.B); - } + if NBL_CONSTEXPR_FUNC (traits_t::type == bxdf::BT_BSDF && traits_t::IsMicrofacet) + { + isointer = iso_interaction_t::template copy(base_t::isointer); + anisointer = aniso_interaction_t::template copy(base_t::anisointer); + } + else + { + isointer = base_t::isointer; + anisointer = base_t::anisointer; + } - virtual void compute() {} + if NBL_CONSTEXPR_FUNC (traits_t::type == bxdf::BT_BRDF && !traits_t::IsMicrofacet) + { + s = base_t::bxdf.generate(anisointer, base_t::rc.u.xy); + } + if NBL_CONSTEXPR_FUNC (traits_t::type == bxdf::BT_BRDF && traits_t::IsMicrofacet) + { + if NBL_CONSTEXPR_FUNC (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 (traits_t::type == bxdf::BT_BSDF && !traits_t::IsMicrofacet) + { + s = base_t::bxdf.generate(anisointer, base_t::rc.u); + } + if NBL_CONSTEXPR_FUNC (traits_t::type == bxdf::BT_BSDF && traits_t::IsMicrofacet) + { + if NBL_CONSTEXPR_FUNC (aniso) + { + s = base_t::bxdf.generate(anisointer, base_t::rc.u, cache); + } + else + { + s = base_t::bxdf.generate(isointer, base_t::rc.u, isocache); + } + } + + if (!s.isValid()) + return BET_INVALID; + + // 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; + } - SBxDFTestResources rc; + 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_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 (!traits_t::IsMicrofacet) + { + 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 (traits_t::type == bxdf::BT_BRDF && traits_t::IsMicrofacet) + { + if NBL_CONSTEXPR_FUNC (aniso) + { + 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 + { + bsdf = float32_t3(base_t::bxdf.eval(s, isointer, isocache)); + rec_bsdf = float32_t3(base_t::bxdf.eval(rec_s, rec_isointer, rec_isocache)); + } + } + if NBL_CONSTEXPR_FUNC (traits_t::type == bxdf::BT_BSDF && traits_t::IsMicrofacet) + { + if NBL_CONSTEXPR_FUNC (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)); + } + } - iso_interaction isointer; - aniso_interaction anisointer; + transmitted = aniso ? cache.isTransmission() : isocache.isTransmission(); #ifndef __HLSL_VERSION - std::string name = "base"; + 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 -}; -struct FailureCallback -{ - virtual void __call(ErrorType error, NBL_REF_ARG(TestBase) failedFor) {} -}; + return BET_NONE; + } -template -struct TestBxDFBase : TestBase -{ - BxDF bxdf; -}; + ErrorType test() + { + if (traits_t::type == bxdf::BT_BRDF) + { + if (base_t::isointer.getNdotV() <= bit_cast(numeric_limits::min)) + return BET_INVALID; + } + else if (traits_t::type == bxdf::BT_BSDF) + { + if (abs(base_t::isointer.getNdotV()) <= bit_cast(numeric_limits::min)) + return BET_INVALID; + } -template -struct TestBxDF : TestBxDFBase -{ - using base_t = TestBxDFBase; + ErrorType res = compute(); + if (res != BET_NONE) + return res; - void initBxDF(SBxDFTestResources _rc) - { - base_t::bxdf = BxDF::create(); // default to lambertian bxdf + 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 / 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 - base_t::name = "Lambertian BxDF"; + if (verbose) + base_t::errMsg += std::format(" front=[{},{},{}] rec=[{},{},{}]", + a.x, a.y, a.z, + b.x, b.y, b.z); #endif - } -}; + return BET_RECIPROCITY; + } -template<> -struct TestBxDF> : TestBxDFBase> -{ - using base_t = TestBxDFBase>; + return BET_NONE; + } - void initBxDF(SBxDFTestResources _rc) + static void run(NBL_CONST_REF_ARG(STestInitParams) initparams, NBL_REF_ARG(FailureCallback) cb) { - base_t::bxdf = bxdf::reflection::SOrenNayarBxDF::create(_rc.alpha.x); -#ifndef __HLSL_VERSION - base_t::name = "OrenNayar BRDF"; -#endif + 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.verbose = initparams.verbose; + 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; + iso_interaction_t isointer, rec_isointer; + aniso_interaction_t anisointer, rec_anisointer; + bool transmitted; + bool verbose; }; -template<> -struct TestBxDF> : TestBxDFBase> +#ifndef __HLSL_VERSION // because unordered_map +template +struct TestBucket : TestBxDF { - using base_t = TestBxDFBase>; + using base_t = TestBxDFBase; + using this_t = TestBucket; + using traits_t = bxdf::traits; - template - void initBxDF(SBxDFTestResources _rc) + void clearBuckets() { - 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 + for (float y = -1.0f; y < 1.0f; y += stride) { - 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 + for (float x = -1.0f; x < 1.0f; x += stride) + { + buckets[float32_t2(x, y)] = 0; + } } } -}; -template<> -struct TestBxDF> : TestBxDFBase> -{ - using base_t = TestBxDFBase>; + float bin(float a) + { + float diff = std::fmod(a, stride); + float b = (a < 0) ? -stride : 0.0f; + return a - diff + b; + } - template - void initBxDF(SBxDFTestResources _rc) + virtual ErrorType compute() override { - if (aniso) + clearBuckets(); + + aniso_cache cache; + iso_cache isocache; + + sample_t s; + quotient_pdf_t pdf; + float32_t3 bsdf; + + nbl::hlsl::random::DimAdaptorRecursive rng_vec3 = nbl::hlsl::random::DimAdaptorRecursive::construct(base_t::rc.rng); + for (uint32_t i = 0; i < numSamples; i++) { - 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 + float32_t3 u = ConvertToFloat01::__call(rng_vec3()); + // u.z = 0.0; + + if NBL_CONSTEXPR_FUNC (traits_t::type == bxdf::BT_BRDF && !traits_t::IsMicrofacet) + { + s = base_t::bxdf.generate(base_t::anisointer, u.xy); + } + if NBL_CONSTEXPR_FUNC (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); + } + } + if NBL_CONSTEXPR_FUNC (traits_t::type == bxdf::BT_BSDF && !traits_t::IsMicrofacet) + { + s = base_t::bxdf.generate(base_t::anisointer, u); + } + if NBL_CONSTEXPR_FUNC (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; + + if NBL_CONSTEXPR_FUNC (!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)); + } + if NBL_CONSTEXPR_FUNC (traits_t::IsMicrofacet) + { + 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)); + } + 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; } - 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 + // 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); + } } +#endif + return BET_NONE; } -}; -template<> -struct TestBxDF> : TestBxDFBase> -{ - using base_t = TestBxDFBase>; + ErrorType test() + { + if (traits_t::type == bxdf::BT_BRDF) + { + if (base_t::isointer.getNdotV() <= bit_cast(numeric_limits::min)) + return BET_INVALID; + } + else if (traits_t::type == bxdf::BT_BSDF) + { + if (abs(base_t::isointer.getNdotV()) <= bit_cast(numeric_limits::min)) + return BET_INVALID; + } - void initBxDF(SBxDFTestResources _rc) + 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) { - base_t::bxdf = bxdf::transmission::SSmoothDielectricBxDF::create(rc.eta); -#ifndef __HLSL_VERSION - base_t::name = "Smooth dielectric BSDF"; -#endif + 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.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; }; -template<> -struct TestBxDF> : TestBxDFBase> +inline float adaptiveSimpson(const std::function& f, float x0, float x1, float eps = 1e-6, int depth = 6) { - using base_t = TestBxDFBase>; + 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); +} - void initBxDF(SBxDFTestResources _rc) +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 { - 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 - } -}; + 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 TestBxDF> : TestBxDFBase> +// 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 base_t = TestBxDFBase; + using this_t = TestChi2; + using traits_t = bxdf::traits; - template - void initBxDF(SBxDFTestResources _rc) + void clearBuckets() { - 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 + 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 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) { - base_t::bxdf = bxdf::transmission::SBeckmannDielectricBxDF::create(rc.eta,rc.alpha.x); -#ifndef __HLSL_VERSION - base_t::name = "Beckmann Dielectric BSDF"; -#endif + 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; } - } -}; -template<> -struct TestBxDF> : TestBxDFBase> -{ - using base_t = TestBxDFBase>; + 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); + } - template - void initBxDF(SBxDFTestResources _rc) + double chi2CDF(double x, int dof) { - if (aniso) + if (dof < 1 || x < 0) { - 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 + return 0.0; + } + else if (dof == 2) + { + return 1.0 - std::exp(-0.5 * x); } else { - base_t::bxdf = bxdf::transmission::SGGXDielectricBxDF::create(rc.eta,rc.alpha.x); -#ifndef __HLSL_VERSION - base_t::name = "GGX Dielectric BSDF"; -#endif + return RLGamma(0.5 * dof, 0.5 * x); } } -}; - -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 bool is_basic_brdf_v = is_basic_brdf::value; -template -NBL_CONSTEXPR bool is_microfacet_brdf_v = is_microfacet_brdf::value; -template -NBL_CONSTEXPR bool is_basic_bsdf_v = is_basic_bsdf::value; -template -NBL_CONSTEXPR bool is_microfacet_bsdf_v = is_microfacet_bsdf::value; - - -template -struct TestUOffset : TestBxDF -{ - using base_t = TestBxDFBase; - using this_t = TestUOffset; - - void compute() override + Imf::Rgba mapColor(float v, float vmin, float vmax) { - aniso_cache cache, dummy; + Imf::Rgba c(1, 1, 1); + float diff = vmax - vmin; + v = clamp(v, vmin, vmax); - 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) + if (v < (vmin + 0.25f * diff)) { - 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); + c.r = 0; + c.g = 4.f * (v - vmin) / diff; } - if NBL_CONSTEXPR_FUNC (is_microfacet_brdf_v) + else if (v < (vmin + 0.5f * diff)) { - 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); + c.r = 0; + c.b = 1.f + 4.f * (vmin + 0.25f * diff - v) / diff; } - if NBL_CONSTEXPR_FUNC (is_basic_bsdf_v) + else if (v < (vmin + 0.75f * diff)) { - 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); + c.r = 4.f * (v - vmin - 0.5f * diff) / diff; + c.b = 0; } - if NBL_CONSTEXPR_FUNC (is_microfacet_bsdf_v) + else { - 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); + 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; - 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)); - } - if NBL_CONSTEXPR_FUNC (is_microfacet_brdf_v || is_microfacet_bsdf_v) + // write sample count from generate, top half + 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, maxCountFreq); + + // for (int x = 0; x < phiSplits; x++) + // pixels[thetaSplits][x] = Rgba(1, 1, 1); + + // write values of pdf, bottom half + 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, maxIntFreq); + + 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; + iso_cache isocache; + aniso_cache cache; + nbl::hlsl::random::DimAdaptorRecursive rng_vec3 = nbl::hlsl::random::DimAdaptorRecursive::construct(base_t::rc.rng); + for (uint32_t i = 0; i < numSamples; i++) { - if NBL_CONSTEXPR_FUNC (aniso) + float32_t3 u = ConvertToFloat01::__call(rng_vec3()); + 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); + // u.z = 0.0; + + if NBL_CONSTEXPR_FUNC (traits_t::type == bxdf::BT_BRDF && !traits_t::IsMicrofacet) { - pdf = base_t::bxdf.quotient_and_pdf(s, base_t::anisointer, cache); - bsdf = float32_t3(base_t::bxdf.eval(s, base_t::anisointer, cache)); + s = base_t::bxdf.generate(base_t::anisointer, u.xy); } - else + if NBL_CONSTEXPR_FUNC (traits_t::type == bxdf::BT_BRDF && traits_t::IsMicrofacet) { - 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)); + 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); + } + if NBL_CONSTEXPR_FUNC (traits_t::type == bxdf::BT_BSDF && !traits_t::IsMicrofacet) + { + s = base_t::bxdf.generate(base_t::anisointer, u); + } + if NBL_CONSTEXPR_FUNC (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; + 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); + + ray_dir_info_t V = base_t::rc.V; + ray_dir_info_t L; + L.direction = hlsl::normalize(float32_t3(sinTheta * cosPhi, sinTheta * sinPhi, cosTheta)); + float32_t3 N = base_t::anisointer.getN(); + float NdotL = hlsl::dot(N, L.direction); + + float32_t3 T = base_t::anisointer.getT(); + float32_t3 B = base_t::anisointer.getB(); + sample_t s = sample_t::create(L, T, B, NdotL); + + NBL_IF_CONSTEXPR(traits_t::IsMicrofacet) + { + const float NdotV = base_t::anisointer.getNdotV(); + NBL_IF_CONSTEXPR(traits_t::type == bxdf::BT_BRDF) + if (NdotV < 0.f) return 0.f; + + float eta = 1.f; + const float NdotL = s.getNdotL(); + if (NdotV * NdotL < 0.f) + eta = NdotV < 0.f ? 1.f/base_t::rc.eta.x : base_t::rc.eta.x; + float32_t3 H = hlsl::normalize(V.getDirection() + L.getDirection() * eta); + 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 >(eta)))) + return 0.f; + + const float32_t3 T = base_t::anisointer.getT(); + const float32_t3 B = base_t::anisointer.getB(); + cache.fillTangents(T, B, H); + } + + float pdf; + if NBL_CONSTEXPR_FUNC (!traits_t::IsMicrofacet) + { + pdf = base_t::bxdf.pdf(s, base_t::isointer); + } + if NBL_CONSTEXPR_FUNC (traits_t::IsMicrofacet) + { + if NBL_CONSTEXPR_FUNC (aniso) + { + pdf = base_t::bxdf.pdf(s, base_t::anisointer, cache); + } + else + { + pdf = base_t::bxdf.pdf(s, base_t::isointer, cache.iso_cache); + } + } + return 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() { - compute(); + if (traits_t::type == bxdf::BT_BRDF) + if (base_t::isointer.getNdotV() <= numeric_limits::min) + return BET_INVALID; + else if (traits_t::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; + }); - if (nbl::hlsl::abs(pdf.pdf) < base_t::rc.eps) // something generated cannot have 0 probability of getting generated - return PDF_ZERO; + float pooledFreqs = 0, pooledExpFreqs = 0, chsq = 0; + int pooledCells = 0, dof = 0; - if (!all(pdf.quotient < (float32_t3)numeric_limits::infinity)) // importance sampler's job to prevent inf - return QUOTIENT_INF; + 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 (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 (pooledExpFreqs > 0 || pooledFreqs > 0) + { + float diff = pooledFreqs - pooledExpFreqs; + chsq += (diff * diff) / pooledExpFreqs; + dof++; + } + dof -= 1; - // 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 (dof <= 0) + { + base_t::errMsg = std::format("degrees of freedom {} too low", dof); + return BET_PRINT_MSG; + } - bool jacobian_test = nbl::hlsl::abs(det*pdf.pdf/s.NdotL) < base_t::rc.eps; - if (!jacobian_test) - return JACOBIAN; + float pval = 1.0f - static_cast(chi2CDF(chsq, dof)); + float alpha = 1.0f - std::pow(1.0f - threshold, 1.0f / numTests); - 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 (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 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); - if NBL_CONSTEXPR_FUNC (is_microfacet_brdf_v || is_microfacet_bsdf_v) - t.template initBxDF(t.rc); - else - t.initBxDF(t.rc); + t.rc.state = initparams.state; + t.numSamples = initparams.samples; + t.thetaSplits = initparams.thetaSplits; + t.phiSplits = initparams.phiSplits; + t.write_frequencies = initparams.writeFrequencies; + 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; - quotient_pdf_t pdf; - float32_t3 bsdf; + 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/app_resources/tests_common.hlsl b/66_HLSLBxDFTests/app_resources/tests_common.hlsl new file mode 100644 index 000000000..c0a8d9614 --- /dev/null +++ b/66_HLSLBxDFTests/app_resources/tests_common.hlsl @@ -0,0 +1,610 @@ +#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" + +#ifndef __HLSL_VERSION +#define GLM_ENABLE_EXPERIMENTAL +#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 +{ +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 = sampling::quotient_and_pdf; +using spectral_t = vector; + +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 = vector; + +template +struct ConvertToFloat01 +{ + using ret_t = conditional_t::Dimension==1, float, 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::abs(a); + T _b = hlsl::abs(b); + return nbl::hlsl::all::Dimension> >(nbl::hlsl::max(_a / _b, _b / _a) <= hlsl::promote(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_t2 seed) + { + SBxDFTestResources retval; + retval.rng = nbl::hlsl::Xoroshiro64Star::construct(seed); + nbl::hlsl::random::DimAdaptorRecursive rng_vec2 = nbl::hlsl::random::DimAdaptorRecursive::construct(retval.rng); + nbl::hlsl::random::DimAdaptorRecursive rng_vec3 = nbl::hlsl::random::DimAdaptorRecursive::construct(retval.rng); + retval.u = ConvertToFloat01::__call(rng_vec3()); + 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.u.z = 0.0; + + retval.V.direction = nbl::hlsl::normalize(sampling::UniformSphere::generate(ConvertToFloat01::__call(rng_vec2()))); + retval.N = nbl::hlsl::normalize(sampling::UniformSphere::generate(ConvertToFloat01::__call(rng_vec2()))); + // if (hlsl::dot(retval.N, retval.V.direction) < 0) + // retval.V.direction = -retval.V.direction; + + 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(rng_vec2()) * hlsl::promote(1.5) + hlsl::promote(1.1); // range [1.1,2.6], also only do eta = eta/1.0 (air) + retval.luma_coeff = float32_t3(0.2126, 0.7152, 0.0722); // luma coefficients for Rec. 709 + return retval; + } + + float eps = 1e-3; // epsilon + uint32_t state; // 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_t2 eta; // (eta, etak) + float32_t3 luma_coeff; +}; + +struct STestInitParams +{ + bool logInfo; + uint32_t state; + uint32_t samples; + uint32_t thetaSplits; + uint32_t phiSplits; + bool writeFrequencies; + bool immediateFail; + bool verbose; +}; + +enum ErrorType : uint32_t +{ + 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_GENERATE_H, + + BET_NOBREAK, // not an error code, ones after this don't break + BET_INVALID, + BET_PRINT_MSG +}; + +struct TestBase +{ + void init(uint32_t2 seed) + { + rc = SBxDFTestResources::create(seed); + + isointer = iso_interaction::create(rc.V, rc.N); + anisointer = aniso_interaction::create(isointer, rc.T, rc.B); + } + + virtual ErrorType compute() { return BET_NONE; } + + SBxDFTestResources rc; + + iso_interaction isointer; + aniso_interaction anisointer; + +#ifndef __HLSL_VERSION + std::string name = "base"; + std::string errMsg = ""; +#endif +}; + +struct FailureCallback +{ + virtual void __call(ErrorType error, NBL_REF_ARG(TestBase) 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(hlsl::promote(_rc.eta.x),hlsl::promote(_rc.eta.y)); +#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(hlsl::promote(_rc.eta.x),hlsl::promote(_rc.eta.y)); +#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(hlsl::promote(_rc.eta.x),hlsl::promote(_rc.eta.y)); +#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(hlsl::promote(_rc.eta.x),hlsl::promote(_rc.eta.y)); +#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_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))); + base_t::bxdf.luminosityContributionHint = _rc.luma_coeff; +#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 + } +}; + + +namespace reciprocity_test_impl +{ +template) +struct SIsotropic +{ + using ray_dir_info_type = RayDirInfo; + using scalar_type = typename RayDirInfo::scalar_type; + using vector3_type = typename RayDirInfo::vector3_type; + + // WARNING: Changed since GLSL, now arguments need to be normalized! + static SIsotropic create(NBL_CONST_REF_ARG(RayDirInfo) normalizedV, const vector3_type normalizedN) + { + SIsotropic retval; + retval.V = normalizedV; + retval.N = normalizedN; + retval.NdotV = nbl::hlsl::dot(retval.N, retval.V.getDirection()); + retval.NdotV2 = retval.NdotV * retval.NdotV; + + return retval; + } + + template) + static SIsotropic copy(NBL_CONST_REF_ARG(I) other) + { + SIsotropic retval; + retval.V = other.getV(); + retval.N = other.getN(); + retval.NdotV = other.getNdotV(); + retval.NdotV2 = other.getNdotV2(); + retval.pathOrigin = bxdf::PathOrigin::PO_SENSOR; + 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; } + + RayDirInfo V; + vector3_type N; + scalar_type NdotV; + scalar_type NdotV2; + bxdf::PathOrigin pathOrigin; +}; + +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 = matrix; + + // 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(); } + + 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; + +#define CUSTOM_MICROFACET_CONF_ISO bxdf::LightSample && bxdf::surface_interactions::Isotropic && !bxdf::surface_interactions::Anisotropic && bxdf::CreatableIsotropicMicrofacetCache && !bxdf::AnisotropicMicrofacetCache && concepts::FloatingPointLikeVectorial + +template +NBL_PARTIAL_REQ_TOP(CUSTOM_MICROFACET_CONF_ISO) +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 = vector; + using vector3_type = vector; + using monochrome_type = vector; + using matrix3x3_type = 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..a65b443c9 100644 --- a/66_HLSLBxDFTests/main.cpp +++ b/66_HLSLBxDFTests/main.cpp @@ -1,73 +1,353 @@ #include -#include +#include #include +#include +#include #include +#ifdef NBL_EMBED_BUILTIN_RESOURCES +#include "CArchive.h" +#endif +#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/test_components.hlsl" #include "app_resources/tests.hlsl" +#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 { - 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 BET_NEGATIVE_VAL: + fprintf(stderr, "[ERROR] seed %u: %s pdf/quotient/eval < 0\n", failedFor.rc.state, failedFor.name.c_str()); + break; + case BET_PDF_ZERO: + fprintf(stderr, "[ERROR] seed %u: %s pdf = 0\n", failedFor.rc.state, failedFor.name.c_str()); + break; + case BET_QUOTIENT_INF: + fprintf(stderr, "[ERROR] seed %u: %s quotient -> inf\n", failedFor.rc.state, failedFor.name.c_str()); break; - case PDF_ZERO: - fprintf(stderr, "%s pdf = 0\n", failedFor.name.c_str()); + case BET_JACOBIAN: + fprintf(stderr, "[ERROR] seed %u: %s failed the jacobian * pdf test %s\n", failedFor.rc.state, failedFor.name.c_str(), failedFor.errMsg.c_str()); break; - case QUOTIENT_INF: - fprintf(stderr, "%s quotient -> inf\n", failedFor.name.c_str()); + case BET_PDF_EVAL_DIFF: + fprintf(stderr, "[ERROR] seed %u: %s quotient * pdf != eval %s\n", failedFor.rc.state, failedFor.name.c_str(), failedFor.errMsg.c_str()); break; - case JACOBIAN: - fprintf(stderr, "%s failed the jacobian * pdf test\n", failedFor.name.c_str()); + case BET_RECIPROCITY: + fprintf(stderr, "[ERROR] seed %u: %s failed the reciprocity test %s\n", failedFor.rc.state, failedFor.name.c_str(), failedFor.errMsg.c_str()); break; - case PDF_EVAL_DIFF: - fprintf(stderr, "%s quotient * pdf - eval not 0\n", failedFor.name.c_str()); + 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; - case RECIPROCITY: - fprintf(stderr, "%s failed the reprocity test\n", failedFor.name.c_str()); + case BET_GENERATE_H: + fprintf(stderr, "[ERROR] seed %u: %s failed invalid H configuration generated %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_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 }); + 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); + auto shaderSrc = smart_refctd_ptr_static_cast(assets[0]); + + auto 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); + + using ndf_beckmann_t = bxdf::ndf::Beckmann; + assert(bxdf::ndf::NDF); + using ndf_ggx_t = bxdf::ndf::GGX; + assert(bxdf::ndf::NDF); + + using fresnel_schlick_t = bxdf::fresnel::Schlick; + assert(bxdf::fresnel::Fresnel); + + assert(bxdf::bxdf_concepts::IsotropicBxDF>); + assert(bxdf::bxdf_concepts::IsotropicBxDF>); + assert(bxdf::bxdf_concepts::IsotropicBxDF>); + + assert(bxdf::bxdf_concepts::MicrofacetBxDF>); + assert(bxdf::bxdf_concepts::MicrofacetBxDF>); + assert(bxdf::bxdf_concepts::MicrofacetBxDF>); + assert(bxdf::bxdf_concepts::MicrofacetBxDF>); + 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; + initparams.verbose = testconfigs["TestJacobian"]["verbose"]; + + 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); + + TestJacobian>::run(initparams, cb); + TestJacobian>::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 = 3; + initparams.verbose = testconfigs["TestReciprocity"]["verbose"]; + + 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); + + TestReciprocity>::run(initparams, cb); + TestReciprocity>::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, 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_EX(rChi2, std::execution::par_unseq) + 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, false>::run(initparams, cb); + TestChi2, true>::run(initparams, cb); + TestChi2, false>::run(initparams, cb); + TestChi2, true>::run(initparams, cb); + FOR_EACH_END + +#if 0 + // 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.state = i; + initparams.verbose = testconfigs["TestNDF"]["verbose"]; + + TestNDF, false>::run(initparams, cb); + TestNDF, true>::run(initparams, cb); + TestNDF, false>::run(initparams, cb); + TestNDF, true>::run(initparams, cb); + + TestNDF, false>::run(initparams, cb); + TestNDF, true>::run(initparams, cb); + TestNDF, false>::run(initparams, cb); + TestNDF, true>::run(initparams, cb); + FOR_EACH_END +#endif +#if 0 + // 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.state = i; + initparams.samples = testconfigs["TestCTGenerateH"]["samples"]; + initparams.immediateFail = testconfigs["TestCTGenerateH"]["immediateFail"]; + + TestCTGenerateH, false>::run(initparams, cb); + TestCTGenerateH, true>::run(initparams, cb); + TestCTGenerateH, false>::run(initparams, cb); + TestCTGenerateH, true>::run(initparams, cb); + + TestCTGenerateH, false>::run(initparams, cb); + TestCTGenerateH, true>::run(initparams, cb); + TestCTGenerateH, false>::run(initparams, cb); + TestCTGenerateH, true>::run(initparams, cb); + FOR_EACH_END +#endif + + // 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